@rapidrest/service-core 2.0.0 → 2.1.0

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 (84) hide show
  1. package/dist/lib/BackgroundServiceManager.js +51 -9
  2. package/dist/lib/BackgroundServiceManager.js.map +1 -1
  3. package/dist/lib/EventListenerManager.js +35 -2
  4. package/dist/lib/EventListenerManager.js.map +1 -1
  5. package/dist/lib/NetUtils.js +215 -21
  6. package/dist/lib/NetUtils.js.map +1 -1
  7. package/dist/lib/ObjectFactory.js +7 -2
  8. package/dist/lib/ObjectFactory.js.map +1 -1
  9. package/dist/lib/RateLimiter.js.map +1 -1
  10. package/dist/lib/Server.js +121 -80
  11. package/dist/lib/Server.js.map +1 -1
  12. package/dist/lib/auth/AuthMiddleware.js +160 -100
  13. package/dist/lib/auth/AuthMiddleware.js.map +1 -1
  14. package/dist/lib/auth/JWTStrategy.js +6 -2
  15. package/dist/lib/auth/JWTStrategy.js.map +1 -1
  16. package/dist/lib/database/ConnectionManager.js +47 -1
  17. package/dist/lib/database/ConnectionManager.js.map +1 -1
  18. package/dist/lib/database/DatabaseErrors.js +88 -0
  19. package/dist/lib/database/DatabaseErrors.js.map +1 -0
  20. package/dist/lib/database/MongoRepository.js +31 -3
  21. package/dist/lib/database/MongoRepository.js.map +1 -1
  22. package/dist/lib/database/MongoSchemaSync.js +7 -1
  23. package/dist/lib/database/MongoSchemaSync.js.map +1 -1
  24. package/dist/lib/database/TypeOrmSupport.js +11 -6
  25. package/dist/lib/database/TypeOrmSupport.js.map +1 -1
  26. package/dist/lib/database/index.js +1 -0
  27. package/dist/lib/database/index.js.map +1 -1
  28. package/dist/lib/decorators/PersistenceDecorators.js +23 -0
  29. package/dist/lib/decorators/PersistenceDecorators.js.map +1 -1
  30. package/dist/lib/decorators/RouteDecorators.js +4 -2
  31. package/dist/lib/decorators/RouteDecorators.js.map +1 -1
  32. package/dist/lib/http/bun/BunRouter.js +119 -9
  33. package/dist/lib/http/bun/BunRouter.js.map +1 -1
  34. package/dist/lib/http/index.js +1 -0
  35. package/dist/lib/http/index.js.map +1 -1
  36. package/dist/lib/http/session/sessionMiddleware.js +62 -14
  37. package/dist/lib/http/session/sessionMiddleware.js.map +1 -1
  38. package/dist/lib/http/types.js +10 -1
  39. package/dist/lib/http/types.js.map +1 -1
  40. package/dist/lib/http/uWS/Adapters.js +31 -13
  41. package/dist/lib/http/uWS/Adapters.js.map +1 -1
  42. package/dist/lib/http/uWS/Router.js +80 -16
  43. package/dist/lib/http/uWS/Router.js.map +1 -1
  44. package/dist/lib/http/uWS/WebSocket.js +4 -2
  45. package/dist/lib/http/uWS/WebSocket.js.map +1 -1
  46. package/dist/lib/models/ModelUtils.js +255 -81
  47. package/dist/lib/models/ModelUtils.js.map +1 -1
  48. package/dist/lib/models/RepoUtils.js +863 -266
  49. package/dist/lib/models/RepoUtils.js.map +1 -1
  50. package/dist/lib/routes/BaseAdminRoute.js +5 -4
  51. package/dist/lib/routes/BaseAdminRoute.js.map +1 -1
  52. package/dist/lib/routes/BasePushRoute.js +104 -40
  53. package/dist/lib/routes/BasePushRoute.js.map +1 -1
  54. package/dist/lib/routes/CRUDRoute.js +31 -17
  55. package/dist/lib/routes/CRUDRoute.js.map +1 -1
  56. package/dist/lib/routes/RouteUtils.js +120 -29
  57. package/dist/lib/routes/RouteUtils.js.map +1 -1
  58. package/dist/lib/security/ACLUtils.js +170 -34
  59. package/dist/lib/security/ACLUtils.js.map +1 -1
  60. package/dist/types/BackgroundServiceManager.d.ts +6 -0
  61. package/dist/types/EventListenerManager.d.ts +4 -0
  62. package/dist/types/NetUtils.d.ts +65 -6
  63. package/dist/types/RateLimiter.d.ts +4 -3
  64. package/dist/types/Server.d.ts +33 -2
  65. package/dist/types/auth/AuthMiddleware.d.ts +35 -4
  66. package/dist/types/database/ConnectionManager.d.ts +18 -0
  67. package/dist/types/database/DatabaseErrors.d.ts +26 -0
  68. package/dist/types/database/MongoRepository.d.ts +21 -2
  69. package/dist/types/database/index.d.ts +1 -0
  70. package/dist/types/decorators/PersistenceDecorators.d.ts +31 -0
  71. package/dist/types/decorators/RouteDecorators.d.ts +4 -2
  72. package/dist/types/http/bun/BunRouter.d.ts +23 -2
  73. package/dist/types/http/index.d.ts +2 -1
  74. package/dist/types/http/session/sessionMiddleware.d.ts +14 -4
  75. package/dist/types/http/types.d.ts +40 -0
  76. package/dist/types/http/uWS/Adapters.d.ts +10 -1
  77. package/dist/types/http/uWS/Router.d.ts +15 -2
  78. package/dist/types/models/ModelUtils.d.ts +96 -1
  79. package/dist/types/models/RepoUtils.d.ts +240 -4
  80. package/dist/types/routes/BasePushRoute.d.ts +5 -0
  81. package/dist/types/routes/CRUDRoute.d.ts +10 -0
  82. package/dist/types/routes/RouteUtils.d.ts +37 -1
  83. package/dist/types/security/ACLUtils.d.ts +68 -7
  84. package/package.json +1 -1
