exisjs 0.5.10 → 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/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +0 -1
- 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/server/bootstrapper.js +1 -1
- package/dist/server/bootstrapper.js.map +1 -1
- package/package.json +8 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OAuth Provider System"
|
|
3
|
+
description: "ExisJS includes a fully-featured, built-in OAuth 2.0 and OpenID Connect system with native providers for Google, GitHub, Microsoft, Discord, and Facebook."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OAuth Provider System
|
|
7
|
+
|
|
8
|
+
Implementing secure, compliant OAuth flows is notoriously difficult. ExisJS solves this by providing a unified, built-in OAuth system. It handles PKCE (Proof Key for Code Exchange), state generation for CSRF protection, and normalizes user profiles automatically.
|
|
9
|
+
|
|
10
|
+
<Info title="Functional vs OOP">
|
|
11
|
+
ExisJS natively supports both a **Functional Paradigm** and a **Class-Based OOP Paradigm** (for dependency injection and decorators). The OAuth middleware helpers can easily be used in either paradigm.
|
|
12
|
+
</Info>
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
Configure your providers globally using the `OAuthManager`. You typically do this once in your application entry point (`server.ts`).
|
|
17
|
+
|
|
18
|
+
```typescript filename="src/http/server.ts"
|
|
19
|
+
import { exis } from 'exisjs'
|
|
20
|
+
import { OAuth } from 'exisjs/auth'
|
|
21
|
+
|
|
22
|
+
export default exis({
|
|
23
|
+
async onStart(app) {
|
|
24
|
+
OAuth.configure({
|
|
25
|
+
google: {
|
|
26
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
27
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
28
|
+
redirectUri: 'http://localhost:3000/auth/google/callback',
|
|
29
|
+
},
|
|
30
|
+
github: {
|
|
31
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
32
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
33
|
+
redirectUri: 'http://localhost:3000/auth/github/callback',
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Routing & Middleware
|
|
41
|
+
|
|
42
|
+
ExisJS provides Express-style middleware (`oauthRedirect` and `oauthCallback`) that makes securing your routes trivial. It securely stores state/PKCE verifiers in sessions or signed cookies.
|
|
43
|
+
|
|
44
|
+
Here is how you handle the login flow using both paradigms.
|
|
45
|
+
|
|
46
|
+
#### Functional Paradigm
|
|
47
|
+
In functional routes, you wrap your handlers using standard middleware chaining techniques.
|
|
48
|
+
|
|
49
|
+
```typescript filename="src/http/auth/route.ts"
|
|
50
|
+
import { controller, route } from 'exisjs/router'
|
|
51
|
+
import { oauthRedirect, oauthCallback } from 'exisjs/auth'
|
|
52
|
+
|
|
53
|
+
export default controller({
|
|
54
|
+
// 1. Redirect user to Google
|
|
55
|
+
login: route.get('/google', {
|
|
56
|
+
middlewares: [oauthRedirect('google')],
|
|
57
|
+
handle() {
|
|
58
|
+
// Never reached, the middleware handles the redirect
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
// 2. Handle the callback from Google
|
|
63
|
+
callback: route.get('/google/callback', {
|
|
64
|
+
middlewares: [
|
|
65
|
+
oauthCallback('google', {
|
|
66
|
+
onSuccess: async (req, res, profile, tokens) => {
|
|
67
|
+
// profile contains the normalized user data
|
|
68
|
+
console.log(profile.email, profile.name)
|
|
69
|
+
|
|
70
|
+
// Log the user into your system
|
|
71
|
+
req.session.userId = profile.id
|
|
72
|
+
res.redirect('/dashboard')
|
|
73
|
+
},
|
|
74
|
+
onError: async (req, res, error) => {
|
|
75
|
+
res.redirect(`/login?error=${encodeURIComponent(error.message)}`)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
],
|
|
79
|
+
handle() {
|
|
80
|
+
// Handled by the onSuccess callback above
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Class-Based (OOP) Paradigm
|
|
87
|
+
In OOP routes, you can use the `@Use()` decorator to apply the exact same middleware helpers.
|
|
88
|
+
|
|
89
|
+
```typescript filename="src/http/auth/route.ts"
|
|
90
|
+
import { Controller, Get, Use } from 'exisjs/decorators'
|
|
91
|
+
import { oauthRedirect, oauthCallback } from 'exisjs/auth'
|
|
92
|
+
|
|
93
|
+
@Controller()
|
|
94
|
+
export default class AuthController {
|
|
95
|
+
|
|
96
|
+
// 1. Redirect user to Google
|
|
97
|
+
@Get('/google')
|
|
98
|
+
@Use(oauthRedirect('google'))
|
|
99
|
+
login() {
|
|
100
|
+
// Never reached, the middleware handles the redirect
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Handle the callback from Google
|
|
104
|
+
@Get('/google/callback')
|
|
105
|
+
@Use(
|
|
106
|
+
oauthCallback('google', {
|
|
107
|
+
onSuccess: async (req, res, profile, tokens) => {
|
|
108
|
+
// Log the user into your system
|
|
109
|
+
req.session.userId = profile.id
|
|
110
|
+
res.redirect('/dashboard')
|
|
111
|
+
},
|
|
112
|
+
onError: async (req, res, error) => {
|
|
113
|
+
res.redirect(`/login?error=${encodeURIComponent(error.message)}`)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
callback() {
|
|
118
|
+
// Handled by the onSuccess callback above
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Route Guards
|
|
124
|
+
|
|
125
|
+
Once authenticated, use the built-in `requireAuth` middleware to protect your endpoints.
|
|
126
|
+
|
|
127
|
+
#### Functional Paradigm
|
|
128
|
+
```typescript filename="src/http/dashboard/route.ts"
|
|
129
|
+
import { controller, route } from 'exisjs/router'
|
|
130
|
+
import { requireAuth } from 'exisjs/auth'
|
|
131
|
+
|
|
132
|
+
export default controller({
|
|
133
|
+
view: route.get('/', {
|
|
134
|
+
middlewares: [requireAuth({ redirectTo: '/login' })],
|
|
135
|
+
handle({ req }) {
|
|
136
|
+
return { message: 'Welcome to the secure dashboard!' }
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Class-Based (OOP) Paradigm
|
|
143
|
+
```typescript filename="src/http/dashboard/route.ts"
|
|
144
|
+
import { Controller, Get, Use } from 'exisjs/decorators'
|
|
145
|
+
import { requireAuth } from 'exisjs/auth'
|
|
146
|
+
|
|
147
|
+
@Controller()
|
|
148
|
+
@Use(requireAuth({ redirectTo: '/login' })) // Protects the whole controller!
|
|
149
|
+
export default class DashboardController {
|
|
150
|
+
@Get('/')
|
|
151
|
+
view() {
|
|
152
|
+
return { message: 'Welcome to the secure dashboard!' }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Creating Custom Providers
|
|
158
|
+
|
|
159
|
+
If you need to connect to an internal identity provider or a service not included by default, use the generic custom provider adapter anywhere in your `server.ts` configuration:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
OAuth.custom({
|
|
163
|
+
name: 'keycloak',
|
|
164
|
+
authUrl: 'https://keycloak.example.com/auth',
|
|
165
|
+
tokenUrl: 'https://keycloak.example.com/token',
|
|
166
|
+
userInfoUrl: 'https://keycloak.example.com/userinfo',
|
|
167
|
+
clientId: process.env.KC_CLIENT_ID!,
|
|
168
|
+
clientSecret: process.env.KC_CLIENT_SECRET!,
|
|
169
|
+
redirectUri: 'http://localhost:3000/auth/keycloak/callback',
|
|
170
|
+
scopes: ['openid', 'profile', 'email'],
|
|
171
|
+
|
|
172
|
+
// Optionally map the raw response to standard profile fields
|
|
173
|
+
mapProfile: (raw) => ({
|
|
174
|
+
id: raw.sub,
|
|
175
|
+
email: raw.email,
|
|
176
|
+
name: raw.preferred_username,
|
|
177
|
+
}),
|
|
178
|
+
})
|
|
179
|
+
```
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/gateways"
|
|
3
|
+
nextTitle: "Gateways"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Providers
|
|
7
|
+
|
|
8
|
+
Providers are a fundamental concept in ExisJS. Many of the basic classes may be treated as a provider: services, repositories, factories, helpers, and so on.
|
|
9
|
+
|
|
10
|
+
The main idea of a provider is that it can be injected as a dependency; this means objects can create various relationships with each other, and the function of "wiring up" instances of objects can largely be delegated to the ExisJS runtime system.
|
|
11
|
+
|
|
12
|
+
## Services
|
|
13
|
+
|
|
14
|
+
Let's start by creating a simple `UsersService`. This service will be responsible for data storage and retrieval, and is designed to be used by the `UsersController`.
|
|
15
|
+
|
|
16
|
+
To mark a class as a provider that the ExisJS Inversion of Control (IoC) container can manage, we use the `@Injectable()` decorator.
|
|
17
|
+
|
|
18
|
+
```typescript filename="src/http/users/service.ts"
|
|
19
|
+
import { Injectable } from 'exisjs/decorators'
|
|
20
|
+
|
|
21
|
+
export interface User {
|
|
22
|
+
id: string;
|
|
23
|
+
email: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Injectable({ scope: 'singleton' })
|
|
27
|
+
export class UsersService {
|
|
28
|
+
private readonly users: User[] = []
|
|
29
|
+
|
|
30
|
+
create(user: User) {
|
|
31
|
+
this.users.push(user)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
findAll(): User[] {
|
|
35
|
+
return this.users
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Now that we have a service class to retrieve users, let's use it inside the `UsersController`. ExisJS provides a powerful `inject()` primitive that automatically resolves dependencies from the current context.
|
|
41
|
+
|
|
42
|
+
```typescript filename="src/http/users/route.ts"
|
|
43
|
+
import { Controller, Get, Post, Body } from 'exisjs/decorators'
|
|
44
|
+
import { inject } from 'exisjs/di'
|
|
45
|
+
import { UsersService, User } from './service'
|
|
46
|
+
|
|
47
|
+
@Controller('users')
|
|
48
|
+
export default class UsersController {
|
|
49
|
+
|
|
50
|
+
// Clean, ergonomic dependency injection
|
|
51
|
+
private usersService = inject(UsersService)
|
|
52
|
+
|
|
53
|
+
@Post()
|
|
54
|
+
async create(@Body() user: User) {
|
|
55
|
+
this.usersService.create(user)
|
|
56
|
+
return { success: true }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Get()
|
|
60
|
+
async findAll() {
|
|
61
|
+
return this.usersService.findAll()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
<Info title="Note on Dependency Injection">
|
|
67
|
+
ExisJS utilizes a runtime container system. By default, ExisJS does not aggressively use `reflect-metadata` for automatic constructor injection (unlike NestJS) to maintain a minimal footprint. The `inject()` function is the primary and recommended way to resolve dependencies.
|
|
68
|
+
</Info>
|
|
69
|
+
|
|
70
|
+
### Advanced: Manual Resolution
|
|
71
|
+
If you are operating completely outside of an ExisJS controller or route (for example, in a background worker, CRON job, or a raw script like a database seeder), you can manually resolve dependencies as an escape hatch via the app container:
|
|
72
|
+
|
|
73
|
+
```typescript filename="scripts/seed.ts"
|
|
74
|
+
import { getActiveApp } from 'exisjs/app'
|
|
75
|
+
import { UsersService } from './service'
|
|
76
|
+
|
|
77
|
+
const app = getActiveApp()
|
|
78
|
+
const usersService = app.container.resolve(UsersService)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Custom Providers
|
|
82
|
+
|
|
83
|
+
ExisJS's DI container is flexible. By default, when you register a class, ExisJS instantiates it for you. However, you often need more control. You can register standard classes, raw values, factories, or aliased classes natively using `app.provide()`.
|
|
84
|
+
|
|
85
|
+
### Functional Paradigm
|
|
86
|
+
|
|
87
|
+
```typescript filename="src/http/server.ts"
|
|
88
|
+
import exis from 'exisjs'
|
|
89
|
+
import { UsersService } from './users/service'
|
|
90
|
+
|
|
91
|
+
export default exis({
|
|
92
|
+
async onStart(app) {
|
|
93
|
+
// 1. Standard Class Provider
|
|
94
|
+
app.provide(UsersService, UsersService)
|
|
95
|
+
|
|
96
|
+
// 2. Value Provider
|
|
97
|
+
app.provide('DATABASE_URL', {
|
|
98
|
+
useValue: 'postgres://localhost:5432/mydb'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// 3. Factory Provider
|
|
102
|
+
app.provide('LoggerService', {
|
|
103
|
+
useFactory: () => {
|
|
104
|
+
return { log: (msg: string) => console.log(`[Logger]: ${msg}`) }
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// 4. Aliased Class Provider
|
|
109
|
+
app.provide('AuthService', {
|
|
110
|
+
useClass: UsersService
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Class-Based (OOP) Paradigm
|
|
117
|
+
|
|
118
|
+
```typescript filename="src/http/server.ts"
|
|
119
|
+
import { Server } from 'exisjs/decorators'
|
|
120
|
+
import { UsersService } from './users/service'
|
|
121
|
+
|
|
122
|
+
@Server({
|
|
123
|
+
providers: [
|
|
124
|
+
// 1. Standard Class Provider
|
|
125
|
+
UsersService,
|
|
126
|
+
// 2. Value Provider
|
|
127
|
+
['DATABASE_URL', { useValue: 'postgres://localhost:5432/mydb' }],
|
|
128
|
+
// 3. Factory Provider
|
|
129
|
+
['LoggerService', { useFactory: () => ({ log: (msg: string) => console.log(`[Logger]: ${msg}`) }) }],
|
|
130
|
+
// 4. Aliased Class Provider
|
|
131
|
+
['AuthService', { useClass: UsersService }]
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
export default class RootServer {}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Injection Scopes
|
|
138
|
+
|
|
139
|
+
For people coming from different programming language backgrounds, it might be unexpected to learn that in ExisJS, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. This is because Node.js doesn't follow the request/response Multi-Threaded Stateless Model.
|
|
140
|
+
|
|
141
|
+
However, there are edge-cases when request-based lifetime may be the desired behavior (e.g., per-request caching in GraphQL, request tracking, multi-tenancy). ExisJS supports scopes directly in the `@Injectable()` decorator:
|
|
142
|
+
|
|
143
|
+
- **Singleton (Default)**: A single instance of the provider is shared across the entire application.
|
|
144
|
+
- **Request**: A new instance of the provider is created exclusively for each incoming request, and garbage collected afterward.
|
|
145
|
+
|
|
146
|
+
> [!WARNING]
|
|
147
|
+
> **Request Scope Constraints**: A provider marked as `scope: 'request'` **must** be resolved within the lifecycle of an active HTTP request. If you try to `inject()` or resolve a request-scoped provider during application startup, inside a background queue worker, or from a cron job, the container will throw a runtime error.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/cron"
|
|
3
|
+
nextTitle: "Cron Jobs"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Job Queues
|
|
7
|
+
|
|
8
|
+
ExisJS provides a built-in Job Queue system designed to handle background processing, delayed tasks, and high-concurrency workloads.
|
|
9
|
+
|
|
10
|
+
Key features include:
|
|
11
|
+
- **Native Worker Threads**: File-based jobs are processed in isolated Node.js Worker Threads (`ThreadPool`) so heavy tasks do not block the main HTTP event loop.
|
|
12
|
+
- **Type-Safe Payloads**: Built-in schema validation for queue payloads.
|
|
13
|
+
- **Driver Agnostic**: Swap between `memory` (for dev) and `redis` (for production) without changing business logic.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Configuration
|
|
18
|
+
|
|
19
|
+
You can configure the Queue engine globally in your main `exis.config.ts` or directly in the `exis({ ... })` initializer.
|
|
20
|
+
|
|
21
|
+
```typescript filename="exis.config.ts"
|
|
22
|
+
export default {
|
|
23
|
+
queue: {
|
|
24
|
+
driver: process.env.NODE_ENV === 'production' ? 'redis' : 'memory',
|
|
25
|
+
redisUrl: process.env.REDIS_URL,
|
|
26
|
+
concurrency: 5,
|
|
27
|
+
maxQueue: 10000,
|
|
28
|
+
prefix: 'myapp:jobs'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Configuration Properties
|
|
34
|
+
|
|
35
|
+
Here are all the properties you can pass to the `queue` configuration object:
|
|
36
|
+
|
|
37
|
+
- **`driver`**: (`'redis' | 'memory'`) The backend storage engine for the queue. Defaults to `'memory'`.
|
|
38
|
+
- **`redisUrl`**: (`string`) The connection string for your Redis instance if using the redis driver.
|
|
39
|
+
- **`redis`**: (`Redis`) An already instantiated `ioredis` client instance.
|
|
40
|
+
- **`concurrency`** (or **`maxConcurrent`**): (`number`) The maximum number of jobs the worker will process concurrently in the thread pool. Defaults to `1`.
|
|
41
|
+
- **`maxQueue`**: (`number`) Maximum number of pending jobs allowed in the queue. If this limit is hit, `enqueue()` will throw a Backpressure error to protect the system.
|
|
42
|
+
- **`prefix`**: (`string`) A namespace prefix for all queue keys in the backend store (e.g., in Redis). Defaults to `'exis:queue'`.
|
|
43
|
+
- **`enableWorkers`**: (`boolean`) Explicitly enable or disable the background worker on this specific Node process (useful for splitting HTTP servers and Worker servers).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. Defining Jobs (Two Approaches)
|
|
48
|
+
|
|
49
|
+
ExisJS provides two distinctly different ways to define and process background jobs, depending on whether the task is CPU-intensive or just a quick asynchronous action.
|
|
50
|
+
|
|
51
|
+
### Approach 1: Auto-Mounted File-Based Jobs (Multi-Threaded)
|
|
52
|
+
If you place a job definition inside the `src/jobs/` directory, ExisJS automatically mounts it using **File-Based Routing**.
|
|
53
|
+
|
|
54
|
+
ExisJS will run these files off the main thread in a dedicated V8 isolate (`ThreadPool`). You do not need to write `worker_threads` boilerplate. Define the job, and ExisJS prevents the HTTP server from being blocked by heavy CPU tasks (like image processing or large calculations).
|
|
55
|
+
|
|
56
|
+
```typescript filename="src/jobs/process-image.ts"
|
|
57
|
+
import { defineJob } from 'exisjs/queue'
|
|
58
|
+
import { tex } from 'exisjs/validator'
|
|
59
|
+
|
|
60
|
+
export default defineJob({
|
|
61
|
+
name: 'process-image',
|
|
62
|
+
|
|
63
|
+
schema: tex.object({
|
|
64
|
+
imageId: tex.string(),
|
|
65
|
+
resize: tex.boolean()
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
// ExisJS automatically runs this handler in a separate Worker Thread!
|
|
69
|
+
async handler({ data }) {
|
|
70
|
+
console.log(`[Worker Thread] Processing image ${data.imageId}...`)
|
|
71
|
+
const result = heavyImageProcessing(data);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Approach 2: Inline Handlers (Main Thread)
|
|
78
|
+
If you have an IO-bound task (like sending an email or calling an external API) and you do not want to create a new file, you can use the global `queue()` helper.
|
|
79
|
+
|
|
80
|
+
Jobs defined this way run functionally in the **main thread** (alongside your HTTP server).
|
|
81
|
+
|
|
82
|
+
```typescript filename="src/http/server.ts"
|
|
83
|
+
import { queue } from 'exisjs/queue'
|
|
84
|
+
|
|
85
|
+
// Define a job inline! Runs in the main event loop.
|
|
86
|
+
queue<{ email: string }>('send-welcome-email', async ({ data }) => {
|
|
87
|
+
console.log(`Sending email to ${data.email}...`)
|
|
88
|
+
}, {
|
|
89
|
+
defaultOptions: { attempts: 3 }
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 3. Enqueuing Jobs
|
|
96
|
+
|
|
97
|
+
Regardless of which approach you used to define the job, you dispatch them the exact same way using the global `enqueue` helper.
|
|
98
|
+
|
|
99
|
+
#### Functional Paradigm
|
|
100
|
+
|
|
101
|
+
```typescript filename="src/http/users/route.ts"
|
|
102
|
+
import { controller, route } from 'exisjs/router'
|
|
103
|
+
import { enqueue } from 'exisjs/queue'
|
|
104
|
+
|
|
105
|
+
export default controller({
|
|
106
|
+
register: route.post('/register', {
|
|
107
|
+
async handle({ req, res }) {
|
|
108
|
+
const user = await db.users.create(req.body)
|
|
109
|
+
|
|
110
|
+
// Enqueue the job
|
|
111
|
+
await enqueue('send-welcome-email', {
|
|
112
|
+
email: user.email
|
|
113
|
+
}, {
|
|
114
|
+
// You can override job options at dispatch time
|
|
115
|
+
delay: 5000 // Wait 5 seconds before processing
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
res.json({ success: true, user })
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Class-Based (OOP) Paradigm
|
|
125
|
+
|
|
126
|
+
```typescript filename="src/http/users/route.ts"
|
|
127
|
+
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
128
|
+
import { enqueue } from 'exisjs/queue'
|
|
129
|
+
|
|
130
|
+
@Controller()
|
|
131
|
+
export default class UsersController {
|
|
132
|
+
@Post('/register')
|
|
133
|
+
async register(@Body() body: any) {
|
|
134
|
+
const user = await db.users.create(body)
|
|
135
|
+
|
|
136
|
+
// Enqueue the job
|
|
137
|
+
await enqueue('send-welcome-email', {
|
|
138
|
+
email: user.email
|
|
139
|
+
}, {
|
|
140
|
+
// You can override job options at dispatch time
|
|
141
|
+
delay: 5000 // Wait 5 seconds before processing
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return { success: true, user }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Advanced Options
|
|
150
|
+
|
|
151
|
+
When enqueuing a job, or setting `defaultOptions` in your job definitions, you can configure:
|
|
152
|
+
|
|
153
|
+
- **`attempts`**: How many times to retry a failed job.
|
|
154
|
+
- **`delay`**: How many milliseconds to wait before the job is processing.
|
|
155
|
+
- **`backoff`**: If a job fails, how to wait before retrying (e.g., `{ type: 'exponential', delay: 1000 }` waits 1s, 2s, 4s, etc.).
|
|
156
|
+
- **`visibilityTimeout`**: How long a job stays in the "processing" state before it is considered stuck and gets re-queued by the Sweeper (default: `30000`ms).
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 4. Queue Drivers & Internals
|
|
161
|
+
|
|
162
|
+
ExisJS comes with two native queue drivers. Both drivers implement atomic operations and strict visibility timeouts to ensure jobs are not processed twice and are safely retried if a worker crashes.
|
|
163
|
+
|
|
164
|
+
### The Memory Driver (`memory`)
|
|
165
|
+
The in-memory driver is designed for development, testing, or simple single-node deployments.
|
|
166
|
+
- It uses ES6 `Map` and `Array.sort` to keep jobs sorted by their execution score (`Date.now() + delay`).
|
|
167
|
+
- Does not require any external infrastructure.
|
|
168
|
+
|
|
169
|
+
### The Redis Driver (`redis`)
|
|
170
|
+
Designed for horizontal scale, the Redis driver powers distributed clusters.
|
|
171
|
+
- **Atomic Operations**: Uses Lua scripts (`EVAL`) to atomically pop jobs from a pending `ZSET` (sorted set) into a processing state.
|
|
172
|
+
- **Zero-Dependency Core**: It dynamically requires `ioredis` only if used, keeping the core framework lightweight.
|
|
173
|
+
- **Cluster Safe**: All jobs for a specific topic map to consistent Redis hash slots.
|
|
174
|
+
|
|
175
|
+
### The Sweeper
|
|
176
|
+
ExisJS runs a background **Sweeper** every 10 seconds.
|
|
177
|
+
If a worker crashes mid-job, or a job takes longer than its `visibilityTimeout`, the Sweeper detects the stalled job in the processing list and re-queues it back into the pending list so another worker can pick it up.
|