@expressots/core 4.0.0-preview.1 → 4.0.0-preview.3

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 (134) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +66 -66
  3. package/lib/CHANGELOG.md +774 -774
  4. package/lib/README.md +66 -66
  5. package/lib/cjs/application/application-factory.js +6 -0
  6. package/lib/cjs/application/bootstrap.js +117 -213
  7. package/lib/cjs/config/define-config.js +1 -1
  8. package/lib/cjs/config/env-field-builders.js +47 -0
  9. package/lib/cjs/config/index.js +7 -1
  10. package/lib/cjs/framework-version.js +10 -0
  11. package/lib/cjs/lazy-loading/index.js +5 -1
  12. package/lib/cjs/lazy-loading/lazy-module-helpers.js +49 -0
  13. package/lib/cjs/middleware/index.js +8 -9
  14. package/lib/cjs/middleware/middleware-service.js +68 -12
  15. package/lib/cjs/middleware/presets-standalone.js +93 -0
  16. package/lib/cjs/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
  17. package/lib/cjs/provider/db-in-memory/index.js +11 -1
  18. package/lib/cjs/provider/db-in-memory/query/query-engine.js +28 -0
  19. package/lib/cjs/provider/db-in-memory/schema/decorators.js +18 -0
  20. package/lib/cjs/provider/db-in-memory/storage/index.js +3 -1
  21. package/lib/cjs/provider/db-in-memory/storage/memory-store.js +72 -1
  22. package/lib/cjs/provider/logger/logger.banner.js +40 -31
  23. package/lib/cjs/provider/logger/logger.config.js +11 -1
  24. package/lib/cjs/provider/logger/logger.formatter.js +22 -1
  25. package/lib/cjs/provider/logger/logger.provider.js +59 -9
  26. package/lib/cjs/provider/logger/transports/console.transport.js +69 -6
  27. package/lib/cjs/provider/logger/transports/file.transport.js +27 -18
  28. package/lib/cjs/provider/logger/utils/log-levels.js +6 -5
  29. package/lib/cjs/provider/validation/adapters/index.js +12 -5
  30. package/lib/cjs/provider/validation/adapters/yup.adapter.js +118 -0
  31. package/lib/cjs/provider/validation/adapters/zod.adapter.js +137 -0
  32. package/lib/cjs/provider/validation/index.js +5 -1
  33. package/lib/cjs/render/adapters/react-adapter.js +14 -14
  34. package/lib/cjs/render/features/type-generator.js +30 -30
  35. package/lib/cjs/render/features/view-debugger.js +75 -55
  36. package/lib/cjs/testing/fluent-request.js +7 -0
  37. package/lib/cjs/testing/snapshot-request.js +2 -0
  38. package/lib/cjs/types/application/application-factory.d.ts +6 -0
  39. package/lib/cjs/types/application/bootstrap.d.ts +196 -24
  40. package/lib/cjs/types/config/config.interfaces.d.ts +7 -1
  41. package/lib/cjs/types/config/env-field-builders.d.ts +39 -0
  42. package/lib/cjs/types/config/index.d.ts +1 -1
  43. package/lib/cjs/types/framework-version.d.ts +7 -0
  44. package/lib/cjs/types/lazy-loading/index.d.ts +1 -0
  45. package/lib/cjs/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
  46. package/lib/cjs/types/middleware/index.d.ts +1 -1
  47. package/lib/cjs/types/middleware/middleware-service.d.ts +21 -0
  48. package/lib/cjs/types/middleware/presets-standalone.d.ts +75 -0
  49. package/lib/cjs/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
  50. package/lib/cjs/types/provider/db-in-memory/index.d.ts +9 -1
  51. package/lib/cjs/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
  52. package/lib/cjs/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
  53. package/lib/cjs/types/provider/db-in-memory/storage/index.d.ts +1 -1
  54. package/lib/cjs/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
  55. package/lib/cjs/types/provider/logger/logger.banner.d.ts +1 -1
  56. package/lib/cjs/types/provider/logger/logger.config.d.ts +7 -0
  57. package/lib/cjs/types/provider/logger/logger.formatter.d.ts +10 -0
  58. package/lib/cjs/types/provider/logger/logger.provider.d.ts +32 -1
  59. package/lib/cjs/types/provider/logger/transports/console.transport.d.ts +7 -0
  60. package/lib/cjs/types/provider/logger/utils/log-levels.d.ts +3 -3
  61. package/lib/cjs/types/provider/validation/adapters/index.d.ts +7 -4
  62. package/lib/cjs/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
  63. package/lib/cjs/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
  64. package/lib/cjs/types/provider/validation/index.d.ts +1 -1
  65. package/lib/cjs/types/render/features/view-debugger.d.ts +10 -0
  66. package/lib/cjs/types/testing/testing.interfaces.d.ts +31 -6
  67. package/lib/esm/application/application-factory.js +6 -0
  68. package/lib/esm/application/bootstrap.js +117 -213
  69. package/lib/esm/config/define-config.js +1 -1
  70. package/lib/esm/config/env-field-builders.js +48 -0
  71. package/lib/esm/config/index.js +6 -1
  72. package/lib/esm/framework-version.js +7 -0
  73. package/lib/esm/lazy-loading/index.js +2 -0
  74. package/lib/esm/lazy-loading/lazy-module-helpers.js +45 -0
  75. package/lib/esm/middleware/index.js +3 -2
  76. package/lib/esm/middleware/middleware-service.js +68 -12
  77. package/lib/esm/middleware/presets-standalone.js +87 -0
  78. package/lib/esm/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
  79. package/lib/esm/provider/db-in-memory/index.js +9 -1
  80. package/lib/esm/provider/db-in-memory/query/query-engine.js +28 -0
  81. package/lib/esm/provider/db-in-memory/schema/decorators.js +18 -0
  82. package/lib/esm/provider/db-in-memory/storage/index.js +1 -1
  83. package/lib/esm/provider/db-in-memory/storage/memory-store.js +75 -0
  84. package/lib/esm/provider/logger/logger.banner.js +40 -31
  85. package/lib/esm/provider/logger/logger.config.js +12 -2
  86. package/lib/esm/provider/logger/logger.formatter.js +22 -1
  87. package/lib/esm/provider/logger/logger.provider.js +61 -10
  88. package/lib/esm/provider/logger/transports/console.transport.js +69 -6
  89. package/lib/esm/provider/logger/transports/file.transport.js +27 -18
  90. package/lib/esm/provider/logger/utils/log-levels.js +6 -5
  91. package/lib/esm/provider/validation/adapters/index.js +7 -4
  92. package/lib/esm/provider/validation/adapters/yup.adapter.js +111 -0
  93. package/lib/esm/provider/validation/adapters/zod.adapter.js +130 -0
  94. package/lib/esm/provider/validation/index.js +1 -1
  95. package/lib/esm/render/adapters/react-adapter.js +14 -14
  96. package/lib/esm/render/features/type-generator.js +30 -30
  97. package/lib/esm/render/features/view-debugger.js +75 -55
  98. package/lib/esm/testing/fluent-request.js +7 -0
  99. package/lib/esm/testing/snapshot-request.js +2 -0
  100. package/lib/esm/types/application/application-factory.d.ts +6 -0
  101. package/lib/esm/types/application/bootstrap.d.ts +196 -24
  102. package/lib/esm/types/config/config.interfaces.d.ts +7 -1
  103. package/lib/esm/types/config/env-field-builders.d.ts +39 -0
  104. package/lib/esm/types/config/index.d.ts +1 -1
  105. package/lib/esm/types/framework-version.d.ts +7 -0
  106. package/lib/esm/types/lazy-loading/index.d.ts +1 -0
  107. package/lib/esm/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
  108. package/lib/esm/types/middleware/index.d.ts +1 -1
  109. package/lib/esm/types/middleware/middleware-service.d.ts +21 -0
  110. package/lib/esm/types/middleware/presets-standalone.d.ts +75 -0
  111. package/lib/esm/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
  112. package/lib/esm/types/provider/db-in-memory/index.d.ts +9 -1
  113. package/lib/esm/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
  114. package/lib/esm/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
  115. package/lib/esm/types/provider/db-in-memory/storage/index.d.ts +1 -1
  116. package/lib/esm/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
  117. package/lib/esm/types/provider/logger/logger.banner.d.ts +1 -1
  118. package/lib/esm/types/provider/logger/logger.config.d.ts +7 -0
  119. package/lib/esm/types/provider/logger/logger.formatter.d.ts +10 -0
  120. package/lib/esm/types/provider/logger/logger.provider.d.ts +32 -1
  121. package/lib/esm/types/provider/logger/transports/console.transport.d.ts +7 -0
  122. package/lib/esm/types/provider/logger/utils/log-levels.d.ts +3 -3
  123. package/lib/esm/types/provider/validation/adapters/index.d.ts +7 -4
  124. package/lib/esm/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
  125. package/lib/esm/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
  126. package/lib/esm/types/provider/validation/index.d.ts +1 -1
  127. package/lib/esm/types/render/features/view-debugger.d.ts +10 -0
  128. package/lib/esm/types/testing/testing.interfaces.d.ts +31 -6
  129. package/lib/package.json +23 -8
  130. package/package.json +23 -8
  131. package/lib/cjs/middleware/middleware-presets.js +0 -294
  132. package/lib/cjs/types/middleware/middleware-presets.d.ts +0 -90
  133. package/lib/esm/middleware/middleware-presets.js +0 -286
  134. package/lib/esm/types/middleware/middleware-presets.d.ts +0 -90
