@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.
- package/LICENSE.md +21 -21
- package/README.md +66 -66
- package/lib/CHANGELOG.md +774 -774
- package/lib/README.md +66 -66
- package/lib/cjs/application/application-factory.js +6 -0
- package/lib/cjs/application/bootstrap.js +117 -213
- package/lib/cjs/config/define-config.js +1 -1
- package/lib/cjs/config/env-field-builders.js +47 -0
- package/lib/cjs/config/index.js +7 -1
- package/lib/cjs/framework-version.js +10 -0
- package/lib/cjs/lazy-loading/index.js +5 -1
- package/lib/cjs/lazy-loading/lazy-module-helpers.js +49 -0
- package/lib/cjs/middleware/index.js +8 -9
- package/lib/cjs/middleware/middleware-service.js +68 -12
- package/lib/cjs/middleware/presets-standalone.js +93 -0
- package/lib/cjs/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
- package/lib/cjs/provider/db-in-memory/index.js +11 -1
- package/lib/cjs/provider/db-in-memory/query/query-engine.js +28 -0
- package/lib/cjs/provider/db-in-memory/schema/decorators.js +18 -0
- package/lib/cjs/provider/db-in-memory/storage/index.js +3 -1
- package/lib/cjs/provider/db-in-memory/storage/memory-store.js +72 -1
- package/lib/cjs/provider/logger/logger.banner.js +40 -31
- package/lib/cjs/provider/logger/logger.config.js +11 -1
- package/lib/cjs/provider/logger/logger.formatter.js +22 -1
- package/lib/cjs/provider/logger/logger.provider.js +59 -9
- package/lib/cjs/provider/logger/transports/console.transport.js +69 -6
- package/lib/cjs/provider/logger/transports/file.transport.js +27 -18
- package/lib/cjs/provider/logger/utils/log-levels.js +6 -5
- package/lib/cjs/provider/validation/adapters/index.js +12 -5
- package/lib/cjs/provider/validation/adapters/yup.adapter.js +118 -0
- package/lib/cjs/provider/validation/adapters/zod.adapter.js +137 -0
- package/lib/cjs/provider/validation/index.js +5 -1
- package/lib/cjs/render/adapters/react-adapter.js +14 -14
- package/lib/cjs/render/features/type-generator.js +30 -30
- package/lib/cjs/render/features/view-debugger.js +75 -55
- package/lib/cjs/testing/fluent-request.js +7 -0
- package/lib/cjs/testing/snapshot-request.js +2 -0
- package/lib/cjs/types/application/application-factory.d.ts +6 -0
- package/lib/cjs/types/application/bootstrap.d.ts +196 -24
- package/lib/cjs/types/config/config.interfaces.d.ts +7 -1
- package/lib/cjs/types/config/env-field-builders.d.ts +39 -0
- package/lib/cjs/types/config/index.d.ts +1 -1
- package/lib/cjs/types/framework-version.d.ts +7 -0
- package/lib/cjs/types/lazy-loading/index.d.ts +1 -0
- package/lib/cjs/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
- package/lib/cjs/types/middleware/index.d.ts +1 -1
- package/lib/cjs/types/middleware/middleware-service.d.ts +21 -0
- package/lib/cjs/types/middleware/presets-standalone.d.ts +75 -0
- package/lib/cjs/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
- package/lib/cjs/types/provider/db-in-memory/index.d.ts +9 -1
- package/lib/cjs/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
- package/lib/cjs/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
- package/lib/cjs/types/provider/db-in-memory/storage/index.d.ts +1 -1
- package/lib/cjs/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
- package/lib/cjs/types/provider/logger/logger.banner.d.ts +1 -1
- package/lib/cjs/types/provider/logger/logger.config.d.ts +7 -0
- package/lib/cjs/types/provider/logger/logger.formatter.d.ts +10 -0
- package/lib/cjs/types/provider/logger/logger.provider.d.ts +32 -1
- package/lib/cjs/types/provider/logger/transports/console.transport.d.ts +7 -0
- package/lib/cjs/types/provider/logger/utils/log-levels.d.ts +3 -3
- package/lib/cjs/types/provider/validation/adapters/index.d.ts +7 -4
- package/lib/cjs/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
- package/lib/cjs/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
- package/lib/cjs/types/provider/validation/index.d.ts +1 -1
- package/lib/cjs/types/render/features/view-debugger.d.ts +10 -0
- package/lib/cjs/types/testing/testing.interfaces.d.ts +31 -6
- package/lib/esm/application/application-factory.js +6 -0
- package/lib/esm/application/bootstrap.js +117 -213
- package/lib/esm/config/define-config.js +1 -1
- package/lib/esm/config/env-field-builders.js +48 -0
- package/lib/esm/config/index.js +6 -1
- package/lib/esm/framework-version.js +7 -0
- package/lib/esm/lazy-loading/index.js +2 -0
- package/lib/esm/lazy-loading/lazy-module-helpers.js +45 -0
- package/lib/esm/middleware/index.js +3 -2
- package/lib/esm/middleware/middleware-service.js +68 -12
- package/lib/esm/middleware/presets-standalone.js +87 -0
- package/lib/esm/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
- package/lib/esm/provider/db-in-memory/index.js +9 -1
- package/lib/esm/provider/db-in-memory/query/query-engine.js +28 -0
- package/lib/esm/provider/db-in-memory/schema/decorators.js +18 -0
- package/lib/esm/provider/db-in-memory/storage/index.js +1 -1
- package/lib/esm/provider/db-in-memory/storage/memory-store.js +75 -0
- package/lib/esm/provider/logger/logger.banner.js +40 -31
- package/lib/esm/provider/logger/logger.config.js +12 -2
- package/lib/esm/provider/logger/logger.formatter.js +22 -1
- package/lib/esm/provider/logger/logger.provider.js +61 -10
- package/lib/esm/provider/logger/transports/console.transport.js +69 -6
- package/lib/esm/provider/logger/transports/file.transport.js +27 -18
- package/lib/esm/provider/logger/utils/log-levels.js +6 -5
- package/lib/esm/provider/validation/adapters/index.js +7 -4
- package/lib/esm/provider/validation/adapters/yup.adapter.js +111 -0
- package/lib/esm/provider/validation/adapters/zod.adapter.js +130 -0
- package/lib/esm/provider/validation/index.js +1 -1
- package/lib/esm/render/adapters/react-adapter.js +14 -14
- package/lib/esm/render/features/type-generator.js +30 -30
- package/lib/esm/render/features/view-debugger.js +75 -55
- package/lib/esm/testing/fluent-request.js +7 -0
- package/lib/esm/testing/snapshot-request.js +2 -0
- package/lib/esm/types/application/application-factory.d.ts +6 -0
- package/lib/esm/types/application/bootstrap.d.ts +196 -24
- package/lib/esm/types/config/config.interfaces.d.ts +7 -1
- package/lib/esm/types/config/env-field-builders.d.ts +39 -0
- package/lib/esm/types/config/index.d.ts +1 -1
- package/lib/esm/types/framework-version.d.ts +7 -0
- package/lib/esm/types/lazy-loading/index.d.ts +1 -0
- package/lib/esm/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
- package/lib/esm/types/middleware/index.d.ts +1 -1
- package/lib/esm/types/middleware/middleware-service.d.ts +21 -0
- package/lib/esm/types/middleware/presets-standalone.d.ts +75 -0
- package/lib/esm/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
- package/lib/esm/types/provider/db-in-memory/index.d.ts +9 -1
- package/lib/esm/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
- package/lib/esm/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
- package/lib/esm/types/provider/db-in-memory/storage/index.d.ts +1 -1
- package/lib/esm/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
- package/lib/esm/types/provider/logger/logger.banner.d.ts +1 -1
- package/lib/esm/types/provider/logger/logger.config.d.ts +7 -0
- package/lib/esm/types/provider/logger/logger.formatter.d.ts +10 -0
- package/lib/esm/types/provider/logger/logger.provider.d.ts +32 -1
- package/lib/esm/types/provider/logger/transports/console.transport.d.ts +7 -0
- package/lib/esm/types/provider/logger/utils/log-levels.d.ts +3 -3
- package/lib/esm/types/provider/validation/adapters/index.d.ts +7 -4
- package/lib/esm/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
- package/lib/esm/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
- package/lib/esm/types/provider/validation/index.d.ts +1 -1
- package/lib/esm/types/render/features/view-debugger.d.ts +10 -0
- package/lib/esm/types/testing/testing.interfaces.d.ts +31 -6
- package/lib/package.json +23 -8
- package/package.json +23 -8
- package/lib/cjs/middleware/middleware-presets.js +0 -294
- package/lib/cjs/types/middleware/middleware-presets.d.ts +0 -90
- package/lib/esm/middleware/middleware-presets.js +0 -286
- 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 &&
|
|
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
|
-
|
|
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
|
|
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:
|
|
1554
|
+
parse: {
|
|
1555
|
+
json: { limit: "10mb" },
|
|
1556
|
+
urlencoded: { extended: true, limit: "10mb" },
|
|
1557
|
+
},
|
|
1518
1558
|
logger: { implementation: "auto" },
|
|
1519
1559
|
security: "api",
|
|
1520
|
-
compress:
|
|
1560
|
+
compress: { level: 6 },
|
|
1521
1561
|
},
|
|
1522
1562
|
web: {
|
|
1523
|
-
parse: {
|
|
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: {
|
|
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: {
|
|
1535
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
66
|
+
* Format a timestamp for structured log output.
|
|
66
67
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 =
|
|
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,
|
|
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(
|
|
395
|
-
|
|
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(
|
|
414
|
+
writeStdout(logLine(`Controllers: ${metrics.controllers} | Providers: ${metrics.providers} | Routes: ${metrics.routes}`));
|
|
404
415
|
}
|
|
405
|
-
writeStdout(
|
|
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
|
|
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
|