@nestjs-pipeline/job-context 0.4.0 → 0.4.2
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/README.md +2 -194
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,12 +8,7 @@ the correlation id and the principal. A job then makes the decisions the request
|
|
|
8
8
|
have made. System-started work, such as a cron job, declares its context explicitly
|
|
9
9
|
instead. Nothing falls back to a default tenant or an anonymous principal.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
correlation id through the sources it is given, usually `tenantSource` of
|
|
13
|
-
[`@nestjs-pipeline/tenant`](https://github.com/aristoteliss/nestjs-pipeline/tree/master/packages/pipeline-tenant#readme)
|
|
14
|
-
and `correlationSource` of
|
|
15
|
-
[`@nestjs-pipeline/correlation`](https://github.com/aristoteliss/nestjs-pipeline/tree/master/packages/pipeline-correlation#readme),
|
|
16
|
-
so pipelines a job dispatches get the same tenant and correlation id.
|
|
11
|
+
**Documentation:** [guide](https://aristoteliss.github.io/nestjs-pipeline/packages/nestjs-pipeline/job-context/) · [API reference](https://aristoteliss.github.io/nestjs-pipeline/api/nestjs-pipeline/job-context/) · [all packages](https://aristoteliss.github.io/nestjs-pipeline/)
|
|
17
12
|
|
|
18
13
|
## Installation
|
|
19
14
|
|
|
@@ -23,194 +18,7 @@ pnpm add @nestjs-pipeline/job-context @nestjs/common
|
|
|
23
18
|
|
|
24
19
|
Requires Node.js 22.12 or later and `@nestjs/common` `^12.1.0`.
|
|
25
20
|
|
|
26
|
-
Published as an ES module; a CommonJS application loads it with `require()`. Coming from
|
|
27
|
-
0.3.x, see [Upgrading from 0.3.x](https://github.com/aristoteliss/nestjs-pipeline#upgrading-from-03x).
|
|
28
|
-
|
|
29
|
-
## Setup
|
|
30
|
-
|
|
31
|
-
Implement `IJobPrincipal` over the application's authentication state, and register it
|
|
32
|
-
with the tenants jobs may run in and the tenant and correlation id sources:
|
|
33
|
-
|
|
34
|
-
`Capability`, `SessionRepository`, `SessionsModule`, `currentPrincipal`,
|
|
35
|
-
`runAsPrincipal` and `SessionRevokedError` stand for the application's own authentication
|
|
36
|
-
code.
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { Injectable, Module } from '@nestjs/common';
|
|
40
|
-
import { correlationSource } from '@nestjs-pipeline/correlation';
|
|
41
|
-
import {
|
|
42
|
-
type IJobPrincipal,
|
|
43
|
-
JobContextModule,
|
|
44
|
-
type PrincipalReference,
|
|
45
|
-
} from '@nestjs-pipeline/job-context';
|
|
46
|
-
import { tenantSource } from '@nestjs-pipeline/tenant';
|
|
47
|
-
|
|
48
|
-
@Injectable()
|
|
49
|
-
export class SessionJobPrincipal implements IJobPrincipal<Capability> {
|
|
50
|
-
constructor(private readonly sessions: SessionRepository) {}
|
|
51
|
-
|
|
52
|
-
capture(): PrincipalReference | undefined {
|
|
53
|
-
const principal = currentPrincipal();
|
|
54
|
-
return principal && { id: principal.id, type: principal.type, sessionId: principal.sid };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async restore<T>(
|
|
58
|
-
principal: PrincipalReference,
|
|
59
|
-
work: () => Promise<T>,
|
|
60
|
-
grants?: readonly Capability[],
|
|
61
|
-
): Promise<T> {
|
|
62
|
-
if (grants) return runAsPrincipal({ ...principal, grants }, work);
|
|
63
|
-
const session = await this.sessions.findActive(principal.sessionId, principal.id);
|
|
64
|
-
if (!session) throw new SessionRevokedError();
|
|
65
|
-
return runAsPrincipal(principal, work);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@Module({
|
|
70
|
-
imports: [
|
|
71
|
-
JobContextModule.forRoot({
|
|
72
|
-
principal: SessionJobPrincipal,
|
|
73
|
-
tenants: ['tenant_a', 'tenant_b'],
|
|
74
|
-
sources: { tenantId: tenantSource, correlationId: correlationSource },
|
|
75
|
-
imports: [SessionsModule],
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
})
|
|
79
|
-
export class JobsModule {}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
`tenants` is a list, or a function that returns one. A function is called once, when the
|
|
83
|
-
application builds the module's providers, so the list can come from configuration read
|
|
84
|
-
at startup (`tenants: () => config().tenants`) instead of when the module file is
|
|
85
|
-
imported. An empty list throws: a list in `forRoot`, a function when the application
|
|
86
|
-
starts.
|
|
87
|
-
|
|
88
|
-
`capture` reads the current principal when a job is enqueued; only its `id`, `type` and
|
|
89
|
-
`sessionId` are kept. `restore` runs when the job does, inside the job's tenant and
|
|
90
|
-
correlation id: it must re-check the principal against current state, bind it the way a
|
|
91
|
-
request would, and throw to refuse the job.
|
|
92
|
-
|
|
93
|
-
## Usage
|
|
94
|
-
|
|
95
|
-
Stamp the payload when enqueuing, from inside the request or handler, so the tenant,
|
|
96
|
-
correlation id and principal are current:
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
import { InjectQueue } from '@nestjs/bullmq';
|
|
100
|
-
import { EventsHandler, type IEventHandler } from '@nestjs/cqrs';
|
|
101
|
-
import { withJobContext, type WithJobContext } from '@nestjs-pipeline/job-context';
|
|
102
|
-
import type { Queue } from 'bullmq';
|
|
103
|
-
|
|
104
|
-
type WelcomeEmail = { userId: string; email: string };
|
|
105
|
-
|
|
106
|
-
@EventsHandler(UserRegisteredEvent)
|
|
107
|
-
export class EnqueueWelcomeEmail implements IEventHandler<UserRegisteredEvent> {
|
|
108
|
-
constructor(
|
|
109
|
-
@InjectQueue(WELCOME_EMAIL_QUEUE)
|
|
110
|
-
private readonly queue: Queue<WithJobContext<WelcomeEmail>>,
|
|
111
|
-
) {}
|
|
112
|
-
|
|
113
|
-
async handle({ userId, email }: UserRegisteredEvent) {
|
|
114
|
-
await this.queue.add('send', withJobContext({ userId, email }));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Restore it in the processor. Place the decorator under the transport decorator:
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
|
123
|
-
import { CommandBus } from '@nestjs/cqrs';
|
|
124
|
-
import { InJobContext, type WithJobContext } from '@nestjs-pipeline/job-context';
|
|
125
|
-
import type { Job } from 'bullmq';
|
|
126
|
-
|
|
127
|
-
@Processor(WELCOME_EMAIL_QUEUE)
|
|
128
|
-
export class SendWelcomeEmailProcessor extends WorkerHost {
|
|
129
|
-
constructor(private readonly commandBus: CommandBus) {
|
|
130
|
-
super();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
@InJobContext()
|
|
134
|
-
async process(job: Job<WithJobContext<WelcomeEmail>>) {
|
|
135
|
-
await this.commandBus.execute(new SendWelcomeEmailCommand(job.data));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
`@InJobContext()` reads `data.jobContext` from the first argument (a BullMQ `Job`); pass
|
|
141
|
-
`{ path: 'jobContext' }` for a transport that hands the payload itself.
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
@EventPattern('user.registered')
|
|
145
|
-
@InJobContext({ path: 'jobContext' })
|
|
146
|
-
async onRegistered(@Payload() data: WithJobContext<WelcomeEmail>) {
|
|
147
|
-
await this.commandBus.execute(new SendWelcomeEmailCommand(data));
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
A refused job throws `MissingJobContextError` or `InvalidJobContextError` before the method
|
|
152
|
-
runs; let the queue mark it failed rather than retrying it, since the payload will not change.
|
|
153
|
-
|
|
154
|
-
Declare system-started work:
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
import { AsSystem } from '@nestjs-pipeline/job-context';
|
|
158
|
-
import { Cron } from '@nestjs/schedule';
|
|
159
|
-
|
|
160
|
-
@Cron('0 3 * * *')
|
|
161
|
-
@AsSystem({
|
|
162
|
-
principal: { id: 'session-cleanup', type: 'service' },
|
|
163
|
-
grants: [{ action: 'delete', subject: 'Auth' }],
|
|
164
|
-
})
|
|
165
|
-
async purgeSessions() {
|
|
166
|
-
await this.commandBus.execute(new PurgeExpiredSessionsCommand());
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## Behavior
|
|
171
|
-
|
|
172
|
-
- **`withJobContext(data)`** returns a copy of `data` with `jobContext`: the tenant and
|
|
173
|
-
the correlation id of the configured sources (a new one from the correlation source's
|
|
174
|
-
`create()` when none is active), and the captured principal. It throws `MissingJobContextError` without a
|
|
175
|
-
running `JobContextModule`, a tenant, or a principal, and `TypeError` for a payload that
|
|
176
|
-
is not a plain object.
|
|
177
|
-
- **`@InJobContext()`** validates the payload's context before the method runs. It refuses
|
|
178
|
-
a missing context (`MissingJobContextError`), and a malformed one, an unconfigured
|
|
179
|
-
tenant, a correlation id the correlation source's `accepts` refuses
|
|
180
|
-
(`correlationSource` accepts at most 128 characters of `A-Z a-z 0-9 . _ ~ : / + = @ -`), or a
|
|
181
|
-
principal carrying any field beyond `id`, `type` and `sessionId`, such as grants
|
|
182
|
-
(`InvalidJobContextError`). It then runs the method inside the tenant, the correlation
|
|
183
|
-
id, and `restore(principal, work)`. The method becomes async.
|
|
184
|
-
- **`@AsSystem({ principal, grants })`** runs the method once per configured tenant, one
|
|
185
|
-
after another, each with a new correlation id from `create()` and `restore(principal, work, grants)`. A
|
|
186
|
-
failing tenant does not stop the others; the method then rejects with an
|
|
187
|
-
`AggregateError` of the failures. Return values are discarded.
|
|
188
|
-
- **`JobContextModule.forRoot`** registers the principal port, tenants and sources when the module
|
|
189
|
-
is instantiated and removes them at application shutdown. The decorators wrap methods
|
|
190
|
-
outside dependency injection, so they use the registration of the running application;
|
|
191
|
-
without one they fail closed. Register it once per application.
|
|
192
|
-
|
|
193
|
-
## Security
|
|
194
|
-
|
|
195
|
-
A payload is data: anyone who can write to the queue can write it. The package therefore
|
|
196
|
-
carries an identity reference only, never grants, and `restore` decides what that
|
|
197
|
-
identity may do now. Re-check a user's session and account in `restore`, so a revoked
|
|
198
|
-
session or a deleted user refuses the job. Grants come only from `@AsSystem`, in code.
|
|
199
|
-
The tenant must be one of the configured tenants.
|
|
200
|
-
|
|
201
|
-
## API
|
|
202
|
-
|
|
203
|
-
| Export | Kind | Description |
|
|
204
|
-
| --- | --- | --- |
|
|
205
|
-
| `withJobContext(data)` | function | Copies `data` and adds the current `jobContext` |
|
|
206
|
-
| `InJobContext(options?)` | decorator | Runs a job method in its payload's context; `path` defaults to `'data.jobContext'` |
|
|
207
|
-
| `AsSystem(options)` | decorator | Runs system work once per tenant as the declared principal and grants |
|
|
208
|
-
| `JobContextModule.forRoot(options)` | module | Registers `principal` (a class), `tenants` (a list, or a function called at startup), `sources` and optional `imports` |
|
|
209
|
-
| `ContextSource`, `CorrelationSource`, `JobContextSources` | type | `{ current, run }`; the correlation source adds `create()` and `accepts(id)`; and the `{ tenantId, correlationId }` pair `sources` takes |
|
|
210
|
-
| `IJobPrincipal<TGrant>` | interface | Application port: `capture()` and `restore(principal, work, grants?)` |
|
|
211
|
-
| `PrincipalReference` | type | `{ id, type, sessionId? }` |
|
|
212
|
-
| `JobContext`, `WithJobContext<T>` | type | The carried context, and a payload with it |
|
|
213
|
-
| `MissingJobContextError`, `InvalidJobContextError` | error | Framework-neutral; map them in the consumer if needed |
|
|
21
|
+
Published as an ES module; a CommonJS application loads it with `require()`. Coming from 0.3.x, see [Upgrading from 0.3.x](https://aristoteliss.github.io/nestjs-pipeline/upgrading/from-0-3/).
|
|
214
22
|
|
|
215
23
|
## License
|
|
216
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs-pipeline/job-context",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Carries the tenant, correlation id and principal of a request into the queue jobs it enqueues, and gives system-started work an explicit context",
|
|
5
5
|
"author": "Aristotelis",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/aristoteliss/nestjs-pipeline.git",
|
|
14
14
|
"directory": "packages/pipeline-job-context"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.
|
|
16
|
+
"homepage": "https://aristoteliss.github.io/nestjs-pipeline/packages/nestjs-pipeline/job-context/",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/aristoteliss/nestjs-pipeline/issues"
|
|
19
19
|
},
|