exisjs 0.9.0 → 0.9.1

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 (77) hide show
  1. package/dist/database/lifecycle.d.ts +19 -2
  2. package/dist/database/lifecycle.d.ts.map +1 -1
  3. package/dist/database/lifecycle.js +92 -53
  4. package/dist/database/lifecycle.js.map +1 -1
  5. package/dist/decorators/transaction.d.ts +0 -21
  6. package/dist/decorators/transaction.d.ts.map +1 -1
  7. package/dist/decorators/transaction.js +52 -32
  8. package/dist/decorators/transaction.js.map +1 -1
  9. package/dist/docs/content/boundaries.mdx +2 -0
  10. package/dist/docs/content/config.mdx +1 -1
  11. package/dist/docs/content/decorators.mdx +1 -1
  12. package/dist/docs/content/middleware.mdx +4 -2
  13. package/dist/docs/content/requests.mdx +2 -0
  14. package/dist/docs/content/responses.mdx +54 -0
  15. package/dist/docs/content/validation.mdx +13 -0
  16. package/dist/docs/reference/README.md +16 -16
  17. package/dist/docs/reference/config.mdx +1 -1
  18. package/dist/docs/reference/database.mdx +3 -2
  19. package/dist/docs/reference/middleware.mdx +1 -1
  20. package/dist/docs/reference/validator.mdx +3 -0
  21. package/dist/middleware/compression.d.ts.map +1 -1
  22. package/dist/middleware/compression.js +6 -5
  23. package/dist/middleware/compression.js.map +1 -1
  24. package/dist/middleware/middleware.d.ts.map +1 -1
  25. package/dist/middleware/middleware.js +47 -34
  26. package/dist/middleware/middleware.js.map +1 -1
  27. package/dist/router/controller-registrar.d.ts.map +1 -1
  28. package/dist/router/controller-registrar.js +4 -7
  29. package/dist/router/controller-registrar.js.map +1 -1
  30. package/dist/router/router.d.ts +3 -0
  31. package/dist/router/router.d.ts.map +1 -1
  32. package/dist/router/router.js +40 -35
  33. package/dist/router/router.js.map +1 -1
  34. package/dist/router/scanner/boundary-loader.d.ts.map +1 -1
  35. package/dist/router/scanner/boundary-loader.js +25 -30
  36. package/dist/router/scanner/boundary-loader.js.map +1 -1
  37. package/dist/router/scanner/functional-compiler.d.ts.map +1 -1
  38. package/dist/router/scanner/functional-compiler.js +29 -29
  39. package/dist/router/scanner/functional-compiler.js.map +1 -1
  40. package/dist/server/app.d.ts.map +1 -1
  41. package/dist/server/app.js +5 -2
  42. package/dist/server/app.js.map +1 -1
  43. package/dist/server/helpers/body-parser.d.ts +1 -1
  44. package/dist/server/helpers/body-parser.d.ts.map +1 -1
  45. package/dist/server/helpers/body-parser.js +9 -7
  46. package/dist/server/helpers/body-parser.js.map +1 -1
  47. package/dist/server/helpers/conditional.d.ts +60 -0
  48. package/dist/server/helpers/conditional.d.ts.map +1 -0
  49. package/dist/server/helpers/conditional.js +189 -0
  50. package/dist/server/helpers/conditional.js.map +1 -0
  51. package/dist/server/helpers/etag.d.ts +0 -1
  52. package/dist/server/helpers/etag.d.ts.map +1 -1
  53. package/dist/server/helpers/etag.js +0 -10
  54. package/dist/server/helpers/etag.js.map +1 -1
  55. package/dist/server/helpers/index.d.ts +2 -1
  56. package/dist/server/helpers/index.d.ts.map +1 -1
  57. package/dist/server/helpers/index.js +16 -2
  58. package/dist/server/helpers/index.js.map +1 -1
  59. package/dist/server/request-handler.d.ts.map +1 -1
  60. package/dist/server/request-handler.js +12 -2
  61. package/dist/server/request-handler.js.map +1 -1
  62. package/dist/server/request.d.ts +11 -3
  63. package/dist/server/request.d.ts.map +1 -1
  64. package/dist/server/request.js +41 -83
  65. package/dist/server/request.js.map +1 -1
  66. package/dist/server/response.d.ts +43 -2
  67. package/dist/server/response.d.ts.map +1 -1
  68. package/dist/server/response.js +151 -70
  69. package/dist/server/response.js.map +1 -1
  70. package/dist/validator/builder.d.ts.map +1 -1
  71. package/dist/validator/builder.js +11 -17
  72. package/dist/validator/builder.js.map +1 -1
  73. package/dist/validator/engine.d.ts +19 -1
  74. package/dist/validator/engine.d.ts.map +1 -1
  75. package/dist/validator/engine.js +65 -29
  76. package/dist/validator/engine.js.map +1 -1
  77. package/package.json +3 -5
@@ -12,7 +12,14 @@ export interface DatabaseRegistration {
12
12
  disconnect: () => Promise<void> | void;
13
13
  isHealthy?: () => Promise<boolean | DatabaseHealthInfo> | boolean | DatabaseHealthInfo;
14
14
  healthCheck?: () => Promise<boolean | DatabaseHealthInfo> | boolean | DatabaseHealthInfo;
15
+ /**
16
+ * How long a health check may take before it counts as failed, so one
17
+ * unresponsive database cannot hang `/_ready`. Default: 5000 ms.
18
+ */
19
+ healthTimeoutMs?: number;
15
20
  }
21
+ /** Rejects when `promise` has not settled after `ms`. */
22
+ export declare function withTimeout<T>(promise: Promise<T> | T, ms: number, message: string): Promise<T>;
16
23
  /**
17
24
  * Universal Database Manager for ExisJS.
18
25
  * Manages connections, graceful shutdown, and health checks for any database
@@ -22,6 +29,7 @@ export declare class DatabaseManager {
22
29
  private static databases;
23
30
  private static isShutdownHookRegistered;
24
31
  private static appManagedShutdown;
32
+ private static healthInFlight?;
25
33
  /**
26
34
  * Called by the server when it starts listening. From then on the app's
27
35
  * graceful shutdown disconnects databases, after in-flight requests have
@@ -33,7 +41,10 @@ export declare class DatabaseManager {
33
41
  */
