exisjs 0.5.9 → 0.5.11
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/auth/index.d.ts +3 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oauth/core.d.ts +96 -0
- package/dist/auth/oauth/core.d.ts.map +1 -0
- package/dist/auth/oauth/core.js +203 -0
- package/dist/auth/oauth/core.js.map +1 -0
- package/dist/auth/oauth/index.d.ts +45 -0
- package/dist/auth/oauth/index.d.ts.map +1 -0
- package/dist/auth/oauth/index.js +64 -0
- package/dist/auth/oauth/index.js.map +1 -0
- package/dist/auth/oauth/middleware.d.ts +63 -0
- package/dist/auth/oauth/middleware.d.ts.map +1 -0
- package/dist/auth/oauth/middleware.js +164 -0
- package/dist/auth/oauth/middleware.js.map +1 -0
- package/dist/auth/oauth/providers/custom.d.ts +41 -0
- package/dist/auth/oauth/providers/custom.d.ts.map +1 -0
- package/dist/auth/oauth/providers/custom.js +204 -0
- package/dist/auth/oauth/providers/custom.js.map +1 -0
- package/dist/auth/oauth/providers/discord.d.ts +23 -0
- package/dist/auth/oauth/providers/discord.d.ts.map +1 -0
- package/dist/auth/oauth/providers/discord.js +130 -0
- package/dist/auth/oauth/providers/discord.js.map +1 -0
- package/dist/auth/oauth/providers/facebook.d.ts +28 -0
- package/dist/auth/oauth/providers/facebook.d.ts.map +1 -0
- package/dist/auth/oauth/providers/facebook.js +124 -0
- package/dist/auth/oauth/providers/facebook.js.map +1 -0
- package/dist/auth/oauth/providers/github.d.ts +25 -0
- package/dist/auth/oauth/providers/github.d.ts.map +1 -0
- package/dist/auth/oauth/providers/github.js +132 -0
- package/dist/auth/oauth/providers/github.js.map +1 -0
- package/dist/auth/oauth/providers/google.d.ts +23 -0
- package/dist/auth/oauth/providers/google.d.ts.map +1 -0
- package/dist/auth/oauth/providers/google.js +134 -0
- package/dist/auth/oauth/providers/google.js.map +1 -0
- package/dist/auth/oauth/providers/microsoft.d.ts +28 -0
- package/dist/auth/oauth/providers/microsoft.d.ts.map +1 -0
- package/dist/auth/oauth/providers/microsoft.js +136 -0
- package/dist/auth/oauth/providers/microsoft.js.map +1 -0
- package/dist/auth/oauth/types.d.ts +158 -0
- package/dist/auth/oauth/types.d.ts.map +1 -0
- package/dist/auth/oauth/types.js +4 -0
- package/dist/auth/oauth/types.js.map +1 -0
- package/dist/auth/session.d.ts +4 -1
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +33 -5
- package/dist/auth/session.js.map +1 -1
- package/dist/cache/store.d.ts +11 -3
- package/dist/cache/store.d.ts.map +1 -1
- package/dist/cache/store.js +87 -19
- package/dist/cache/store.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +4 -6
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/database/adapters/mongodb.d.ts +58 -0
- package/dist/database/adapters/mongodb.d.ts.map +1 -0
- package/dist/database/adapters/mongodb.js +264 -0
- package/dist/database/adapters/mongodb.js.map +1 -0
- package/dist/database/adapters/mysql.d.ts +27 -0
- package/dist/database/adapters/mysql.d.ts.map +1 -0
- package/dist/database/adapters/mysql.js +188 -0
- package/dist/database/adapters/mysql.js.map +1 -0
- package/dist/database/adapters/postgres.d.ts +28 -0
- package/dist/database/adapters/postgres.d.ts.map +1 -0
- package/dist/database/adapters/postgres.js +174 -0
- package/dist/database/adapters/postgres.js.map +1 -0
- package/dist/database/adapters/sqlite.d.ts +27 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +149 -0
- package/dist/database/adapters/sqlite.js.map +1 -0
- package/dist/database/index.d.ts +42 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +58 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/manager.d.ts +114 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/manager.js +269 -0
- package/dist/database/manager.js.map +1 -0
- package/dist/database/migration.d.ts +57 -0
- package/dist/database/migration.d.ts.map +1 -0
- package/dist/database/migration.js +329 -0
- package/dist/database/migration.js.map +1 -0
- package/dist/database/query-builder.d.ts +89 -0
- package/dist/database/query-builder.d.ts.map +1 -0
- package/dist/database/query-builder.js +353 -0
- package/dist/database/query-builder.js.map +1 -0
- package/dist/database/types.d.ts +184 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +4 -0
- package/dist/database/types.js.map +1 -0
- package/dist/docs/ARCHITECTURE.md +349 -0
- package/dist/docs/BENCHMARKS.md +36 -0
- package/dist/docs/README.md +34 -0
- package/dist/docs/content/README.md +44 -0
- package/dist/docs/content/adapters.mdx +199 -0
- package/dist/docs/content/cache.mdx +239 -0
- package/dist/docs/content/circuit-breaker.mdx +130 -0
- package/dist/docs/content/cli/commands.mdx +214 -0
- package/dist/docs/content/cli/overview.mdx +92 -0
- package/dist/docs/content/cli/usage.mdx +61 -0
- package/dist/docs/content/config.mdx +214 -0
- package/dist/docs/content/controllers.mdx +241 -0
- package/dist/docs/content/cron.mdx +109 -0
- package/dist/docs/content/database.mdx +209 -0
- package/dist/docs/content/dataloader.mdx +170 -0
- package/dist/docs/content/decorators.mdx +204 -0
- package/dist/docs/content/errors.mdx +344 -0
- package/dist/docs/content/gateways.mdx +376 -0
- package/dist/docs/content/installation.mdx +210 -0
- package/dist/docs/content/introduction.mdx +96 -0
- package/dist/docs/content/middleware.mdx +441 -0
- package/dist/docs/content/modules.mdx +144 -0
- package/dist/docs/content/oauth.mdx +179 -0
- package/dist/docs/content/providers.mdx +147 -0
- package/dist/docs/content/queues.mdx +177 -0
- package/dist/docs/content/requests.mdx +293 -0
- package/dist/docs/content/responses.mdx +271 -0
- package/dist/docs/content/routing.mdx +541 -0
- package/dist/docs/content/rpc.mdx +146 -0
- package/dist/docs/content/security.mdx +294 -0
- package/dist/docs/content/sse.mdx +159 -0
- package/dist/docs/content/structure.mdx +116 -0
- package/dist/docs/content/testing.mdx +139 -0
- package/dist/docs/content/validation.mdx +249 -0
- package/dist/docs/content/websockets.mdx +289 -0
- package/dist/docs/integrations/README.md +23 -0
- package/dist/docs/integrations/jwt.mdx +151 -0
- package/dist/docs/integrations/mongo.mdx +183 -0
- package/dist/docs/integrations/observability.mdx +125 -0
- package/dist/docs/integrations/openai.mdx +108 -0
- package/dist/docs/integrations/postgres.mdx +190 -0
- package/dist/docs/integrations/posthog.mdx +122 -0
- package/dist/docs/integrations/redis.mdx +141 -0
- package/dist/docs/integrations/resend.mdx +117 -0
- package/dist/docs/integrations/s3.mdx +122 -0
- package/dist/docs/integrations/swagger.mdx +113 -0
- package/dist/docs/reference/README.md +3 -0
- package/dist/docs/reference/adapters.mdx +130 -0
- package/dist/docs/reference/auth.mdx +183 -0
- package/dist/docs/reference/cache.mdx +132 -0
- package/dist/docs/reference/circuit-breaker.mdx +71 -0
- package/dist/docs/reference/config.mdx +118 -0
- package/dist/docs/reference/context.mdx +168 -0
- package/dist/docs/reference/dataloader.mdx +91 -0
- package/dist/docs/reference/decorators.mdx +293 -0
- package/dist/docs/reference/di.mdx +123 -0
- package/dist/docs/reference/error.mdx +170 -0
- package/dist/docs/reference/exis.mdx +124 -0
- package/dist/docs/reference/middleware.mdx +304 -0
- package/dist/docs/reference/plugin.mdx +113 -0
- package/dist/docs/reference/queue.mdx +155 -0
- package/dist/docs/reference/router.mdx +248 -0
- package/dist/docs/reference/sanitize.mdx +130 -0
- package/dist/docs/reference/storage.mdx +116 -0
- package/dist/docs/reference/swagger.mdx +132 -0
- package/dist/docs/reference/testing.mdx +145 -0
- package/dist/docs/reference/validator.mdx +186 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/backpressure.d.ts.map +1 -1
- package/dist/middleware/backpressure.js +21 -12
- package/dist/middleware/backpressure.js.map +1 -1
- package/dist/middleware/cache.d.ts +6 -0
- package/dist/middleware/cache.d.ts.map +1 -1
- package/dist/middleware/cache.js +84 -28
- package/dist/middleware/cache.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +4 -1
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +29 -4
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/ip-filter.d.ts.map +1 -1
- package/dist/middleware/ip-filter.js +16 -0
- package/dist/middleware/ip-filter.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +29 -12
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/queue/drivers/MemoryDriver.d.ts +6 -3
- package/dist/queue/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/queue/drivers/MemoryDriver.js +76 -23
- package/dist/queue/drivers/MemoryDriver.js.map +1 -1
- package/dist/server/bootstrapper.d.ts +3 -0
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +56 -8
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/bun-adapter.d.ts +2 -0
- package/dist/server/bun-adapter.d.ts.map +1 -1
- package/dist/server/bun-adapter.js +23 -12
- package/dist/server/bun-adapter.js.map +1 -1
- package/dist/server/request-handler.d.ts +0 -2
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +2 -24
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +24 -11
- package/dist/server/request.js.map +1 -1
- package/dist/server/uws-adapter.d.ts +80 -0
- package/dist/server/uws-adapter.d.ts.map +1 -0
- package/dist/server/uws-adapter.js +357 -0
- package/dist/server/uws-adapter.js.map +1 -0
- package/dist/server/ws-orchestrator.d.ts +1 -0
- package/dist/server/ws-orchestrator.d.ts.map +1 -1
- package/dist/server/ws-orchestrator.js +83 -0
- package/dist/server/ws-orchestrator.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/validator/tex.d.ts +21 -16
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js.map +1 -1
- package/package.json +11 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Testing"
|
|
3
|
+
description: "Strict API Reference for all Testing utilities and mocking objects."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing (`exisjs/testing`)
|
|
7
|
+
|
|
8
|
+
This document provides the strict API signatures and TypeScript interfaces exported by the `exisjs/testing` module. For high-level testing guides, see the [Testing Guide](/docs/testing).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Native Node Runner Exports
|
|
13
|
+
|
|
14
|
+
ExisJS fully re-exports the native `node:test` runner methods, ensuring maximum IDE compatibility while providing powerful internal enhancements (like `.each()`).
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
export function test(name: string, fn?: TestFunction): void
|
|
18
|
+
export function test(name: string, options: TestOptions, fn?: TestFunction): void
|
|
19
|
+
|
|
20
|
+
export function describe(name: string, fn?: TestFunction): void
|
|
21
|
+
export function it(name: string, fn?: TestFunction): void
|
|
22
|
+
|
|
23
|
+
export function before(fn: HookFunction): void
|
|
24
|
+
export function after(fn: HookFunction): void
|
|
25
|
+
export function beforeEach(fn: HookFunction): void
|
|
26
|
+
export function afterEach(fn: HookFunction): void
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Parametrized Tests (`.each`)
|
|
30
|
+
All test blocks (`test`, `describe`, `it`) support a Jest-like `.each()` signature.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
test.each([
|
|
34
|
+
[1, 1, 2],
|
|
35
|
+
[2, 3, 5]
|
|
36
|
+
])('adds %d and %d to equal %d', (a, b, expected) => {
|
|
37
|
+
assert.equal(a + b, expected)
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Context Management
|
|
44
|
+
|
|
45
|
+
### `createTestContext`
|
|
46
|
+
Automatically handles booting an ExisJS `App`, processing its startup hooks, and safely closing all database and worker connections. Returns a `TestApp` client for performing HTTP requests.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
function createTestContext(app: App | typeof Class): TestApp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `registerCleanup`
|
|
53
|
+
Allows third-party plugins or custom test setups to register a global cleanup function that will be executed when the test runner terminates.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
function registerCleanup(fn: () => Promise<void> | void): void
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## HTTP Client (TestApp & TestRequest)
|
|
62
|
+
|
|
63
|
+
The `TestApp` interface provides a fluent API for making HTTP requests directly against your application without binding to a network port.
|
|
64
|
+
|
|
65
|
+
### `TestApp`
|
|
66
|
+
```typescript
|
|
67
|
+
interface TestApp {
|
|
68
|
+
get(path: string): TestRequest
|
|
69
|
+
post(path: string): TestRequest
|
|
70
|
+
put(path: string): TestRequest
|
|
71
|
+
patch(path: string): TestRequest
|
|
72
|
+
delete(path: string): TestRequest
|
|
73
|
+
options(path: string): TestRequest
|
|
74
|
+
head(path: string): TestRequest
|
|
75
|
+
request(method: string, path: string): TestRequest
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `TestRequest`
|
|
80
|
+
Supports chaining headers, bodies, and assertions. Once `.execute()` is called (or awaited), it returns a `TestResponse`.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
class TestRequest {
|
|
84
|
+
send(body: unknown): this
|
|
85
|
+
set(header: string, value: string): this
|
|
86
|
+
|
|
87
|
+
// Assertions
|
|
88
|
+
expect(status: number): this
|
|
89
|
+
expect(header: string, value: string | RegExp): this
|
|
90
|
+
expect(body: unknown): this
|
|
91
|
+
|
|
92
|
+
// Execution
|
|
93
|
+
execute(): Promise<TestResponse>
|
|
94
|
+
then<T>(onfulfilled: (res: TestResponse) => T): Promise<T>
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `TestResponse`
|
|
99
|
+
```typescript
|
|
100
|
+
interface TestResponse {
|
|
101
|
+
status: number
|
|
102
|
+
headers: Record<string, string | string[] | undefined>
|
|
103
|
+
body: any // The parsed JSON payload (if applicable)
|
|
104
|
+
text: string // The raw string response
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Mocks & Spies (`ex`)
|
|
111
|
+
|
|
112
|
+
The `ex` object provides a Jest-compatible API wrapper over the native `node:test` mocking utilities.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const ex = {
|
|
116
|
+
// Spies
|
|
117
|
+
fn: (impl?: Function) => MockFunction,
|
|
118
|
+
spyOn: (obj: any, methodName: string) => MockFunction,
|
|
119
|
+
|
|
120
|
+
// Modules
|
|
121
|
+
mock: (specifier: string, factory?: () => any) => void,
|
|
122
|
+
|
|
123
|
+
// Timers
|
|
124
|
+
setSystemTime: (time: number | Date) => void,
|
|
125
|
+
advanceTimersByTime: (ms: number) => void,
|
|
126
|
+
useFakeTimers: () => void,
|
|
127
|
+
useRealTimers: () => void,
|
|
128
|
+
|
|
129
|
+
clearAllMocks: () => void
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `MockFunction` Extensions
|
|
134
|
+
ExisJS extends the native `MockFunction` returned by `ex.fn()` and `ex.spyOn()` with Jest-like helpers:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
interface MockFunction {
|
|
138
|
+
mockRestore(): void
|
|
139
|
+
mockImplementation(impl: Function): this
|
|
140
|
+
mockResolvedValue(val: any): this
|
|
141
|
+
mockReturnValue(val: any): this
|
|
142
|
+
mockRejectedValue(err: any): this
|
|
143
|
+
mockClear(): void
|
|
144
|
+
}
|
|
145
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Validation"
|
|
3
|
+
description: "API Reference for ExisJS built-in Schema Validator (tex)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Validation (`tex`)
|
|
7
|
+
|
|
8
|
+
ExisJS ships with a blazingly fast, zero-dependency validation library out of the box, exposed as `tex` from `exisjs/validator`. It is powered by a high-performance Rust engine, fully isomorphic, and deeply integrated with TypeScript and OpenAPI (Swagger).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Core Types
|
|
13
|
+
|
|
14
|
+
The `tex` object provides factory functions for every common primitive and data structure.
|
|
15
|
+
|
|
16
|
+
- `tex.string()`: Validates a string.
|
|
17
|
+
- `tex.number()`: Validates a number.
|
|
18
|
+
- `tex.boolean()`: Validates a boolean.
|
|
19
|
+
- `tex.email()`: Validates standard email formatting.
|
|
20
|
+
- `tex.enum(['A', 'B'])`: Validates against a specific set of allowed strings.
|
|
21
|
+
|
|
22
|
+
### Objects & Arrays
|
|
23
|
+
|
|
24
|
+
- `tex.object({ ... })`: Validates an object against a strict shape.
|
|
25
|
+
- `tex.array(schema)`: Validates an array of items matching the given schema.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const UserProfile = tex.object({
|
|
29
|
+
id: tex.string(),
|
|
30
|
+
tags: tex.array(tex.string()),
|
|
31
|
+
role: tex.enum(['admin', 'user'])
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Usage in Routes
|
|
38
|
+
|
|
39
|
+
ExisJS tightly integrates the `tex` validator directly into its routing layer, providing immediate TypeScript inference for `ctx.body`, `@Body()`, and other parameters.
|
|
40
|
+
|
|
41
|
+
**Class-Based (OOP)**
|
|
42
|
+
```typescript filename="src/http/users/route.ts"
|
|
43
|
+
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
44
|
+
import { tex } from 'exisjs/validator'
|
|
45
|
+
|
|
46
|
+
const CreateUserSchema = tex.object({ email: tex.email() })
|
|
47
|
+
|
|
48
|
+
@Controller('/users')
|
|
49
|
+
export default class UserController {
|
|
50
|
+
@Post('/', { body: CreateUserSchema })
|
|
51
|
+
createUser(@Body() body: any) {
|
|
52
|
+
return { success: true, email: body.email }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Functional**
|
|
58
|
+
```typescript filename="src/http/users/route.ts"
|
|
59
|
+
import { controller, route } from 'exisjs/router'
|
|
60
|
+
import { tex } from 'exisjs/validator'
|
|
61
|
+
|
|
62
|
+
const CreateUserSchema = tex.object({ email: tex.email() })
|
|
63
|
+
|
|
64
|
+
export default controller({
|
|
65
|
+
createUser: route.post('/users', {
|
|
66
|
+
body: CreateUserSchema,
|
|
67
|
+
async handle(ctx) {
|
|
68
|
+
// ctx.body is strictly inferred as { email: string }
|
|
69
|
+
return { success: true, email: ctx.body.email }
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Type Options
|
|
78
|
+
|
|
79
|
+
All validators accept an options object that allows you to configure validation constraints and behaviors.
|
|
80
|
+
|
|
81
|
+
- `optional`: Marks the field as optional (allows `undefined`).
|
|
82
|
+
- `coerce`: Instructs the ultra-fast Rust engine to automatically convert types when possible before validating. For example, converting `"123"` into a number, or `3000` into a string.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Type Conversion Examples using { coerce: true }
|
|
86
|
+
const EnvSchema = tex.object({
|
|
87
|
+
// Converts string "3000" from process.env to number 3000
|
|
88
|
+
PORT: tex.number({ coerce: true }),
|
|
89
|
+
|
|
90
|
+
// Converts boolean 'false' or number '0' to string "false" / "0"
|
|
91
|
+
ID_AS_STRING: tex.string({ coerce: true }),
|
|
92
|
+
|
|
93
|
+
// Converts string "true" or number '1' to boolean true
|
|
94
|
+
ENABLE_FEATURE: tex.boolean({ coerce: true })
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### String Options
|
|
99
|
+
- `min`: Enforces a minimum string length.
|
|
100
|
+
- `max`: Enforces a maximum string length.
|
|
101
|
+
|
|
102
|
+
### Number Options
|
|
103
|
+
- `min`: Enforces a minimum numeric value.
|
|
104
|
+
- `max`: Enforces a maximum numeric value.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const User = tex.object({
|
|
108
|
+
id: tex.string(),
|
|
109
|
+
email: tex.email(),
|
|
110
|
+
password: tex.string({ min: 8, max: 100 }),
|
|
111
|
+
age: tex.number({ min: 13, optional: true })
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Custom Data Manipulation
|
|
118
|
+
|
|
119
|
+
### `.sanitize(...fns)`
|
|
120
|
+
Runs one or more synchronous sanitization functions over the data **before** the Rust validation executes. Use this for complex object transformations or string formatting. ExisJS provides a rich suite of built-in sanitizers through the `exisjs/sanitize` module.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { sanitize } from 'exisjs/sanitize'
|
|
124
|
+
|
|
125
|
+
// Built-in sanitization
|
|
126
|
+
const CleanString = tex.string().sanitize(sanitize.trim, sanitize.toLowerCase)
|
|
127
|
+
|
|
128
|
+
// Custom data transformation
|
|
129
|
+
const WhiteListIPs = tex.array(tex.string())
|
|
130
|
+
.sanitize((val) => typeof val === 'string' ? val.split(',') : val)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `.refine(fn, message)`
|
|
134
|
+
Runs custom validation logic **after** the Rust validation executes. Useful for complex business logic that cannot be expressed purely with basic schema types.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const PasswordConfirm = tex.object({
|
|
138
|
+
password: tex.string(),
|
|
139
|
+
confirm: tex.string()
|
|
140
|
+
}).refine((data) => data.password === data.confirm, "Passwords must match")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Type Inference
|
|
146
|
+
|
|
147
|
+
You can extract the raw TypeScript interface directly from any schema using the `ResolveSchema` utility.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { tex, type ResolveSchema } from 'exisjs/validator'
|
|
151
|
+
|
|
152
|
+
const ProfileSchema = tex.object({
|
|
153
|
+
name: tex.string(),
|
|
154
|
+
age: tex.number({ optional: true })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Resolves to: { name: string; age?: number | undefined }
|
|
158
|
+
type Profile = ResolveSchema<typeof ProfileSchema>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Integrations
|
|
164
|
+
|
|
165
|
+
Because the ExisJS Validator is native to the framework, it is tightly coupled with our database and documentation adapters.
|
|
166
|
+
|
|
167
|
+
### OpenAPI Generation
|
|
168
|
+
The `TexEngine` automatically converts validation schemas into strictly compliant OpenAPI/Swagger JSON schemas when attached to routes. This powers the zero-config ExisJS Swagger integration without any manual YAML authoring.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Environment Validation
|
|
173
|
+
|
|
174
|
+
You can safely parse and validate your environment variables at startup using the standard `.parse()` method.
|
|
175
|
+
|
|
176
|
+
```typescript filename="env.ts"
|
|
177
|
+
import { tex } from 'exisjs/validator'
|
|
178
|
+
|
|
179
|
+
// Validates process.env synchronously. Crashes the app on boot with a
|
|
180
|
+
// beautiful error message if validation fails.
|
|
181
|
+
export const env = tex.object({
|
|
182
|
+
PORT: tex.number({ coerce: true, optional: true }),
|
|
183
|
+
DATABASE_URL: tex.string(),
|
|
184
|
+
JWT_SECRET: tex.string({ min: 32 })
|
|
185
|
+
}).parse(process.env)
|
|
186
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export { defineConfig } from './utils/config';
|
|
|
5
5
|
export { createLogger } from './utils/logger';
|
|
6
6
|
export { parseEnv, loadEnv } from './utils/env';
|
|
7
7
|
export { CircuitBreaker, CircuitBreakerError, CircuitState, } from './utils/circuit-breaker';
|
|
8
|
+
export { db, createDatabase, DatabaseManager } from './database';
|
|
9
|
+
export { Migrator } from './database/migration';
|
|
10
|
+
export { QueryBuilder } from './database/query-builder';
|
|
11
|
+
export type { DatabaseConfig, DatabaseDialect, DatabaseAdapter, QueryResult, ExecuteResult, Transaction, } from './database/types';
|
|
8
12
|
export type { ExisConfig, ExisPlugin, ExisPluginInstance, Logger, CorsConfig, LoggerConfig, HookRequest, } from './types';
|
|
9
13
|
export interface ExisUser {
|
|
10
14
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAGpB,OAAO,EAAE,SAAS,IAAI,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,KAAK,GAAG,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAG7C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAI/C,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,YAAY,GACb,MAAM,yBAAyB,CAAA;AAGhC,YAAY,EACV,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAA;AAGhB,MAAM,WAAW,QAAQ;CAAG"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAGpB,OAAO,EAAE,SAAS,IAAI,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,KAAK,GAAG,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAG7C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAI/C,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,YAAY,GACb,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,YAAY,EACV,cAAc,EACd,eAAe,EACf,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,kBAAkB,CAAA;AAGzB,YAAY,EACV,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAA;AAGhB,MAAM,WAAW,QAAQ;CAAG"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CircuitState = exports.CircuitBreakerError = exports.CircuitBreaker = exports.loadEnv = exports.parseEnv = exports.createLogger = exports.defineConfig = exports.getActiveApp = exports.exis = void 0;
|
|
3
|
+
exports.QueryBuilder = exports.Migrator = exports.DatabaseManager = exports.createDatabase = exports.db = exports.CircuitState = exports.CircuitBreakerError = exports.CircuitBreaker = exports.loadEnv = exports.parseEnv = exports.createLogger = exports.defineConfig = exports.getActiveApp = exports.exis = void 0;
|
|
4
4
|
require("./utils/env");
|
|
5
5
|
// ─── Core ─────────────────────────────────────────────────────────────────────
|
|
6
6
|
var define_1 = require("./server/define");
|
|
@@ -20,4 +20,13 @@ var circuit_breaker_1 = require("./utils/circuit-breaker");
|
|
|
20
20
|
Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return circuit_breaker_1.CircuitBreaker; } });
|
|
21
21
|
Object.defineProperty(exports, "CircuitBreakerError", { enumerable: true, get: function () { return circuit_breaker_1.CircuitBreakerError; } });
|
|
22
22
|
Object.defineProperty(exports, "CircuitState", { enumerable: true, get: function () { return circuit_breaker_1.CircuitState; } });
|
|
23
|
+
// ─── Database ─────────────────────────────────────────────────────────────────
|
|
24
|
+
var database_1 = require("./database");
|
|
25
|
+
Object.defineProperty(exports, "db", { enumerable: true, get: function () { return database_1.db; } });
|
|
26
|
+
Object.defineProperty(exports, "createDatabase", { enumerable: true, get: function () { return database_1.createDatabase; } });
|
|
27
|
+
Object.defineProperty(exports, "DatabaseManager", { enumerable: true, get: function () { return database_1.DatabaseManager; } });
|
|
28
|
+
var migration_1 = require("./database/migration");
|
|
29
|
+
Object.defineProperty(exports, "Migrator", { enumerable: true, get: function () { return migration_1.Migrator; } });
|
|
30
|
+
var query_builder_1 = require("./database/query-builder");
|
|
31
|
+
Object.defineProperty(exports, "QueryBuilder", { enumerable: true, get: function () { return query_builder_1.QueryBuilder; } });
|
|
23
32
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uBAAoB;AAEpB,iFAAiF;AACjF,0CAAmD;AAA1C,8FAAA,SAAS,OAAQ;AAC1B,oCAAqD;AAAlC,mGAAA,YAAY,OAAA;AAC/B,yCAA6C;AAApC,sGAAA,YAAY,OAAA;AAErB,iFAAiF;AACjF,yCAA6C;AAApC,sGAAA,YAAY,OAAA;AAErB,mCAA+C;AAAtC,+FAAA,QAAQ,OAAA;AAAE,8FAAA,OAAO,OAAA;AAE1B,8EAA8E;AAE9E,2DAIgC;AAH9B,iHAAA,cAAc,OAAA;AACd,sHAAA,mBAAmB,OAAA;AACnB,+GAAA,YAAY,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uBAAoB;AAEpB,iFAAiF;AACjF,0CAAmD;AAA1C,8FAAA,SAAS,OAAQ;AAC1B,oCAAqD;AAAlC,mGAAA,YAAY,OAAA;AAC/B,yCAA6C;AAApC,sGAAA,YAAY,OAAA;AAErB,iFAAiF;AACjF,yCAA6C;AAApC,sGAAA,YAAY,OAAA;AAErB,mCAA+C;AAAtC,+FAAA,QAAQ,OAAA;AAAE,8FAAA,OAAO,OAAA;AAE1B,8EAA8E;AAE9E,2DAIgC;AAH9B,iHAAA,cAAc,OAAA;AACd,sHAAA,mBAAmB,OAAA;AACnB,+GAAA,YAAY,OAAA;AAGd,iFAAiF;AACjF,uCAAgE;AAAvD,8FAAA,EAAE,OAAA;AAAE,0GAAA,cAAc,OAAA;AAAE,2GAAA,eAAe,OAAA;AAC5C,kDAA+C;AAAtC,qGAAA,QAAQ,OAAA;AACjB,0DAAuD;AAA9C,6GAAA,YAAY,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backpressure.d.ts","sourceRoot":"","sources":["../../src/middleware/backpressure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAmC,MAAM,UAAU,CAAA;AAGxE,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,mBAAwB,GAChC,OAAO,
|
|
1
|
+
{"version":3,"file":"backpressure.d.ts","sourceRoot":"","sources":["../../src/middleware/backpressure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAmC,MAAM,UAAU,CAAA;AAGxE,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAqFT"}
|
|
@@ -13,6 +13,11 @@ function backpressureMiddleware(options = {}) {
|
|
|
13
13
|
const queued = queue.shift();
|
|
14
14
|
if (queued) {
|
|
15
15
|
clearTimeout(queued.timer);
|
|
16
|
+
// O(1) skip if the request timed out
|
|
17
|
+
if (queued.isCancelled) {
|
|
18
|
+
process.nextTick(processNext);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
16
21
|
activeCount++;
|
|
17
22
|
// Use _onFinish to decrement activeCount when done
|
|
18
23
|
queued.res._onFinish.push(() => {
|
|
@@ -37,19 +42,23 @@ function backpressureMiddleware(options = {}) {
|
|
|
37
42
|
// Return 503 Service Unavailable immediately
|
|
38
43
|
return next(errors_1.HttpError.serviceUnavailable('Server is at capacity. Please try again later.'));
|
|
39
44
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
const queuedReq = {
|
|
46
|
+
req,
|
|
47
|
+
res,
|
|
48
|
+
next,
|
|
49
|
+
timer: null,
|
|
50
|
+
isCancelled: false,
|
|
51
|
+
};
|
|
52
|
+
const timeoutCallback = () => {
|
|
53
|
+
queuedReq.isCancelled = true;
|
|
54
|
+
next(errors_1.HttpError.serviceUnavailable('Request timed out in queue.'));
|
|
55
|
+
};
|
|
56
|
+
// Refresh timer logic
|
|
57
|
+
queuedReq.timer = setTimeout(timeoutCallback, timeoutMs);
|
|
58
|
+
if (queuedReq.timer.unref) {
|
|
59
|
+
queuedReq.timer.unref();
|
|
51
60
|
}
|
|
52
|
-
queue.push(
|
|
61
|
+
queue.push(queuedReq);
|
|
53
62
|
};
|
|
54
63
|
}
|
|
55
64
|
//# sourceMappingURL=backpressure.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backpressure.js","sourceRoot":"","sources":["../../src/middleware/backpressure.ts"],"names":[],"mappings":";;AAYA,
|
|
1
|
+
{"version":3,"file":"backpressure.js","sourceRoot":"","sources":["../../src/middleware/backpressure.ts"],"names":[],"mappings":";;AAYA,wDAuFC;AAlGD,4CAA2C;AAW3C,SAAgB,sBAAsB,CACpC,UAA+B,EAAE;IAEjC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,IAAI,CAAA;IACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAA;IACxC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAA;IAE5C,IAAI,WAAW,GAAG,CAAC,CAAA;IAUnB,MAAM,KAAK,GAAoB,EAAE,CAAA;IAEjC,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,WAAW,GAAG,aAAa,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,CAAA;YAC5B,IAAI,MAAM,EAAE,CAAC;gBACX,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBAE1B,qCAAqC;gBACrC,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;oBACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;oBAC7B,OAAM;gBACR,CAAC;gBAED,WAAW,EAAE,CAAA;gBAEb,mDAAmD;gBACnD,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE;oBAC7B,WAAW,EAAE,CAAA;oBACb,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;gBAC/B,CAAC,CAAC,CAAA;gBAEF,MAAM,CAAC,IAAI,EAAE,CAAA;YACf,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IAED,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,WAAW,GAAG,aAAa,EAAE,CAAC;YAChC,WAAW,EAAE,CAAA;YAEb,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE;gBACtB,WAAW,EAAE,CAAA;gBACb,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;YAC/B,CAAC,CAAC,CAAA;YAEF,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QAED,sCAAsC;QACtC,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;YAC7B,6CAA6C;YAC7C,OAAO,IAAI,CACT,kBAAS,CAAC,kBAAkB,CAC1B,gDAAgD,CACjD,CACF,CAAA;QACH,CAAC;QAED,MAAM,SAAS,GAAkB;YAC/B,GAAG;YACH,GAAG;YACH,IAAI;YACJ,KAAK,EAAE,IAAW;YAClB,WAAW,EAAE,KAAK;SACnB,CAAA;QAED,MAAM,eAAe,GAAG,GAAG,EAAE;YAC3B,SAAS,CAAC,WAAW,GAAG,IAAI,CAAA;YAC5B,IAAI,CAAC,kBAAS,CAAC,kBAAkB,CAAC,6BAA6B,CAAC,CAAC,CAAA;QACnE,CAAC,CAAA;QAED,sBAAsB;QACtB,SAAS,CAAC,KAAK,GAAG,UAAU,CAAC,eAAe,EAAE,SAAS,CAAC,CAAA;QACxD,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAC1B,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACzB,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACvB,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -10,6 +10,12 @@ export interface CacheOptions {
|
|
|
10
10
|
* Override this for user-scoped caching (e.g. `(req) => req.user.id + req.path`).
|
|
11
11
|
*/
|
|
12
12
|
keyGenerator?: (req: Request) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Stale-While-Revalidate duration in milliseconds.
|
|
15
|
+
* If the cache is older than ttlMs but younger than ttlMs + swrMs,
|
|
16
|
+
* stale data is served instantly while it revalidates in the background.
|
|
17
|
+
*/
|
|
18
|
+
swrMs?: number;
|
|
13
19
|
}
|
|
14
20
|
export declare function cacheMiddleware(options: CacheOptions): Handler;
|
|
15
21
|
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAA0B,MAAM,UAAU,CAAA;AACxE,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE/D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,EAAE,CAAC,CAAA;IAC9C;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAA0B,MAAM,UAAU,CAAA;AACxE,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE/D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,EAAE,CAAC,CAAA;IAC9C;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAA;IACvC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAKD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CA4K9D"}
|
package/dist/middleware/cache.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.cacheMiddleware = cacheMiddleware;
|
|
4
4
|
const store_1 = require("../cache/store");
|
|
5
|
+
// Global in-flight requests map to prevent Cache Stampedes (Dogpile effect)
|
|
6
|
+
const inFlightRequests = new Map();
|
|
5
7
|
function cacheMiddleware(options) {
|
|
6
8
|
const ttlMs = options.ttlMs;
|
|
7
9
|
if (!options || typeof options.keyGenerator !== 'function') {
|
|
@@ -17,37 +19,74 @@ function cacheMiddleware(options) {
|
|
|
17
19
|
try {
|
|
18
20
|
const cached = await store.get(key);
|
|
19
21
|
if (cached) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const isStale = ttlMs && Date.now() - cached.createdAt > ttlMs;
|
|
23
|
+
// If it's stale and we have no SWR allowance, we act as a miss
|
|
24
|
+
if (isStale &&
|
|
25
|
+
(!options.swrMs ||
|
|
26
|
+
Date.now() - cached.createdAt > ttlMs + options.swrMs)) {
|
|
27
|
+
// Treated as miss
|
|
22
28
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
else {
|
|
30
|
+
// Handle Buffer reconstruction for Redis
|
|
31
|
+
if (cached.data.contentType) {
|
|
32
|
+
res.set('Content-Type', cached.data.contentType);
|
|
33
|
+
}
|
|
34
|
+
res.set('X-Exis-Cache', isStale ? 'STALE' : 'HIT');
|
|
35
|
+
let bodyToSend = cached.data.body;
|
|
36
|
+
if (typeof cached.data.body === 'string' && cached.data.isBuffer) {
|
|
37
|
+
bodyToSend = Buffer.from(cached.data.body, 'base64');
|
|
38
|
+
}
|
|
39
|
+
if (cached.data.contentType?.includes('application/json') &&
|
|
40
|
+
typeof bodyToSend === 'string') {
|
|
41
|
+
try {
|
|
42
|
+
res.status(cached.data.statusCode).json(JSON.parse(bodyToSend));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
res.status(cached.data.statusCode).send(bodyToSend);
|
|
46
|
+
}
|
|
35
47
|
}
|
|
36
|
-
|
|
37
|
-
// If parsing fails, fall back to send
|
|
48
|
+
else {
|
|
38
49
|
res.status(cached.data.statusCode).send(bodyToSend);
|
|
39
50
|
}
|
|
51
|
+
if (!isStale) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// If it was stale, we returned the response already. Now we fall through
|
|
55
|
+
// to trigger a background revalidation! We MUST NOT return here if stale.
|
|
40
56
|
}
|
|
41
|
-
else {
|
|
42
|
-
res.status(cached.data.statusCode).send(bodyToSend);
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
catch {
|
|
48
60
|
// Fallback to normal execution if cache read fails
|
|
49
61
|
}
|
|
50
|
-
|
|
62
|
+
// --- Cache Stampede Prevention ---
|
|
63
|
+
// If another request is currently resolving this exact same cache key, we just wait for it.
|
|
64
|
+
if (!res.headersSent) {
|
|
65
|
+
if (inFlightRequests.has(key)) {
|
|
66
|
+
res.set('X-Exis-Cache', 'DEDUPED');
|
|
67
|
+
const dedupeResponse = await inFlightRequests.get(key);
|
|
68
|
+
if (dedupeResponse.contentType) {
|
|
69
|
+
res.set('Content-Type', dedupeResponse.contentType);
|
|
70
|
+
}
|
|
71
|
+
if (dedupeResponse.contentType?.includes('application/json') &&
|
|
72
|
+
typeof dedupeResponse.body === 'string') {
|
|
73
|
+
return res
|
|
74
|
+
.status(dedupeResponse.statusCode)
|
|
75
|
+
.json(JSON.parse(dedupeResponse.body));
|
|
76
|
+
}
|
|
77
|
+
return res.status(dedupeResponse.statusCode).send(dedupeResponse.body);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Set headers if not already sent (by SWR)
|
|
81
|
+
if (!res.headersSent) {
|
|
82
|
+
res.set('X-Exis-Cache', 'MISS');
|
|
83
|
+
}
|
|
84
|
+
let resolveInFlight;
|
|
85
|
+
const inFlightPromise = new Promise((resolve) => {
|
|
86
|
+
resolveInFlight = resolve;
|
|
87
|
+
});
|
|
88
|
+
inFlightRequests.set(key, inFlightPromise);
|
|
89
|
+
// --- End Stampede Prevention ---
|
|
51
90
|
// Intercept send/json
|
|
52
91
|
const originalSend = res.send.bind(res);
|
|
53
92
|
const originalJson = res.json.bind(res);
|
|
@@ -60,19 +99,27 @@ function cacheMiddleware(options) {
|
|
|
60
99
|
else if (options.tags) {
|
|
61
100
|
tags = options.tags;
|
|
62
101
|
}
|
|
63
|
-
|
|
64
|
-
Promise.resolve(store.set(key, {
|
|
102
|
+
const payload = {
|
|
65
103
|
body,
|
|
66
104
|
contentType: typeof res.getHeader === 'function'
|
|
67
105
|
? res.getHeader('content-type')
|
|
68
106
|
: undefined,
|
|
69
107
|
statusCode: res.statusCode,
|
|
70
108
|
isBuffer: Buffer.isBuffer(body),
|
|
71
|
-
}
|
|
109
|
+
};
|
|
110
|
+
Promise.resolve(store.set(key, payload, tags, ttlMs)).catch(() => {
|
|
72
111
|
/* noop */
|
|
73
112
|
});
|
|
113
|
+
resolveInFlight(payload);
|
|
114
|
+
inFlightRequests.delete(key);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
resolveInFlight({});
|
|
118
|
+
inFlightRequests.delete(key);
|
|
119
|
+
}
|
|
120
|
+
if (!res.headersSent) {
|
|
121
|
+
return originalSend(body);
|
|
74
122
|
}
|
|
75
|
-
return originalSend(body);
|
|
76
123
|
};
|
|
77
124
|
res.json = function (data) {
|
|
78
125
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
@@ -83,16 +130,25 @@ function cacheMiddleware(options) {
|
|
|
83
130
|
else if (options.tags) {
|
|
84
131
|
tags = options.tags;
|
|
85
132
|
}
|
|
86
|
-
|
|
133
|
+
const payload = {
|
|
87
134
|
body: JSON.stringify(data),
|
|
88
135
|
contentType: 'application/json; charset=utf-8',
|
|
89
136
|
statusCode: res.statusCode,
|
|
90
137
|
isBuffer: false,
|
|
91
|
-
}
|
|
138
|
+
};
|
|
139
|
+
Promise.resolve(store.set(key, payload, tags, ttlMs)).catch(() => {
|
|
92
140
|
/* noop */
|
|
93
141
|
});
|
|
142
|
+
resolveInFlight(payload);
|
|
143
|
+
inFlightRequests.delete(key);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
resolveInFlight({});
|
|
147
|
+
inFlightRequests.delete(key);
|
|
148
|
+
}
|
|
149
|
+
if (!res.headersSent) {
|
|
150
|
+
return originalJson(data);
|
|
94
151
|
}
|
|
95
|
-
return originalJson(data);
|
|
96
152
|
};
|
|
97
153
|
next();
|
|
98
154
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":"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"}
|