@zucker-framework/config 1.0.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/LICENSE +201 -0
- package/NOTICE +22 -0
- package/THIRD_PARTY_NOTICES +4 -0
- package/dist/index.d.mts +382 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +1307 -0
- package/dist/index.mjs +1265 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
CONFIG_CHANGED_EVENT: () => CONFIG_CHANGED_EVENT,
|
|
25
|
+
CONFIG_CHANGED_TOPIC: () => CONFIG_CHANGED_TOPIC,
|
|
26
|
+
CONFIG_ENCRYPTOR: () => CONFIG_ENCRYPTOR,
|
|
27
|
+
CONFIG_LAYER_PRIORITY: () => CONFIG_LAYER_PRIORITY,
|
|
28
|
+
CONFIG_MODULE_OPTIONS: () => CONFIG_MODULE_OPTIONS,
|
|
29
|
+
CONFIG_SCOPE_REGISTRY: () => CONFIG_SCOPE_REGISTRY,
|
|
30
|
+
ConfigFileWatcher: () => ConfigFileWatcher,
|
|
31
|
+
ConfigGuard: () => ConfigGuard,
|
|
32
|
+
ConfigScopeManager: () => ConfigScopeManager,
|
|
33
|
+
ConfigService: () => ConfigService,
|
|
34
|
+
DATABASE_ADAPTER: () => import_crud.DATABASE_ADAPTER,
|
|
35
|
+
FeatureFlagEvaluator: () => import_feature_flags2.FeatureFlagEvaluator,
|
|
36
|
+
FeatureFlagService: () => FeatureFlagService,
|
|
37
|
+
LAYERED_CONFIG_CHANGED_EVENT: () => LAYERED_CONFIG_CHANGED_EVENT,
|
|
38
|
+
LAYERED_CONFIG_CHANGED_TOPIC: () => LAYERED_CONFIG_CHANGED_TOPIC,
|
|
39
|
+
LayeredConfigService: () => LayeredConfigService,
|
|
40
|
+
ZuckerConfigModule: () => ZuckerConfigModule,
|
|
41
|
+
defaultFeatureFlagEnvKey: () => import_feature_flags2.defaultFeatureFlagEnvKey
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/config.service.ts
|
|
46
|
+
var import_common2 = require("@nestjs/common");
|
|
47
|
+
var import_core = require("@zucker-framework/core");
|
|
48
|
+
var import_eventemitter2 = require("eventemitter2");
|
|
49
|
+
|
|
50
|
+
// src/config-scope-manager.ts
|
|
51
|
+
var import_common = require("@nestjs/common");
|
|
52
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
53
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
54
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
55
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
56
|
+
} else {
|
|
57
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
58
|
+
if (d = decorators[i]) {
|
|
59
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
64
|
+
}
|
|
65
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
66
|
+
var ConfigScopeManager = class _ConfigScopeManager {
|
|
67
|
+
static {
|
|
68
|
+
__name(this, "ConfigScopeManager");
|
|
69
|
+
}
|
|
70
|
+
logger = new import_common.Logger(_ConfigScopeManager.name);
|
|
71
|
+
scopes = /* @__PURE__ */ new Map();
|
|
72
|
+
/**
|
|
73
|
+
* Register a fully defined scope.
|
|
74
|
+
*/
|
|
75
|
+
register(scope) {
|
|
76
|
+
this.scopes.set(scope.id, scope);
|
|
77
|
+
this.logger.debug(`Config scope registered: ${scope.id} (${scope.name})`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Add a scope with separate property definitions — mirrors jetlinks ConfigScopeManager.addScope().
|
|
81
|
+
*
|
|
82
|
+
* Reference: jetlinks ConfigScopeManager.addScope(ConfigScope, List<ConfigPropertyDef>)
|
|
83
|
+
*/
|
|
84
|
+
addScope(scopePartial, properties) {
|
|
85
|
+
const existing = this.scopes.get(scopePartial.id);
|
|
86
|
+
if (existing) {
|
|
87
|
+
const existingKeys = new Set(existing.properties.map((p) => p.key));
|
|
88
|
+
for (const prop of properties) {
|
|
89
|
+
if (!existingKeys.has(prop.key)) {
|
|
90
|
+
existing.properties.push(prop);
|
|
91
|
+
existingKeys.add(prop.key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const scope = {
|
|
97
|
+
id: scopePartial.id,
|
|
98
|
+
name: scopePartial.name,
|
|
99
|
+
isPublic: scopePartial.publicAccess ?? false,
|
|
100
|
+
publicAccess: scopePartial.publicAccess ?? false,
|
|
101
|
+
properties
|
|
102
|
+
};
|
|
103
|
+
this.scopes.set(scope.id, scope);
|
|
104
|
+
this.logger.debug(`Config scope added: ${scope.id} (${scope.name})`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Apply customizers — call during module initialization.
|
|
108
|
+
*
|
|
109
|
+
* Reference: jetlinks ConfigScopeProperties.custom()
|
|
110
|
+
*/
|
|
111
|
+
applyCustomizers(customizers) {
|
|
112
|
+
for (const customizer of customizers) {
|
|
113
|
+
customizer.custom(this);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
get(scopeId) {
|
|
117
|
+
return this.scopes.get(scopeId);
|
|
118
|
+
}
|
|
119
|
+
getAll() {
|
|
120
|
+
return Array.from(this.scopes.values());
|
|
121
|
+
}
|
|
122
|
+
has(scopeId) {
|
|
123
|
+
return this.scopes.has(scopeId);
|
|
124
|
+
}
|
|
125
|
+
unregister(scopeId) {
|
|
126
|
+
return this.scopes.delete(scopeId);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get all scopes marked as public access.
|
|
130
|
+
*
|
|
131
|
+
* Reference: jetlinks SystemConfigManagerController checks isPublicAccess()
|
|
132
|
+
* for unauthenticated access.
|
|
133
|
+
*/
|
|
134
|
+
getPublicScopes() {
|
|
135
|
+
return Array.from(this.scopes.values()).filter((s) => s.publicAccess);
|
|
136
|
+
}
|
|
137
|
+
getProperties(scopeId) {
|
|
138
|
+
return this.scopes.get(scopeId)?.properties ?? [];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validate a config value against its scope property definition.
|
|
142
|
+
* Returns error messages if invalid.
|
|
143
|
+
*/
|
|
144
|
+
validate(scopeId, key, value) {
|
|
145
|
+
const scope = this.scopes.get(scopeId);
|
|
146
|
+
if (!scope) return [
|
|
147
|
+
`Unknown scope: ${scopeId}`
|
|
148
|
+
];
|
|
149
|
+
const prop = scope.properties.find((p) => p.key === key);
|
|
150
|
+
if (!prop) return [
|
|
151
|
+
`Unknown property: ${key} in scope ${scopeId}`
|
|
152
|
+
];
|
|
153
|
+
const errors = [];
|
|
154
|
+
if (prop.readonly) {
|
|
155
|
+
errors.push(`Property ${key} is read-only`);
|
|
156
|
+
}
|
|
157
|
+
if (value === null || value === void 0) {
|
|
158
|
+
return errors;
|
|
159
|
+
}
|
|
160
|
+
const actualType = typeof value;
|
|
161
|
+
switch (prop.type) {
|
|
162
|
+
case "string":
|
|
163
|
+
if (actualType !== "string") errors.push(`Expected string for ${key}, got ${actualType}`);
|
|
164
|
+
break;
|
|
165
|
+
case "number":
|
|
166
|
+
if (actualType !== "number") errors.push(`Expected number for ${key}, got ${actualType}`);
|
|
167
|
+
break;
|
|
168
|
+
case "boolean":
|
|
169
|
+
if (actualType !== "boolean") errors.push(`Expected boolean for ${key}, got ${actualType}`);
|
|
170
|
+
break;
|
|
171
|
+
case "json":
|
|
172
|
+
case "object":
|
|
173
|
+
if (actualType !== "object") errors.push(`Expected object for ${key}, got ${actualType}`);
|
|
174
|
+
break;
|
|
175
|
+
case "password":
|
|
176
|
+
case "encrypted":
|
|
177
|
+
if (actualType !== "string") errors.push(`Expected string for encrypted ${key}, got ${actualType}`);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
return errors;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
ConfigScopeManager = _ts_decorate([
|
|
184
|
+
(0, import_common.Injectable)()
|
|
185
|
+
], ConfigScopeManager);
|
|
186
|
+
|
|
187
|
+
// src/config.service.ts
|
|
188
|
+
var import_crud = require("@zucker-framework/crud");
|
|
189
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
190
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
191
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
192
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
193
|
+
} else {
|
|
194
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
195
|
+
if (d = decorators[i]) {
|
|
196
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
201
|
+
}
|
|
202
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
203
|
+
function _ts_metadata(metadataKey, metadataValue) {
|
|
204
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
205
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
209
|
+
function _ts_param(paramIndex, decorator) {
|
|
210
|
+
return function(target, key) {
|
|
211
|
+
decorator(target, key, paramIndex);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
__name(_ts_param, "_ts_param");
|
|
215
|
+
var CONFIG_CHANGED_EVENT = "zucker.config-changed";
|
|
216
|
+
var CONFIG_CHANGED_TOPIC = "system/config/changed";
|
|
217
|
+
var CONFIG_ENCRYPTOR = "CONFIG_ENCRYPTOR";
|
|
218
|
+
var ConfigService = class _ConfigService {
|
|
219
|
+
static {
|
|
220
|
+
__name(this, "ConfigService");
|
|
221
|
+
}
|
|
222
|
+
db;
|
|
223
|
+
eventEmitter;
|
|
224
|
+
scopeManager;
|
|
225
|
+
encryptor;
|
|
226
|
+
topicEventBus;
|
|
227
|
+
logger = new import_common2.Logger(_ConfigService.name);
|
|
228
|
+
cache = /* @__PURE__ */ new Map();
|
|
229
|
+
static CACHE_MAX_SIZE = 1024;
|
|
230
|
+
constructor(db, eventEmitter, scopeManager, encryptor, topicEventBus) {
|
|
231
|
+
this.db = db;
|
|
232
|
+
this.eventEmitter = eventEmitter;
|
|
233
|
+
this.scopeManager = scopeManager;
|
|
234
|
+
this.encryptor = encryptor;
|
|
235
|
+
this.topicEventBus = topicEventBus;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 获取指定 scope 和 key 的配置值
|
|
239
|
+
* @param scope 配置域
|
|
240
|
+
* @param key 配置键
|
|
241
|
+
* @returns 配置值,不存在时返回 undefined
|
|
242
|
+
*/
|
|
243
|
+
async get(scope, key) {
|
|
244
|
+
const cacheKey = `${scope}:${key}`;
|
|
245
|
+
if (this.cache.has(cacheKey)) {
|
|
246
|
+
return this.cache.get(cacheKey);
|
|
247
|
+
}
|
|
248
|
+
const record = await this.db.model("config").findUnique({
|
|
249
|
+
where: {
|
|
250
|
+
scope_key: {
|
|
251
|
+
scope,
|
|
252
|
+
key
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
if (!record) return void 0;
|
|
257
|
+
let value = record.value;
|
|
258
|
+
if (this.encryptor && this.isEncryptedProperty(scope, key)) {
|
|
259
|
+
try {
|
|
260
|
+
value = await this.encryptor.decrypt(value);
|
|
261
|
+
} catch (decryptError) {
|
|
262
|
+
this.logger.error(`Failed to decrypt config ${scope}.${key}: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}`);
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
while (this.cache.size >= _ConfigService.CACHE_MAX_SIZE) {
|
|
267
|
+
const firstKey = this.cache.keys().next().value;
|
|
268
|
+
this.cache.delete(firstKey);
|
|
269
|
+
}
|
|
270
|
+
this.cache.set(cacheKey, value);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get a config value with a fallback default.
|
|
275
|
+
* If no stored value exists, checks the scope definition default,
|
|
276
|
+
* then falls back to the provided default.
|
|
277
|
+
*
|
|
278
|
+
* Reference: jetlinks getProperties merges defaults from ConfigPropertyDef.
|
|
279
|
+
*/
|
|
280
|
+
async getOrDefault(scope, key, defaultValue) {
|
|
281
|
+
const value = await this.get(scope, key);
|
|
282
|
+
if (value !== void 0) return value;
|
|
283
|
+
if (this.scopeManager?.has(scope)) {
|
|
284
|
+
const prop = this.scopeManager.getProperties(scope).find((p) => p.key === key);
|
|
285
|
+
if (prop?.defaultValue !== void 0) return prop.defaultValue;
|
|
286
|
+
}
|
|
287
|
+
return defaultValue;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 设置配置值(自动触发变更事件)
|
|
291
|
+
* @param scope 配置域
|
|
292
|
+
* @param key 配置键
|
|
293
|
+
* @param value 配置值
|
|
294
|
+
* @throws ValidationException 校验失败时
|
|
295
|
+
*/
|
|
296
|
+
async set(scope, key, value) {
|
|
297
|
+
if (this.scopeManager?.has(scope)) {
|
|
298
|
+
const errors = this.scopeManager.validate(scope, key, value);
|
|
299
|
+
if (errors.length > 0) {
|
|
300
|
+
throw new import_core.ValidationException({
|
|
301
|
+
[key]: errors
|
|
302
|
+
}, `Config validation failed: ${errors.join(", ")}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const cacheKey = `${scope}:${key}`;
|
|
306
|
+
let oldValue = this.cache.get(cacheKey);
|
|
307
|
+
if (oldValue === void 0) {
|
|
308
|
+
const existing = await this.db.model("config").findUnique({
|
|
309
|
+
where: {
|
|
310
|
+
scope_key: {
|
|
311
|
+
scope,
|
|
312
|
+
key
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
if (existing) {
|
|
317
|
+
oldValue = existing.value;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
let storedValue = value;
|
|
321
|
+
if (this.encryptor && this.isEncryptedProperty(scope, key) && typeof value === "string") {
|
|
322
|
+
storedValue = await this.encryptor.encrypt(value);
|
|
323
|
+
}
|
|
324
|
+
await this.db.model("config").update({
|
|
325
|
+
where: {
|
|
326
|
+
scope_key: {
|
|
327
|
+
scope,
|
|
328
|
+
key
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
data: {
|
|
332
|
+
value: storedValue
|
|
333
|
+
}
|
|
334
|
+
}).catch(async (updateError) => {
|
|
335
|
+
this.logger.debug(`Config key ${scope}.${key} not found, creating: ${updateError instanceof Error ? updateError.message : String(updateError)}`);
|
|
336
|
+
await this.db.model("config").create({
|
|
337
|
+
data: {
|
|
338
|
+
scope,
|
|
339
|
+
key,
|
|
340
|
+
value: storedValue
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
while (this.cache.size >= _ConfigService.CACHE_MAX_SIZE) {
|
|
345
|
+
const firstKey = this.cache.keys().next().value;
|
|
346
|
+
this.cache.delete(firstKey);
|
|
347
|
+
}
|
|
348
|
+
this.cache.set(cacheKey, value);
|
|
349
|
+
await this.recordHistory(scope, key, oldValue, value);
|
|
350
|
+
const payload = {
|
|
351
|
+
scope,
|
|
352
|
+
key,
|
|
353
|
+
oldValue,
|
|
354
|
+
newValue: value
|
|
355
|
+
};
|
|
356
|
+
this.eventEmitter?.emit(CONFIG_CHANGED_EVENT, payload);
|
|
357
|
+
if (this.topicEventBus) {
|
|
358
|
+
await this.topicEventBus.publish(CONFIG_CHANGED_TOPIC, payload);
|
|
359
|
+
}
|
|
360
|
+
this.logger.debug(`Config updated: ${scope}.${key}`);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Set multiple properties at once for a scope.
|
|
364
|
+
* Performs batch validation first, then persists all values and emits
|
|
365
|
+
* a single consolidated event — mirrors jetlinks ConfigManager.setProperties(scope, Map)
|
|
366
|
+
* which does repository.save + cache.evict instead of N individual updates.
|
|
367
|
+
*/
|
|
368
|
+
async setProperties(scope, values) {
|
|
369
|
+
const entries = Object.entries(values);
|
|
370
|
+
if (entries.length === 0) return;
|
|
371
|
+
if (this.scopeManager?.has(scope)) {
|
|
372
|
+
const allErrors = {};
|
|
373
|
+
for (const [key, value] of entries) {
|
|
374
|
+
const errors = this.scopeManager.validate(scope, key, value);
|
|
375
|
+
if (errors.length > 0) {
|
|
376
|
+
allErrors[key] = errors;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (Object.keys(allErrors).length > 0) {
|
|
380
|
+
throw new import_core.ValidationException(allErrors, `Config batch validation failed`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const changes = [];
|
|
384
|
+
await this.db.$transaction(async (tx) => {
|
|
385
|
+
for (const [key, value] of entries) {
|
|
386
|
+
const cacheKey = `${scope}:${key}`;
|
|
387
|
+
const oldValue = this.cache.get(cacheKey);
|
|
388
|
+
let storedValue = value;
|
|
389
|
+
if (this.encryptor && this.isEncryptedProperty(scope, key) && typeof value === "string") {
|
|
390
|
+
storedValue = await this.encryptor.encrypt(value);
|
|
391
|
+
}
|
|
392
|
+
await tx.model("config").update({
|
|
393
|
+
where: {
|
|
394
|
+
scope_key: {
|
|
395
|
+
scope,
|
|
396
|
+
key
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
data: {
|
|
400
|
+
value: storedValue
|
|
401
|
+
}
|
|
402
|
+
}).catch(async (updateError) => {
|
|
403
|
+
this.logger.debug(`Config key ${scope}.${key} not found, creating: ${updateError instanceof Error ? updateError.message : String(updateError)}`);
|
|
404
|
+
await tx.model("config").create({
|
|
405
|
+
data: {
|
|
406
|
+
scope,
|
|
407
|
+
key,
|
|
408
|
+
value: storedValue
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
if (this.cache.size >= _ConfigService.CACHE_MAX_SIZE) {
|
|
413
|
+
const firstKey = this.cache.keys().next().value;
|
|
414
|
+
this.cache.delete(firstKey);
|
|
415
|
+
}
|
|
416
|
+
this.cache.set(cacheKey, value);
|
|
417
|
+
changes.push({
|
|
418
|
+
scope,
|
|
419
|
+
key,
|
|
420
|
+
oldValue,
|
|
421
|
+
newValue: value
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
for (const payload of changes) {
|
|
426
|
+
this.eventEmitter?.emit(CONFIG_CHANGED_EVENT, payload);
|
|
427
|
+
}
|
|
428
|
+
if (this.topicEventBus) {
|
|
429
|
+
for (const payload of changes) {
|
|
430
|
+
await this.topicEventBus.publish(CONFIG_CHANGED_TOPIC, payload);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.logger.debug(`Config batch updated: ${scope} (${entries.length} properties)`);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* 获取指定 scope 下的所有配置
|
|
437
|
+
* @param scope 配置域
|
|
438
|
+
* @returns 键值对集合(包含默认值)
|
|
439
|
+
*/
|
|
440
|
+
async getAll(scope) {
|
|
441
|
+
const records = await this.db.model("config").findMany({
|
|
442
|
+
where: {
|
|
443
|
+
scope
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
const result = {};
|
|
447
|
+
if (this.scopeManager?.has(scope)) {
|
|
448
|
+
const props = this.scopeManager.getProperties(scope);
|
|
449
|
+
for (const prop of props) {
|
|
450
|
+
if (prop.defaultValue !== void 0) {
|
|
451
|
+
result[prop.key] = prop.defaultValue;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (const record of records) {
|
|
456
|
+
let value = record.value;
|
|
457
|
+
if (this.encryptor && this.isEncryptedProperty(scope, record.key)) {
|
|
458
|
+
try {
|
|
459
|
+
value = await this.encryptor.decrypt(value);
|
|
460
|
+
} catch (decryptError) {
|
|
461
|
+
this.logger.error(`Failed to decrypt config ${scope}.${record.key}: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}`);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
result[record.key] = value;
|
|
466
|
+
if (this.cache.size >= _ConfigService.CACHE_MAX_SIZE) {
|
|
467
|
+
const firstKey = this.cache.keys().next().value;
|
|
468
|
+
this.cache.delete(firstKey);
|
|
469
|
+
}
|
|
470
|
+
this.cache.set(`${scope}:${record.key}`, value);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Remove a specific config property.
|
|
476
|
+
*/
|
|
477
|
+
async remove(scope, key) {
|
|
478
|
+
const cacheKey = `${scope}:${key}`;
|
|
479
|
+
let oldValue = this.cache.get(cacheKey);
|
|
480
|
+
if (oldValue === void 0) {
|
|
481
|
+
const existing = await this.db.model("config").findUnique({
|
|
482
|
+
where: {
|
|
483
|
+
scope_key: {
|
|
484
|
+
scope,
|
|
485
|
+
key
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
if (existing) {
|
|
490
|
+
oldValue = existing.value;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
await this.db.model("config").delete({
|
|
494
|
+
where: {
|
|
495
|
+
scope_key: {
|
|
496
|
+
scope,
|
|
497
|
+
key
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}).catch(() => {
|
|
501
|
+
});
|
|
502
|
+
this.cache.delete(cacheKey);
|
|
503
|
+
const payload = {
|
|
504
|
+
scope,
|
|
505
|
+
key,
|
|
506
|
+
oldValue,
|
|
507
|
+
newValue: void 0
|
|
508
|
+
};
|
|
509
|
+
this.eventEmitter?.emit(CONFIG_CHANGED_EVENT, payload);
|
|
510
|
+
if (this.topicEventBus) {
|
|
511
|
+
await this.topicEventBus.publish(CONFIG_CHANGED_TOPIC, payload);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 监听配置变更事件
|
|
516
|
+
* @param callback 变更回调
|
|
517
|
+
* @returns 订阅句柄,调用 unsubscribe() 取消监听
|
|
518
|
+
*/
|
|
519
|
+
onChange(callback) {
|
|
520
|
+
if (!this.eventEmitter) return {
|
|
521
|
+
unsubscribe: /* @__PURE__ */ __name(() => void 0, "unsubscribe")
|
|
522
|
+
};
|
|
523
|
+
const ee = this.eventEmitter;
|
|
524
|
+
ee.on(CONFIG_CHANGED_EVENT, callback);
|
|
525
|
+
return {
|
|
526
|
+
unsubscribe: /* @__PURE__ */ __name(() => ee.off(CONFIG_CHANGED_EVENT, callback), "unsubscribe")
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 监听指定 scope.key 的配置变更,每次变更时执行回调。
|
|
531
|
+
*
|
|
532
|
+
* @param scope 配置域
|
|
533
|
+
* @param key 配置键(可选,不传则监听整个 scope)
|
|
534
|
+
* @param callback 变更回调
|
|
535
|
+
* @returns 取消监听函数
|
|
536
|
+
*/
|
|
537
|
+
watch(scope, key, callback) {
|
|
538
|
+
const handler = /* @__PURE__ */ __name((payload) => {
|
|
539
|
+
if (payload.scope !== scope) return;
|
|
540
|
+
if (key && payload.key !== key) return;
|
|
541
|
+
callback(payload.newValue, payload.oldValue);
|
|
542
|
+
}, "handler");
|
|
543
|
+
if (!this.eventEmitter) return {
|
|
544
|
+
unsubscribe: /* @__PURE__ */ __name(() => void 0, "unsubscribe")
|
|
545
|
+
};
|
|
546
|
+
const ee = this.eventEmitter;
|
|
547
|
+
ee.on(CONFIG_CHANGED_EVENT, handler);
|
|
548
|
+
return {
|
|
549
|
+
unsubscribe: /* @__PURE__ */ __name(() => ee.off(CONFIG_CHANGED_EVENT, handler), "unsubscribe")
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* 清除配置缓存
|
|
554
|
+
* @param scope 可选的配置域,不传则清除所有缓存
|
|
555
|
+
*/
|
|
556
|
+
clearCache(scope) {
|
|
557
|
+
if (scope) {
|
|
558
|
+
const prefix = `${scope}:`;
|
|
559
|
+
for (const key of this.cache.keys()) {
|
|
560
|
+
if (key.startsWith(prefix)) this.cache.delete(key);
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
this.cache.clear();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// ─── 配置版本控制 ──────────────────────────────────────────────────
|
|
567
|
+
/**
|
|
568
|
+
* 记录配置变更历史(自动版本号递增)
|
|
569
|
+
* 配置版本控制
|
|
570
|
+
*/
|
|
571
|
+
async recordHistory(scope, key, oldValue, newValue) {
|
|
572
|
+
try {
|
|
573
|
+
await this.db.model("configHistory").create({
|
|
574
|
+
data: {
|
|
575
|
+
scope,
|
|
576
|
+
key,
|
|
577
|
+
oldValue: oldValue !== void 0 ? JSON.stringify(oldValue) : null,
|
|
578
|
+
newValue: JSON.stringify(newValue),
|
|
579
|
+
changedAt: /* @__PURE__ */ new Date()
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* 查询配置变更历史
|
|
587
|
+
* @param scope 配置域
|
|
588
|
+
* @param key 配置键(可选,不传则查询该 scope 所有变更)
|
|
589
|
+
* @param limit 返回条数(默认 20)
|
|
590
|
+
*/
|
|
591
|
+
async getHistory(scope, key, limit = 20) {
|
|
592
|
+
try {
|
|
593
|
+
const where = {
|
|
594
|
+
scope
|
|
595
|
+
};
|
|
596
|
+
if (key) where.key = key;
|
|
597
|
+
return await this.db.model("configHistory").findMany({
|
|
598
|
+
where,
|
|
599
|
+
orderBy: {
|
|
600
|
+
changedAt: "desc"
|
|
601
|
+
},
|
|
602
|
+
take: limit
|
|
603
|
+
});
|
|
604
|
+
} catch {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 回滚配置到指定历史版本
|
|
610
|
+
* @param historyId 历史记录 ID
|
|
611
|
+
*/
|
|
612
|
+
async rollback(historyId) {
|
|
613
|
+
try {
|
|
614
|
+
const record = await this.db.model("configHistory").findUnique({
|
|
615
|
+
where: {
|
|
616
|
+
id: historyId
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
if (!record || record.oldValue === null) return;
|
|
620
|
+
const value = JSON.parse(record.oldValue);
|
|
621
|
+
await this.set(record.scope, record.key, value);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
this.logger.warn(`Config rollback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Check if a property is marked as encrypted in its scope definition.
|
|
628
|
+
*/
|
|
629
|
+
isEncryptedProperty(scope, key) {
|
|
630
|
+
if (!this.scopeManager) return false;
|
|
631
|
+
const props = this.scopeManager.getProperties(scope);
|
|
632
|
+
const prop = props.find((p) => p.key === key);
|
|
633
|
+
return prop?.encrypted === true;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
ConfigService = _ts_decorate2([
|
|
637
|
+
(0, import_common2.Injectable)(),
|
|
638
|
+
_ts_param(0, (0, import_common2.Inject)(import_crud.DATABASE_ADAPTER)),
|
|
639
|
+
_ts_param(1, (0, import_common2.Optional)()),
|
|
640
|
+
_ts_param(2, (0, import_common2.Optional)()),
|
|
641
|
+
_ts_param(3, (0, import_common2.Optional)()),
|
|
642
|
+
_ts_param(3, (0, import_common2.Inject)(CONFIG_ENCRYPTOR)),
|
|
643
|
+
_ts_param(4, (0, import_common2.Optional)()),
|
|
644
|
+
_ts_param(4, (0, import_common2.Inject)("TOPIC_EVENT_BUS")),
|
|
645
|
+
_ts_metadata("design:type", Function),
|
|
646
|
+
_ts_metadata("design:paramtypes", [
|
|
647
|
+
typeof DatabaseAdapter === "undefined" ? Object : DatabaseAdapter,
|
|
648
|
+
typeof import_eventemitter2.EventEmitter2 === "undefined" ? Object : import_eventemitter2.EventEmitter2,
|
|
649
|
+
typeof ConfigScopeManager === "undefined" ? Object : ConfigScopeManager,
|
|
650
|
+
typeof ConfigEncryptor === "undefined" ? Object : ConfigEncryptor,
|
|
651
|
+
typeof TopicEventBusLike === "undefined" ? Object : TopicEventBusLike
|
|
652
|
+
])
|
|
653
|
+
], ConfigService);
|
|
654
|
+
|
|
655
|
+
// src/config.module.ts
|
|
656
|
+
var import_common4 = require("@nestjs/common");
|
|
657
|
+
|
|
658
|
+
// src/layered-config.service.ts
|
|
659
|
+
var import_common3 = require("@nestjs/common");
|
|
660
|
+
var import_core2 = require("@zucker-framework/core");
|
|
661
|
+
var import_eventemitter22 = require("eventemitter2");
|
|
662
|
+
var import_crud2 = require("@zucker-framework/crud");
|
|
663
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
664
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
665
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
666
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
667
|
+
} else {
|
|
668
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
669
|
+
if (d = decorators[i]) {
|
|
670
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
675
|
+
}
|
|
676
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
677
|
+
function _ts_metadata2(metadataKey, metadataValue) {
|
|
678
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
679
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
683
|
+
function _ts_param2(paramIndex, decorator) {
|
|
684
|
+
return function(target, key) {
|
|
685
|
+
decorator(target, key, paramIndex);
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
__name(_ts_param2, "_ts_param");
|
|
689
|
+
var CONFIG_LAYER_PRIORITY = {
|
|
690
|
+
system: 0,
|
|
691
|
+
tenant: 1,
|
|
692
|
+
user: 2
|
|
693
|
+
};
|
|
694
|
+
var LAYERED_CONFIG_CHANGED_EVENT = "zucker.layered-config-changed";
|
|
695
|
+
var LAYERED_CONFIG_CHANGED_TOPIC = "system/config/layered-changed";
|
|
696
|
+
var LayeredConfigService = class _LayeredConfigService {
|
|
697
|
+
static {
|
|
698
|
+
__name(this, "LayeredConfigService");
|
|
699
|
+
}
|
|
700
|
+
db;
|
|
701
|
+
scopeManager;
|
|
702
|
+
eventEmitter;
|
|
703
|
+
topicEventBus;
|
|
704
|
+
logger = new import_common3.Logger(_LayeredConfigService.name);
|
|
705
|
+
cache = /* @__PURE__ */ new Map();
|
|
706
|
+
constructor(db, scopeManager, eventEmitter, topicEventBus) {
|
|
707
|
+
this.db = db;
|
|
708
|
+
this.scopeManager = scopeManager;
|
|
709
|
+
this.eventEmitter = eventEmitter;
|
|
710
|
+
this.topicEventBus = topicEventBus;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Get a config value, resolving through layers (user > tenant > system > default).
|
|
714
|
+
*/
|
|
715
|
+
async get(scope, key, context) {
|
|
716
|
+
if (context?.userId) {
|
|
717
|
+
const userValue = await this.getLayerValue(scope, key, "user", context.userId);
|
|
718
|
+
if (userValue !== void 0) return userValue;
|
|
719
|
+
}
|
|
720
|
+
if (context?.tenantId) {
|
|
721
|
+
const tenantValue = await this.getLayerValue(scope, key, "tenant", context.tenantId);
|
|
722
|
+
if (tenantValue !== void 0) return tenantValue;
|
|
723
|
+
}
|
|
724
|
+
const systemValue = await this.getLayerValue(scope, key, "system");
|
|
725
|
+
if (systemValue !== void 0) return systemValue;
|
|
726
|
+
const scopeDef = this.scopeManager.get(scope);
|
|
727
|
+
if (scopeDef) {
|
|
728
|
+
const prop = scopeDef.properties.find((p) => p.key === key);
|
|
729
|
+
if (prop) return prop.defaultValue;
|
|
730
|
+
}
|
|
731
|
+
return void 0;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Set a config value at a specific layer.
|
|
735
|
+
*/
|
|
736
|
+
async set(scope, key, value, layer, ownerId) {
|
|
737
|
+
const errors = this.scopeManager.validate(scope, key, value);
|
|
738
|
+
if (errors.length > 0) {
|
|
739
|
+
throw new import_core2.ValidationException({
|
|
740
|
+
[key]: errors
|
|
741
|
+
}, `Config validation failed: ${errors.join(", ")}`);
|
|
742
|
+
}
|
|
743
|
+
const compositeKey = this.buildLayerKey(scope, key, layer, ownerId);
|
|
744
|
+
const oldValue = await this.getLayerValue(scope, key, layer, ownerId);
|
|
745
|
+
await this.db.model("config").update({
|
|
746
|
+
where: {
|
|
747
|
+
scope_key: {
|
|
748
|
+
scope: compositeKey,
|
|
749
|
+
key
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
data: {
|
|
753
|
+
value,
|
|
754
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
755
|
+
}
|
|
756
|
+
}).catch(async () => {
|
|
757
|
+
await this.db.model("config").create({
|
|
758
|
+
data: {
|
|
759
|
+
scope: compositeKey,
|
|
760
|
+
key,
|
|
761
|
+
value
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
const payload = {
|
|
766
|
+
scope,
|
|
767
|
+
key,
|
|
768
|
+
layer,
|
|
769
|
+
oldValue,
|
|
770
|
+
newValue: value
|
|
771
|
+
};
|
|
772
|
+
this.eventEmitter?.emit(LAYERED_CONFIG_CHANGED_EVENT, payload);
|
|
773
|
+
if (this.topicEventBus) {
|
|
774
|
+
await this.topicEventBus.publish(LAYERED_CONFIG_CHANGED_TOPIC, payload);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Get all values for a scope, merged across layers.
|
|
779
|
+
*/
|
|
780
|
+
async getAll(scope, context) {
|
|
781
|
+
const result = {};
|
|
782
|
+
const scopeDef = this.scopeManager.get(scope);
|
|
783
|
+
if (scopeDef) {
|
|
784
|
+
for (const prop of scopeDef.properties) {
|
|
785
|
+
if (prop.defaultValue !== void 0) {
|
|
786
|
+
result[prop.key] = prop.defaultValue;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const systemRecords = await this.getLayerRecords(scope, "system");
|
|
791
|
+
for (const [k, v] of Object.entries(systemRecords)) {
|
|
792
|
+
result[k] = v;
|
|
793
|
+
}
|
|
794
|
+
if (context?.tenantId) {
|
|
795
|
+
const tenantRecords = await this.getLayerRecords(scope, "tenant", context.tenantId);
|
|
796
|
+
for (const [k, v] of Object.entries(tenantRecords)) {
|
|
797
|
+
result[k] = v;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (context?.userId) {
|
|
801
|
+
const userRecords = await this.getLayerRecords(scope, "user", context.userId);
|
|
802
|
+
for (const [k, v] of Object.entries(userRecords)) {
|
|
803
|
+
result[k] = v;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Set multiple properties at a specific layer.
|
|
810
|
+
* Validates all entries first, then persists in batch.
|
|
811
|
+
*
|
|
812
|
+
* Reference: jetlinks ConfigManager.setProperties(scope, Map)
|
|
813
|
+
*/
|
|
814
|
+
async setProperties(scope, values, layer, ownerId) {
|
|
815
|
+
const entries = Object.entries(values);
|
|
816
|
+
if (entries.length === 0) return;
|
|
817
|
+
const allErrors = {};
|
|
818
|
+
for (const [key, value] of entries) {
|
|
819
|
+
const errors = this.scopeManager.validate(scope, key, value);
|
|
820
|
+
if (errors.length > 0) {
|
|
821
|
+
allErrors[key] = errors;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (Object.keys(allErrors).length > 0) {
|
|
825
|
+
throw new import_core2.ValidationException(allErrors, `Layered config batch validation failed`);
|
|
826
|
+
}
|
|
827
|
+
const changes = [];
|
|
828
|
+
await this.db.$transaction(async (txDb) => {
|
|
829
|
+
for (const [key, value] of entries) {
|
|
830
|
+
const compositeKey = this.buildLayerKey(scope, key, layer, ownerId);
|
|
831
|
+
const cacheKey = `${compositeKey}:${key}`;
|
|
832
|
+
const oldValue = this.cache.get(cacheKey);
|
|
833
|
+
await txDb.model("config").update({
|
|
834
|
+
where: {
|
|
835
|
+
scope_key: {
|
|
836
|
+
scope: compositeKey,
|
|
837
|
+
key
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
data: {
|
|
841
|
+
value,
|
|
842
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
843
|
+
}
|
|
844
|
+
}).catch(async () => {
|
|
845
|
+
await txDb.model("config").create({
|
|
846
|
+
data: {
|
|
847
|
+
scope: compositeKey,
|
|
848
|
+
key,
|
|
849
|
+
value
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
this.cache.set(cacheKey, value);
|
|
854
|
+
changes.push({
|
|
855
|
+
scope,
|
|
856
|
+
key,
|
|
857
|
+
layer,
|
|
858
|
+
oldValue,
|
|
859
|
+
newValue: value
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
for (const payload of changes) {
|
|
864
|
+
this.eventEmitter?.emit(LAYERED_CONFIG_CHANGED_EVENT, payload);
|
|
865
|
+
}
|
|
866
|
+
if (this.topicEventBus) {
|
|
867
|
+
for (const payload of changes) {
|
|
868
|
+
await this.topicEventBus.publish(LAYERED_CONFIG_CHANGED_TOPIC, payload);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Remove a config value at a specific layer.
|
|
874
|
+
*/
|
|
875
|
+
async remove(scope, key, layer, ownerId) {
|
|
876
|
+
const compositeKey = this.buildLayerKey(scope, key, layer, ownerId);
|
|
877
|
+
const cacheKey = `${compositeKey}:${key}`;
|
|
878
|
+
const oldValue = this.cache.get(cacheKey);
|
|
879
|
+
await this.db.model("config").delete({
|
|
880
|
+
where: {
|
|
881
|
+
scope_key: {
|
|
882
|
+
scope: compositeKey,
|
|
883
|
+
key
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}).catch(() => {
|
|
887
|
+
});
|
|
888
|
+
this.cache.delete(cacheKey);
|
|
889
|
+
const payload = {
|
|
890
|
+
scope,
|
|
891
|
+
key,
|
|
892
|
+
layer,
|
|
893
|
+
oldValue,
|
|
894
|
+
newValue: void 0
|
|
895
|
+
};
|
|
896
|
+
this.eventEmitter?.emit(LAYERED_CONFIG_CHANGED_EVENT, payload);
|
|
897
|
+
if (this.topicEventBus) {
|
|
898
|
+
await this.topicEventBus.publish(LAYERED_CONFIG_CHANGED_TOPIC, payload);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Clear the in-memory cache.
|
|
903
|
+
* @param scope Optional — only clear entries for this scope prefix
|
|
904
|
+
*/
|
|
905
|
+
clearCache(scope) {
|
|
906
|
+
if (scope) {
|
|
907
|
+
for (const key of this.cache.keys()) {
|
|
908
|
+
if (key.startsWith(scope)) this.cache.delete(key);
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
this.cache.clear();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async getLayerValue(scope, key, layer, ownerId) {
|
|
915
|
+
const compositeKey = this.buildLayerKey(scope, key, layer, ownerId);
|
|
916
|
+
const cacheKey = `${compositeKey}:${key}`;
|
|
917
|
+
if (this.cache.has(cacheKey)) {
|
|
918
|
+
return this.cache.get(cacheKey);
|
|
919
|
+
}
|
|
920
|
+
const record = await this.db.model("config").findUnique({
|
|
921
|
+
where: {
|
|
922
|
+
scope_key: {
|
|
923
|
+
scope: compositeKey,
|
|
924
|
+
key
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
if (record) {
|
|
929
|
+
this.cache.set(cacheKey, record.value);
|
|
930
|
+
}
|
|
931
|
+
return record?.value;
|
|
932
|
+
}
|
|
933
|
+
async getLayerRecords(scope, layer, ownerId) {
|
|
934
|
+
const compositeScope = ownerId ? `${scope}@${layer}:${ownerId}` : `${scope}@${layer}`;
|
|
935
|
+
const records = await this.db.model("config").findMany({
|
|
936
|
+
where: {
|
|
937
|
+
scope: compositeScope
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
const result = {};
|
|
941
|
+
for (const record of records) {
|
|
942
|
+
result[record.key] = record.value;
|
|
943
|
+
}
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
buildLayerKey(scope, _key, layer, ownerId) {
|
|
947
|
+
if (ownerId) {
|
|
948
|
+
return `${scope}@${layer}:${ownerId}`;
|
|
949
|
+
}
|
|
950
|
+
return `${scope}@${layer}`;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
LayeredConfigService = _ts_decorate3([
|
|
954
|
+
(0, import_common3.Injectable)(),
|
|
955
|
+
_ts_param2(0, (0, import_common3.Inject)(import_crud2.DATABASE_ADAPTER)),
|
|
956
|
+
_ts_param2(2, (0, import_common3.Optional)()),
|
|
957
|
+
_ts_param2(3, (0, import_common3.Optional)()),
|
|
958
|
+
_ts_param2(3, (0, import_common3.Inject)("TOPIC_EVENT_BUS")),
|
|
959
|
+
_ts_metadata2("design:type", Function),
|
|
960
|
+
_ts_metadata2("design:paramtypes", [
|
|
961
|
+
typeof DatabaseAdapter === "undefined" ? Object : DatabaseAdapter,
|
|
962
|
+
typeof ConfigScopeManager === "undefined" ? Object : ConfigScopeManager,
|
|
963
|
+
typeof import_eventemitter22.EventEmitter2 === "undefined" ? Object : import_eventemitter22.EventEmitter2,
|
|
964
|
+
typeof TopicEventBusLike === "undefined" ? Object : TopicEventBusLike
|
|
965
|
+
])
|
|
966
|
+
], LayeredConfigService);
|
|
967
|
+
|
|
968
|
+
// src/config.module.ts
|
|
969
|
+
function _ts_decorate4(decorators, target, key, desc) {
|
|
970
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
971
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
972
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
973
|
+
} else {
|
|
974
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
975
|
+
if (d = decorators[i]) {
|
|
976
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
981
|
+
}
|
|
982
|
+
__name(_ts_decorate4, "_ts_decorate");
|
|
983
|
+
var CONFIG_MODULE_OPTIONS = /* @__PURE__ */ Symbol("CONFIG_MODULE_OPTIONS");
|
|
984
|
+
var ZuckerConfigModule = class _ZuckerConfigModule {
|
|
985
|
+
static {
|
|
986
|
+
__name(this, "ZuckerConfigModule");
|
|
987
|
+
}
|
|
988
|
+
static forRoot(options) {
|
|
989
|
+
const providers = [
|
|
990
|
+
ConfigService,
|
|
991
|
+
ConfigScopeManager,
|
|
992
|
+
LayeredConfigService
|
|
993
|
+
];
|
|
994
|
+
if (options?.encryptor) {
|
|
995
|
+
providers.push({
|
|
996
|
+
provide: CONFIG_ENCRYPTOR,
|
|
997
|
+
useValue: options.encryptor
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
module: _ZuckerConfigModule,
|
|
1002
|
+
global: true,
|
|
1003
|
+
providers,
|
|
1004
|
+
exports: [
|
|
1005
|
+
ConfigService,
|
|
1006
|
+
ConfigScopeManager,
|
|
1007
|
+
LayeredConfigService
|
|
1008
|
+
]
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
static forRootAsync(options) {
|
|
1012
|
+
const asyncProviders = this.createAsyncProviders(options);
|
|
1013
|
+
return {
|
|
1014
|
+
module: _ZuckerConfigModule,
|
|
1015
|
+
global: true,
|
|
1016
|
+
imports: options.imports ?? [],
|
|
1017
|
+
providers: [
|
|
1018
|
+
...asyncProviders,
|
|
1019
|
+
{
|
|
1020
|
+
provide: CONFIG_ENCRYPTOR,
|
|
1021
|
+
useFactory: /* @__PURE__ */ __name((opts) => opts.encryptor ?? null, "useFactory"),
|
|
1022
|
+
inject: [
|
|
1023
|
+
CONFIG_MODULE_OPTIONS
|
|
1024
|
+
]
|
|
1025
|
+
},
|
|
1026
|
+
ConfigService,
|
|
1027
|
+
ConfigScopeManager,
|
|
1028
|
+
LayeredConfigService
|
|
1029
|
+
],
|
|
1030
|
+
exports: [
|
|
1031
|
+
ConfigService,
|
|
1032
|
+
ConfigScopeManager,
|
|
1033
|
+
LayeredConfigService
|
|
1034
|
+
]
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
static createAsyncProviders(options) {
|
|
1038
|
+
if (options.useFactory) {
|
|
1039
|
+
return [
|
|
1040
|
+
{
|
|
1041
|
+
provide: CONFIG_MODULE_OPTIONS,
|
|
1042
|
+
useFactory: options.useFactory,
|
|
1043
|
+
inject: options.inject ?? []
|
|
1044
|
+
}
|
|
1045
|
+
];
|
|
1046
|
+
}
|
|
1047
|
+
const optionsProvider = {
|
|
1048
|
+
provide: CONFIG_MODULE_OPTIONS,
|
|
1049
|
+
useFactory: /* @__PURE__ */ __name(async (factory) => factory.createConfigOptions(), "useFactory"),
|
|
1050
|
+
inject: [
|
|
1051
|
+
options.useClass ?? options.useExisting
|
|
1052
|
+
]
|
|
1053
|
+
};
|
|
1054
|
+
if (options.useClass) {
|
|
1055
|
+
return [
|
|
1056
|
+
{
|
|
1057
|
+
provide: options.useClass,
|
|
1058
|
+
useClass: options.useClass
|
|
1059
|
+
},
|
|
1060
|
+
optionsProvider
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
1063
|
+
return [
|
|
1064
|
+
optionsProvider
|
|
1065
|
+
];
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
ZuckerConfigModule = _ts_decorate4([
|
|
1069
|
+
(0, import_common4.Global)(),
|
|
1070
|
+
(0, import_common4.Module)({})
|
|
1071
|
+
], ZuckerConfigModule);
|
|
1072
|
+
|
|
1073
|
+
// src/config.guard.ts
|
|
1074
|
+
var import_common5 = require("@nestjs/common");
|
|
1075
|
+
function _ts_decorate5(decorators, target, key, desc) {
|
|
1076
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1077
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
1078
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
1079
|
+
} else {
|
|
1080
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1081
|
+
if (d = decorators[i]) {
|
|
1082
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1087
|
+
}
|
|
1088
|
+
__name(_ts_decorate5, "_ts_decorate");
|
|
1089
|
+
function _ts_metadata3(metadataKey, metadataValue) {
|
|
1090
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
1091
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
1095
|
+
function _ts_param3(paramIndex, decorator) {
|
|
1096
|
+
return function(target, key) {
|
|
1097
|
+
decorator(target, key, paramIndex);
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
__name(_ts_param3, "_ts_param");
|
|
1101
|
+
var CONFIG_SCOPE_REGISTRY = "CONFIG_SCOPE_REGISTRY";
|
|
1102
|
+
var ConfigGuard = class _ConfigGuard {
|
|
1103
|
+
static {
|
|
1104
|
+
__name(this, "ConfigGuard");
|
|
1105
|
+
}
|
|
1106
|
+
scopeRegistry;
|
|
1107
|
+
logger = new import_common5.Logger(_ConfigGuard.name);
|
|
1108
|
+
constructor(scopeRegistry) {
|
|
1109
|
+
this.scopeRegistry = scopeRegistry;
|
|
1110
|
+
}
|
|
1111
|
+
canActivate(context) {
|
|
1112
|
+
const request = context.switchToHttp().getRequest();
|
|
1113
|
+
const scope = request.params?.scope ?? request.query?.scope;
|
|
1114
|
+
if (!scope) {
|
|
1115
|
+
return this.isAuthenticated(request);
|
|
1116
|
+
}
|
|
1117
|
+
const entry = this.scopeRegistry.get(scope);
|
|
1118
|
+
if (entry?.publicAccess) {
|
|
1119
|
+
this.logger.debug(`\u516C\u5F00\u8BBF\u95EE scope: ${scope}`);
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
return this.isAuthenticated(request);
|
|
1123
|
+
}
|
|
1124
|
+
isAuthenticated(request) {
|
|
1125
|
+
return !!request.user;
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
ConfigGuard = _ts_decorate5([
|
|
1129
|
+
(0, import_common5.Injectable)(),
|
|
1130
|
+
_ts_param3(0, (0, import_common5.Inject)(CONFIG_SCOPE_REGISTRY)),
|
|
1131
|
+
_ts_metadata3("design:type", Function),
|
|
1132
|
+
_ts_metadata3("design:paramtypes", [
|
|
1133
|
+
typeof Map === "undefined" ? Object : Map
|
|
1134
|
+
])
|
|
1135
|
+
], ConfigGuard);
|
|
1136
|
+
|
|
1137
|
+
// src/feature-flags.ts
|
|
1138
|
+
var import_common6 = require("@nestjs/common");
|
|
1139
|
+
var import_feature_flags = require("@zucker-framework/feature-flags");
|
|
1140
|
+
var import_feature_flags2 = require("@zucker-framework/feature-flags");
|
|
1141
|
+
function _ts_decorate6(decorators, target, key, desc) {
|
|
1142
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1143
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
1144
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
1145
|
+
} else {
|
|
1146
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1147
|
+
if (d = decorators[i]) {
|
|
1148
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1153
|
+
}
|
|
1154
|
+
__name(_ts_decorate6, "_ts_decorate");
|
|
1155
|
+
function _ts_metadata4(metadataKey, metadataValue) {
|
|
1156
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
1157
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
__name(_ts_metadata4, "_ts_metadata");
|
|
1161
|
+
var FeatureFlagService = class extends import_feature_flags.FeatureFlagEvaluator {
|
|
1162
|
+
static {
|
|
1163
|
+
__name(this, "FeatureFlagService");
|
|
1164
|
+
}
|
|
1165
|
+
constructor() {
|
|
1166
|
+
super((key) => process.env[key]);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
FeatureFlagService = _ts_decorate6([
|
|
1170
|
+
(0, import_common6.Injectable)(),
|
|
1171
|
+
_ts_metadata4("design:type", Function),
|
|
1172
|
+
_ts_metadata4("design:paramtypes", [])
|
|
1173
|
+
], FeatureFlagService);
|
|
1174
|
+
|
|
1175
|
+
// src/config-watcher.ts
|
|
1176
|
+
var import_common7 = require("@nestjs/common");
|
|
1177
|
+
var import_fs = require("fs");
|
|
1178
|
+
var ConfigFileWatcher = class _ConfigFileWatcher {
|
|
1179
|
+
static {
|
|
1180
|
+
__name(this, "ConfigFileWatcher");
|
|
1181
|
+
}
|
|
1182
|
+
logger = new import_common7.Logger(_ConfigFileWatcher.name);
|
|
1183
|
+
stabilityMs;
|
|
1184
|
+
deleteGraceMs;
|
|
1185
|
+
watched = /* @__PURE__ */ new Map();
|
|
1186
|
+
constructor(options = {}) {
|
|
1187
|
+
this.stabilityMs = options.stabilityMs ?? 1e3;
|
|
1188
|
+
this.deleteGraceMs = options.deleteGraceMs ?? 1700;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* 开始监听文件
|
|
1192
|
+
* @returns 取消监听函数
|
|
1193
|
+
*/
|
|
1194
|
+
watch(filePath, callback) {
|
|
1195
|
+
if (this.watched.has(filePath)) {
|
|
1196
|
+
this.unwatch(filePath);
|
|
1197
|
+
}
|
|
1198
|
+
const entry = {
|
|
1199
|
+
path: filePath,
|
|
1200
|
+
callback,
|
|
1201
|
+
watcher: null,
|
|
1202
|
+
debounceTimer: null,
|
|
1203
|
+
deleteGraceTimer: null,
|
|
1204
|
+
lastModified: this.getModifiedTime(filePath)
|
|
1205
|
+
};
|
|
1206
|
+
try {
|
|
1207
|
+
entry.watcher = (0, import_fs.watch)(filePath, (eventType) => {
|
|
1208
|
+
this.handleEvent(entry, eventType);
|
|
1209
|
+
});
|
|
1210
|
+
entry.watcher.on("error", (err) => {
|
|
1211
|
+
this.logger.warn(`Watch error on ${filePath}: ${err.message}`);
|
|
1212
|
+
});
|
|
1213
|
+
} catch {
|
|
1214
|
+
this.logger.warn(`Cannot watch ${filePath} \u2014 file may not exist yet`);
|
|
1215
|
+
}
|
|
1216
|
+
this.watched.set(filePath, entry);
|
|
1217
|
+
return () => this.unwatch(filePath);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* 停止监听指定文件
|
|
1221
|
+
*/
|
|
1222
|
+
unwatch(filePath) {
|
|
1223
|
+
const entry = this.watched.get(filePath);
|
|
1224
|
+
if (!entry) return;
|
|
1225
|
+
entry.watcher?.close();
|
|
1226
|
+
if (entry.debounceTimer) clearTimeout(entry.debounceTimer);
|
|
1227
|
+
if (entry.deleteGraceTimer) clearTimeout(entry.deleteGraceTimer);
|
|
1228
|
+
this.watched.delete(filePath);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* 关闭所有监听
|
|
1232
|
+
*/
|
|
1233
|
+
close() {
|
|
1234
|
+
for (const path of [
|
|
1235
|
+
...this.watched.keys()
|
|
1236
|
+
]) {
|
|
1237
|
+
this.unwatch(path);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* 获取当前监听的文件列表
|
|
1242
|
+
*/
|
|
1243
|
+
getWatchedFiles() {
|
|
1244
|
+
return [
|
|
1245
|
+
...this.watched.keys()
|
|
1246
|
+
];
|
|
1247
|
+
}
|
|
1248
|
+
handleEvent(entry, eventType) {
|
|
1249
|
+
if (eventType === "rename") {
|
|
1250
|
+
if (!(0, import_fs.existsSync)(entry.path)) {
|
|
1251
|
+
if (entry.deleteGraceTimer) clearTimeout(entry.deleteGraceTimer);
|
|
1252
|
+
entry.deleteGraceTimer = setTimeout(() => {
|
|
1253
|
+
entry.deleteGraceTimer = null;
|
|
1254
|
+
if (!(0, import_fs.existsSync)(entry.path)) {
|
|
1255
|
+
this.logger.warn(`Config file deleted: ${entry.path}`);
|
|
1256
|
+
}
|
|
1257
|
+
}, this.deleteGraceMs);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (entry.deleteGraceTimer) {
|
|
1261
|
+
clearTimeout(entry.deleteGraceTimer);
|
|
1262
|
+
entry.deleteGraceTimer = null;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (entry.debounceTimer) clearTimeout(entry.debounceTimer);
|
|
1266
|
+
entry.debounceTimer = setTimeout(() => {
|
|
1267
|
+
entry.debounceTimer = null;
|
|
1268
|
+
const newModified = this.getModifiedTime(entry.path);
|
|
1269
|
+
if (newModified === entry.lastModified) return;
|
|
1270
|
+
entry.lastModified = newModified;
|
|
1271
|
+
this.logger.log(`Config changed: ${entry.path}`);
|
|
1272
|
+
try {
|
|
1273
|
+
entry.callback();
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
this.logger.error(`Config reload callback failed: ${err}`);
|
|
1276
|
+
}
|
|
1277
|
+
}, this.stabilityMs);
|
|
1278
|
+
}
|
|
1279
|
+
getModifiedTime(filePath) {
|
|
1280
|
+
try {
|
|
1281
|
+
return (0, import_fs.statSync)(filePath).mtimeMs;
|
|
1282
|
+
} catch {
|
|
1283
|
+
return 0;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1288
|
+
0 && (module.exports = {
|
|
1289
|
+
CONFIG_CHANGED_EVENT,
|
|
1290
|
+
CONFIG_CHANGED_TOPIC,
|
|
1291
|
+
CONFIG_ENCRYPTOR,
|
|
1292
|
+
CONFIG_LAYER_PRIORITY,
|
|
1293
|
+
CONFIG_MODULE_OPTIONS,
|
|
1294
|
+
CONFIG_SCOPE_REGISTRY,
|
|
1295
|
+
ConfigFileWatcher,
|
|
1296
|
+
ConfigGuard,
|
|
1297
|
+
ConfigScopeManager,
|
|
1298
|
+
ConfigService,
|
|
1299
|
+
DATABASE_ADAPTER,
|
|
1300
|
+
FeatureFlagEvaluator,
|
|
1301
|
+
FeatureFlagService,
|
|
1302
|
+
LAYERED_CONFIG_CHANGED_EVENT,
|
|
1303
|
+
LAYERED_CONFIG_CHANGED_TOPIC,
|
|
1304
|
+
LayeredConfigService,
|
|
1305
|
+
ZuckerConfigModule,
|
|
1306
|
+
defaultFeatureFlagEnvKey
|
|
1307
|
+
});
|