34
42
  static register(db: DatabaseRegistration): void;
35
43
  /**
36
- * Connects all registered databases that are not already connected.
44
+ * Connects all registered databases that are not already connected, in
45
+ * parallel: boot waits for the slowest database, not for the sum of all.
46
+ * If any fail, the error names every database that failed; the ones that
47
+ * connected stay connected, and a later call retries only the failed ones.
37
48
  */
38
49
  static connectAll(): Promise<void>;
39
50
  /**
@@ -41,12 +52,18 @@ export declare class DatabaseManager {
41
52
  */
42
53
  static disconnectAll(): Promise<void>;
43
54
  /**
44
- * Checks the health of all registered databases.
55
+ * Checks the health of all registered databases, in parallel. Each check
56
+ * has a timeout (`healthTimeoutMs`, default 5 s) so an unresponsive
57
+ * database is reported as an error instead of hanging the probe, and
58
+ * calls made while a check is running share its result, so a burst of
59
+ * probes costs one round of pings.
45
60
  */
46
61
  static checkHealth(): Promise<{
47
62
  healthy: boolean;
48
63
  databases: Record<string, DatabaseHealthInfo>;
49
64
  }>;
65
+ private static runHealthChecks;
66
+ private static checkOne;
50
67
  /**
51
68
  * Returns the number of registered databases.
52
69
  */
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/database/lifecycle.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC9B,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,OAAO,CAAA;AAEvD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,oBAAoB,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACnC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACtC,SAAS,CAAC,EAAE,MACV,OAAO,CAAC,OAAO,GAAG,kBAAkB,CAAC,GAAG,OAAO,GAAG,kBAAkB,CAAA;IACtE,WAAW,CAAC,EAAE,MACZ,OAAO,CAAC,OAAO,GAAG,kBAAkB,CAAC,GAAG,OAAO,GAAG,kBAAkB,CAAA;CACvE;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,SAAS,CAMrB;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB,CAAQ;IAC/C,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAQ;IAEzC;;;;OAIG;IACH,MAAM,CAAC,kBAAkB,IAAI,IAAI;IAIjC;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,oBAAoB,GAAG,IAAI;IAK/C;;OAEG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBxC;;OAEG;WACU,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3C;;OAEG;WACU,WAAW,IAAI,OAAO,CAAC;QAClC,OAAO,EAAE,OAAO,CAAA;QAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;KAC9C,CAAC;IAgDF;;OAEG;IACH,MAAM,KAAK,IAAI,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,IAAI,IAAI;IAKpB,OAAO,CAAC,MAAM,CAAC,mBAAmB;CAgBnC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,GAAG,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,IAAI,CAGN"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/database/lifecycle.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC9B,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,OAAO,CAAA;AAEvD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,oBAAoB,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACnC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACtC,SAAS,CAAC,EAAE,MACV,OAAO,CAAC,OAAO,GAAG,kBAAkB,CAAC,GAAG,OAAO,GAAG,kBAAkB,CAAA;IACtE,WAAW,CAAC,EAAE,MACZ,OAAO,CAAC,OAAO,GAAG,kBAAkB,CAAC,GAAG,OAAO,GAAG,kBAAkB,CAAA;IACtE;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAID,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACvB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,CASZ;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,SAAS,CAMrB;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB,CAAQ;IAC/C,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAQ;IAEzC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAG5B;IAEF;;;;OAIG;IACH,MAAM,CAAC,kBAAkB,IAAI,IAAI;IAIjC;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,oBAAoB,GAAG,IAAI;IAK/C;;;;;OAKG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiCxC;;OAEG;WACU,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3C;;;;;;OAMG;IACH,MAAM,CAAC,WAAW,IAAI,OAAO,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAA;QAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;KAC9C,CAAC;mBASmB,eAAe;mBAgBf,QAAQ;IA4C7B;;OAEG;IACH,MAAM,KAAK,IAAI,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,IAAI,IAAI;IAMpB,OAAO,CAAC,MAAM,CAAC,mBAAmB;CAgBnC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,GAAG,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,IAAI,CAGN"}
@@ -1,7 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DatabaseManager = void 0;
4
+ exports.withTimeout = withTimeout;
4
5
  exports.registerDatabase = registerDatabase;
6
+ const DEFAULT_HEALTH_TIMEOUT_MS = 5000;
7
+ /** Rejects when `promise` has not settled after `ms`. */
8
+ function withTimeout(promise, ms, message) {
9
+ let timer;
10
+ const timeout = new Promise((_, reject) => {
11
+ timer = setTimeout(() => reject(new Error(message)), ms);
12
+ timer.unref?.();
13
+ });
14
+ return Promise.race([Promise.resolve(promise), timeout]).finally(() => clearTimeout(timer));
15
+ }
5
16
  /**
6
17
  * Universal Database Manager for ExisJS.
7
18
  * Manages connections, graceful shutdown, and health checks for any database
@@ -24,23 +35,36 @@ class DatabaseManager {
24
35
  this.ensureShutdownHooks();
25
36
  }
26
37
  /**
27
- * Connects all registered databases that are not already connected.
38
+ * Connects all registered databases that are not already connected, in
39
+ * parallel: boot waits for the slowest database, not for the sum of all.
40
+ * If any fail, the error names every database that failed; the ones that
41
+ * connected stay connected, and a later call retries only the failed ones.
28
42
  */