@@ -16,6 +16,12 @@ export declare class AuthMiddleware {
16
16
  /**
17
17
  * Performs authentication of the given request using one of the provided strategies.
18
18
  *
19
+ * Each strategy is tried in order until one succeeds. A strategy that throws doesn't stop the others from
20
+ * being tried, since several strategies can claim the same credential (e.g. `jwt` and `oauth_bearer` both
21
+ * read an `Authorization: Bearer` header). If no strategy succeeds and at least one threw, the first error
22
+ * thrown is rethrown, whether or not `required` is set, so callers can still tell a bad credential from a
23
+ * missing one.
24
+ *
19
25
  * @param strategies The list of strategy names to attempt authentication with.
20
26
  * @param req The request containing data to perform authenticate with.
21
27
  * @param res The response to use when writing back directly to the client.
@@ -25,7 +31,8 @@ export declare class AuthMiddleware {
25
31
  /**
26
32
  * Performs authentication of the given request using one of the provided strategies.
27
33
  *
28
- * This is the synchronous version of `authenticate` that performs blocking based authentication.
34
+ * This is the synchronous version of `authenticate` that performs blocking based authentication. It follows
35
+ * the same rules for strategies that throw.
29
36
  *
30
37
  * @param strategies The list of strategy names to attempt authentication with.
31
38
  * @param req The request containing data to perform authenticate with.
@@ -33,18 +40,42 @@ export declare class AuthMiddleware {
33
40
  * @param required Set to `true` to if authentication is required to pass, otherwise set to `false`.
34
41
  */
35
42
  authenticateSync(strategies: string[], req: HttpRequest, res?: HttpResponse, required?: boolean): AuthResult | undefined;
43
+ /**
44
+ * Returns the registered strategy with the given name, throwing if there is none. A missing strategy is a
45
+ * configuration error, so it is never treated as an ordinary authentication failure.
46
+ */
47
+ private getStrategy;
48
+ /**
49
+ * Handles the case where no strategy authenticated the request: rethrows the first strategy error if there was
50
+ * one, throws if authentication is required, and otherwise returns `undefined`.
51
+ */
52
+ private authFailed;
53
+ /**
54
+ * Builds a copy of `req` that carries only the given token, as an `Authorization: Bearer` header. Used to run
55
+ * a WebSocket `LOGIN` message's token through the route's strategies. Query parameters and cookies from the
56
+ * upgrade request are removed, so a strategy can't authenticate from a credential other than the one supplied.
57
+ */
58
+ private createTokenRequest;
36
59
  /**
37
60
  * Returns a request handler function that will perform authentication of a websocket connection. Authentication
38
61
  * can be handled in two ways:
39
62
  *
40
- * 1. Authorization header
63
+ * 1. Authorization header (or any other credential a strategy reads from the upgrade request)
41
64
  * 2. Negotiation via handshake
42
65
  *
43
- * This middleware function primarily provides the implementation for item 2 above.
66
+ * Pre-upgrade auth (see `RouteUtils.registerRoute()`) can only run strategies synchronously. When it didn't
67
+ * authenticate the connection, this handler first runs the strategies asynchronously against the upgrade
68
+ * request, which covers async-only strategies such as `oauth_bearer`. If that doesn't authenticate either, it
69
+ * waits for a `LOGIN` message whose `data` token is also verified through `strategies`, sent to them as an
70
+ * `Authorization: Bearer` header.
71
+ *
72
+ * When `required` is `false`, a failed or missing credential never closes the connection; the handler proceeds
73
+ * anonymously.
44
74
  *
45
75
  * @param required Set to `true` to indicate that auth is required, otherwise `false`.
76
+ * @param strategies The strategy names to authenticate with. Defaults to `["jwt"]`, matching `RouteUtils`.
46
77
  */
47
- authWebSocket(required: boolean): RequestHandler;
78
+ authWebSocket(required: boolean, strategies?: string[]): RequestHandler;
48
79
  /**
49
80
  * Registers the provided authentication strategy to be used
50
81
  * @param name The name of the authentication type to associate the given strategy with
@@ -1,6 +1,24 @@
1
1
  import type { DataSource } from "typeorm";
2
2
  import { MongoConnection } from "./MongoConnection.js";
3
3
  import type { RedisClientType } from "redis";
4
+ /**
5
+ * Attaches `error`, `reconnecting` and `ready` listeners that log a redis client's connection problems.
6
+ *
7
+ * A node-redis client emits `error` whenever its socket fails (e.g. the server restarts or the network drops). An
8
+ * `EventEmitter` with no `error` listener throws on that event, which crashes the whole process. With a listener,
9
+ * the client's own reconnect strategy (exponential backoff, retried indefinitely by default) takes over and pending
10
+ * subscriptions are restored once it reconnects.
11
+ *
12
+ * To avoid flooding the log while a server is unreachable, the first error of an outage is logged as an error and
13
+ * the following ones as debug messages, until the client is `ready` again. Every client this library creates, and
14
+ * every `duplicate()` of one, must be passed through this function. Calling it again on the same client does nothing.
15
+ *
16
+ * @param client The redis client to attach the listeners to. Clients without an `on()` method are ignored.
17
+ * @param logger The logger to report errors to.
18
+ * @param name A name for the client, used in log messages.
19
+ * @returns The given client.
20
+ */
21
+ export declare function attachRedisErrorHandler<T>(client: T, logger: any, name: string): T;
4
22
  /**
5
23
  * Provides database connection management.
6
24
  *
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Determines whether `err` is a unique/primary key violation reported by any of the supported database drivers:
3
+ * - MongoDB: code `11000` (or `11001`);
4
+ * - PostgreSQL: SQLSTATE `23505`;
5
+ * - MySQL/MariaDB: `ER_DUP_ENTRY` (errno `1062`);
6
+ * - SQLite (`better-sqlite3`/`sqlite3`): `SQLITE_CONSTRAINT_UNIQUE`/`SQLITE_CONSTRAINT_PRIMARYKEY`, or a generic
7
+ * `SQLITE_CONSTRAINT` whose message is a `UNIQUE constraint failed` error.
8
+ *
9
+ * @param err The error to inspect.
10
+ */
11
+ export declare function isDuplicateKeyError(err: any): boolean;
12
+ /**
13
+ * Best-effort extraction of the field names involved in a duplicate key error, for telling a primary key/version
14
+ * clash apart from a clash on some other unique column. Returns `undefined` when the driver's error doesn't say.
15
+ * The special value `"PRIMARY"` is returned when the driver only names the primary key (MySQL, a PostgreSQL `PK_`
16
+ * constraint).
17
+ */
18
+ export declare function duplicateKeyFields(err: any): string[] | undefined;
19
+ /**
20
+ * Determines whether a duplicate key error is a clash on a record's identity - its `_id`/primary key or its
21
+ * `(uid, version)` pair - rather than on some other unique column.
22
+ *
23
+ * @param err The duplicate key error.
24
+ * @param fallback The answer to give when the driver's error doesn't name the fields involved.
25
+ */
26
+ export declare function isIdentityDuplicate(err: any, fallback: boolean): boolean;
@@ -1,4 +1,4 @@
1
- import type { Abortable, AggregateOptions, AggregationCursor, Collection, CountDocumentsOptions, Db, DeleteOptions, DeleteResult, DistinctOptions, Document, Filter, FindCursor, FindOneAndDeleteOptions, FindOptions, InsertOneOptions, ReplaceOptions, UpdateOptions, UpdateResult } from "mongodb";
1
+ import type { Abortable, AggregateOptions, AggregationCursor, Collection, CountDocumentsOptions, Db, DeleteOptions, DeleteResult, DistinctOptions, Document, Filter, FindCursor, FindOneAndDeleteOptions, FindOneAndUpdateOptions, FindOptions, InsertOneOptions, ReplaceOptions, UpdateOptions, UpdateResult } from "mongodb";
2
2
  /**
3
3
  * Provides a lightweight repository for performing common operations against a single MongoDB collection using the
4
4
  * native `mongodb` driver. Instances of this class are obtained via `MongoConnection.getRepository`.
@@ -93,13 +93,32 @@ export declare class MongoRepository<T extends Document = any> {
93
93
  * a plain object) without preserving `_id`, where the default `_id`-only behavior would insert a *second*,
94
94
  * duplicate document instead of updating the existing one (see `ACLUtils.saveACL()`).
95
95
  *
96
+ * Pass `insertOnly: true` to always insert, even when `doc` carries an `_id`: the document is inserted with
97
+ * that `_id`, and the driver's duplicate-key error (code `11000`) is thrown if a document with it already
98
+ * exists. Use it for any create-style write whose `_id` may have come from untrusted input, where the default
99
+ * `_id`-present behavior would silently replace whichever existing document owns that `_id` (see
100
+ * `RepoUtils.create()`). Takes precedence over `mergeByUid`.
101
+ *
96
102
  * @param doc The document to save.
97
- * @param options Driver options, plus optionally `mergeByUid` (see above).
103
+ * @param options Driver options, plus optionally `mergeByUid` or `insertOnly` (see above).
98
104
  * @returns The saved document.
99
105
  */
100
106
  save(doc: any, options?: (InsertOneOptions | ReplaceOptions) & {
101
107
  mergeByUid?: boolean;
108
+ insertOnly?: boolean;
102
109
  }): Promise<T>;
110
+ /**
111
+ * Atomically finds the first document matching the given filter, applies the given update operations to it and
112
+ * returns the resulting document (or `null` if none matched). By default the document is returned as it was
113
+ * *after* the update; pass `returnDocument: "before"` to get the original. A single round trip, so a caller that
114
+ * needs the exact document its own update produced isn't racing a separate follow-up read against a concurrent
115
+ * write to the same document (see `RepoUtils.update()`).
116
+ *
117
+ * @param filter The query filter to match documents against.
118
+ * @param update The update operations (e.g. `$set`) to apply.
119
+ * @param options - Optional settings for the command
120
+ */
121
+ findOneAndUpdate(filter: any, update: any, options?: FindOneAndUpdateOptions): Promise<T | null>;
103
122
  /**
104
123
  * Updates all documents matching the given filter with the provided update operations.
105
124
  *
@@ -1,5 +1,6 @@
1
1
  export * from "./ConnectionKinds.js";
2
2
  export * from "./ConnectionManager.js";
3
+ export * from "./DatabaseErrors.js";
3
4
  export * from "./MongoConnection.js";
4
5
  export * from "./MongoRepository.js";
5
6
  export * from "./MongoSchemaSync.js";
@@ -33,7 +33,38 @@ export interface ColumnOptions {
33
33
  * a complex/nested structure that has no direct SQL column equivalent (e.g. `"simple-json"`).
34
34
  */
35
35
  type?: string | Function;
36
+ /**
37
+ * The column's database default, used for rows inserted without a value and, importantly, to fill existing
38
+ * rows when `synchronize` adds a new `NOT NULL` column to a table that already has data (without a default that
39
+ * fails with a `NOT NULL constraint failed` error). A function returns a raw SQL expression, e.g.
40
+ * `() => "CURRENT_TIMESTAMP"`. (SQL only; on MongoDB use a property initializer instead.)
41
+ */
42
+ default?: string | number | boolean | null | (() => string);
43
+ /** The maximum length of a string column, e.g. `varchar(255)`. (SQL only) */
44
+ length?: number | string;
45
+ /**
46
+ * Set to `true` to enforce that every value of the column is unique. Equivalent to also applying `@Unique()` to
47
+ * the property, so it creates a unique index on both SQL and MongoDB.
48
+ */
49
+ unique?: boolean;
50
+ /** The total number of digits stored by a `decimal`/`numeric` column. (SQL only) */
51
+ precision?: number;
52
+ /** The number of digits after the decimal point stored by a `decimal`/`numeric` column. (SQL only) */
53
+ scale?: number;
54
+ /** Set to `true` to declare an array column. Only supported by PostgreSQL and CockroachDB. (SQL only) */
55
+ array?: boolean;
56
+ /** The allowed values of an `enum`/`simple-enum` column. (SQL only) */
57
+ enum?: (string | number)[] | object;
58
+ /** Set to `true` to declare a numeric column `UNSIGNED`. Only supported by MySQL/MariaDB. (SQL only) */
59
+ unsigned?: boolean;
60
+ /** A comment stored with the column definition, where the database supports it. (SQL only) */
61
+ comment?: string;
36
62
  }
