alepha 0.13.5 → 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.
Files changed (136) hide show
  1. package/dist/api-audits/index.browser.js +116 -0
  2. package/dist/api-audits/index.browser.js.map +1 -0
  3. package/dist/api-audits/index.d.ts +1194 -0
  4. package/dist/api-audits/index.js +674 -0
  5. package/dist/api-audits/index.js.map +1 -0
  6. package/dist/api-notifications/index.d.ts +147 -147
  7. package/dist/api-parameters/index.browser.js +36 -5
  8. package/dist/api-parameters/index.browser.js.map +1 -1
  9. package/dist/api-parameters/index.d.ts +711 -33
  10. package/dist/api-parameters/index.js +831 -17
  11. package/dist/api-parameters/index.js.map +1 -1
  12. package/dist/api-users/index.d.ts +16 -3
  13. package/dist/api-users/index.js +699 -19
  14. package/dist/api-users/index.js.map +1 -1
  15. package/dist/api-verifications/index.js +2 -1
  16. package/dist/api-verifications/index.js.map +1 -1
  17. package/dist/bin/index.js +1 -0
  18. package/dist/bin/index.js.map +1 -1
  19. package/dist/cli/index.d.ts +85 -31
  20. package/dist/cli/index.js +205 -33
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/command/index.d.ts +67 -6
  23. package/dist/command/index.js +30 -3
  24. package/dist/command/index.js.map +1 -1
  25. package/dist/core/index.browser.js +241 -61
  26. package/dist/core/index.browser.js.map +1 -1
  27. package/dist/core/index.d.ts +170 -90
  28. package/dist/core/index.js +264 -67
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.native.js +248 -65
  31. package/dist/core/index.native.js.map +1 -1
  32. package/dist/email/index.js +15 -10554
  33. package/dist/email/index.js.map +1 -1
  34. package/dist/logger/index.d.ts +4 -4
  35. package/dist/logger/index.js +77 -72
  36. package/dist/logger/index.js.map +1 -1
  37. package/dist/orm/index.d.ts +5 -1
  38. package/dist/orm/index.js +24 -7
  39. package/dist/orm/index.js.map +1 -1
  40. package/dist/queue/index.d.ts +4 -4
  41. package/dist/redis/index.d.ts +10 -10
  42. package/dist/security/index.d.ts +28 -28
  43. package/dist/server/index.d.ts +10 -1
  44. package/dist/server/index.js +20 -6
  45. package/dist/server/index.js.map +1 -1
  46. package/dist/server-auth/index.d.ts +163 -152
  47. package/dist/server-auth/index.js +40 -10
  48. package/dist/server-auth/index.js.map +1 -1
  49. package/dist/server-cookies/index.js +5 -1
  50. package/dist/server-cookies/index.js.map +1 -1
  51. package/dist/server-links/index.d.ts +33 -33
  52. package/dist/server-security/index.d.ts +9 -9
  53. package/dist/thread/index.js +2 -2
  54. package/dist/thread/index.js.map +1 -1
  55. package/dist/vite/index.d.ts +2 -2
  56. package/dist/vite/index.js +102 -45
  57. package/dist/vite/index.js.map +1 -1
  58. package/dist/websocket/index.browser.js +3 -3
  59. package/dist/websocket/index.browser.js.map +1 -1
  60. package/dist/websocket/index.d.ts +7 -7
  61. package/dist/websocket/index.js +4 -4
  62. package/dist/websocket/index.js.map +1 -1
  63. package/package.json +14 -9
  64. package/src/api-audits/controllers/AuditController.ts +186 -0
  65. package/src/api-audits/entities/audits.ts +132 -0
  66. package/src/api-audits/index.browser.ts +18 -0
  67. package/src/api-audits/index.ts +58 -0
  68. package/src/api-audits/primitives/$audit.ts +159 -0
  69. package/src/api-audits/schemas/auditQuerySchema.ts +23 -0
  70. package/src/api-audits/schemas/auditResourceSchema.ts +9 -0
  71. package/src/api-audits/schemas/createAuditSchema.ts +27 -0
  72. package/src/api-audits/services/AuditService.ts +412 -0
  73. package/src/api-parameters/controllers/ConfigController.ts +324 -0
  74. package/src/api-parameters/entities/parameters.ts +93 -10
  75. package/src/api-parameters/index.ts +43 -4
  76. package/src/api-parameters/primitives/$config.ts +291 -19
  77. package/src/api-parameters/schedulers/ConfigActivationScheduler.ts +30 -0
  78. package/src/api-parameters/services/ConfigStore.ts +491 -0
  79. package/src/api-users/atoms/realmAuthSettingsAtom.ts +19 -0
  80. package/src/api-users/controllers/UserRealmController.ts +0 -2
  81. package/src/api-users/index.ts +2 -0
  82. package/src/api-users/primitives/$userRealm.ts +18 -3
  83. package/src/api-users/providers/UserRealmProvider.ts +6 -3
  84. package/src/api-users/services/RegistrationService.ts +2 -1
  85. package/src/api-users/services/SessionService.ts +4 -0
  86. package/src/api-users/services/UserService.ts +3 -0
  87. package/src/api-verifications/index.ts +7 -1
  88. package/src/bin/index.ts +1 -0
  89. package/src/cli/assets/biomeJson.ts +1 -1
  90. package/src/cli/assets/dummySpecTs.ts +7 -0
  91. package/src/cli/assets/editorconfig.ts +13 -0
  92. package/src/cli/assets/mainTs.ts +14 -0
  93. package/src/cli/commands/BiomeCommands.ts +2 -0
  94. package/src/cli/commands/CoreCommands.ts +28 -9
  95. package/src/cli/commands/VerifyCommands.ts +2 -1
  96. package/src/cli/commands/ViteCommands.ts +8 -9
  97. package/src/cli/services/AlephaCliUtils.ts +214 -23
  98. package/src/command/helpers/Asker.ts +0 -1
  99. package/src/command/primitives/$command.ts +67 -0
  100. package/src/command/providers/CliProvider.ts +39 -8
  101. package/src/core/Alepha.ts +40 -30
  102. package/src/core/helpers/jsonSchemaToTypeBox.ts +307 -0
  103. package/src/core/index.shared.ts +1 -0
  104. package/src/core/index.ts +30 -3
  105. package/src/core/providers/EventManager.ts +1 -1
  106. package/src/core/providers/StateManager.ts +23 -12
  107. package/src/core/providers/TypeProvider.ts +26 -34
  108. package/src/logger/index.ts +8 -6
  109. package/src/logger/primitives/$logger.ts +1 -1
  110. package/src/logger/providers/{SimpleFormatterProvider.ts → PrettyFormatterProvider.ts} +10 -1
  111. package/src/orm/index.ts +6 -0
  112. package/src/orm/services/PgRelationManager.ts +2 -2
  113. package/src/orm/services/PostgresModelBuilder.ts +11 -7
  114. package/src/orm/services/Repository.ts +16 -7
  115. package/src/orm/services/SqliteModelBuilder.ts +10 -0
  116. package/src/server/index.ts +6 -0
  117. package/src/server/primitives/$action.ts +10 -1
  118. package/src/server/providers/ServerBodyParserProvider.ts +11 -5
  119. package/src/server/providers/ServerRouterProvider.ts +13 -7
  120. package/src/server-auth/primitives/$auth.ts +7 -0
  121. package/src/server-auth/providers/ServerAuthProvider.ts +51 -8
  122. package/src/server-cookies/index.ts +2 -1
  123. package/src/thread/primitives/$thread.ts +2 -2
  124. package/src/vite/index.ts +0 -2
  125. package/src/vite/tasks/buildServer.ts +3 -4
  126. package/src/vite/tasks/generateCloudflare.ts +35 -19
  127. package/src/vite/tasks/generateDocker.ts +18 -4
  128. package/src/vite/tasks/generateSitemap.ts +5 -7
  129. package/src/vite/tasks/generateVercel.ts +76 -41
  130. package/src/vite/tasks/runAlepha.ts +16 -1
  131. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -11
  132. package/src/websocket/services/WebSocketClient.ts +3 -3
  133. package/dist/cli/dist-BlfFtOk2.js +0 -2770
  134. package/dist/cli/dist-BlfFtOk2.js.map +0 -1
  135. package/src/api-parameters/controllers/ParameterController.ts +0 -45
  136. package/src/api-parameters/services/ParameterStore.ts +0 -23
@@ -1,63 +1,877 @@
1
- import { $module, Primitive, createPrimitive, t } from "alepha";
2
- import { $entity, pg } from "alepha/orm";
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.string(),
25
+ name: t.text(),
12
26
  content: t.json(),
13
- tags: t.optional(t.array(t.string())),
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.string()),
16
- activationDate: t.datetime({ description: "Optional activation date. Default to now. Must be now or later." })
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
- get next() {}
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
- * Apply a new configuration object.
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 set(value, options) {}
40
- sub(fn) {}
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 parameter management API endpoints for Alepha applications.
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
- * This module includes configuration parameter storage, retrieval,
52
- * and dynamic application settings management.
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