29
43
  static async connectAll() {
30
- for (const [name, db] of this.databases) {
31
- if (db._connected)
32
- continue;
33
- try {
34
- await db.connect();
35
- db._connected = true;
36
- db._disconnecting = undefined;
37
- }
38
- catch (err) {
39
- const error = new Error(`[ExisJS Database] Failed to connect database "${name}": ${err.message}`);
40
- error.cause = err;
41
- throw error;
44
+ const pending = [...this.databases.entries()].filter(([, db]) => !db._connected);
45
+ const results = await Promise.allSettled(pending.map(async ([, db]) => {
46
+ await db.connect();
47
+ db._connected = true;
48
+ db._disconnecting = undefined;
49
+ }));
50
+ const failures = [];
51
+ results.forEach((result, i) => {
52
+ if (result.status === 'rejected') {
53
+ failures.push([pending[i][0], result.reason]);
42
54
  }
43
- }
55
+ });
56
+ if (failures.length === 0)
57
+ return;
58
+ const summary = failures
59
+ .map(([name, err]) => `"${name}": ${err?.message ?? String(err)}`)
60
+ .join('; ');
61
+ const error = new Error(`[ExisJS Database] Failed to connect database ${summary}`);
62
+ error.cause = failures[0][1];
63
+ error.failures = failures.map(([name, err]) => ({
64
+ name,
65
+ error: err,
66
+ }));
67
+ throw error;
44
68
  }
45
69
  /**
46
70
  * Disconnects all registered databases gracefully.
@@ -66,54 +90,68 @@ class DatabaseManager {
66
90
  await Promise.allSettled(disconnectPromises);
67
91
  }
68
92
  /**
69
- * Checks the health of all registered databases.
93
+ * Checks the health of all registered databases, in parallel. Each check
94
+ * has a timeout (`healthTimeoutMs`, default 5 s) so an unresponsive
95
+ * database is reported as an error instead of hanging the probe, and
96
+ * calls made while a check is running share its result, so a burst of
97
+ * probes costs one round of pings.
70
98
  */
71
- static async checkHealth() {
72
- const results = {};
73
- let allHealthy = true;
74
- for (const [name, db] of this.databases) {
75
- const start = Date.now();
76
- try {
77
- const healthFn = db.isHealthy || db.healthCheck;
78
- if (!healthFn) {
79
- results[name] = { name, status: 'connected', latencyMs: 0 };
80
- continue;
81
- }
82
- const healthRes = await healthFn();
83
- const latencyMs = Date.now() - start;
84
- if (typeof healthRes === 'boolean') {
85
- results[name] = {
99
+ static checkHealth() {
100
+ if (this.healthInFlight)
101
+ return this.healthInFlight;
102
+ const run = this.runHealthChecks().finally(() => {
103
+ this.healthInFlight = undefined;
104
+ });
105
+ this.healthInFlight = run;
106
+ return run;
107
+ }
108
+ static async runHealthChecks() {
109
+ const entries = await Promise.all([...this.databases].map(([name, db]) => this.checkOne(name, db)));
110
+ const databases = {};
111
+ let healthy = true;
112
+ for (const info of entries) {
113
+ databases[info.key] = info.result;
114
+ if (info.result.status !== 'connected')
115
+ healthy = false;
116
+ }
117
+ return { healthy, databases };
118
+ }
119
+ static async checkOne(name, db) {
120
+ const healthFn = db.isHealthy || db.healthCheck;
121
+ if (!healthFn) {
122
+ return { key: name, result: { name, status: 'connected', latencyMs: 0 } };
123
+ }
124
+ const start = Date.now();
125
+ const timeoutMs = db.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
126
+ try {
127
+ const healthRes = await withTimeout(healthFn(), timeoutMs, `Health check timed out after ${timeoutMs}ms`);
128
+ const latencyMs = Date.now() - start;
129
+ if (typeof healthRes === 'boolean') {
130
+ return {
131
+ key: name,
132
+ result: {
86
133
  name,
87
134
  status: healthRes ? 'connected' : 'disconnected',
88
135
  latencyMs,
89
- };
90
- if (!healthRes)
91
- allHealthy = false;
92
- }
93
- else {
94
- results[name] = {
95
- latencyMs,
96
- ...healthRes,
97
- name: healthRes.name || name,
98
- };
99
- if (healthRes.status !== 'connected')
100
- allHealthy = false;
101
- }
136
+ },
137
+ };
102
138
  }
103
- catch (err) {
104
- allHealthy = false;
105
- results[name] = {
139
+ return {
140
+ key: name,
141
+ result: { latencyMs, ...healthRes, name: healthRes.name || name },
142
+ };
143
+ }
144
+ catch (err) {
145
+ return {
146
+ key: name,
147
+ result: {
106
148
  name,
107
149
  status: 'error',
108
150
  latencyMs: Date.now() - start,
109
- error: err.message,
110
- };
111
- }
151
+ error: err?.message ?? String(err),
152
+ },
153
+ };
112
154
  }
113
- return {
114
- healthy: allHealthy,
115
- databases: results,
116
- };
117
155
  }
118
156
  /**
119
157
  * Returns the number of registered databases.
@@ -127,6 +165,7 @@ class DatabaseManager {
127
165
  static clear() {
128
166
  this.databases.clear();
129
167
  this.appManagedShutdown = false;
168
+ this.healthInFlight = undefined;
130
169
  }
131
170
  static ensureShutdownHooks() {
132
171
  if (this.isShutdownHookRegistered)
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/database/lifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/database/lifecycle.ts"],"names":[],"mappings":";;;AA6BA,kCAaC;AA8ND,4CAMC;AApPD,MAAM,yBAAyB,GAAG,IAAI,CAAA;AAEtC,yDAAyD;AACzD,SAAgB,WAAW,CACzB,OAAuB,EACvB,EAAU,EACV,OAAe;IAEf,IAAI,KAAiC,CAAA;IACrC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC/C,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;QACxD,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;IACjB,CAAC,CAAC,CAAA;IACF,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CACpE,YAAY,CAAC,KAAK,CAAC,CACpB,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAa,eAAe;IAgB1B;;;;OAIG;IACH,MAAM,CAAC,kBAAkB;QACvB,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAA;IAChC,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAwB;QACtC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;QACzD,IAAI,CAAC,mBAAmB,EAAE,CAAA;IAC5B,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,UAAU;QACrB,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAClD,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,UAAU,CAC3B,CAAA;QACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;YAC3B,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;YAClB,EAAE,CAAC,UAAU,GAAG,IAAI,CAAA;YACpB,EAAE,CAAC,cAAc,GAAG,SAAS,CAAA;QAC/B,CAAC,CAAC,CACH,CAAA;QACD,MAAM,QAAQ,GAAoB,EAAE,CAAA;QACpC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YAC5B,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;YAC/C,CAAC;QACH,CAAC,CAAC,CAAA;QACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAEjC,MAAM,OAAO,GAAG,QAAQ;aACrB,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;aACjE,IAAI,CAAC,IAAI,CAAC,CAAA;QACb,MAAM,KAAK,GAAG,IAAI,KAAK,CACrB,gDAAgD,OAAO,EAAE,CAC1D,CACA;QAAC,KAAa,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACrC;QAAC,KAAa,CAAC,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YACxD,IAAI;YACJ,KAAK,EAAE,GAAG;SACX,CAAC,CAAC,CAAA;QACH,MAAM,KAAK,CAAA;IACb,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,aAAa;QACxB,MAAM,kBAAkB,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;YACxE,mEAAmE;YACnE,oEAAoE;YACpE,8DAA8D;YAC9D,IAAI,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC;gBACvB,EAAE,CAAC,cAAc,GAAG,CAAC,KAAK,IAAI,EAAE;oBAC9B,IAAI,CAAC;wBACH,MAAM,EAAE,CAAC,UAAU,EAAE,CAAA;wBACrB,EAAE,CAAC,UAAU,GAAG,KAAK,CAAA;oBACvB,CAAC;oBAAC,OAAO,GAAQ,EAAE,CAAC;wBAClB,OAAO,CAAC,KAAK,CACX,0CAA0C,EAAE,CAAC,IAAI,IAAI,EACrD,GAAG,CACJ,CAAA;oBACH,CAAC;gBACH,CAAC,CAAC,EAAE,CAAA;YACN,CAAC;YACD,OAAO,EAAE,CAAC,cAAc,CAAA;QAC1B,CAAC,CAAC,CAAA;QACF,MAAM,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAA;IAC9C,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,WAAW;QAIhB,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC,cAAc,CAAA;QACnD,MAAM,GAAG,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAC9C,IAAI,CAAC,cAAc,GAAG,SAAS,CAAA;QACjC,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,cAAc,GAAG,GAAG,CAAA;QACzB,OAAO,GAAG,CAAA;IACZ,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,eAAe;QAIlC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CACjE,CAAA;QACD,MAAM,SAAS,GAAuC,EAAE,CAAA;QACxD,IAAI,OAAO,GAAG,IAAI,CAAA;QAClB,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAA;YACjC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW;gBAAE,OAAO,GAAG,KAAK,CAAA;QACzD,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IAC/B,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,QAAQ,CAC3B,IAAY,EACZ,EAAwB;QAExB,MAAM,QAAQ,GAAG,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,WAAW,CAAA;QAC/C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,CAAA;QAC3E,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACxB,MAAM,SAAS,GAAG,EAAE,CAAC,eAAe,IAAI,yBAAyB,CAAA;QACjE,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,WAAW,CACjC,QAAQ,EAAE,EACV,SAAS,EACT,gCAAgC,SAAS,IAAI,CAC9C,CAAA;YACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;YACpC,IAAI,OAAO,SAAS,KAAK,SAAS,EAAE,CAAC;gBACnC,OAAO;oBACL,GAAG,EAAE,IAAI;oBACT,MAAM,EAAE;wBACN,IAAI;wBACJ,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc;wBAChD,SAAS;qBACV;iBACF,CAAA;YACH,CAAC;YACD,OAAO;gBACL,GAAG,EAAE,IAAI;gBACT,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,IAAI,EAAE;aAClE,CAAA;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO;gBACL,GAAG,EAAE,IAAI;gBACT,MAAM,EAAE;oBACN,IAAI;oBACJ,MAAM,EAAE,OAAO;oBACf,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;oBAC7B,KAAK,EAAE,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC;iBACnC;aACF,CAAA;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,KAAK,IAAI;QACb,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAA;IAC5B,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK;QACV,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAA;QACtB,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAA;QAC/B,IAAI,CAAC,cAAc,GAAG,SAAS,CAAA;IACjC,CAAC;IAEO,MAAM,CAAC,mBAAmB;QAChC,IAAI,IAAI,CAAC,wBAAwB;YAAE,OAAM;QACzC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAA;QAEpC,gEAAgE;QAChE,yBAAyB;QACzB,MAAM,eAAe,GAAG,KAAK,IAAI,EAAE;YACjC,IAAI,eAAe,CAAC,kBAAkB;gBAAE,OAAM;YAC9C,MAAM,eAAe,CAAC,aAAa,EAAE,CAAA;QACvC,CAAC,CAAA;QAED,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;YACvE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAA;YACvC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAA;QAC1C,CAAC;IACH,CAAC;;AAhNH,0CAiNC;AAhNgB,yBAAS,GAAG,IAAI,GAAG,EAM/B,CAAA;AACY,wCAAwB,GAAG,KAAK,CAAA;AAChC,kCAAkB,GAAG,KAAK,CAAA;AA0M3C;;;GAGG;AACH,SAAgB,gBAAgB,CAC9B,OAAY,EACZ,OAA8B;IAE9B,MAAM,EAAE,GAAG,OAAO,IAAI,OAAO,CAAA;IAC7B,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC9B,CAAC"}
@@ -4,26 +4,5 @@ export interface TransactionalOptions {
4
4
  /** Whether the transaction is read-only */
5
5
  readOnly?: boolean;
6
6
  }
7
- /**
8
- * Automatically wraps a service method inside a database transaction.
9
- * Commits automatically on success, and rolls back if an unhandled exception is thrown.
10
- *
11
- * Compatible with ExisJS built-in Database client, Prisma, Drizzle, TypeORM, Knex, and raw SQL pools.
12
- *
13
- * @example
14
- * ```ts
15
- * @Injectable()
16
- * export class OrderService {
17
- * constructor(private db: DatabaseService) {}
18
- *
19
- * @Transactional()
20
- * async placeOrder(userId: string, items: CartItem[]) {
21
- * const order = await this.db.orders.create({ userId })
22
- * await this.db.inventory.decrement(items)
23
- * return order
24
- * }
25
- * }
26
- * ```
27
- */
28
7
  export declare function Transactional(_options?: TransactionalOptions): MethodDecorator;
29
8
  //# sourceMappingURL=transaction.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/decorators/transaction.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,GAAE,oBAAyB,GAClC,eAAe,CAmEjB"}
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/decorators/transaction.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AA4ED,wBAAgB,aAAa,CAC3B,QAAQ,GAAE,oBAAyB,GAClC,eAAe,CAiDjB"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Transactional = Transactional;
4
+ const node_async_hooks_1 = require("node:async_hooks");
4
5
  /**
5
6
  * Automatically wraps a service method inside a database transaction.
6
7
  * Commits automatically on success, and rolls back if an unhandled exception is thrown.
@@ -22,49 +23,68 @@ exports.Transactional = Transactional;
22
23
  * }
23
24
  * ```
24
25
  */
26
+ // Instance properties that may hold the database client, in lookup order
27
+ const DB_KEYS = ['db', 'database', 'prisma', 'dataSource', 'connection'];
28
+ // Services are usually shared singletons, so the transaction must not be
29
+ // written onto the instance: a concurrent request using the same service
30
+ // would run its queries inside this request's transaction. Instead the
31
+ // database property becomes a getter that returns the transaction only
32
+ // within the async context of the @Transactional call (and anything it
33
+ // awaits or calls, which therefore joins the transaction).
34
+ const activeTransactions = new node_async_hooks_1.AsyncLocalStorage();
35
+ const BOUND = Symbol('exis.transactionalProperty');
36
+ function bindDbProperty(self, key) {
37
+ const own = Object.getOwnPropertyDescriptor(self, key);
38
+ if (own?.get && own.get[BOUND])
39
+ return true;
40
+ // An inherited or hand-written accessor cannot be wrapped safely
41
+ if (own === undefined ? key in self : own.get || own.set)
42
+ return false;
43
+ if (own !== undefined && !own.configurable)
44
+ return false;
45
+ let value = own?.value;
46
+ const get = function () {
47
+ const tx = activeTransactions.getStore()?.get(self);
48
+ return tx !== undefined ? tx : value;
49
+ };
50
+ get[BOUND] = true;
51
+ Object.defineProperty(self, key, {
52
+ configurable: true,
53
+ enumerable: own?.enumerable ?? true,
54
+ get,
55
+ set(next) {
56
+ value = next;
57
+ },
58
+ });
59
+ return true;
60
+ }
61
+ function runWithTransaction(self, key, tx, fn) {
62
+ if (bindDbProperty(self, key)) {
63
+ const store = new Map(activeTransactions.getStore());
64
+ store.set(self, tx);
65
+ return activeTransactions.run(store, () => fn(self));
66
+ }
67
+ // Fallback for accessor-defined clients: a per-call view of the instance
68
+ const scoped = Object.create(self);
69
+ scoped[key] = tx;
70
+ return fn(scoped);
71
+ }
25
72
  function Transactional(_options = {}) {
26
73
  return function (_target, _propertyKey, descriptor) {
27
74
  const originalMethod = descriptor.value;
28
75
  descriptor.value = async function (...args) {
29
76
  const self = this;
30
- const db = self.db ||
31
- self.database ||
32
- self.prisma ||
33
- self.dataSource ||
34
- self.connection ||
35
- globalThis.__EXIS_DB__;
77
+ const key = DB_KEYS.find((k) => self[k]);
78
+ const db = key ? self[key] : globalThis.__EXIS_DB__;
36
79
  if (db) {
80
+ const runInTransaction = (tx) => runWithTransaction(self, key ?? 'db', tx, (target) => originalMethod.apply(target, args));
37
81
  // 1. Functional transaction wrapper: db.transaction((tx) => ...)
38
82
  if (typeof db.transaction === 'function') {
39
- return db.transaction(async (tx) => {
40
- const originalDb = self.db;
41
- try {
42
- self.db = tx;
43
- return await originalMethod.apply(self, args);
44
- }
45
- finally {
46
- self.db = originalDb;
47
- }
48
- });
83
+ return db.transaction(runInTransaction);
49
84
  }
50
85
  // 2. Prisma-style $transaction: db.$transaction(async (tx) => ...)
51
86
  if (typeof db.$transaction === 'function') {
52
- return db.$transaction(async (tx) => {
53
- const originalPrisma = self.prisma || self.db;
54
- try {
55
- if (self.prisma)
56
- self.prisma = tx;
57
- if (self.db)
58
- self.db = tx;
59
- return await originalMethod.apply(self, args);
60
- }
61
- finally {
62
- if (self.prisma)
63
- self.prisma = originalPrisma;
64
- if (self.db)
65
- self.db = originalPrisma;
66
- }
67
- });
87
+ return db.$transaction(runInTransaction);
68
88
  }
69
89
  // 3. Imperative beginTransaction / commit / rollback
70
90
  if (typeof db.beginTransaction === 'function') {
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../src/decorators/transaction.ts"],"names":[],"mappings":";;AA4BA,sCAqEC;AA1FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,aAAa,CAC3B,WAAiC,EAAE;IAEnC,OAAO,UACL,OAAY,EACZ,YAA6B,EAC7B,UAA8B;QAE9B,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAA;QAEvC,UAAU,CAAC,KAAK,GAAG,KAAK,WAAW,GAAG,IAAW;YAC/C,MAAM,IAAI,GAAG,IAAW,CAAA;YACxB,MAAM,EAAE,GACN,IAAI,CAAC,EAAE;gBACP,IAAI,CAAC,QAAQ;gBACb,IAAI,CAAC,MAAM;gBACX,IAAI,CAAC,UAAU;gBACf,IAAI,CAAC,UAAU;gBACd,UAAkB,CAAC,WAAW,CAAA;YAEjC,IAAI,EAAE,EAAE,CAAC;gBACP,iEAAiE;gBACjE,IAAI,OAAO,EAAE,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;oBACzC,OAAO,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAO,EAAE,EAAE;wBACtC,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,CAAA;wBAC1B,IAAI,CAAC;4BACH,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;4BACZ,OAAO,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;wBAC/C,CAAC;gCAAS,CAAC;4BACT,IAAI,CAAC,EAAE,GAAG,UAAU,CAAA;wBACtB,CAAC;oBACH,CAAC,CAAC,CAAA;gBACJ,CAAC;gBAED,mEAAmE;gBACnE,IAAI,OAAO,EAAE,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;oBAC1C,OAAO,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,EAAO,EAAE,EAAE;wBACvC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAA;wBAC7C,IAAI,CAAC;4BACH,IAAI,IAAI,CAAC,MAAM;gCAAE,IAAI,CAAC,MAAM,GAAG,EAAE,CAAA;4BACjC,IAAI,IAAI,CAAC,EAAE;gCAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;4BACzB,OAAO,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;wBAC/C,CAAC;gCAAS,CAAC;4BACT,IAAI,IAAI,CAAC,MAAM;gCAAE,IAAI,CAAC,MAAM,GAAG,cAAc,CAAA;4BAC7C,IAAI,IAAI,CAAC,EAAE;gCAAE,IAAI,CAAC,EAAE,GAAG,cAAc,CAAA;wBACvC,CAAC;oBACH,CAAC,CAAC,CAAA;gBACJ,CAAC;gBAED,qDAAqD;gBACrD,IAAI,OAAO,EAAE,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;oBAC9C,MAAM,EAAE,CAAC,gBAAgB,EAAE,CAAA;oBAC3B,IAAI,CAAC;wBACH,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;wBACrD,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,UAAU;4BAAE,MAAM,EAAE,CAAC,MAAM,EAAE,CAAA;wBACtD,OAAO,MAAM,CAAA;oBACf,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,IAAI,OAAO,EAAE,CAAC,QAAQ,KAAK,UAAU;4BAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAA;wBAC1D,MAAM,KAAK,CAAA;oBACb,CAAC;gBACH,CAAC;YACH,CAAC;YAED,8DAA8D;YAC9D,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACzC,CAAC,CAAA;QAED,OAAO,UAAU,CAAA;IACnB,CAAC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../src/decorators/transaction.ts"],"names":[],"mappings":";;AAmFA,sCAmDC;AAtID,uDAAoD;AASpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,yEAAyE;AACzE,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,CAAC,CAAA;AAExE,yEAAyE;AACzE,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,uEAAuE;AACvE,2DAA2D;AAC3D,MAAM,kBAAkB,GAAG,IAAI,oCAAiB,EAAoB,CAAA;AACpE,MAAM,KAAK,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAA;AAElD,SAAS,cAAc,CAAC,IAAS,EAAE,GAAW;IAC5C,MAAM,GAAG,GAAG,MAAM,CAAC,wBAAwB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACtD,IAAI,GAAG,EAAE,GAAG,IAAK,GAAG,CAAC,GAAW,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpD,iEAAiE;IACjE,IAAI,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG;QAAE,OAAO,KAAK,CAAA;IACtE,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,YAAY;QAAE,OAAO,KAAK,CAAA;IAExD,IAAI,KAAK,GAAG,GAAG,EAAE,KAAK,CAAA;IACtB,MAAM,GAAG,GAAG;QACV,MAAM,EAAE,GAAG,kBAAkB,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;QACnD,OAAO,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IACtC,CAAC,CACA;IAAC,GAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAA;IAC3B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE;QAC/B,YAAY,EAAE,IAAI;QAClB,UAAU,EAAE,GAAG,EAAE,UAAU,IAAI,IAAI;QACnC,GAAG;QACH,GAAG,CAAC,IAAS;YACX,KAAK,GAAG,IAAI,CAAA;QACd,CAAC;KACF,CAAC,CAAA;IACF,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,kBAAkB,CACzB,IAAS,EACT,GAAW,EACX,EAAO,EACP,EAAsB;IAEtB,IAAI,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,QAAQ,EAAE,CAAC,CAAA;QACpD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACnB,OAAO,kBAAkB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IACtD,CAAC;IACD,yEAAyE;IACzE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAA;IAChB,OAAO,EAAE,CAAC,MAAM,CAAC,CAAA;AACnB,CAAC;AAED,SAAgB,aAAa,CAC3B,WAAiC,EAAE;IAEnC,OAAO,UACL,OAAY,EACZ,YAA6B,EAC7B,UAA8B;QAE9B,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAA;QAEvC,UAAU,CAAC,KAAK,GAAG,KAAK,WAAW,GAAG,IAAW;YAC/C,MAAM,IAAI,GAAG,IAAW,CAAA;YACxB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YACxC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,UAAkB,CAAC,WAAW,CAAA;YAE5D,IAAI,EAAE,EAAE,CAAC;gBACP,MAAM,gBAAgB,GAAG,CAAC,EAAO,EAAE,EAAE,CACnC,kBAAkB,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CACnD,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CACnC,CAAA;gBAEH,iEAAiE;gBACjE,IAAI,OAAO,EAAE,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;oBACzC,OAAO,EAAE,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAA;gBACzC,CAAC;gBAED,mEAAmE;gBACnE,IAAI,OAAO,EAAE,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;oBAC1C,OAAO,EAAE,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAA;gBAC1C,CAAC;gBAED,qDAAqD;gBACrD,IAAI,OAAO,EAAE,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;oBAC9C,MAAM,EAAE,CAAC,gBAAgB,EAAE,CAAA;oBAC3B,IAAI,CAAC;wBACH,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;wBACrD,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,UAAU;4BAAE,MAAM,EAAE,CAAC,MAAM,EAAE,CAAA;wBACtD,OAAO,MAAM,CAAA;oBACf,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,IAAI,OAAO,EAAE,CAAC,QAAQ,KAAK,UAAU;4BAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAA;wBAC1D,MAAM,KAAK,CAAA;oBACb,CAAC;gBACH,CAAC;YACH,CAAC;YAED,8DAA8D;YAC9D,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACzC,CAAC,CAAA;QAED,OAAO,UAAU,CAAA;IACnB,CAAC,CAAA;AACH,CAAC"}
@@ -148,6 +148,8 @@ export default class AdminBoundary {
148
148
  }
149
149
  ```
150
150
 
151
+ `next()` resolves with what the route sent. Returning that value (or returning nothing) sends the route's response exactly as the route sent it, with the same type and `Content-Type`; a `Buffer` stays binary. Returning a different value replaces the response, and that value is sent like a handler's return value.
152
+
151
153
  ---
152
154
 
153
155
  ## Boundary Cascading & Execution Order
@@ -153,7 +153,7 @@ export default defineConfig({
153
153
  | `compression` | `boolean` | `false` | Gzip/Brotli HTTP response compression. |
154
154
  | `bodyLimit` | `number` | `10485760` (10MB) | Maximum request payload body size in bytes. |
155
155
  | `blockSuspiciousProbes` | `boolean \| BlockProbesOptions` | `false` | Blocks known vulnerability scanners and malicious probes (`.php`, `wp-admin`). |
156
- | `etag` | `boolean` | `false` | Generates weak ETags for cache validation on responses. |
156
+ | `etag` | `boolean` | `false` | Weak ETags on successful GET/HEAD responses, with `304 Not Modified` for a matching `If-None-Match`. See [Caching & Conditional Requests](/docs/responses#caching--conditional-requests-etag-304-412). |
157
157
  | `transformResponse` | `boolean \| Function` | `false` | Standardizes handler response payload envelopes. |
158
158
 
159
159
  ### Logging & Diagnostics
@@ -475,7 +475,7 @@ export class OrderService implements OnInit, OnDestroy {
475
475
  | :--- | :--- | :--- |
476
476
  | `@Cacheable(options?)` | Method | Caches return value. Options: `{ ttl?: number, key?: string \| ((...args) => string) }`. |
477
477
  | `@CacheEvict(options)` | Method | Evicts cache keys on method completion. Options: `{ key: string \| ((...args) => string) }`. |
478
- | `@Transactional(options?)` | Method | Wraps execution in a database transaction (Prisma, Mongoose, Drizzle, SQL). |
478
+ | `@Transactional(options?)` | Method | Wraps execution in a database transaction (Prisma, Drizzle, Knex, SQL). Inside the call, the service's client property (`db`, `database`, `prisma`, `dataSource` or `connection`) is the transaction, including in methods it calls; concurrent requests using the same service keep the normal client. |
479
479
  | `@Profile(options?)` | Method | Measures execution time using `performance.now()`. |
480
480
  | `OnInit` (`onInit()`) | Class Interface | Invoked once after service instantiation. |
481
481
  | `OnDestroy` (`onDestroy()`) | Class Interface | Invoked once during graceful shutdown. |
@@ -28,7 +28,7 @@ All standard middlewares can be imported directly from `exisjs/middleware`. Cust
28
28
  | `idempotent` | `idempotent(options?)` | Duplicate mutation caching keyed by `Idempotency-Key` headers. |
29
29
  | `dedupe` | `dedupe(options)` | Merges simultaneous identical in-flight `GET` requests into a single operation. |
30
30
  | `backpressure` | `backpressure(options?)` | Queues requests during high load and sheds excess traffic with `503 Service Unavailable`. |
31
- | `serveStatic` | `serveStatic(root, options?)` | Serves static assets from disk with strong ETags and Brotli/Gzip pre-compression. |
31
+ | `serveStatic` | `serveStatic(root, options?)` | Serves static assets from disk with ETag / Last-Modified revalidation and Brotli/Gzip pre-compression. |
32
32
  | `timeout` | `timeout(ms \| options)` | Terminates requests exceeding the duration limit with `503 Service Unavailable`. |
33
33
  | `fileUpload` | `fileUpload(options?)` | Streaming multipart/form-data parser supporting disk/memory storage and MIME checks. |
34
34
  | `fromExpress` | `fromExpress(middleware)` | Connect/Express bridge adapter running third-party middleware (e.g. Multer) directly. |
@@ -151,6 +151,8 @@ export const requireAuth = defineMiddleware<AuthContext>(async (req, res, next)
151
151
  })
152
152
  ```
153
153
 
154
+ Middleware may be `async`. ExisJS awaits the returned promise. If it throws, or rejects with an `HttpError` (such as `UnauthorizedException` above), that error becomes the response through the error handler, with the standard error envelope. You don't need to catch it and call `next(err)` yourself.
155
+
154
156
  ### 2. Using Typed Middleware in Functional Routes
155
157
 
156
158
  ```typescript filename="src/http/profile/route.ts"
@@ -287,4 +289,4 @@ export default exis({
287
289
  })
288
290
  ```
289
291
 
290
- `serveStatic` automatically handles MIME detection, conditional `If-None-Match` 304 caching, and serves pre-compressed `.br` or `.gz` files if present on disk.
292
+ `serveStatic` handles MIME detection and serves a pre-compressed `.br` or `.gz` file when one exists and the client accepts that encoding (`br;q=0` counts as a refusal). Every file gets an `ETag` (from its size and modification time; each encoded file has its own) and `Last-Modified`, and conditional requests are answered with `304` or `412` as described in [Caching & Conditional Requests](/docs/responses#caching--conditional-requests-etag-304-412). `Vary: Accept-Encoding` is always sent so shared caches keep the variants apart.
@@ -106,6 +106,8 @@ const data = await req.json<{ username: string }>()
106
106
  const { fields, files } = await req.formData()
107
107
  ```
108
108
 
109
+ The body is read from the network once; these methods share the result. `req.buffer()` returns the bytes exactly as the client sent them, so binary payloads (images, protobuf, compressed or signed data) are safe. A body over `bodyLimit` makes these calls fail with `413 Payload Too Large`.
110
+
109
111
  ### Webhooks with `@RawBody()` (Class Controllers)
110
112
 
111
113
  In class-based controllers, use the `@RawBody()` parameter decorator to inject the raw `Buffer` or decoded string directly:
@@ -451,6 +451,60 @@ export default controller({
451
451
  })
452
452
  ```
453
453
 
454
+ ## Caching & Conditional Requests (ETag, 304, 412)
455
+
456
+ ExisJS follows the conditional request rules of RFC 9110 §13 everywhere: for automatic ETags, in `res.checkPreconditions()`, for `serveStatic` and for `res.download(path)`.
457
+
458
+ | Request header | Checked against | Result when it does not hold |
459
+ | :--- | :--- | :--- |
460
+ | `If-Match` | `ETag`, strong comparison | `412 Precondition Failed` |
461
+ | `If-Unmodified-Since` (only without `If-Match`) | `Last-Modified` | `412 Precondition Failed` |
462
+ | `If-None-Match` | `ETag`, weak comparison; lists and `*` supported | Matches: `304` for GET/HEAD, `412` for writes |
463
+ | `If-Modified-Since` (only without `If-None-Match`, GET/HEAD) | `Last-Modified` | Unchanged: `304 Not Modified` |
464
+
465
+ A `304` carries the `ETag`, `Last-Modified`, `Cache-Control` and `Vary` of the `200` it replaces, and no body or `Content-Type`. Errors use the standard envelope (`PRECONDITION_FAILED`, `PRECONDITION_REQUIRED`).
466
+
467
+ ### Automatic ETags
468
+
469
+ With `etag: true` in `exis.config.ts` (or `conditionalGet()` on some routes), every successful `GET`/`HEAD` response sent with `res.json()`, `res.send()` or a returned value gets a weak ETag hashed from its body, and a matching `If-None-Match` is answered with `304`. The handler still runs, so this saves bandwidth, not work. Writes and error responses get no ETag.
470
+
471
+ ### Validators from your data: `res.checkPreconditions()`
472
+
473
+ When the resource has a version or update time, check it **before** doing the work. On reads this skips the query for the body and serialization; on writes it prevents lost updates: a client that edited an old copy gets `412` instead of overwriting someone else's change.
474
+
475
+ ```typescript filename="src/http/documents/[id]/route.ts"
476
+ import { controller, route } from 'exisjs/router'
477
+
478
+ export default controller({
479
+ read: route.get('/', {
480
+ async handle({ params, res }) {
481
+ const meta = await documents.meta(params.id) // cheap: version + updatedAt
482
+ // Sets ETag / Last-Modified; answers 304 and returns true when current
483
+ if (res.checkPreconditions({ etag: `v${meta.version}`, lastModified: meta.updatedAt })) return
484
+ return documents.find(params.id)
485
+ },
486
+ }),
487
+
488
+ update: route.put('/', {
489
+ async handle({ params, body, res }) {
490
+ const current = await documents.meta(params.id)
491
+ // 412 if the client's If-Match is stale; 428 if it sent no precondition
492
+ res.checkPreconditions({ etag: `v${current.version}` }, { required: true })
493
+ const updated = await documents.update(params.id, body)
494
+ res.setHeader('ETag', `"v${updated.version}"`) // the new state
495
+ return updated
496
+ },
497
+ }),
498
+ })
499
+ ```
500
+
501
+ - `etag` may be bare (`v3` is sent as `"v3"`) or a full tag (`W/"v3"`). Use a value that changes whenever the representation changes: a version column, a hash, `updatedAt`.
502
+ - `lastModified` takes a `Date`, a timestamp or an HTTP date. It is rounded to whole seconds, and a time in the future is capped at the current time.
503
+ - For "create only if absent" (`PUT` with `If-None-Match: *`), pass `exists: false` when the resource does not exist yet; an existing one answers `412`.
504
+ - On writes, the old validators are not sent back. Set the new `ETag` after the change, as above.
505
+
506
+ `req.fresh` (and `req.stale`) apply the same rules to the response headers set so far.
507
+
454
508
  ---
455
509
 
456
510
  ## Next Steps
@@ -92,6 +92,19 @@ const Schema = tex.object({
92
92
  })
93
93
  ```
94
94
 
95
+ Nested object schemas take the same modifiers as methods: `.optional()`, `.nullable()` and `.nullish()`. A plain object literal is also a nested object:
96
+
97
+ ```typescript filename="src/http/leads/schema.ts"
98
+ const Address = tex.object({ city: tex.string(), pin: tex.string({ optional: true }) })
99
+
100
+ export const UpdateLead = tex.object({
101
+ status: tex.enum(['new', 'won', 'lost']),
102
+ address: Address.optional(), // may be left out
103
+ billing: Address.nullish(), // may be left out or sent as null to clear it
104
+ meta: { source: tex.string() }, // same as tex.object({ source: tex.string() })
105
+ })
106
+ ```
107
+
95
108
  ### Input and Output Types
96
109
 
97
110
  A field with a `default` may be left out of a request but is always present after parsing, so the two sides have different types: