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.
- package/dist/database/lifecycle.d.ts +19 -2
- package/dist/database/lifecycle.d.ts.map +1 -1
- package/dist/database/lifecycle.js +92 -53
- package/dist/database/lifecycle.js.map +1 -1
- package/dist/decorators/transaction.d.ts +0 -21
- package/dist/decorators/transaction.d.ts.map +1 -1
- package/dist/decorators/transaction.js +52 -32
- package/dist/decorators/transaction.js.map +1 -1
- package/dist/docs/content/boundaries.mdx +2 -0
- package/dist/docs/content/config.mdx +1 -1
- package/dist/docs/content/decorators.mdx +1 -1
- package/dist/docs/content/middleware.mdx +4 -2
- package/dist/docs/content/requests.mdx +2 -0
- package/dist/docs/content/responses.mdx +54 -0
- package/dist/docs/content/validation.mdx +13 -0
- package/dist/docs/reference/README.md +16 -16
- package/dist/docs/reference/config.mdx +1 -1
- package/dist/docs/reference/database.mdx +3 -2
- package/dist/docs/reference/middleware.mdx +1 -1
- package/dist/docs/reference/validator.mdx +3 -0
- package/dist/middleware/compression.d.ts.map +1 -1
- package/dist/middleware/compression.js +6 -5
- package/dist/middleware/compression.js.map +1 -1
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +47 -34
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +4 -7
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/router.d.ts +3 -0
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +40 -35
- package/dist/router/router.js.map +1 -1
- package/dist/router/scanner/boundary-loader.d.ts.map +1 -1
- package/dist/router/scanner/boundary-loader.js +25 -30
- package/dist/router/scanner/boundary-loader.js.map +1 -1
- package/dist/router/scanner/functional-compiler.d.ts.map +1 -1
- package/dist/router/scanner/functional-compiler.js +29 -29
- package/dist/router/scanner/functional-compiler.js.map +1 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +5 -2
- package/dist/server/app.js.map +1 -1
- package/dist/server/helpers/body-parser.d.ts +1 -1
- package/dist/server/helpers/body-parser.d.ts.map +1 -1
- package/dist/server/helpers/body-parser.js +9 -7
- package/dist/server/helpers/body-parser.js.map +1 -1
- package/dist/server/helpers/conditional.d.ts +60 -0
- package/dist/server/helpers/conditional.d.ts.map +1 -0
- package/dist/server/helpers/conditional.js +189 -0
- package/dist/server/helpers/conditional.js.map +1 -0
- package/dist/server/helpers/etag.d.ts +0 -1
- package/dist/server/helpers/etag.d.ts.map +1 -1
- package/dist/server/helpers/etag.js +0 -10
- package/dist/server/helpers/etag.js.map +1 -1
- package/dist/server/helpers/index.d.ts +2 -1
- package/dist/server/helpers/index.d.ts.map +1 -1
- package/dist/server/helpers/index.js +16 -2
- package/dist/server/helpers/index.js.map +1 -1
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +12 -2
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +11 -3
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +41 -83
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +43 -2
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +151 -70
- package/dist/server/response.js.map +1 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +11 -17
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/engine.d.ts +19 -1
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +65 -29
- package/dist/validator/engine.js.map +1 -1
- 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;
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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":";;;
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/database/lifecycle.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":"
|
|
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
|
|
31
|
-
|
|
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(
|
|
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(
|
|
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":";;
|
|
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` |
|
|
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,
|
|
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
|
|
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`
|
|
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:
|