alepha 0.13.6 → 0.13.7
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/api-audits/index.browser.js +116 -0
- package/dist/api-audits/index.browser.js.map +1 -0
- package/dist/api-audits/index.d.ts +1194 -0
- package/dist/api-audits/index.js +674 -0
- package/dist/api-audits/index.js.map +1 -0
- package/dist/api-notifications/index.d.ts +147 -147
- package/dist/api-parameters/index.browser.js +36 -5
- package/dist/api-parameters/index.browser.js.map +1 -1
- package/dist/api-parameters/index.d.ts +711 -33
- package/dist/api-parameters/index.js +831 -17
- package/dist/api-parameters/index.js.map +1 -1
- package/dist/api-users/index.d.ts +793 -780
- package/dist/api-users/index.js +699 -19
- package/dist/api-users/index.js.map +1 -1
- package/dist/api-verifications/index.js +2 -1
- package/dist/api-verifications/index.js.map +1 -1
- package/dist/bin/index.js +1 -0
- package/dist/bin/index.js.map +1 -1
- package/dist/cli/index.d.ts +85 -31
- package/dist/cli/index.js +205 -33
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +67 -6
- package/dist/command/index.js +30 -3
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +241 -61
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +170 -90
- package/dist/core/index.js +264 -67
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +248 -65
- package/dist/core/index.native.js.map +1 -1
- package/dist/email/index.js +15 -10554
- package/dist/email/index.js.map +1 -1
- package/dist/logger/index.d.ts +4 -4
- package/dist/logger/index.js +77 -72
- package/dist/logger/index.js.map +1 -1
- package/dist/orm/index.d.ts +5 -1
- package/dist/orm/index.js +24 -7
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +4 -4
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/server/index.d.ts +10 -1
- package/dist/server/index.js +20 -6
- package/dist/server/index.js.map +1 -1
- package/dist/server-auth/index.d.ts +163 -152
- package/dist/server-auth/index.js +40 -10
- package/dist/server-auth/index.js.map +1 -1
- package/dist/server-cookies/index.js +5 -1
- package/dist/server-cookies/index.js.map +1 -1
- package/dist/server-links/index.d.ts +33 -33
- package/dist/server-security/index.d.ts +9 -9
- package/dist/thread/index.js +2 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +102 -45
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +3 -3
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js +4 -4
- package/dist/websocket/index.js.map +1 -1
- package/package.json +14 -9
- package/src/api-audits/controllers/AuditController.ts +186 -0
- package/src/api-audits/entities/audits.ts +132 -0
- package/src/api-audits/index.browser.ts +18 -0
- package/src/api-audits/index.ts +58 -0
- package/src/api-audits/primitives/$audit.ts +159 -0
- package/src/api-audits/schemas/auditQuerySchema.ts +23 -0
- package/src/api-audits/schemas/auditResourceSchema.ts +9 -0
- package/src/api-audits/schemas/createAuditSchema.ts +27 -0
- package/src/api-audits/services/AuditService.ts +412 -0
- package/src/api-parameters/controllers/ConfigController.ts +324 -0
- package/src/api-parameters/entities/parameters.ts +93 -10
- package/src/api-parameters/index.ts +43 -4
- package/src/api-parameters/primitives/$config.ts +291 -19
- package/src/api-parameters/schedulers/ConfigActivationScheduler.ts +30 -0
- package/src/api-parameters/services/ConfigStore.ts +491 -0
- package/src/api-users/atoms/realmAuthSettingsAtom.ts +19 -0
- package/src/api-users/controllers/UserRealmController.ts +0 -2
- package/src/api-users/index.ts +2 -0
- package/src/api-users/primitives/$userRealm.ts +18 -3
- package/src/api-users/providers/UserRealmProvider.ts +6 -3
- package/src/api-users/services/RegistrationService.ts +2 -1
- package/src/api-users/services/SessionService.ts +4 -0
- package/src/api-users/services/UserService.ts +3 -0
- package/src/api-verifications/index.ts +7 -1
- package/src/bin/index.ts +1 -0
- package/src/cli/assets/biomeJson.ts +1 -1
- package/src/cli/assets/dummySpecTs.ts +7 -0
- package/src/cli/assets/editorconfig.ts +13 -0
- package/src/cli/assets/mainTs.ts +14 -0
- package/src/cli/commands/BiomeCommands.ts +2 -0
- package/src/cli/commands/CoreCommands.ts +28 -9
- package/src/cli/commands/VerifyCommands.ts +2 -1
- package/src/cli/commands/ViteCommands.ts +8 -9
- package/src/cli/services/AlephaCliUtils.ts +214 -23
- package/src/command/helpers/Asker.ts +0 -1
- package/src/command/primitives/$command.ts +67 -0
- package/src/command/providers/CliProvider.ts +39 -8
- package/src/core/Alepha.ts +40 -30
- package/src/core/helpers/jsonSchemaToTypeBox.ts +307 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +30 -3
- package/src/core/providers/EventManager.ts +1 -1
- package/src/core/providers/StateManager.ts +23 -12
- package/src/core/providers/TypeProvider.ts +26 -34
- package/src/logger/index.ts +8 -6
- package/src/logger/primitives/$logger.ts +1 -1
- package/src/logger/providers/{SimpleFormatterProvider.ts → PrettyFormatterProvider.ts} +10 -1
- package/src/orm/index.ts +6 -0
- package/src/orm/services/PgRelationManager.ts +2 -2
- package/src/orm/services/PostgresModelBuilder.ts +11 -7
- package/src/orm/services/Repository.ts +16 -7
- package/src/orm/services/SqliteModelBuilder.ts +10 -0
- package/src/server/index.ts +6 -0
- package/src/server/primitives/$action.ts +10 -1
- package/src/server/providers/ServerBodyParserProvider.ts +11 -5
- package/src/server/providers/ServerRouterProvider.ts +13 -7
- package/src/server-auth/primitives/$auth.ts +7 -0
- package/src/server-auth/providers/ServerAuthProvider.ts +51 -8
- package/src/server-cookies/index.ts +2 -1
- package/src/thread/primitives/$thread.ts +2 -2
- package/src/vite/index.ts +0 -2
- package/src/vite/tasks/buildServer.ts +3 -4
- package/src/vite/tasks/generateCloudflare.ts +35 -19
- package/src/vite/tasks/generateDocker.ts +18 -4
- package/src/vite/tasks/generateSitemap.ts +5 -7
- package/src/vite/tasks/generateVercel.ts +76 -41
- package/src/vite/tasks/runAlepha.ts +16 -1
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -11
- package/src/websocket/services/WebSocketClient.ts +3 -3
- package/dist/cli/dist-BlfFtOk2.js +0 -2770
- package/dist/cli/dist-BlfFtOk2.js.map +0 -1
- package/src/api-parameters/controllers/ParameterController.ts +0 -45
- package/src/api-parameters/services/ParameterStore.ts +0 -23
|
@@ -1,63 +1,877 @@
|
|
|
1
|
-
import { $module, Primitive, createPrimitive, t } from "alepha";
|
|
2
|
-
import { $
|
|
1
|
+
import { $hook, $inject, $module, KIND, Primitive, createPrimitive, t } from "alepha";
|
|
2
|
+
import { $action } from "alepha/server";
|
|
3
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
4
|
+
import { $logger } from "alepha/logger";
|
|
5
|
+
import { $entity, $repository, pg } from "alepha/orm";
|
|
6
|
+
import { $topic } from "alepha/topic";
|
|
7
|
+
import { $scheduler } from "alepha/scheduler";
|
|
3
8
|
|
|
4
9
|
//#region ../../src/api-parameters/entities/parameters.ts
|
|
10
|
+
/**
|
|
11
|
+
* Configuration parameter entity for versioned configuration management.
|
|
12
|
+
*
|
|
13
|
+
* Stores all versions of configuration parameters with:
|
|
14
|
+
* - Automatic status management (expired, current, next, future)
|
|
15
|
+
* - Schema versioning for migrations
|
|
16
|
+
* - Activation scheduling
|
|
17
|
+
* - Audit trail (creator info)
|
|
18
|
+
*/
|
|
5
19
|
const parameters = $entity({
|
|
6
20
|
name: "parameters",
|
|
7
21
|
schema: t.object({
|
|
8
22
|
id: pg.primaryKey(t.uuid()),
|
|
9
23
|
createdAt: pg.createdAt(),
|
|
10
24
|
updatedAt: pg.updatedAt(),
|
|
11
|
-
name: t.
|
|
25
|
+
name: t.text(),
|
|
12
26
|
content: t.json(),
|
|
13
|
-
|
|
27
|
+
schemaHash: t.text(),
|
|
28
|
+
status: pg.default(t.enum([
|
|
29
|
+
"expired",
|
|
30
|
+
"current",
|
|
31
|
+
"next",
|
|
32
|
+
"future"
|
|
33
|
+
]), "future"),
|
|
34
|
+
activationDate: t.datetime(),
|
|
35
|
+
expiredAt: t.optional(t.datetime()),
|
|
36
|
+
version: t.integer(),
|
|
37
|
+
changeDescription: t.optional(t.text()),
|
|
38
|
+
tags: t.optional(t.array(t.text())),
|
|
14
39
|
creatorId: t.optional(t.uuid()),
|
|
15
|
-
creatorName: t.optional(t.
|
|
16
|
-
|
|
17
|
-
|
|
40
|
+
creatorName: t.optional(t.text()),
|
|
41
|
+
previousContent: t.optional(t.json()),
|
|
42
|
+
migrationLog: t.optional(t.text())
|
|
43
|
+
}),
|
|
44
|
+
indexes: [
|
|
45
|
+
{ columns: ["name", "status"] },
|
|
46
|
+
{ columns: ["name", "activationDate"] },
|
|
47
|
+
{
|
|
48
|
+
columns: ["name", "version"],
|
|
49
|
+
unique: true
|
|
50
|
+
},
|
|
51
|
+
{ columns: ["status"] },
|
|
52
|
+
{ columns: ["activationDate"] }
|
|
53
|
+
]
|
|
18
54
|
});
|
|
19
55
|
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region ../../src/api-parameters/services/ConfigStore.ts
|
|
58
|
+
/**
|
|
59
|
+
* ConfigStore manages versioned configuration persistence and synchronization.
|
|
60
|
+
*
|
|
61
|
+
* Features:
|
|
62
|
+
* - Stores all config versions in PostgreSQL
|
|
63
|
+
* - Manages status transitions (future → next → current → expired)
|
|
64
|
+
* - Provides cross-instance sync via topic
|
|
65
|
+
* - Supports schema migrations via hash comparison
|
|
66
|
+
* - Auto-activates scheduled configs
|
|
67
|
+
*/
|
|
68
|
+
var ConfigStore = class {
|
|
69
|
+
log = $logger();
|
|
70
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
71
|
+
repo = $repository(parameters);
|
|
72
|
+
/** Unique identifier for this instance (to avoid self-updates) */
|
|
73
|
+
instanceId = crypto.randomUUID();
|
|
74
|
+
/** In-memory cache of registered configs */
|
|
75
|
+
configs = /* @__PURE__ */ new Map();
|
|
76
|
+
/** Topic for cross-instance synchronization */
|
|
77
|
+
syncTopic = $topic({
|
|
78
|
+
name: "config:sync",
|
|
79
|
+
schema: { payload: t.object({
|
|
80
|
+
name: t.text(),
|
|
81
|
+
version: t.integer(),
|
|
82
|
+
content: t.json(),
|
|
83
|
+
status: t.enum([
|
|
84
|
+
"expired",
|
|
85
|
+
"current",
|
|
86
|
+
"next",
|
|
87
|
+
"future"
|
|
88
|
+
]),
|
|
89
|
+
instanceId: t.text()
|
|
90
|
+
}) },
|
|
91
|
+
handler: async ({ payload }) => {
|
|
92
|
+
await this.handleSyncMessage(payload);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Register a config primitive with the store.
|
|
97
|
+
*/
|
|
98
|
+
register(config) {
|
|
99
|
+
this.configs.set(config.name, config);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Load the current config value from database.
|
|
103
|
+
* Returns the current or next version if no current exists.
|
|
104
|
+
*/
|
|
105
|
+
async load(name) {
|
|
106
|
+
const all = await this.repo.findMany({
|
|
107
|
+
where: { name },
|
|
108
|
+
orderBy: {
|
|
109
|
+
column: "version",
|
|
110
|
+
direction: "desc"
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
let param = all.find((p) => p.status === "current");
|
|
114
|
+
if (!param) param = all.filter((p) => p.status === "next").sort((a, b) => {
|
|
115
|
+
return new Date(a.activationDate).getTime() - new Date(b.activationDate).getTime();
|
|
116
|
+
})[0];
|
|
117
|
+
return param?.content;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Save a new config version.
|
|
121
|
+
*
|
|
122
|
+
* @param name - Config name (e.g., "app.features.flags")
|
|
123
|
+
* @param content - New config content
|
|
124
|
+
* @param schemaHash - Hash of the schema for migration detection
|
|
125
|
+
* @param options - Additional options (activation date, creator info, etc.)
|
|
126
|
+
*/
|
|
127
|
+
async save(name, content, schemaHash, options = {}) {
|
|
128
|
+
const now = this.dateTimeProvider.now().toDate();
|
|
129
|
+
const activationDate = options.activationDate ?? now;
|
|
130
|
+
const isImmediate = activationDate <= now;
|
|
131
|
+
const versions = await this.repo.findMany({
|
|
132
|
+
where: { name },
|
|
133
|
+
orderBy: {
|
|
134
|
+
column: "version",
|
|
135
|
+
direction: "desc"
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const latestVersion = versions[0];
|
|
139
|
+
const newVersion = (latestVersion?.version ?? 0) + 1;
|
|
140
|
+
let status = "future";
|
|
141
|
+
if (isImmediate) status = "current";
|
|
142
|
+
const previousContent = versions.find((v) => v.status === "current")?.content;
|
|
143
|
+
let migrationLog;
|
|
144
|
+
if (latestVersion && latestVersion.schemaHash !== schemaHash) {
|
|
145
|
+
migrationLog = `Schema changed from ${latestVersion.schemaHash} to ${schemaHash} at version ${newVersion}`;
|
|
146
|
+
this.log.info("Config schema migration detected", {
|
|
147
|
+
name,
|
|
148
|
+
migrationLog
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (isImmediate) await this.transitionStatuses(name, now);
|
|
152
|
+
const inserted = await this.repo.create({
|
|
153
|
+
name,
|
|
154
|
+
content,
|
|
155
|
+
schemaHash,
|
|
156
|
+
status,
|
|
157
|
+
activationDate: activationDate.toISOString(),
|
|
158
|
+
version: newVersion,
|
|
159
|
+
changeDescription: options.changeDescription,
|
|
160
|
+
tags: options.tags,
|
|
161
|
+
creatorId: options.creatorId,
|
|
162
|
+
creatorName: options.creatorName,
|
|
163
|
+
previousContent,
|
|
164
|
+
migrationLog
|
|
165
|
+
});
|
|
166
|
+
await this.recalculateStatuses(name);
|
|
167
|
+
await this.publishSync(name, newVersion, content, status);
|
|
168
|
+
this.log.info("Config saved", {
|
|
169
|
+
name,
|
|
170
|
+
version: newVersion,
|
|
171
|
+
status
|
|
172
|
+
});
|
|
173
|
+
return inserted;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get all versions of a config.
|
|
177
|
+
*/
|
|
178
|
+
async getHistory(name) {
|
|
179
|
+
return this.repo.findMany({
|
|
180
|
+
where: { name },
|
|
181
|
+
orderBy: {
|
|
182
|
+
column: "version",
|
|
183
|
+
direction: "desc"
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get a specific version of a config.
|
|
189
|
+
*/
|
|
190
|
+
async getVersion(name, version) {
|
|
191
|
+
return (await this.repo.findMany({ where: {
|
|
192
|
+
name,
|
|
193
|
+
version
|
|
194
|
+
} }))[0] ?? null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Rollback to a previous version by creating a new version with old content.
|
|
198
|
+
*/
|
|
199
|
+
async rollback(name, targetVersion, options = {}) {
|
|
200
|
+
const target = await this.getVersion(name, targetVersion);
|
|
201
|
+
if (!target) throw new Error(`Config version not found: ${name}@${targetVersion}`);
|
|
202
|
+
return this.save(name, target.content, target.schemaHash, {
|
|
203
|
+
...options,
|
|
204
|
+
changeDescription: options.changeDescription ?? `Rollback to version ${targetVersion}`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get all configs by status.
|
|
209
|
+
*/
|
|
210
|
+
async getByStatus(status) {
|
|
211
|
+
return this.repo.findMany({
|
|
212
|
+
where: { status },
|
|
213
|
+
orderBy: {
|
|
214
|
+
column: "name",
|
|
215
|
+
direction: "asc"
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get current config value with fallback to default from registered primitive.
|
|
221
|
+
* Returns the in-memory current value which may be the default if never saved.
|
|
222
|
+
*/
|
|
223
|
+
getCurrentValue(name) {
|
|
224
|
+
const config = this.configs.get(name);
|
|
225
|
+
if (!config) return null;
|
|
226
|
+
return {
|
|
227
|
+
content: config.current,
|
|
228
|
+
isDefault: true
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get config info including current value with default fallback.
|
|
233
|
+
*/
|
|
234
|
+
async getCurrentWithDefault(name) {
|
|
235
|
+
const history = await this.getHistory(name);
|
|
236
|
+
const current = history.find((v) => v.status === "current") ?? null;
|
|
237
|
+
const next = history.find((v) => v.status === "next") ?? null;
|
|
238
|
+
const config = this.configs.get(name);
|
|
239
|
+
return {
|
|
240
|
+
current,
|
|
241
|
+
next,
|
|
242
|
+
defaultValue: config?.options.default ?? null,
|
|
243
|
+
currentValue: config?.current ?? null,
|
|
244
|
+
schema: config?.schema ?? null
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get all unique config names (for tree view).
|
|
249
|
+
*/
|
|
250
|
+
async getConfigNames() {
|
|
251
|
+
const results = await this.repo.findMany({ orderBy: {
|
|
252
|
+
column: "name",
|
|
253
|
+
direction: "asc"
|
|
254
|
+
} });
|
|
255
|
+
const names = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const r of results) names.add(r.name);
|
|
257
|
+
return Array.from(names);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Build a tree structure from config names for UI.
|
|
261
|
+
* Includes both database configs and registered (but not yet saved) configs.
|
|
262
|
+
*/
|
|
263
|
+
async getConfigTree() {
|
|
264
|
+
const dbNames = await this.getConfigNames();
|
|
265
|
+
const registeredNames = Array.from(this.configs.keys());
|
|
266
|
+
const allNames = [...new Set([...dbNames, ...registeredNames])].sort();
|
|
267
|
+
return this.buildTree(allNames);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check and activate scheduled configs that are due.
|
|
271
|
+
* Should be called periodically (e.g., via scheduler).
|
|
272
|
+
*/
|
|
273
|
+
async activateScheduledConfigs() {
|
|
274
|
+
const now = this.dateTimeProvider.now().toDate();
|
|
275
|
+
const dueConfigs = await this.repo.findMany({ where: { status: "next" } });
|
|
276
|
+
for (const config of dueConfigs) if (new Date(config.activationDate) <= now) {
|
|
277
|
+
await this.transitionStatuses(config.name, now);
|
|
278
|
+
await this.recalculateStatuses(config.name);
|
|
279
|
+
const primitive = this.configs.get(config.name);
|
|
280
|
+
if (primitive) await primitive.reload();
|
|
281
|
+
await this.publishSync(config.name, config.version, config.content, "current");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Transition config statuses when a new current is activated.
|
|
286
|
+
*/
|
|
287
|
+
async transitionStatuses(name, now) {
|
|
288
|
+
const currentConfigs = await this.repo.findMany({ where: {
|
|
289
|
+
name,
|
|
290
|
+
status: "current"
|
|
291
|
+
} });
|
|
292
|
+
for (const config of currentConfigs) await this.repo.updateById(config.id, {
|
|
293
|
+
status: "expired",
|
|
294
|
+
expiredAt: now.toISOString()
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Recalculate statuses based on activation dates.
|
|
299
|
+
*/
|
|
300
|
+
async recalculateStatuses(name) {
|
|
301
|
+
const now = this.dateTimeProvider.now().toDate();
|
|
302
|
+
const nonExpired = (await this.repo.findMany({
|
|
303
|
+
where: { name },
|
|
304
|
+
orderBy: {
|
|
305
|
+
column: "activationDate",
|
|
306
|
+
direction: "asc"
|
|
307
|
+
}
|
|
308
|
+
})).filter((v) => v.status !== "expired");
|
|
309
|
+
const shouldBeCurrent = nonExpired.filter((v) => new Date(v.activationDate) <= now).pop();
|
|
310
|
+
const shouldBeNext = nonExpired.filter((v) => new Date(v.activationDate) > now)[0];
|
|
311
|
+
for (const v of nonExpired) {
|
|
312
|
+
let newStatus;
|
|
313
|
+
if (shouldBeCurrent && v.id === shouldBeCurrent.id) newStatus = "current";
|
|
314
|
+
else if (shouldBeNext && v.id === shouldBeNext.id) newStatus = "next";
|
|
315
|
+
else if (new Date(v.activationDate) > now) newStatus = "future";
|
|
316
|
+
else newStatus = "expired";
|
|
317
|
+
if (v.status !== newStatus) await this.repo.updateById(v.id, {
|
|
318
|
+
status: newStatus,
|
|
319
|
+
expiredAt: newStatus === "expired" ? now.toISOString() : void 0
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Publish sync event to other instances.
|
|
325
|
+
*/
|
|
326
|
+
async publishSync(name, version, content, status) {
|
|
327
|
+
await this.syncTopic.publish({
|
|
328
|
+
name,
|
|
329
|
+
version,
|
|
330
|
+
content,
|
|
331
|
+
status,
|
|
332
|
+
instanceId: this.instanceId
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Handle incoming sync message from other instances.
|
|
337
|
+
*/
|
|
338
|
+
async handleSyncMessage(payload) {
|
|
339
|
+
if (payload.instanceId === this.instanceId) return;
|
|
340
|
+
const config = this.configs.get(payload.name);
|
|
341
|
+
if (!config) return;
|
|
342
|
+
if (payload.status === "current") await config.updateFromSync(payload.content);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Build tree structure from dot-notation names.
|
|
346
|
+
*/
|
|
347
|
+
buildTree(names) {
|
|
348
|
+
const root = [];
|
|
349
|
+
for (const name of names) {
|
|
350
|
+
const parts = name.split(".");
|
|
351
|
+
let currentLevel = root;
|
|
352
|
+
for (let i = 0; i < parts.length; i++) {
|
|
353
|
+
const part = parts[i];
|
|
354
|
+
const isLeaf = i === parts.length - 1;
|
|
355
|
+
const path = parts.slice(0, i + 1).join(".");
|
|
356
|
+
let existing = currentLevel.find((n) => n.name === part);
|
|
357
|
+
if (!existing) {
|
|
358
|
+
existing = {
|
|
359
|
+
name: part,
|
|
360
|
+
path,
|
|
361
|
+
isLeaf,
|
|
362
|
+
children: []
|
|
363
|
+
};
|
|
364
|
+
currentLevel.push(existing);
|
|
365
|
+
}
|
|
366
|
+
if (isLeaf) existing.isLeaf = true;
|
|
367
|
+
currentLevel = existing.children;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return root;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region ../../src/api-parameters/controllers/ConfigController.ts
|
|
376
|
+
const parameterResponseSchema = t.object({
|
|
377
|
+
id: t.uuid(),
|
|
378
|
+
createdAt: t.datetime(),
|
|
379
|
+
updatedAt: t.datetime(),
|
|
380
|
+
name: t.text(),
|
|
381
|
+
content: t.json(),
|
|
382
|
+
schemaHash: t.text(),
|
|
383
|
+
status: t.enum([
|
|
384
|
+
"expired",
|
|
385
|
+
"current",
|
|
386
|
+
"next",
|
|
387
|
+
"future"
|
|
388
|
+
]),
|
|
389
|
+
activationDate: t.datetime(),
|
|
390
|
+
expiredAt: t.optional(t.datetime()),
|
|
391
|
+
version: t.integer(),
|
|
392
|
+
changeDescription: t.optional(t.text()),
|
|
393
|
+
tags: t.optional(t.array(t.text())),
|
|
394
|
+
creatorId: t.optional(t.uuid()),
|
|
395
|
+
creatorName: t.optional(t.text()),
|
|
396
|
+
previousContent: t.optional(t.json()),
|
|
397
|
+
migrationLog: t.optional(t.text())
|
|
398
|
+
});
|
|
399
|
+
const treeNodeSchema = t.object({
|
|
400
|
+
name: t.text(),
|
|
401
|
+
path: t.text(),
|
|
402
|
+
isLeaf: t.boolean(),
|
|
403
|
+
children: t.array(t.any())
|
|
404
|
+
});
|
|
405
|
+
/**
|
|
406
|
+
* REST API controller for versioned configuration management.
|
|
407
|
+
*
|
|
408
|
+
* Provides endpoints for:
|
|
409
|
+
* - Listing all configurations (tree view support)
|
|
410
|
+
* - Getting configuration history (all versions)
|
|
411
|
+
* - Getting current/next configuration values
|
|
412
|
+
* - Creating new configuration versions (immediate or scheduled)
|
|
413
|
+
* - Rolling back to previous versions
|
|
414
|
+
* - Activating scheduled versions immediately
|
|
415
|
+
*/
|
|
416
|
+
var ConfigController = class {
|
|
417
|
+
store = $inject(ConfigStore);
|
|
418
|
+
/**
|
|
419
|
+
* Get tree structure of all configuration names.
|
|
420
|
+
* Useful for admin UI navigation.
|
|
421
|
+
*/
|
|
422
|
+
getConfigTree = $action({
|
|
423
|
+
description: "Get tree structure of all configuration names for navigation.",
|
|
424
|
+
path: "/configs/tree",
|
|
425
|
+
method: "GET",
|
|
426
|
+
schema: { response: t.array(treeNodeSchema) },
|
|
427
|
+
handler: async () => {
|
|
428
|
+
return this.store.getConfigTree();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
/**
|
|
432
|
+
* List all unique configuration names.
|
|
433
|
+
*/
|
|
434
|
+
listConfigNames = $action({
|
|
435
|
+
description: "List all unique configuration names.",
|
|
436
|
+
path: "/configs",
|
|
437
|
+
method: "GET",
|
|
438
|
+
schema: { response: t.object({ names: t.array(t.text()) }) },
|
|
439
|
+
handler: async () => {
|
|
440
|
+
return { names: await this.store.getConfigNames() };
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
/**
|
|
444
|
+
* Get configurations by status.
|
|
445
|
+
*/
|
|
446
|
+
getByStatus = $action({
|
|
447
|
+
description: "Get all configurations with a specific status.",
|
|
448
|
+
path: "/configs/status/:status",
|
|
449
|
+
method: "GET",
|
|
450
|
+
schema: {
|
|
451
|
+
params: t.object({ status: t.enum([
|
|
452
|
+
"expired",
|
|
453
|
+
"current",
|
|
454
|
+
"next",
|
|
455
|
+
"future"
|
|
456
|
+
]) }),
|
|
457
|
+
response: t.object({ configs: t.array(parameterResponseSchema) })
|
|
458
|
+
},
|
|
459
|
+
handler: async ({ params }) => {
|
|
460
|
+
return { configs: await this.store.getByStatus(params.status) };
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
/**
|
|
464
|
+
* Get version history for a specific configuration.
|
|
465
|
+
*/
|
|
466
|
+
getHistory = $action({
|
|
467
|
+
description: "Get all versions of a specific configuration.",
|
|
468
|
+
path: "/configs/:name/history",
|
|
469
|
+
method: "GET",
|
|
470
|
+
schema: {
|
|
471
|
+
params: t.object({ name: t.text({ description: "Configuration name (e.g., app.features.flags)" }) }),
|
|
472
|
+
response: t.object({ versions: t.array(parameterResponseSchema) })
|
|
473
|
+
},
|
|
474
|
+
handler: async ({ params }) => {
|
|
475
|
+
return { versions: await this.store.getHistory(params.name) };
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
/**
|
|
479
|
+
* Get current and next values for a configuration.
|
|
480
|
+
* Includes defaultValue and currentValue from the registered primitive
|
|
481
|
+
* even if no versions exist in the database yet.
|
|
482
|
+
*/
|
|
483
|
+
getCurrent = $action({
|
|
484
|
+
description: "Get current and next scheduled values for a configuration.",
|
|
485
|
+
path: "/configs/:name",
|
|
486
|
+
method: "GET",
|
|
487
|
+
schema: {
|
|
488
|
+
params: t.object({ name: t.text({ description: "Configuration name (e.g., app.features.flags)" }) }),
|
|
489
|
+
response: t.object({
|
|
490
|
+
current: t.optional(parameterResponseSchema),
|
|
491
|
+
next: t.optional(parameterResponseSchema),
|
|
492
|
+
defaultValue: t.optional(t.json()),
|
|
493
|
+
currentValue: t.optional(t.json()),
|
|
494
|
+
schema: t.optional(t.json())
|
|
495
|
+
})
|
|
496
|
+
},
|
|
497
|
+
handler: async ({ params }) => {
|
|
498
|
+
const result = await this.store.getCurrentWithDefault(params.name);
|
|
499
|
+
return {
|
|
500
|
+
current: result.current ?? void 0,
|
|
501
|
+
next: result.next ?? void 0,
|
|
502
|
+
defaultValue: result.defaultValue ?? void 0,
|
|
503
|
+
currentValue: result.currentValue ?? void 0,
|
|
504
|
+
schema: result.schema ?? void 0
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
/**
|
|
509
|
+
* Get a specific version of a configuration.
|
|
510
|
+
*/
|
|
511
|
+
getVersion = $action({
|
|
512
|
+
description: "Get a specific version of a configuration.",
|
|
513
|
+
path: "/configs/:name/versions/:version",
|
|
514
|
+
method: "GET",
|
|
515
|
+
schema: {
|
|
516
|
+
params: t.object({
|
|
517
|
+
name: t.text(),
|
|
518
|
+
version: t.integer()
|
|
519
|
+
}),
|
|
520
|
+
response: t.object({ config: t.optional(parameterResponseSchema) })
|
|
521
|
+
},
|
|
522
|
+
handler: async ({ params }) => {
|
|
523
|
+
return { config: await this.store.getVersion(params.name, params.version) ?? void 0 };
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
/**
|
|
527
|
+
* Create a new configuration version.
|
|
528
|
+
*/
|
|
529
|
+
createVersion = $action({
|
|
530
|
+
description: "Create a new version of a configuration (immediate or scheduled).",
|
|
531
|
+
path: "/configs/:name",
|
|
532
|
+
method: "POST",
|
|
533
|
+
schema: {
|
|
534
|
+
params: t.object({ name: t.text({ description: "Configuration name (e.g., app.features.flags)" }) }),
|
|
535
|
+
body: t.object({
|
|
536
|
+
content: t.json({ description: "New configuration content" }),
|
|
537
|
+
schemaHash: t.text({ description: "Hash of the schema for migration detection" }),
|
|
538
|
+
activationDate: t.optional(t.datetime({ description: "When to activate (default: now)" })),
|
|
539
|
+
changeDescription: t.optional(t.text({ description: "Description of changes" })),
|
|
540
|
+
tags: t.optional(t.array(t.text())),
|
|
541
|
+
creatorId: t.optional(t.uuid()),
|
|
542
|
+
creatorName: t.optional(t.text())
|
|
543
|
+
}),
|
|
544
|
+
response: parameterResponseSchema
|
|
545
|
+
},
|
|
546
|
+
handler: async ({ params, body }) => {
|
|
547
|
+
return this.store.save(params.name, body.content, body.schemaHash, {
|
|
548
|
+
activationDate: body.activationDate ? new Date(body.activationDate) : void 0,
|
|
549
|
+
changeDescription: body.changeDescription,
|
|
550
|
+
tags: body.tags,
|
|
551
|
+
creatorId: body.creatorId,
|
|
552
|
+
creatorName: body.creatorName
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
/**
|
|
557
|
+
* Rollback to a previous version.
|
|
558
|
+
*/
|
|
559
|
+
rollback = $action({
|
|
560
|
+
description: "Rollback a configuration to a previous version (creates new version with old content).",
|
|
561
|
+
path: "/configs/:name/rollback",
|
|
562
|
+
method: "POST",
|
|
563
|
+
schema: {
|
|
564
|
+
params: t.object({ name: t.text() }),
|
|
565
|
+
body: t.object({
|
|
566
|
+
targetVersion: t.integer({ description: "Version number to rollback to" }),
|
|
567
|
+
changeDescription: t.optional(t.text()),
|
|
568
|
+
creatorId: t.optional(t.uuid()),
|
|
569
|
+
creatorName: t.optional(t.text())
|
|
570
|
+
}),
|
|
571
|
+
response: parameterResponseSchema
|
|
572
|
+
},
|
|
573
|
+
handler: async ({ params, body }) => {
|
|
574
|
+
return this.store.rollback(params.name, body.targetVersion, {
|
|
575
|
+
changeDescription: body.changeDescription,
|
|
576
|
+
creatorId: body.creatorId,
|
|
577
|
+
creatorName: body.creatorName
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
/**
|
|
582
|
+
* Activate a scheduled version immediately.
|
|
583
|
+
*/
|
|
584
|
+
activateNow = $action({
|
|
585
|
+
description: "Activate a future/next configuration version immediately.",
|
|
586
|
+
path: "/configs/:name/activate",
|
|
587
|
+
method: "POST",
|
|
588
|
+
schema: {
|
|
589
|
+
params: t.object({ name: t.text() }),
|
|
590
|
+
body: t.object({
|
|
591
|
+
version: t.integer({ description: "Version number to activate" }),
|
|
592
|
+
creatorId: t.optional(t.uuid()),
|
|
593
|
+
creatorName: t.optional(t.text())
|
|
594
|
+
}),
|
|
595
|
+
response: parameterResponseSchema
|
|
596
|
+
},
|
|
597
|
+
handler: async ({ params, body }) => {
|
|
598
|
+
const target = await this.store.getVersion(params.name, body.version);
|
|
599
|
+
if (!target) throw new Error(`Version ${body.version} not found for config ${params.name}`);
|
|
600
|
+
if (target.status === "current") return target;
|
|
601
|
+
if (target.status === "expired") throw new Error("Cannot activate an expired version. Use rollback instead.");
|
|
602
|
+
return this.store.save(params.name, target.content, target.schemaHash, {
|
|
603
|
+
changeDescription: `Early activation of version ${body.version}`,
|
|
604
|
+
creatorId: body.creatorId,
|
|
605
|
+
creatorName: body.creatorName
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
/**
|
|
610
|
+
* Trigger activation check for all scheduled configs.
|
|
611
|
+
* Normally called by a scheduler, but exposed for manual triggering.
|
|
612
|
+
*/
|
|
613
|
+
checkScheduled = $action({
|
|
614
|
+
description: "Manually trigger activation check for all scheduled configurations.",
|
|
615
|
+
path: "/configs/activate-scheduled",
|
|
616
|
+
method: "POST",
|
|
617
|
+
schema: { response: t.object({ message: t.text() }) },
|
|
618
|
+
handler: async () => {
|
|
619
|
+
await this.store.activateScheduledConfigs();
|
|
620
|
+
return { message: "Scheduled configuration activation check completed" };
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region ../../src/api-parameters/schedulers/ConfigActivationScheduler.ts
|
|
627
|
+
/**
|
|
628
|
+
* Scheduler that periodically checks for scheduled configurations
|
|
629
|
+
* that should be activated.
|
|
630
|
+
*
|
|
631
|
+
* Runs every minute to check if any NEXT configurations have reached
|
|
632
|
+
* their activation date and need to be promoted to CURRENT.
|
|
633
|
+
*/
|
|
634
|
+
var ConfigActivationScheduler = class {
|
|
635
|
+
log = $logger();
|
|
636
|
+
store = $inject(ConfigStore);
|
|
637
|
+
/**
|
|
638
|
+
* Check for scheduled configurations every minute.
|
|
639
|
+
*/
|
|
640
|
+
checkActivations = $scheduler({
|
|
641
|
+
name: "config-activation-check",
|
|
642
|
+
description: "Checks for scheduled configurations that should be activated",
|
|
643
|
+
interval: [1, "minute"],
|
|
644
|
+
lock: true,
|
|
645
|
+
handler: async () => {
|
|
646
|
+
this.log.debug("Checking for scheduled config activations");
|
|
647
|
+
await this.store.activateScheduledConfigs();
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
};
|
|
651
|
+
|
|
20
652
|
//#endregion
|
|
21
653
|
//#region ../../src/api-parameters/primitives/$config.ts
|
|
22
654
|
var ConfigPrimitive = class extends Primitive {
|
|
655
|
+
log = $logger();
|
|
656
|
+
store = $inject(ConfigStore);
|
|
657
|
+
/** Internal atom key for state management */
|
|
658
|
+
atomKey;
|
|
659
|
+
/** Schema hash for migration detection */
|
|
660
|
+
schemaHash;
|
|
661
|
+
/** Whether we're currently syncing (to avoid loops) */
|
|
662
|
+
syncing = false;
|
|
663
|
+
/** Whether initial load has completed */
|
|
664
|
+
loaded = false;
|
|
665
|
+
/**
|
|
666
|
+
* Configuration name (uses property key if not specified).
|
|
667
|
+
*/
|
|
23
668
|
get name() {
|
|
24
669
|
return this.options.name || this.config.propertyKey;
|
|
25
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* The TypeBox schema for this configuration.
|
|
673
|
+
*/
|
|
26
674
|
get schema() {
|
|
27
675
|
return this.options.schema;
|
|
28
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Get the current configuration value.
|
|
679
|
+
*/
|
|
29
680
|
get current() {
|
|
30
|
-
return this.options.default;
|
|
681
|
+
return this.alepha.store.get(this.atomKey) ?? this.options.default;
|
|
31
682
|
}
|
|
32
|
-
|
|
683
|
+
/**
|
|
684
|
+
* Get a specific field from the current configuration.
|
|
685
|
+
*/
|
|
33
686
|
get(key) {
|
|
34
687
|
return this.current[key];
|
|
35
688
|
}
|
|
36
689
|
/**
|
|
37
|
-
*
|
|
690
|
+
* Set a new configuration value.
|
|
691
|
+
*
|
|
692
|
+
* @param value - The new configuration value
|
|
693
|
+
* @param options - Optional settings (activation date, creator info, etc.)
|
|
694
|
+
*/
|
|
695
|
+
async set(value, options = {}) {
|
|
696
|
+
await this.store.save(this.name, value, this.schemaHash, {
|
|
697
|
+
activationDate: options.activationDate,
|
|
698
|
+
changeDescription: options.changeDescription,
|
|
699
|
+
tags: options.tags,
|
|
700
|
+
creatorId: options.user?.id,
|
|
701
|
+
creatorName: options.user?.name ?? options.user?.email
|
|
702
|
+
});
|
|
703
|
+
const now = /* @__PURE__ */ new Date();
|
|
704
|
+
if (!options.activationDate || options.activationDate <= now) {
|
|
705
|
+
this.syncing = true;
|
|
706
|
+
try {
|
|
707
|
+
this.alepha.store.set(this.atomKey, value);
|
|
708
|
+
} finally {
|
|
709
|
+
this.syncing = false;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Subscribe to configuration changes.
|
|
715
|
+
*/
|
|
716
|
+
sub(fn) {
|
|
717
|
+
return this.alepha.events.on("state:mutate", { callback: ({ key, value }) => {
|
|
718
|
+
if (key === this.atomKey) fn(value);
|
|
719
|
+
} });
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Reload configuration from database.
|
|
723
|
+
* Called when scheduled config activates or sync message received.
|
|
38
724
|
*/
|
|
39
|
-
async
|
|
40
|
-
|
|
725
|
+
async reload() {
|
|
726
|
+
const value = await this.store.load(this.name);
|
|
727
|
+
if (value !== null) {
|
|
728
|
+
this.syncing = true;
|
|
729
|
+
try {
|
|
730
|
+
this.alepha.store.set(this.atomKey, value, { skipEvents: true });
|
|
731
|
+
} finally {
|
|
732
|
+
this.syncing = false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Update from sync message (called by ConfigStore).
|
|
738
|
+
* Uses skipEvents to avoid infinite loops.
|
|
739
|
+
*/
|
|
740
|
+
async updateFromSync(content) {
|
|
741
|
+
this.syncing = true;
|
|
742
|
+
try {
|
|
743
|
+
this.alepha.store.set(this.atomKey, content, { skipEvents: true });
|
|
744
|
+
} finally {
|
|
745
|
+
this.syncing = false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Get version history for this configuration.
|
|
750
|
+
*/
|
|
751
|
+
async getHistory() {
|
|
752
|
+
return this.store.getHistory(this.name);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Rollback to a specific version.
|
|
756
|
+
*/
|
|
757
|
+
async rollback(version, options) {
|
|
758
|
+
await this.store.rollback(this.name, version, {
|
|
759
|
+
changeDescription: options?.changeDescription,
|
|
760
|
+
creatorId: options?.user?.id,
|
|
761
|
+
creatorName: options?.user?.name ?? options?.user?.email
|
|
762
|
+
});
|
|
763
|
+
await this.reload();
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Hook to load initial value from database on start.
|
|
767
|
+
*/
|
|
768
|
+
onStart = $hook({
|
|
769
|
+
on: "start",
|
|
770
|
+
handler: async () => {
|
|
771
|
+
await this.loadInitial();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
/**
|
|
775
|
+
* Called after primitive creation to initialize.
|
|
776
|
+
*/
|
|
777
|
+
onInit() {
|
|
778
|
+
this.atomKey = `config:${this.name}`;
|
|
779
|
+
this.schemaHash = this.calculateSchemaHash();
|
|
780
|
+
this.store.register(this);
|
|
781
|
+
this.alepha.store.set(this.atomKey, this.options.default, { skipEvents: true });
|
|
782
|
+
this.alepha.events.on("state:mutate", {
|
|
783
|
+
caller: this.config.service,
|
|
784
|
+
callback: async ({ key, value, prevValue }) => {
|
|
785
|
+
if (key !== this.atomKey) return;
|
|
786
|
+
if (this.syncing) return;
|
|
787
|
+
if (JSON.stringify(value) === JSON.stringify(prevValue)) return;
|
|
788
|
+
this.log.debug("Config state mutated, persisting to database", { name: this.name });
|
|
789
|
+
await this.store.save(this.name, value, this.schemaHash);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Load initial value from database.
|
|
795
|
+
*/
|
|
796
|
+
async loadInitial() {
|
|
797
|
+
if (this.loaded) return;
|
|
798
|
+
const value = await this.store.load(this.name);
|
|
799
|
+
if (value !== null) {
|
|
800
|
+
this.syncing = true;
|
|
801
|
+
try {
|
|
802
|
+
this.alepha.store.set(this.atomKey, value, { skipEvents: true });
|
|
803
|
+
} finally {
|
|
804
|
+
this.syncing = false;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
this.loaded = true;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Calculate a hash of the schema for migration detection.
|
|
811
|
+
*/
|
|
812
|
+
calculateSchemaHash() {
|
|
813
|
+
const schemaJson = JSON.stringify(this.options.schema);
|
|
814
|
+
let hash = 0;
|
|
815
|
+
for (let i = 0; i < schemaJson.length; i++) {
|
|
816
|
+
const char = schemaJson.charCodeAt(i);
|
|
817
|
+
hash = (hash << 5) - hash + char;
|
|
818
|
+
hash = hash & hash;
|
|
819
|
+
}
|
|
820
|
+
return hash.toString(16);
|
|
821
|
+
}
|
|
41
822
|
};
|
|
42
823
|
const $config = (options) => {
|
|
43
824
|
return createPrimitive(ConfigPrimitive, options);
|
|
44
825
|
};
|
|
826
|
+
$config[KIND] = ConfigPrimitive;
|
|
45
827
|
|
|
46
828
|
//#endregion
|
|
47
829
|
//#region ../../src/api-parameters/index.ts
|
|
48
830
|
/**
|
|
49
|
-
* Provides
|
|
831
|
+
* Provides versioned configuration management for Alepha applications.
|
|
832
|
+
*
|
|
833
|
+
* Features:
|
|
834
|
+
* - Type-safe, versioned configuration with `$config` primitive
|
|
835
|
+
* - Schema validation with auto-migration detection
|
|
836
|
+
* - Scheduled activation (FUTURE, NEXT, CURRENT, EXPIRED statuses)
|
|
837
|
+
* - PostgreSQL persistence with full version history
|
|
838
|
+
* - Cross-instance synchronization via topic
|
|
839
|
+
* - Tree view support via dot-notation naming
|
|
840
|
+
* - REST API for configuration management
|
|
841
|
+
* - Automatic activation scheduler
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```ts
|
|
845
|
+
* import { Alepha } from "alepha";
|
|
846
|
+
* import { AlephaApiParameters } from "alepha/api-parameters";
|
|
847
|
+
*
|
|
848
|
+
* const alepha = Alepha.create();
|
|
849
|
+
* alepha.with(AlephaApiParameters);
|
|
50
850
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
851
|
+
* // Then use $config in your services:
|
|
852
|
+
* class AppConfig {
|
|
853
|
+
* features = $config({
|
|
854
|
+
* name: "app.features.flags",
|
|
855
|
+
* schema: t.object({
|
|
856
|
+
* enableBeta: t.boolean(),
|
|
857
|
+
* maxUploadSize: t.number()
|
|
858
|
+
* }),
|
|
859
|
+
* default: { enableBeta: false, maxUploadSize: 10485760 }
|
|
860
|
+
* });
|
|
861
|
+
* }
|
|
862
|
+
* ```
|
|
53
863
|
*
|
|
54
864
|
* @module alepha.api.parameters
|
|
55
865
|
*/
|
|
56
866
|
const AlephaApiParameters = $module({
|
|
57
867
|
name: "alepha.api.parameters",
|
|
58
|
-
services: [
|
|
868
|
+
services: [
|
|
869
|
+
ConfigStore,
|
|
870
|
+
ConfigController,
|
|
871
|
+
ConfigActivationScheduler
|
|
872
|
+
]
|
|
59
873
|
});
|
|
60
874
|
|
|
61
875
|
//#endregion
|
|
62
|
-
export { $config, AlephaApiParameters, ConfigPrimitive, parameters };
|
|
876
|
+
export { $config, AlephaApiParameters, ConfigActivationScheduler, ConfigController, ConfigPrimitive, ConfigStore, parameters };
|
|
63
877
|
//# sourceMappingURL=index.js.map
|