@@ -221,6 +221,8 @@ export class Middleware {
221
221
  profilingEnabled = false;
222
222
  // v4: Custom presets storage
223
223
  customPresets = new Map();
224
+ // v4: Last applied preset info (for Studio integration)
225
+ _lastPreset = null;
224
226
  // v4: Middleware registry reference
225
227
  registry = getMiddlewareRegistry();
226
228
  // v4: Buffered startup logs (displayed after banner)
@@ -274,7 +276,9 @@ export class Middleware {
274
276
  * @returns The type of the middleware.
275
277
  */
276
278
  getMiddlewareType(middleware) {
277
- if (middleware && typeof middleware === "object" && "path" in middleware) {
279
+ if (middleware &&
280
+ typeof middleware === "object" &&
281
+ ("path" in middleware || "middlewares" in middleware)) {
278
282
  return MiddlewareType.Config;
279
283
  }
280
284
  if (typeof middleware === "function") {
@@ -337,7 +341,12 @@ export class Middleware {
337
341
  const middlewareType = this.getMiddlewareType(m.middleware);
338
342
  if (middlewareType === MiddlewareType.Config) {
339
343
  const config = m.middleware;
340
- return config.path || "ConfigMiddleware";
344
+ if (config.path)
345
+ return config.path;
346
+ const names = config.middlewares
347
+ .map((fn) => typeof fn === "function" ? fn.name : fn?.constructor?.name)
348
+ .filter((n) => n && n !== "anonymous" && n !== "");
349
+ return names.length > 0 ? names.join(", ") : "ConfigMiddleware";
341
350
  }
342
351
  else if (middlewareType === MiddlewareType.IExpressoMiddleware) {
343
352
  return m.middleware.constructor.name;
@@ -434,7 +443,11 @@ export class Middleware {
434
443
  this._logger.warn(`No middlewares in the route [${config.path}]. Skipping...`, "middleware-service");
435
444
  return;
436
445
  }
437
- const configKey = config.path || `config_${this.insertionOrder}`;
446
+ const inferredName = config.middlewares
447
+ .map((fn) => (typeof fn === "function" ? fn.name : fn?.constructor?.name))
448
+ .filter((n) => n && n !== "anonymous" && n !== "")
449
+ .join(", ");
450
+ const configKey = config.path || inferredName || `custom_${this.insertionOrder}`;
438
451
  if (this.middlewareExists(configKey)) {
439
452
  this._logger.warn(`[${config.path}] route already exists. Skipping...`, "middleware-service");
440
453
  return;
@@ -1206,6 +1219,7 @@ export class Middleware {
1206
1219
  origin: true,
1207
1220
  credentials: true,
1208
1221
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
1222
+ allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
1209
1223
  },
1210
1224
  rateLimit: { windowMs: 60000, max: 100 },
1211
1225
  },
@@ -1459,6 +1473,11 @@ export class Middleware {
1459
1473
  const finalConfig = overrides
1460
1474
  ? this.mergeConfigs(config, overrides)
1461
1475
  : config;
1476
+ this._lastPreset = {
1477
+ name: preset,
1478
+ hasOverrides: !!overrides,
1479
+ config: finalConfig,
1480
+ };
1462
1481
  // Apply each category
1463
1482
  if (finalConfig.parse) {
1464
1483
  if (typeof finalConfig.parse === "boolean") {
@@ -1508,31 +1527,62 @@ export class Middleware {
1508
1527
  const custom = Object.fromEntries(this.customPresets);
1509
1528
  return { ...builtIn, ...custom };
1510
1529
  }
1530
+ /**
1531
+ * Returns info about the last applied preset (name, whether overrides
1532
+ * were used, and the effective merged config). Used by the adapter to
1533
+ * forward preset metadata to Studio.
1534
+ */
1535
+ getLastAppliedPreset() {
1536
+ return this._lastPreset;
1537
+ }
1511
1538
  /**
1512
1539
  * Get built-in presets.
1540
+ *
1541
+ * Each preset is tuned for a specific workload:
1542
+ * - api: REST APIs (large payloads, rate-limited, strict CORS)
1543
+ * - web: traditional server-rendered apps (cookies, sessions, relaxed CORS)
1544
+ * - spa: single-page apps served with static fallback
1545
+ * - microservice: internal service-to-service (minimal surface, no security)
1546
+ * - graphql: single endpoint with large JSON payloads
1547
+ * - minimal: parsing only, no security or compression
1548
+ * - development: relaxed for local iteration, verbose logging
1549
+ * - production: hardened defaults for shipped deployments
1513
1550
  */
1514
1551
  getBuiltInPresets() {
1515
1552
  return {
1516
1553
  api: {
1517
- parse: true,
1554
+ parse: {
1555
+ json: { limit: "10mb" },
1556
+ urlencoded: { extended: true, limit: "10mb" },
1557
+ },
1518
1558
  logger: { implementation: "auto" },
1519
1559
  security: "api",
1520
- compress: true,
1560
+ compress: { level: 6 },
1521
1561
  },
1522
1562
  web: {
1523
- parse: { json: true, urlencoded: true, cookies: true },
1563
+ parse: {
1564
+ json: { limit: "5mb" },
1565
+ urlencoded: { extended: true, limit: "5mb" },
1566
+ cookies: true,
1567
+ },
1524
1568
  logger: { implementation: "auto" },
1525
1569
  security: "standard",
1526
1570
  compress: true,
1527
1571
  },
1528
1572
  spa: {
1529
- parse: { json: true, urlencoded: true },
1573
+ parse: {
1574
+ json: { limit: "5mb" },
1575
+ urlencoded: { extended: true, limit: "5mb" },
1576
+ },
1530
1577
  security: "standard",
1531
1578
  compress: true,
1532
1579
  },
1533
1580
  microservice: {
1534
- parse: { json: { limit: "1mb" } },
1535
- compress: true,
1581
+ parse: {
1582
+ json: { limit: "1mb" },
1583
+ urlencoded: { extended: false, limit: "1mb" },
1584
+ },
1585
+ compress: { level: 6 },
1536
1586
  },
1537
1587
  graphql: {
1538
1588
  parse: { json: { limit: "50mb" } },
@@ -1546,15 +1596,21 @@ export class Middleware {
1546
1596
  parse: true,
1547
1597
  },
1548
1598
  development: {
1549
- parse: true,
1599
+ parse: {
1600
+ json: { limit: "50mb" },
1601
+ urlencoded: { extended: true, limit: "50mb" },
1602
+ },
1550
1603
  logger: { implementation: "morgan", options: { format: "dev" } },
1551
1604
  security: "relaxed",
1552
1605
  },
1553
1606
  production: {
1554
- parse: true,
1607
+ parse: {
1608
+ json: { limit: "10mb" },
1609
+ urlencoded: { extended: true, limit: "10mb" },
1610
+ },
1555
1611
  logger: { implementation: "auto", disableInTest: true },
1556
1612
  security: "strict",
1557
- compress: true,
1613
+ compress: { level: 6 },
1558
1614
  },
1559
1615
  };
1560
1616
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Standalone (free-function) wrappers around the v4 middleware preset system.
3
+ *
4
+ * These exist alongside the `Middleware.definePreset` / `Middleware.applyPreset`
5
+ * instance methods so user code that doesn't have DI access to the `Middleware`
6
+ * provider — for example, declarative config files outside of the request
7
+ * lifecycle — can still register and apply presets.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { definePreset, applyPreset } from "@expressots/core";
12
+ *
13
+ * definePreset("my-api", {
14
+ * parse: { json: { limit: "1mb" } },
15
+ * security: { cors: { origin: "https://app.example.com" } },
16
+ * });
17
+ *
18
+ * // ...later, inside configureServices(), with the Middleware DI provider:
19
+ * applyPreset(this.services.middleware, "my-api");
20
+ * ```
21
+ *
22
+ * @public API
23
+ */
24
+ /** Module-level registry of presets defined via the standalone helper. */
25
+ const standalonePresets = new Map();
26
+ /**
27
+ * Register a custom middleware preset under the given name.
28
+ *
29
+ * The preset is stored in a module-level registry so it can be referenced
30
+ * later by `applyPreset(middleware, name)`. Calling `definePreset` with an
31
+ * existing name overwrites the previous definition.
32
+ *
33
+ * @param name unique identifier for the preset
34
+ * @param config v4 middleware config object describing the preset
35
+ *
36
+ * @public API
37
+ */
38
+ export function definePreset(name, config) {
39
+ standalonePresets.set(name, config);
40
+ }
41
+ /**
42
+ * Apply a previously defined preset (built-in or registered via
43
+ * `definePreset`) to the supplied `Middleware` instance.
44
+ *
45
+ * Resolution order:
46
+ * 1. Built-in v4 presets (`api`, `web`, `spa`, `microservice`, `graphql`,
47
+ * `minimal`, `development`, `production`) are matched by the
48
+ * `Middleware` instance itself.
49
+ * 2. Custom presets previously registered via `Middleware.definePreset`
50
+ * on the same instance.
51
+ * 3. Custom presets registered via the standalone `definePreset` here —
52
+ * these are forwarded to the instance on demand.
53
+ *
54
+ * @param middleware the active `Middleware` provider (typically resolved from
55
+ * the DI container inside `configureServices()`)
56
+ * @param name preset to apply
57
+ * @param overrides optional partial config that is merged on top of the
58
+ * preset before application
59
+ *
60
+ * @public API
61
+ */
62
+ export function applyPreset(middleware, name, overrides) {
63
+ const standalone = standalonePresets.get(name);
64
+ if (standalone !== undefined) {
65
+ middleware.definePreset(name, standalone);
66
+ }
67
+ middleware.applyPreset(name, overrides);
68
+ }
69
+ /**
70
+ * Returns the names of every preset registered through the standalone
71
+ * `definePreset` helper. Built-in presets and per-instance custom presets
72
+ * are NOT included — those live on the `Middleware` instance.
73
+ *
74
+ * @public API
75
+ */
76
+ export function getStandalonePresetNames() {
77
+ return Array.from(standalonePresets.keys());
78
+ }
79
+ /**
80
+ * Remove every preset registered through the standalone `definePreset`
81
+ * helper. Useful in tests; rarely needed at runtime.
82
+ *
83
+ * @public API
84
+ */
85
+ export function clearStandalonePresets() {
86
+ standalonePresets.clear();
87
+ }
@@ -383,6 +383,28 @@ export class InMemoryAdapter {
383
383
  where: { id: foreignKeyValue },
384
384
  });
385
385
  }
386
+ case "manyToMany": {
387
+ // Resolve through a join table. Convention: the `through` table holds
388
+ // link records with two foreign keys named `<sourceClass>Id` and
389
+ // `<targetClass>Id` (class names lowercased). For example, a
390
+ // Post <-> Tag relation through "post_tags" stores rows shaped like
391
+ // `{ postId, tagId }`.
392
+ if (!relation.through || !this.entityClass)
393
+ return [];
394
+ const sourceKey = `${this.entityClass.name.toLowerCase()}Id`;
395
+ const targetKey = `${relation.target().name.toLowerCase()}Id`;
396
+ const joinAdapter = this.database.table(relation.through);
397
+ const joinRecords = await joinAdapter.findMany({
398
+ where: { [sourceKey]: entity.id },
399
+ });
400
+ const targetIds = joinRecords
401
+ .map((record) => record[targetKey])
402
+ .filter((value) => typeof value === "string");
403
+ if (targetIds.length === 0)
404
+ return [];
405
+ const related = await Promise.all(targetIds.map((id) => relatedAdapter.findUnique({ where: { id } })));
406
+ return related.filter((item) => item !== null);
407
+ }
386
408
  default:
387
409
  return null;
388
410
  }
@@ -533,6 +555,7 @@ export class InMemoryDatabase {
533
555
  entityClass,
534
556
  timestamps: this.options.timestamps,
535
557
  softDelete: this.options.softDelete,
558
+ maxRecordsPerTable: this.options.maxRecordsPerTable,
536
559
  }));
537
560
  }
538
561
  return this.tables.get(tableName);
@@ -4,6 +4,14 @@
4
4
  * A high-performance, Prisma-compatible in-memory database
5
5
  * for ExpressoTS applications.
6
6
  *
7
+ * Scope: this database is intended for development, testing, and
8
+ * prototyping. Data lives in process memory (with optional file
9
+ * snapshots) and does not provide the crash safety, concurrency, or
10
+ * multi-process guarantees of a real database engine. It implements the
11
+ * universal `IDataAdapter` contract so it can be swapped for a
12
+ * production adapter (Prisma, TypeORM, etc.) without rewriting
13
+ * repositories.
14
+ *
7
15
  * Features:
8
16
  * - Prisma-like query API
9
17
  * - Type-safe queries with TypeScript
@@ -81,7 +89,7 @@ QueryEngine, } from "./query/index.js";
81
89
  // ═══════════════════════════════════════════════════════════════════════════
82
90
  // STORAGE
83
91
  // ═══════════════════════════════════════════════════════════════════════════
84
- export { MemoryStore, IndexManager, IdGenerator, UniqueConstraintError, EntityNotFoundError, EntityAlreadyExistsError, } from "./storage/index.js";
92
+ export { MemoryStore, IndexManager, IdGenerator, UniqueConstraintError, EntityNotFoundError, EntityAlreadyExistsError, MaxRecordsExceededError, EntityValidationError, } from "./storage/index.js";
85
93
  // Legacy provider (deprecated, use InMemoryDBProvider)
86
94
  export { InMemoryDataProvider, InMemoryDataTable, } from "./db-in-memory.provider.js";
87
95
  // Legacy errors (now exported from storage)
@@ -409,6 +409,33 @@ export class QueryEngine {
409
409
  return result;
410
410
  }
411
411
  // ═══════════════════════════════════════════════════════════════════════════
412
+ // CURSOR
413
+ // ═══════════════════════════════════════════════════════════════════════════
414
+ /**
415
+ * Apply cursor-based pagination. The cursor identifies a record (by id or
416
+ * any other field combination) within the ordered result set; the returned
417
+ * slice starts at that record. Combine with `skip` (typically `skip: 1` to
418
+ * exclude the cursor itself) and `take`.
419
+ *
420
+ * Should be applied after `orderBy` and before `skip`/`take`.
421
+ *
422
+ * @param entities - Ordered entities to slice
423
+ * @param cursor - Unique cursor identifying the start record
424
+ * @returns Entities starting at the cursor (empty array if not found)
425
+ */
426
+ executeCursor(entities, cursor) {
427
+ if (!cursor)
428
+ return entities;
429
+ const keys = Object.keys(cursor).filter((key) => cursor[key] !== undefined);
430
+ if (keys.length === 0)
431
+ return entities;
432
+ const index = entities.findIndex((entity) => keys.every((key) => entity[key] ===
433
+ cursor[key]));
434
+ if (index === -1)
435
+ return [];
436
+ return entities.slice(index);
437
+ }
438
+ // ═══════════════════════════════════════════════════════════════════════════
412
439
  // DISTINCT
413
440
  // ═══════════════════════════════════════════════════════════════════════════
414
441
  /**
@@ -561,6 +588,7 @@ export class QueryEngine {
561
588
  let entities = this.executeWhere(args.where);
562
589
  entities = this.executeDistinct(entities, args.distinct);
563
590
  entities = this.executeOrderBy(entities, args.orderBy);
591
+ entities = this.executeCursor(entities, args.cursor);
564
592
  entities = this.executePagination(entities, args.skip, args.take);
565
593
  if (args.select) {
566
594
  return this.executeSelect(entities, args.select);
@@ -376,6 +376,24 @@ export class SchemaRegistry {
376
376
  static getRelations(target) {
377
377
  return Reflect.getMetadata(DB_METADATA_KEYS.relation, target) || [];
378
378
  }
379
+ /**
380
+ * Get primary key field names for an entity.
381
+ * @param target - Entity class
382
+ * @returns Array of primary key field names
383
+ * @public API
384
+ */
385
+ static getPrimaryKeys(target) {
386
+ return Reflect.getMetadata(DB_METADATA_KEYS.primaryKey, target) || [];
387
+ }
388
+ /**
389
+ * Get nullable field names for an entity.
390
+ * @param target - Entity class
391
+ * @returns Array of nullable field names
392
+ * @public API
393
+ */
394
+ static getNullableFields(target) {
395
+ return Reflect.getMetadata(DB_METADATA_KEYS.nullable, target) || [];
396
+ }
379
397
  /**
380
398
  * Clear all registered entities (useful for testing).
381
399
  * @public API
@@ -2,4 +2,4 @@
2
2
  * Storage Module Exports
3
3
  * @module db-in-memory/storage
4
4
  */
5
- export { MemoryStore, IndexManager, IdGenerator, UniqueConstraintError, EntityNotFoundError, EntityAlreadyExistsError, } from "./memory-store.js";
5
+ export { MemoryStore, IndexManager, IdGenerator, UniqueConstraintError, EntityNotFoundError, EntityAlreadyExistsError, MaxRecordsExceededError, EntityValidationError, } from "./memory-store.js";
@@ -190,6 +190,35 @@ export class EntityNotFoundError extends Error {
190
190
  this.id = id;
191
191
  }
192
192
  }
193
+ /**
194
+ * Error thrown when a table reaches its configured record limit.
195
+ * @public API
196
+ */
197
+ export class MaxRecordsExceededError extends Error {
198
+ tableName;
199
+ limit;
200
+ constructor(tableName, limit) {
201
+ super(`Table '${tableName}' has reached its maximum of ${limit} records`);
202
+ this.name = "MaxRecordsExceededError";
203
+ this.tableName = tableName;
204
+ this.limit = limit;
205
+ }
206
+ }
207
+ /**
208
+ * Error thrown when entity validation fails (when `@Entity({ validate: true })`).
209
+ * @public API
210
+ */
211
+ export class EntityValidationError extends Error {
212
+ tableName;
213
+ field;
214
+ constructor(tableName, field, message) {
215
+ super(message ??
216
+ `Validation failed on '${tableName}': field '${field}' is required`);
217
+ this.name = "EntityValidationError";
218
+ this.tableName = tableName;
219
+ this.field = field;
220
+ }
221
+ }
193
222
  /**
194
223
  * Error thrown when an entity already exists.
195
224
  * @public API
@@ -291,11 +320,18 @@ export class MemoryStore {
291
320
  autoGenerateFields = {};
292
321
  /** Default values */
293
322
  defaultValues = {};
323
+ /** Maximum number of records allowed (0 = unlimited) */
324
+ maxRecords;
325
+ /** Enable runtime validation (from @Entity({ validate: true })) */
326
+ validate = false;
327
+ /** Fields that must be present and non-null when validation is enabled */
328
+ requiredFields = [];
294
329
  constructor(tableName, options = {}) {
295
330
  this.tableName = tableName;
296
331
  this.entityClass = options.entityClass;
297
332
  this.timestamps = options.timestamps ?? true;
298
333
  this.softDelete = options.softDelete ?? false;
334
+ this.maxRecords = options.maxRecordsPerTable ?? 0;
299
335
  // Load schema metadata if entity class is provided
300
336
  if (this.entityClass) {
301
337
  this.loadSchemaMetadata();
@@ -335,6 +371,37 @@ export class MemoryStore {
335
371
  if (entityMeta) {
336
372
  this.timestamps = entityMeta.timestamps;
337
373
  this.softDelete = entityMeta.softDelete;
374
+ this.validate = entityMeta.validate;
375
+ }
376
+ // When validation is enabled, treat primary-key and unique fields as
377
+ // required (must be present and non-null) unless explicitly @Nullable.
378
+ if (this.validate) {
379
+ const nullable = new Set(SchemaRegistry.getNullableFields(this.entityClass).map(String));
380
+ const required = new Set();
381
+ for (const field of SchemaRegistry.getPrimaryKeys(this.entityClass)) {
382
+ required.add(String(field));
383
+ }
384
+ for (const field of SchemaRegistry.getUniqueFields(this.entityClass)) {
385
+ required.add(String(field));
386
+ }
387
+ this.requiredFields = Array.from(required).filter((field) => !nullable.has(field));
388
+ }
389
+ }
390
+ /**
391
+ * Validate an entity against the schema (only when `validate` is enabled).
392
+ * Ensures required fields (primary key + unique, excluding @Nullable) are
393
+ * present and non-null.
394
+ * @private
395
+ * @throws ValidationError when a required field is missing or null
396
+ */
397
+ validateEntity(entity) {
398
+ if (!this.validate)
399
+ return;
400
+ for (const field of this.requiredFields) {
401
+ const value = entity[field];
402
+ if (value === undefined || value === null) {
403
+ throw new EntityValidationError(this.tableName, field);
404
+ }
338
405
  }
339
406
  }
340
407
  /**
@@ -384,6 +451,12 @@ export class MemoryStore {
384
451
  if (this.data.has(prepared.id)) {
385
452
  throw new EntityAlreadyExistsError(this.tableName, prepared.id);
386
453
  }
454
+ // Enforce per-table record limit (0 = unlimited)
455
+ if (this.maxRecords > 0 && this.data.size >= this.maxRecords) {
456
+ throw new MaxRecordsExceededError(this.tableName, this.maxRecords);
457
+ }
458
+ // Validate required fields (no-op unless @Entity({ validate: true }))
459
+ this.validateEntity(prepared);
387
460
  // Index first (will throw if unique constraint violated)
388
461
  this.indexManager.indexEntity(prepared);
389
462
  // Then store
@@ -430,6 +503,8 @@ export class MemoryStore {
430
503
  if (this.timestamps) {
431
504
  updated.updatedAt = new Date();
432
505
  }
506
+ // Validate required fields (no-op unless @Entity({ validate: true }))
507
+ this.validateEntity(updated);
433
508
  // Update indexes
434
509
  this.indexManager.updateIndex(existing, updated);
435
510
  // Store updated entity
@@ -1,5 +1,6 @@
1
1
  import { colorCodes } from "../../console/color-codes.js";
2
2
  import { formatMemory, } from "./logger.metrics.js";
3
+ import { FRAMEWORK_VERSION } from "../../framework-version.js";
3
4
  // eslint-disable-next-line no-control-regex
4
5
  const ANSI_STRIP_REGEX = /\x1b\[[0-9;]*m/g;
5
6
  // Helper to write to stdout - uses process.stdout directly to allow runtime interception
@@ -53,22 +54,41 @@ function getExpressoTSLogo() {
53
54
  const ts = normalizedTs[i];
54
55
  return `║${padLeft}${green}${exp}${reset}${white}${ts}${reset}${padRight}║`;
55
56
  });
56
- return `
57
- ${topBorder}
58
- ${emptyLine}
59
- ${contentLines.join("\n")}
60
- ${emptyLine}
61
- ${bottomBorder}
57
+ return `
58
+ ${topBorder}
59
+ ${emptyLine}
60
+ ${contentLines.join("\n")}
61
+ ${emptyLine}
62
+ ${bottomBorder}
62
63
  `.trim();
63
64
  }
64
65
  /**
65
- * Compact ASCII art logo.
66
+ * Format a timestamp for structured log output.
66
67
  */
67
- const EXPRESSOTS_LOGO_COMPACT = `
68
- ╔════════════════════════════════════════════════════════════════╗
69
- ║ ExpressoTS - Enterprise TypeScript Framework ║
70
- ╚════════════════════════════════════════════════════════════════╝
71
- `.trim();
68
+ function formatBannerTimestamp() {
69
+ const options = {
70
+ year: "numeric",
71
+ month: "2-digit",
72
+ day: "2-digit",
73
+ hour: "2-digit",
74
+ minute: "2-digit",
75
+ second: "2-digit",
76
+ };
77
+ return new Date().toLocaleString(undefined, options).replace(",", "");
78
+ }
79
+ /**
80
+ * Build a structured log line matching the standard ExpressoTS log format.
81
+ */
82
+ function logLine(message) {
83
+ const timestamp = formatBannerTimestamp();
84
+ const pid = process.pid;
85
+ return (colorText("[ExpressoTS]", "green") +
86
+ ` ${timestamp}` +
87
+ ` ${colorText(`[PID:${pid}]`, "green")}` +
88
+ ` ${colorText("INFO ", "green")}` +
89
+ ` ${colorText("[app]", "green")}` +
90
+ ` ${message}\n`);
91
+ }
72
92
  // Removed unused EXPRESSOTS_LOGO_MINIMAL constant
73
93
  /**
74
94
  * Color text helper.
@@ -117,7 +137,6 @@ export class BannerGenerator {
117
137
  const startupTime = Date.now() - this.startTime;
118
138
  const memoryUsage = process.memoryUsage().heapUsed;
119
139
  const memoryFormatted = formatMemory(memoryUsage);
120
- writeStdout("\n");
121
140
  // Display logo based on style
122
141
  switch (this.config.style) {
123
142
  case "full":
@@ -130,7 +149,6 @@ export class BannerGenerator {
130
149
  this.displayMinimalBanner(port, environment, appInfo);
131
150
  break;
132
151
  }
133
- writeStdout("\n");
134
152
  }
135
153
  /**
136
154
  * Display full banner with clean 3-column layout.
@@ -138,6 +156,7 @@ export class BannerGenerator {
138
156
  * Optional (user-enabled): Features, Middleware Pipeline, Provider Registry, Resources
139
157
  */
140
158
  displayFullBanner(port, environment, appInfo, metrics, features, config, startupTime, memoryFormatted, bannerData) {
159
+ writeStdout("\n");
141
160
  // Banner box width is ~93 chars, calculate column widths for 3 columns
142
161
  const totalWidth = 93;
143
162
  const colWidth = 29;
@@ -148,7 +167,7 @@ export class BannerGenerator {
148
167
  // ══════════════════════════════════════════════════════════════════════
149
168
  // Version info line (matches banner width)
150
169
  // ══════════════════════════════════════════════════════════════════════
151
- const frameworkVersion = "4.0.0-beta.1";
170
+ const frameworkVersion = FRAMEWORK_VERSION;
152
171
  const nodeVersion = process.version;
153
172
  const platform = process.platform;
154
173
  const appName = appInfo?.appName || "App";
@@ -384,27 +403,17 @@ export class BannerGenerator {
384
403
  writeStdout("\n");
385
404
  }
386
405
  /**
387
- * Display compact banner.
406
+ * Display compact banner using structured log format (cloud-friendly).
388
407
  */
389
- displayCompactBanner(port, environment, appInfo, metrics, features, startupTime, memoryFormatted) {
390
- writeStdout(colorText(EXPRESSOTS_LOGO_COMPACT, "green"));
391
- writeStdout("\n\n");
408
+ displayCompactBanner(port, environment, appInfo, metrics, _features, startupTime, memoryFormatted) {
392
409
  const appName = appInfo?.appName || "App";
393
410
  const appVersion = appInfo?.appVersion || "not provided";
394
- writeStdout(colorText(`ExpressoTS v4.0.0-beta.1`, "green") +
395
- colorText(` | ${appName} v${appVersion}`, "blue") +
396
- colorText(` | Node ${process.version}`, "blue") +
397
- "\n");
398
- writeStdout(colorText(`Environment: ${this.colorEnvironment(environment)}`, "yellow") +
399
- colorText(` | Port: ${port}`, "blue") +
400
- colorText(` | PID: ${process.pid}`, "blue") +
401
- "\n");
411
+ writeStdout(logLine(`ExpressoTS v${FRAMEWORK_VERSION} | ${appName} v${appVersion} | Node ${process.version}`));
412
+ writeStdout(logLine(`Environment: ${environment} | Port: ${port} | PID: ${process.pid}`));
402
413
  if (metrics) {
403
- writeStdout(colorText(`Controllers: ${metrics.controllers} | Providers: ${metrics.providers} | Routes: ${metrics.routes}`, "white") + "\n");
414
+ writeStdout(logLine(`Controllers: ${metrics.controllers} | Providers: ${metrics.providers} | Routes: ${metrics.routes}`));
404
415
  }
405
- writeStdout(colorText(`Startup: ${startupTime.toFixed(2)}ms`, "yellow") +
406
- colorText(` | Memory: ${memoryFormatted}`, "yellow") +
407
- "\n");
416
+ writeStdout(logLine(`Startup: ${startupTime.toFixed(2)}ms | Memory: ${memoryFormatted}`));
408
417
  }
409
418
  /**
410
419
  * Display minimal banner.
@@ -1,12 +1,22 @@
1
- import { LogLevel } from "./utils/log-levels.js";
1
+ import { LogLevel, parseLogLevel } from "./utils/log-levels.js";
2
2
  /**
3
3
  * Default logger configuration.
4
+ *
5
+ * Honours `process.env.LOG_LEVEL` when set so that any logs emitted
6
+ * BEFORE the application's `globalConfiguration()` runs (e.g. interceptor
7
+ * registration during container construction) are gated by the user's
8
+ * desired log level instead of the development default of `DEBUG`.
9
+ * Falls back to `DEBUG` in development and `INFO` in production.
10
+ *
4
11
  * @public API
5
12
  */
6
13
  export function getDefaultLoggerConfig() {
7
14
  const isDevelopment = process.env.NODE_ENV !== "production";
15
+ const envLevel = process.env.LOG_LEVEL;
16
+ const defaultLevel = isDevelopment ? LogLevel.DEBUG : LogLevel.INFO;
17
+ const level = envLevel ? parseLogLevel(envLevel) : defaultLevel;
8
18
  return {
9
- level: isDevelopment ? LogLevel.DEBUG : LogLevel.INFO,
19
+ level,
10
20
  transports: [], // Will be populated with ConsoleTransport by default
11
21
  filters: {},
12
22
  structured: !isDevelopment, // JSON in production, pretty in dev