63
+ /**
64
+ * The `ColumnOptions` keys that are forwarded to TypeORM verbatim when set. `unique` is not among them: it is
65
+ * registered as a unique index instead (see `Column`), so both backends enforce it the same way.
66
+ */
67
+ export declare const FORWARDED_SQL_COLUMN_OPTIONS: readonly ["name", "nullable", "default", "length", "precision", "scale", "array", "enum", "unsigned", "comment"];
37
68
  /**
38
69
  * The set of options to use when declaring a persistent class type via the `@Entity` decorator.
39
70
  */
@@ -146,8 +146,10 @@ export interface RateLimitOptions extends RateLimitConfig {
146
146
  * endpoints in the class will have rate limiting applied.
147
147
  *
148
148
  * Optionally pass in options to control the identifier of the rate limit. Set `options.id` to set an explicit identifier.
149
- * By default, the method and path of the request (without the query portion) is used as the identifier. Set
150
- * the `options.perUser` option to `true` to have the limit scoped to each user individually.
149
+ * By default, the method and the percent-decoded, normalized path of the request (without the query portion) is used
150
+ * as the identifier. Set the `options.perUser` option to `true` to have the limit scoped to each user individually;
151
+ * anonymous callers are then scoped by client IP address (see `trusted_proxies`). Calling `@RateLimit()` with no
152
+ * options enables `perUser`. See `RouteUtils.checkRateLimiter()`.
151
153
  */
152
154
  export declare function RateLimit(options?: RateLimitOptions): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => void;
153
155
  /**
@@ -1,4 +1,4 @@
1
- import type { IHttpRouter, RequestHandler } from "../types.js";
1
+ import { type IHttpRouter, type RequestHandler, type WebSocketOptions } from "../types.js";
2
2
  import { type WsUpgradeAuth } from "../MiddlewareChain.js";
3
3
  /**
4
4
  * Bun-native HTTP/WebSocket router built on `Bun.serve()`. Exposes the same public surface as the
@@ -21,6 +21,8 @@ export declare class BunRouter implements IHttpRouter {
21
21
  * global CORS middleware so a real app-defined `OPTIONS` handler gets a chance to run). */
22
22
  private readonly explicitOptionsPaths;
23
23
  private readonly maxBodySize;
24
+ /** The HTTP requests currently being handled. */
25
+ private inFlight;
24
26
  private readonly sslConfig;
25
27
  private _bunServer;
26
28
  /** The port the server is currently listening on (set after a successful `listen()` call). */
@@ -59,8 +61,18 @@ export declare class BunRouter implements IHttpRouter {
59
61
  * is aborted. If it returns `{ user, ... }`, those credentials are attached to the request so
60
62
  * downstream middleware sees an authenticated user. If it returns `{}`, auth falls through to
61
63
  * the post-upgrade message-based LOGIN flow.
64
+ *
65
+ * `wsOptions` uses the uWS option names (`maxPayloadLength`, `idleTimeout`, `maxBackpressure`) over
66
+ * `DEFAULT_WS_OPTIONS`. `Bun.serve()` only accepts one WebSocket configuration for the whole server, so
67
+ * `maxPayloadLength` is additionally enforced per route (a larger message closes the socket with 1009), while
68
+ * `idleTimeout` and `maxBackpressure` are applied server-wide using the largest value any route registered.
69
+ */
70
+ ws(routePath: string, handlers: RequestHandler[], wsOptions?: WebSocketOptions, upgradeAuth?: WsUpgradeAuth): this;
71
+ /**
72
+ * Builds the server-wide `Bun.serve()` WebSocket limits from every registered WebSocket route: the largest value
73
+ * of each option, or `DEFAULT_WS_OPTIONS` when no route is registered.
62
74
  */
63
- ws(routePath: string, handlers: RequestHandler[], _wsOptions?: any, upgradeAuth?: WsUpgradeAuth): this;
75
+ private buildWebSocketLimits;
64
76
  /**
65
77
  * Matches a request against the route table. Exact-length static/param routes always outrank
66
78
  * wildcard-suffixed routes, matching uWS's `best ?? wildcard` fallback behavior. Among wildcard
@@ -84,5 +96,14 @@ export declare class BunRouter implements IHttpRouter {
84
96
  listen(host: string, port: number): Promise<void>;
85
97
  /** Stops the server, closing the listen socket. */
86
98
  close(): void;
99
+ /** The number of HTTP requests currently being handled. */
100
+ get inFlightRequests(): number;
101
+ /**
102
+ * Stops accepting new connections, waits up to `timeoutMs` for in-flight HTTP requests to finish, then closes
103
+ * every remaining connection (keep-alive and WebSocket connections included).
104
+ *
105
+ * @param timeoutMs The maximum time to wait for in-flight requests, in milliseconds.
106
+ */
107
+ shutdown(timeoutMs?: number): Promise<void>;
87
108
  [key: string]: any;
88
109
  }
@@ -1,4 +1,5 @@
1
- export type { ErrorHandler, HttpRequest, HttpResponse, IHttpRouter, NextFunction, RequestHandler } from "./types.js";
1
+ export type { ErrorHandler, HttpRequest, HttpResponse, IHttpRouter, NextFunction, RequestHandler, WebSocketOptions, } from "./types.js";
2
+ export { DEFAULT_WS_OPTIONS } from "./types.js";
2
3
  export { isBunRuntime } from "./RuntimeDetect.js";
3
4
  export type { IWebSocketShim } from "./IWebSocketShim.js";
4
5
  export { extractParamNames, makeWsStubResponse, runChain } from "./MiddlewareChain.js";
@@ -2,9 +2,19 @@ import type { RequestHandler } from "../types.js";
2
2
  import type { SessionManager } from "./SessionManager.js";
3
3
  /**
4
4
  * Builds a global, framework-agnostic session middleware backed by the given `SessionManager`.
5
- * Lazily loads-or-creates `req.session` and persists any mutations back to the store once the
6
- * response finishes. Cheap for requests that never touch sessions: no store round-trip unless a
7
- * valid session cookie was actually sent, and no `Set-Cookie`/store write for sessions that end up
8
- * empty.
5
+ *
6
+ * `req.session` is always an object, and `req.sessionIsNew` tells whether it is backed by a stored session:
7
+ *
8
+ * - When the request carries a validly signed session cookie whose session still exists, `req.session` holds that
9
+ * session's data (`req.sessionIsNew === false`) and any non-empty data is saved back once the response finishes.
10
+ * - Otherwise `req.session` starts out as an empty, unsaved object (`req.sessionIsNew === true`). Nothing is stored
11
+ * and no cookie is sent unless a handler explicitly asks for a session by writing to it (setting, defining or
12
+ * deleting a property, or assigning a new object to `req.session`). The first such write issues a fresh session ID
13
+ * via `Set-Cookie`, and the data is saved once the response finishes. A write after the response headers were sent
14
+ * can no longer deliver the cookie, so it isn't saved.
15
+ *
16
+ * This keeps clients that never send cookies (e.g. API clients authenticating with a bearer token) from creating a
17
+ * new stored session on every request. Middleware that only records bookkeeping about the caller (last access time,
18
+ * IP address, ...) should skip `req.session` when `req.sessionIsNew` is `true`.
9
19
  */
10
20
  export declare function createSessionMiddleware(mgr: SessionManager): RequestHandler;
@@ -20,6 +20,12 @@ export interface HttpRequest {
20
20
  * session middleware is registered (i.e. the `session` config block is absent).
21
21
  */
22
22
  session?: Record<string, any>;
23
+ /**
24
+ * Set by the session middleware. `true` when `session` is not backed by a stored session yet (the request carried
25
+ * no valid session cookie): nothing is stored and no cookie is sent unless a handler writes to `session`.
26
+ * Bookkeeping-only writes (last access time, IP address, ...) should be skipped while this is `true`.
27
+ */
28
+ sessionIsNew?: boolean;
23
29
  /** Minimal socket interface; populated with remote address for IP extraction. */
24
30
  socket: {
25
31
  remoteAddress?: string;
@@ -30,6 +36,13 @@ export interface HttpRequest {
30
36
  authPayload?: any;
31
37
  /** Raw JWT token string, set by JWT auth middleware. */
32
38
  authToken?: string;
39
+ /**
40
+ * The registered route pattern that matched this request (e.g. `/items/:id`), set by the router before any
41
+ * middleware runs. `undefined` when no application route matched, i.e. the request is handled by the router's
42
+ * own not-found or CORS preflight fallback. Unlike `path`, the set of possible values is bounded by the
43
+ * application's routes, so it is safe to use as a metrics label.
44
+ */
45
+ routePattern?: string;
33
46
  /** Allow arbitrary per-request properties (e.g. req.websocket, req.wsHandled). */
34
47
  [key: string]: any;
35
48
  }
@@ -92,8 +105,35 @@ export interface IHttpRouter {
92
105
  hasExplicitOptionsRoute(path: string): boolean;
93
106
  ws(path: string, handlers: RequestHandler[], wsOptions?: any, upgradeAuth?: any): this;
94
107
  listen(host: string, port: number): Promise<void>;
108
+ /** Stops accepting new connections. Connections that are already open (including keep-alive and WebSocket
109
+ * connections) are left alone. */
95
110
  close(): void;
111
+ /**
112
+ * Gracefully shuts the server down: stops accepting new connections, waits up to `timeoutMs` milliseconds for
113
+ * HTTP requests that are still being handled to finish, then force-closes every remaining connection
114
+ * (keep-alive and WebSocket connections included). Optional, so custom routers without it keep working;
115
+ * `Server.stop()` falls back to `close()` for those.
116
+ */
117
+ shutdown?(timeoutMs: number): Promise<void>;
96
118
  readonly isListening: boolean;
97
119
  listenPort: number;
98
120
  [key: string]: any;
99
121
  }
122
+ /**
123
+ * WebSocket options shared by every router implementation. Names follow uWebSockets.js' `WebSocketBehavior`.
124
+ */
125
+ export interface WebSocketOptions {
126
+ /** Maximum size of a single incoming message, in bytes. A larger message closes the connection. */
127
+ maxPayloadLength?: number;
128
+ /** Seconds without any received data after which the connection is closed. `0` disables the timeout. */
129
+ idleTimeout?: number;
130
+ /** Maximum number of bytes that may be queued for sending before further messages are dropped. */
131
+ maxBackpressure?: number;
132
+ [key: string]: any;
133
+ }
134
+ /**
135
+ * The WebSocket defaults used by both the uWS and Bun routers when a route doesn't override them. These match
136
+ * uWebSockets.js' own defaults, so a route behaves the same on both runtimes (Bun's own defaults are far larger,
137
+ * e.g. a 16 MiB payload limit).
138
+ */
139
+ export declare const DEFAULT_WS_OPTIONS: Readonly<Required<Pick<WebSocketOptions, "maxPayloadLength" | "idleTimeout" | "maxBackpressure">>>;
@@ -2,7 +2,16 @@ import type { HttpRequest, HttpResponse } from "../types.js";
2
2
  import type { HttpRequest as UWSHttpRequest, HttpResponse as UWSHttpResponse } from "uWebSockets.js";
3
3
  /** Parses a `cookie` header string into a key/value map. */
4
4
  export declare function parseCookies(cookieHeader: string): Record<string, string>;
5
- /** Parses a URL query string (without leading `?`) into a key/value map. */
5
+ /**
6
+ * Parses a URL query string (without leading `?`) into a key/value map. A key given more than once becomes an array
7
+ * of its values.
8
+ *
9
+ * The result is a plain object, so spreading it and calling `Object.prototype` methods through it behave as usual.
10
+ * Only own properties are considered when detecting a repeated key, so a key like `hasOwnProperty` or `constructor`
11
+ * is stored as a normal value instead of being merged with the inherited function. `__proto__` keys are dropped:
12
+ * assigning one would replace the object's prototype, and even as an own property it would do so again when the
13
+ * object is later copied with `Object.assign()` or a deep merge.
14
+ */
6
15
  export declare function parseQueryString(qs: string): Record<string, string | string[]>;
7
16
  /**
8
17
  * Parses a raw request body Buffer according to its content-type header. Shared by both the
@@ -1,5 +1,5 @@
1
1
  import uWS from "uWebSockets.js";
2
- import type { IHttpRouter, RequestHandler } from "../types.js";
2
+ import { type IHttpRouter, type RequestHandler, type WebSocketOptions } from "../types.js";
3
3
  import { type WsUpgradeAuth } from "../MiddlewareChain.js";
4
4
  export { runChain, extractParamNames } from "../MiddlewareChain.js";
5
5
  export type { WsUpgradeAuth, WsUpgradeAuthResult } from "../MiddlewareChain.js";
@@ -40,6 +40,8 @@ export declare class HttpRouter implements IHttpRouter {
40
40
  private readonly explicitOptionsPaths;
41
41
  /** Maximum accepted request body size, in bytes. */
42
42
  private readonly maxBodySize;
43
+ /** The HTTP requests currently being handled. */
44
+ private readonly inFlight;
43
45
  constructor(uwsApp: uWS.TemplatedApp, maxBodySize?: number);
44
46
  /** Returns `true` if the server is currently listening. */
45
47
  get isListening(): boolean;
@@ -51,6 +53,8 @@ export declare class HttpRouter implements IHttpRouter {
51
53
  * everything added after is "post-route" (error handlers, metrics, etc.).
52
54
  */
53
55
  private capturePreRouteCount;
56
+ /** Builds the uWS handler for a route registered at `routePattern` (`undefined` for the router's own fallbacks). */
57
+ private makeHandler;
54
58
  get(routePath: string, ...handlers: RequestHandler[]): this;
55
59
  post(routePath: string, ...handlers: RequestHandler[]): this;
56
60
  put(routePath: string, ...handlers: RequestHandler[]): this;
@@ -77,7 +81,7 @@ export declare class HttpRouter implements IHttpRouter {
77
81
  *
78
82
  * Both `path` and `path + "/"` are registered to avoid trailing-slash mismatch.
79
83
  */
80
- ws(routePath: string, handlers: RequestHandler[], wsOptions?: Partial<uWS.WebSocketBehavior<any>>, upgradeAuth?: WsUpgradeAuth): this;
84
+ ws(routePath: string, handlers: RequestHandler[], wsOptions?: Partial<uWS.WebSocketBehavior<any>> | WebSocketOptions, upgradeAuth?: WsUpgradeAuth): this;
81
85
  /**
82
86
  * Starts listening on the given host and port.
83
87
  * Resolves when the server is ready; rejects if the port cannot be bound.
@@ -85,5 +89,14 @@ export declare class HttpRouter implements IHttpRouter {
85
89
  listen(host: string, port: number): Promise<void>;
86
90
  /** Closes the listen socket, stopping the server from accepting new connections. */
87
91
  close(): void;
92
+ /** The number of HTTP requests currently being handled. */
93
+ get inFlightRequests(): number;
94
+ /**
95
+ * Stops accepting new connections, waits up to `timeoutMs` for in-flight HTTP requests to finish, then closes
96
+ * every remaining connection (keep-alive and WebSocket connections included).
97
+ *
98
+ * @param timeoutMs The maximum time to wait for in-flight requests, in milliseconds.
99
+ */
100
+ shutdown(timeoutMs?: number): Promise<void>;
88
101
  [key: string]: any;
89
102
  }
@@ -15,6 +15,30 @@ export interface PredicateNode {
15
15
  field: string;
16
16
  op: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "range" | "like" | "regex" | "exists";
17
17
  value: unknown;
18
+ /**
19
+ * Set to `true` to use a string `value` (or each string element of an `in`/`nin`/`range` array) exactly as
20
+ * given, skipping `me` substitution, the `null` literal and declared-type coercion. Only the hidden-operator
21
+ * check still applies. See `ModelUtils.literal`.
22
+ */
23
+ literal?: boolean;
24
+ }
25
+ /** The comparison operators a `QueryLiteral` may carry. */
26
+ export type LiteralOperator = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "range";
27
+ /**
28
+ * A search query value that is compared exactly as given rather than parsed as `op(value)` syntax. Create one via
29
+ * `ModelUtils.literal()`. Only code can produce one: a client's query string or `q` JSON can only ever yield plain
30
+ * strings, arrays and objects.
31
+ */
32
+ export declare class QueryLiteral {
33
+ readonly op: LiteralOperator;
34
+ readonly value: unknown;
35
+ constructor(value: unknown, op?: LiteralOperator);
36
+ /**
37
+ * Serializes under a `$`-prefixed key so a query containing a literal hashes (e.g. for `RepoUtils`' result
38
+ * cache) differently from any plain value, and so a client echoing the same JSON back is rejected by the
39
+ * query builders' operator-injection guard instead of being mistaken for a literal.
40
+ */
41
+ toJSON(): any;
18
42
  }
19
43
  /**
20
44
  * A boolean grouping node in a search query AST: combines child nodes with `and`/`or`, optionally negated.
@@ -43,6 +67,34 @@ export declare class ModelUtils {
43
67
  private static idPropertyCache;
44
68
  private static readOnlyPropertyCache;
45
69
  private static columnTypeCache;
70
+ /** Sequence used to give every `Raw()` SQL expression's named parameter a unique name within one query. */
71
+ private static rawParamSeq;
72
+ /**
73
+ * Marks `value` as a literal search value for `buildSearchQuery()` (and so `RepoUtils.find/count/truncate`),
74
+ * so it is compared exactly as given instead of being parsed as `op(value)` syntax. Use this whenever a query
75
+ * value comes from outside the code (a client, an email header, an iCalendar UID, a display name): a raw string
76
+ * such as `ne(x)` would otherwise be read as an operator, `Support(EU)` would be rejected as an unknown
77
+ * operator, `me`/`null` would be substituted, and a comma inside an `in()` list would split the value.
78
+ *
79
+ * The value is not type-coerced, so pass it with the column's real type (number, boolean, `Date`, string).
80
+ * `null` still compiles to `IS NULL` on SQL. Object values are still checked for hidden `$`/dotted keys.
81
+ *
82
+ * ```
83
+ * repoUtils.find({ messageId: ModelUtils.literal(header) });
84
+ * repoUtils.find({ uid: ModelUtils.literal(["a,b", "c"], "in") });
85
+ * repoUtils.find({ name: ModelUtils.literal(displayName, "ne") });
86
+ * repoUtils.find({ size: ModelUtils.literal([10, 20], "range") });
87
+ * ```
88
+ *
89
+ * @param value The value to compare against. An array for `in`, `nin` and `range` (exactly two elements).
90
+ * @param op The comparison to apply. Defaults to `eq`.
91
+ */
92
+ static literal(value: unknown, op?: LiteralOperator): QueryLiteral;
93
+ /**
94
+ * Splits the operand of a list operator (`in()`, `nin()`, `range()`) on unescaped commas. `\,` yields a literal
95
+ * comma and `\\` a literal backslash; a backslash before any other character (or at the end) is kept as is.
96
+ */
97
+ private static splitListOperand;
46
98
  /**
47
99
  * Provides the `typeorm` module to use when building SQL queries. This is called automatically when a SQL
48
100
  * datasource connection is established.
@@ -140,7 +192,7 @@ export declare class ModelUtils {
140
192
  * Coerces an already-typed AST predicate value (see `QueryNode`): a string operand is routed through
141
193
  * `coerceOperand` (type coercion, `me` substitution, injection guard) exactly like the flat `op(value)`
142
194
  * form; any other value is assumed to already be correctly typed by the caller and is only checked for a
143
- * hidden operator/dotted key.
195
+ * hidden operator/dotted key. With `literal` set, a string operand is kept exactly as given too.
144
196
  */
145
197
  private static coerceNodeValue;
146
198
  /**
@@ -238,6 +290,28 @@ export declare class ModelUtils {
238
290
  page: number;
239
291
  skip: number;
240
292
  };
293
+ /** Returns `true` for the boolean grouping keys (`$or`, `$and`) accepted in a flat search query. */
294
+ private static isGroupKey;
295
+ /**
296
+ * Validates the value of a `$or`/`$and` key: a non-empty array (at most `MAX_QUERY_NODES` long) of plain query
297
+ * objects. Anything else is a 400 on both backends. In particular an empty array is rejected rather than
298
+ * compiled: on SQL it used to expand to zero branches, which dropped the whole `where` (every other condition
299
+ * included) and matched every row, while MongoDB rejects `$or: []` outright.
300
+ */
301
+ private static assertQueryGroup;
302
+ /**
303
+ * ANDs two lists of OR-ed SQL `where` branches: `(L1 OR L2) AND (R1 OR R2)` becomes the cross product
304
+ * `(L1 AND R1) OR (L1 AND R2) OR ...`. Throws before allocating if the product exceeds `MAX_QUERY_NODES`.
305
+ */
306
+ private static andBranches;
307
+ /**
308
+ * ANDs two SQL `where` branch objects. A key present on only one side is copied; a key present on both keeps
309
+ * both conditions via TypeORM's `And()` (a plain object spread would let the right side silently replace the
310
+ * left, e.g. a forced scope key being overridden by a `$or` branch). Nested plain objects (embedded entities or
311
+ * relations) are merged recursively.
312
+ */
313
+ private static mergeWhereBranches;
314
+ private static andWhereValues;
241
315
  /**
242
316
  * Builds a query object for the given criteria and repository. Query params can have a value containing a
243
317
  * conditional operator to apply for the search. The operator is encoded with the format `op(value)`. The following
@@ -257,6 +331,27 @@ export declare class ModelUtils {
257
331
  * When no operator is provided the comparison is evaluated as `eq`, unless `exactMatch` is `false`, in which
258
332
  * case a string-valued parameter is instead matched as a case-insensitive "contains" search.
259
333
  *
334
+ * Operand and escaping rules:
335
+ * * A value is only parsed as an operator when the WHOLE value has the shape `name(...)`. The operand is
336
+ * everything between the first `(` and the last `)`, verbatim: parentheses, commas, leading/trailing spaces,
337
+ * newlines and nested `op(...)` text included. So `eq(Support(EU))` matches `Support(EU)`, `eq( a,b )`
338
+ * matches ` a,b ` and `eq(ne(x))` matches `ne(x)`. The operator name is case-insensitive.
339
+ * * A bare value shaped like `name(...)` whose `name` is not a known operator is rejected with a 400; wrap it
340
+ * in `eq(...)` to match it literally.
341
+ * * `eq()`/`ne()` operands are then coerced like any operand: `me` resolves to the requesting user's uid, `null`
342
+ * matches a null value, and the value is converted to the column's declared type (number, boolean, date).
343
+ * With no column metadata a JSON/date heuristic is used instead.
344
+ * * `in()`, `nin()` and `range()` split their operand on commas. Write `\,` for a comma inside one value and
345
+ * `\\` for a backslash, e.g. `in(a\,b,c)` matches `a,b` or `c`. A backslash before any other character is
346
+ * kept as is.
347
+ * * `$or` and `$and` (only from programmatic queries or the `q` JSON parameter; a query string can't build them)
348
+ * take a non-empty array of sub-query objects that are ANDed with every other key, the same on both backends.
349
+ * A `$`-prefixed field name is rejected with a 400.
350
+ *
351
+ * Code that passes a value it doesn't control (an email header, an iCalendar UID, a display name, ...) should
352
+ * use `ModelUtils.literal(value)` instead of building an `eq(...)` string. A literal skips all of the parsing
353
+ * and coercion above. Other non-string values (numbers, booleans, `Date`, `null`) are also compared as given.
354
+ *
260
355
  * A repeated query parameter name (e.g. `?a=1&a=2`) OR-combines its values, "zipped" positionally against
261
356
  * every other repeated parameter rather than as a cartesian product: `?a=1&a=2&b=3&b=4` compiles to
262
357
  * `(a=1 AND b=3) OR (a=2 AND b=4)`, not `a IN (1,2)` and not all four combinations. A shorter array is padded