@nestjs-pipeline/job-context 0.2.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.
- package/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +210 -0
- package/dist/constants/job-context.constants.d.ts +7 -0
- package/dist/constants/job-context.constants.d.ts.map +1 -0
- package/dist/constants/job-context.constants.js +11 -0
- package/dist/constants/job-context.constants.js.map +1 -0
- package/dist/decorators/as-system.decorator.d.ts +32 -0
- package/dist/decorators/as-system.decorator.d.ts.map +1 -0
- package/dist/decorators/as-system.decorator.js +58 -0
- package/dist/decorators/as-system.decorator.js.map +1 -0
- package/dist/decorators/in-job-context.decorator.d.ts +32 -0
- package/dist/decorators/in-job-context.decorator.d.ts.map +1 -0
- package/dist/decorators/in-job-context.decorator.js +53 -0
- package/dist/decorators/in-job-context.decorator.js.map +1 -0
- package/dist/errors/invalid-job-context.error.d.ts +15 -0
- package/dist/errors/invalid-job-context.error.d.ts.map +1 -0
- package/dist/errors/invalid-job-context.error.js +24 -0
- package/dist/errors/invalid-job-context.error.js.map +1 -0
- package/dist/errors/missing-job-context.error.d.ts +14 -0
- package/dist/errors/missing-job-context.error.d.ts.map +1 -0
- package/dist/errors/missing-job-context.error.js +23 -0
- package/dist/errors/missing-job-context.error.js.map +1 -0
- package/dist/helpers/parse-job-context.d.ts +20 -0
- package/dist/helpers/parse-job-context.d.ts.map +1 -0
- package/dist/helpers/parse-job-context.js +71 -0
- package/dist/helpers/parse-job-context.js.map +1 -0
- package/dist/helpers/principal-reference.d.ts +12 -0
- package/dist/helpers/principal-reference.d.ts.map +1 -0
- package/dist/helpers/principal-reference.js +18 -0
- package/dist/helpers/principal-reference.js.map +1 -0
- package/dist/helpers/registration.d.ts +39 -0
- package/dist/helpers/registration.d.ts.map +1 -0
- package/dist/helpers/registration.js +49 -0
- package/dist/helpers/registration.js.map +1 -0
- package/dist/helpers/with-job-context.d.ts +21 -0
- package/dist/helpers/with-job-context.d.ts.map +1 -0
- package/dist/helpers/with-job-context.js +49 -0
- package/dist/helpers/with-job-context.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/context-source.interface.d.ts +23 -0
- package/dist/interfaces/context-source.interface.d.ts.map +1 -0
- package/dist/interfaces/context-source.interface.js +4 -0
- package/dist/interfaces/context-source.interface.js.map +1 -0
- package/dist/interfaces/job-context-options.interface.d.ts +22 -0
- package/dist/interfaces/job-context-options.interface.d.ts.map +1 -0
- package/dist/interfaces/job-context-options.interface.js +4 -0
- package/dist/interfaces/job-context-options.interface.js.map +1 -0
- package/dist/interfaces/job-context.interface.d.ts +12 -0
- package/dist/interfaces/job-context.interface.d.ts.map +1 -0
- package/dist/interfaces/job-context.interface.js +4 -0
- package/dist/interfaces/job-context.interface.js.map +1 -0
- package/dist/interfaces/job-principal.interface.d.ts +28 -0
- package/dist/interfaces/job-principal.interface.d.ts.map +1 -0
- package/dist/interfaces/job-principal.interface.js +4 -0
- package/dist/interfaces/job-principal.interface.js.map +1 -0
- package/dist/interfaces/principal-reference.interface.d.ts +12 -0
- package/dist/interfaces/principal-reference.interface.d.ts.map +1 -0
- package/dist/interfaces/principal-reference.interface.js +4 -0
- package/dist/interfaces/principal-reference.interface.js.map +1 -0
- package/dist/job-context.module.d.ts +42 -0
- package/dist/job-context.module.d.ts.map +1 -0
- package/dist/job-context.module.js +99 -0
- package/dist/job-context.module.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @nestjs-pipeline/job-context
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@nestjs-pipeline/job-context)
|
|
4
|
+
[](https://www.npmjs.com/package/@nestjs-pipeline/job-context)
|
|
5
|
+
|
|
6
|
+
The execution context of a request, carried into the queue jobs it enqueues: the tenant,
|
|
7
|
+
the correlation id and the principal. A job then makes the decisions the request would
|
|
8
|
+
have made. System-started work, such as a cron job, declares its context explicitly
|
|
9
|
+
instead. Nothing falls back to a default tenant or an anonymous principal.
|
|
10
|
+
|
|
11
|
+
It depends on no other pipeline package. It reads and restores the tenant and the
|
|
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.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @nestjs-pipeline/job-context @nestjs/common
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js 22 or later.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
Implement `IJobPrincipal` over the application's authentication state, and register it
|
|
29
|
+
with the tenants jobs may run in and the tenant and correlation id sources:
|
|
30
|
+
|
|
31
|
+
`Capability`, `SessionRepository`, `SessionsModule`, `currentPrincipal`,
|
|
32
|
+
`runAsPrincipal` and `SessionRevokedError` stand for the application's own authentication
|
|
33
|
+
code.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Injectable, Module } from '@nestjs/common';
|
|
37
|
+
import { correlationSource } from '@nestjs-pipeline/correlation';
|
|
38
|
+
import {
|
|
39
|
+
type IJobPrincipal,
|
|
40
|
+
JobContextModule,
|
|
41
|
+
type PrincipalReference,
|
|
42
|
+
} from '@nestjs-pipeline/job-context';
|
|
43
|
+
import { tenantSource } from '@nestjs-pipeline/tenant';
|
|
44
|
+
|
|
45
|
+
@Injectable()
|
|
46
|
+
export class SessionJobPrincipal implements IJobPrincipal<Capability> {
|
|
47
|
+
constructor(private readonly sessions: SessionRepository) {}
|
|
48
|
+
|
|
49
|
+
capture(): PrincipalReference | undefined {
|
|
50
|
+
const principal = currentPrincipal();
|
|
51
|
+
return principal && { id: principal.id, type: principal.type, sessionId: principal.sid };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async restore<T>(
|
|
55
|
+
principal: PrincipalReference,
|
|
56
|
+
work: () => Promise<T>,
|
|
57
|
+
grants?: readonly Capability[],
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
if (grants) return runAsPrincipal({ ...principal, grants }, work);
|
|
60
|
+
const session = await this.sessions.findActive(principal.sessionId, principal.id);
|
|
61
|
+
if (!session) throw new SessionRevokedError();
|
|
62
|
+
return runAsPrincipal(principal, work);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Module({
|
|
67
|
+
imports: [
|
|
68
|
+
JobContextModule.forRoot({
|
|
69
|
+
principal: SessionJobPrincipal,
|
|
70
|
+
tenants: ['tenant_a', 'tenant_b'],
|
|
71
|
+
sources: { tenantId: tenantSource, correlationId: correlationSource },
|
|
72
|
+
imports: [SessionsModule],
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
export class JobsModule {}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`capture` reads the current principal when a job is enqueued; only its `id`, `type` and
|
|
80
|
+
`sessionId` are kept. `restore` runs when the job does, inside the job's tenant and
|
|
81
|
+
correlation id: it must re-check the principal against current state, bind it the way a
|
|
82
|
+
request would, and throw to refuse the job.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
Stamp the payload when enqueuing, from inside the request or handler, so the tenant,
|
|
87
|
+
correlation id and principal are current:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { InjectQueue } from '@nestjs/bullmq';
|
|
91
|
+
import { EventsHandler, type IEventHandler } from '@nestjs/cqrs';
|
|
92
|
+
import { withJobContext, type WithJobContext } from '@nestjs-pipeline/job-context';
|
|
93
|
+
import type { Queue } from 'bullmq';
|
|
94
|
+
|
|
95
|
+
type WelcomeEmail = { userId: string; email: string };
|
|
96
|
+
|
|
97
|
+
@EventsHandler(UserRegisteredEvent)
|
|
98
|
+
export class EnqueueWelcomeEmail implements IEventHandler<UserRegisteredEvent> {
|
|
99
|
+
constructor(
|
|
100
|
+
@InjectQueue(WELCOME_EMAIL_QUEUE)
|
|
101
|
+
private readonly queue: Queue<WithJobContext<WelcomeEmail>>,
|
|
102
|
+
) {}
|
|
103
|
+
|
|
104
|
+
async handle({ userId, email }: UserRegisteredEvent) {
|
|
105
|
+
await this.queue.add('send', withJobContext({ userId, email }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Restore it in the processor. Place the decorator under the transport decorator:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
|
114
|
+
import { CommandBus } from '@nestjs/cqrs';
|
|
115
|
+
import { InJobContext, type WithJobContext } from '@nestjs-pipeline/job-context';
|
|
116
|
+
import type { Job } from 'bullmq';
|
|
117
|
+
|
|
118
|
+
@Processor(WELCOME_EMAIL_QUEUE)
|
|
119
|
+
export class SendWelcomeEmailProcessor extends WorkerHost {
|
|
120
|
+
constructor(private readonly commandBus: CommandBus) {
|
|
121
|
+
super();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@InJobContext()
|
|
125
|
+
async process(job: Job<WithJobContext<WelcomeEmail>>) {
|
|
126
|
+
await this.commandBus.execute(new SendWelcomeEmailCommand(job.data));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`@InJobContext()` reads `data.jobContext` from the first argument (a BullMQ `Job`); pass
|
|
132
|
+
`{ path: 'jobContext' }` for a transport that hands the payload itself.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
@EventPattern('user.registered')
|
|
136
|
+
@InJobContext({ path: 'jobContext' })
|
|
137
|
+
async onRegistered(@Payload() data: WithJobContext<WelcomeEmail>) {
|
|
138
|
+
await this.commandBus.execute(new SendWelcomeEmailCommand(data));
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A refused job throws `MissingJobContextError` or `InvalidJobContextError` before the method
|
|
143
|
+
runs; let the queue mark it failed rather than retrying it, since the payload will not change.
|
|
144
|
+
|
|
145
|
+
Declare system-started work:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { AsSystem } from '@nestjs-pipeline/job-context';
|
|
149
|
+
import { Cron } from '@nestjs/schedule';
|
|
150
|
+
|
|
151
|
+
@Cron('0 3 * * *')
|
|
152
|
+
@AsSystem({
|
|
153
|
+
principal: { id: 'session-cleanup', type: 'service' },
|
|
154
|
+
grants: [{ action: 'delete', subject: 'Auth' }],
|
|
155
|
+
})
|
|
156
|
+
async purgeSessions() {
|
|
157
|
+
await this.commandBus.execute(new PurgeExpiredSessionsCommand());
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Behavior
|
|
162
|
+
|
|
163
|
+
- **`withJobContext(data)`** returns a copy of `data` with `jobContext`: the tenant and
|
|
164
|
+
the correlation id of the configured sources (a new one from the correlation source's
|
|
165
|
+
`create()` when none is active), and the captured principal. It throws `MissingJobContextError` without a
|
|
166
|
+
running `JobContextModule`, a tenant, or a principal, and `TypeError` for a payload that
|
|
167
|
+
is not a plain object.
|
|
168
|
+
- **`@InJobContext()`** validates the payload's context before the method runs. It refuses
|
|
169
|
+
a missing context (`MissingJobContextError`), and a malformed one, an unconfigured
|
|
170
|
+
tenant, a correlation id the correlation source's `accepts` refuses
|
|
171
|
+
(`correlationSource` accepts at most 128 characters of `A-Z a-z 0-9 . _ ~ : / + = @ -`), or a
|
|
172
|
+
principal carrying any field beyond `id`, `type` and `sessionId`, such as grants
|
|
173
|
+
(`InvalidJobContextError`). It then runs the method inside the tenant, the correlation
|
|
174
|
+
id, and `restore(principal, work)`. The method becomes async.
|
|
175
|
+
- **`@AsSystem({ principal, grants })`** runs the method once per configured tenant, one
|
|
176
|
+
after another, each with a new correlation id from `create()` and `restore(principal, work, grants)`. A
|
|
177
|
+
failing tenant does not stop the others; the method then rejects with an
|
|
178
|
+
`AggregateError` of the failures. Return values are discarded.
|
|
179
|
+
- **`JobContextModule.forRoot`** registers the principal port, tenants and sources when the module
|
|
180
|
+
is instantiated and removes them at application shutdown. The decorators wrap methods
|
|
181
|
+
outside dependency injection, so they use the registration of the running application;
|
|
182
|
+
without one they fail closed. Register it once per application.
|
|
183
|
+
|
|
184
|
+
## Security
|
|
185
|
+
|
|
186
|
+
A payload is data: anyone who can write to the queue can write it. The package therefore
|
|
187
|
+
carries an identity reference only, never grants, and `restore` decides what that
|
|
188
|
+
identity may do now. Re-check a user's session and account in `restore`, so a revoked
|
|
189
|
+
session or a deleted user refuses the job. Grants come only from `@AsSystem`, in code.
|
|
190
|
+
The tenant must be one of the configured tenants.
|
|
191
|
+
|
|
192
|
+
## API
|
|
193
|
+
|
|
194
|
+
| Export | Kind | Description |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| `withJobContext(data)` | function | Copies `data` and adds the current `jobContext` |
|
|
197
|
+
| `InJobContext(options?)` | decorator | Runs a job method in its payload's context; `path` defaults to `'data.jobContext'` |
|
|
198
|
+
| `AsSystem(options)` | decorator | Runs system work once per tenant as the declared principal and grants |
|
|
199
|
+
| `JobContextModule.forRoot(options)` | module | Registers `principal` (a class), `tenants`, `sources` and optional `imports` |
|
|
200
|
+
| `ContextSource`, `CorrelationSource`, `JobContextSources` | type | `{ current, run }`; the correlation source adds `create()` and `accepts(id)`; and the `{ tenantId, correlationId }` pair `sources` takes |
|
|
201
|
+
| `IJobPrincipal<TGrant>` | interface | Application port: `capture()` and `restore(principal, work, grants?)` |
|
|
202
|
+
| `PrincipalReference` | type | `{ id, type, sessionId? }` |
|
|
203
|
+
| `JobContext`, `WithJobContext<T>` | type | The carried context, and a payload with it |
|
|
204
|
+
| `MissingJobContextError`, `InvalidJobContextError` | error | Framework-neutral; map them in the consumer if needed |
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
Dual-licensed under **AGPL-3.0-or-later** or a **Commercial License**. See
|
|
209
|
+
[`LICENSE`](https://github.com/aristoteliss/nestjs-pipeline/blob/master/LICENSE) and [`COMMERCIAL_LICENSE.txt`](https://github.com/aristoteliss/nestjs-pipeline/blob/master/COMMERCIAL_LICENSE.txt)
|
|
210
|
+
at the repository root.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Injection token of the application's `IJobPrincipal`. */
|
|
2
|
+
export declare const JOB_PRINCIPAL: unique symbol;
|
|
3
|
+
/** Injection token of the configured tenant list. */
|
|
4
|
+
export declare const JOB_TENANTS: unique symbol;
|
|
5
|
+
/** Injection token of the tenant and correlation id sources. */
|
|
6
|
+
export declare const JOB_SOURCES: unique symbol;
|
|
7
|
+
//# sourceMappingURL=job-context.constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-context.constants.d.ts","sourceRoot":"","sources":["../../src/constants/job-context.constants.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,eAAO,MAAM,aAAa,eAA0B,CAAC;AAErD,qDAAqD;AACrD,eAAO,MAAM,WAAW,eAAwB,CAAC;AAEjD,gEAAgE;AAChE,eAAO,MAAM,WAAW,eAAwB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.JOB_SOURCES = exports.JOB_TENANTS = exports.JOB_PRINCIPAL = void 0;
|
|
5
|
+
/** Injection token of the application's `IJobPrincipal`. */
|
|
6
|
+
exports.JOB_PRINCIPAL = Symbol('JOB_PRINCIPAL');
|
|
7
|
+
/** Injection token of the configured tenant list. */
|
|
8
|
+
exports.JOB_TENANTS = Symbol('JOB_TENANTS');
|
|
9
|
+
/** Injection token of the tenant and correlation id sources. */
|
|
10
|
+
exports.JOB_SOURCES = Symbol('JOB_SOURCES');
|
|
11
|
+
//# sourceMappingURL=job-context.constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-context.constants.js","sourceRoot":"","sources":["../../src/constants/job-context.constants.ts"],"names":[],"mappings":";AAAA,sEAAsE;;;AAEtE,4DAA4D;AAC/C,QAAA,aAAa,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;AAErD,qDAAqD;AACxC,QAAA,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;AAEjD,gEAAgE;AACnD,QAAA,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PrincipalReference } from '../interfaces/principal-reference.interface';
|
|
2
|
+
/** Options of {@link AsSystem}. */
|
|
3
|
+
export interface AsSystemOptions<TGrant = unknown> {
|
|
4
|
+
/** The service principal the work acts as. */
|
|
5
|
+
principal: PrincipalReference;
|
|
6
|
+
/** The principal's complete authorization; nothing else grants it anything. */
|
|
7
|
+
grants: readonly TGrant[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs system-started work, such as a cron job, once per configured tenant.
|
|
11
|
+
* Each run has its own tenant, a new correlation id, and the declared service
|
|
12
|
+
* principal and grants, bound through the registered `IJobPrincipal`. Grants
|
|
13
|
+
* come only from this declaration, never from data.
|
|
14
|
+
*
|
|
15
|
+
* Tenants run one after another. A failing tenant does not stop the others;
|
|
16
|
+
* the method then rejects with an `AggregateError` of the failures. Its return
|
|
17
|
+
* values are discarded. Place it under the scheduling decorator.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* @Cron('0 3 * * *')
|
|
22
|
+
* @AsSystem({
|
|
23
|
+
* principal: { id: 'session-cleanup', type: 'service' },
|
|
24
|
+
* grants: [{ action: 'delete', subject: 'Auth' }],
|
|
25
|
+
* })
|
|
26
|
+
* async purgeSessions() {
|
|
27
|
+
* await this.commandBus.execute(new PurgeExpiredSessionsCommand());
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function AsSystem<TGrant>(options: AsSystemOptions<TGrant>): MethodDecorator;
|
|
32
|
+
//# sourceMappingURL=as-system.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"as-system.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/as-system.decorator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AAEtF,mCAAmC;AACnC,MAAM,WAAW,eAAe,CAAC,MAAM,GAAG,OAAO;IAC/C,8CAA8C;IAC9C,SAAS,EAAE,kBAAkB,CAAC;IAC9B,+EAA+E;IAC/E,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAC7B,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAC/B,eAAe,CA2CjB"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AsSystem = AsSystem;
|
|
5
|
+
const principal_reference_1 = require("../helpers/principal-reference");
|
|
6
|
+
const registration_1 = require("../helpers/registration");
|
|
7
|
+
/**
|
|
8
|
+
* Runs system-started work, such as a cron job, once per configured tenant.
|
|
9
|
+
* Each run has its own tenant, a new correlation id, and the declared service
|
|
10
|
+
* principal and grants, bound through the registered `IJobPrincipal`. Grants
|
|
11
|
+
* come only from this declaration, never from data.
|
|
12
|
+
*
|
|
13
|
+
* Tenants run one after another. A failing tenant does not stop the others;
|
|
14
|
+
* the method then rejects with an `AggregateError` of the failures. Its return
|
|
15
|
+
* values are discarded. Place it under the scheduling decorator.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* @Cron('0 3 * * *')
|
|
20
|
+
* @AsSystem({
|
|
21
|
+
* principal: { id: 'session-cleanup', type: 'service' },
|
|
22
|
+
* grants: [{ action: 'delete', subject: 'Auth' }],
|
|
23
|
+
* })
|
|
24
|
+
* async purgeSessions() {
|
|
25
|
+
* await this.commandBus.execute(new PurgeExpiredSessionsCommand());
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function AsSystem(options) {
|
|
30
|
+
const reference = (0, principal_reference_1.toReference)(options.principal);
|
|
31
|
+
const grants = [...options.grants];
|
|
32
|
+
return (_target, _propertyKey, descriptor) => {
|
|
33
|
+
const original = descriptor.value;
|
|
34
|
+
descriptor.value = async function (...args) {
|
|
35
|
+
const { principal, tenants, sources } = (0, registration_1.activeRegistration)();
|
|
36
|
+
const failures = [];
|
|
37
|
+
const failed = [];
|
|
38
|
+
for (const tenant of tenants) {
|
|
39
|
+
try {
|
|
40
|
+
await sources.tenantId.run(tenant, () => sources.correlationId.run(sources.correlationId.create(), () => principal.restore(reference, async () => original.apply(this, args), grants)));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
failures.push(error);
|
|
44
|
+
failed.push(tenant);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (failures.length > 0) {
|
|
48
|
+
throw new AggregateError(failures, `System work failed in tenants: ${failed.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(descriptor.value, 'name', {
|
|
52
|
+
value: original.name,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
return descriptor;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=as-system.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"as-system.decorator.js","sourceRoot":"","sources":["../../src/decorators/as-system.decorator.ts"],"names":[],"mappings":";AAAA,sEAAsE;;AAoCtE,4BA6CC;AA/ED,wEAA6D;AAC7D,0DAA6D;AAW7D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,SAAgB,QAAQ,CACtB,OAAgC;IAEhC,MAAM,SAAS,GAAG,IAAA,iCAAW,EAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnC,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,UAA8B,EAAE,EAAE;QAC/D,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAwC,CAAC;QAErE,UAAU,CAAC,KAAK,GAAG,KAAK,WAEtB,GAAG,IAAe;YAElB,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAA,iCAAkB,GAAE,CAAC;YAC7D,MAAM,QAAQ,GAAc,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CACtC,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAC7D,SAAS,CAAC,OAAO,CACf,SAAS,EACT,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,EACtC,MAAM,CACP,CACF,CACF,CAAC;gBACJ,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACrB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACtB,CAAC;YACH,CAAC;YACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,cAAc,CACtB,QAAQ,EACR,kCAAkC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACtD,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;QACF,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE;YAC9C,KAAK,EAAE,QAAQ,CAAC,IAAI;YACpB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Options of {@link InJobContext}. */
|
|
2
|
+
export interface InJobContextOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Dot path to the job context in the method's first argument.
|
|
5
|
+
*
|
|
6
|
+
* @default 'data.jobContext' (a BullMQ `Job`)
|
|
7
|
+
*/
|
|
8
|
+
path?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Runs a job-processing method inside the context `withJobContext` stamped on
|
|
12
|
+
* its payload: the tenant, the correlation id, and the principal the registered
|
|
13
|
+
* `IJobPrincipal` re-checks and binds. The method becomes async.
|
|
14
|
+
*
|
|
15
|
+
* It fails closed before the method runs: a payload without a context, with a
|
|
16
|
+
* malformed one, with an unconfigured tenant, or with a principal carrying
|
|
17
|
+
* anything beyond `id`, `type` and `sessionId` is refused, and so is a
|
|
18
|
+
* principal `restore` rejects. Place it under the transport decorator.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* @Processor(WELCOME_EMAIL_QUEUE)
|
|
23
|
+
* export class SendWelcomeEmailProcessor extends WorkerHost {
|
|
24
|
+
* @InJobContext()
|
|
25
|
+
* async process(job: Job<WithJobContext<WelcomeEmail>>) {
|
|
26
|
+
* await this.commandBus.execute(new SendWelcomeEmailCommand(job.data));
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function InJobContext(options?: InJobContextOptions): MethodDecorator;
|
|
32
|
+
//# sourceMappingURL=in-job-context.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-job-context.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/in-job-context.decorator.ts"],"names":[],"mappings":"AAKA,uCAAuC;AACvC,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAWD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAC1B,OAAO,GAAE,mBAAwB,GAChC,eAAe,CA2BjB"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.InJobContext = InJobContext;
|
|
5
|
+
const parse_job_context_1 = require("../helpers/parse-job-context");
|
|
6
|
+
const registration_1 = require("../helpers/registration");
|
|
7
|
+
function readPath(value, segments) {
|
|
8
|
+
let current = value;
|
|
9
|
+
for (const segment of segments) {
|
|
10
|
+
if (typeof current !== 'object' || current === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
current = current[segment];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs a job-processing method inside the context `withJobContext` stamped on
|
|
18
|
+
* its payload: the tenant, the correlation id, and the principal the registered
|
|
19
|
+
* `IJobPrincipal` re-checks and binds. The method becomes async.
|
|
20
|
+
*
|
|
21
|
+
* It fails closed before the method runs: a payload without a context, with a
|
|
22
|
+
* malformed one, with an unconfigured tenant, or with a principal carrying
|
|
23
|
+
* anything beyond `id`, `type` and `sessionId` is refused, and so is a
|
|
24
|
+
* principal `restore` rejects. Place it under the transport decorator.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* @Processor(WELCOME_EMAIL_QUEUE)
|
|
29
|
+
* export class SendWelcomeEmailProcessor extends WorkerHost {
|
|
30
|
+
* @InJobContext()
|
|
31
|
+
* async process(job: Job<WithJobContext<WelcomeEmail>>) {
|
|
32
|
+
* await this.commandBus.execute(new SendWelcomeEmailCommand(job.data));
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function InJobContext(options = {}) {
|
|
38
|
+
const segments = (options.path ?? 'data.jobContext').split('.');
|
|
39
|
+
return (_target, _propertyKey, descriptor) => {
|
|
40
|
+
const original = descriptor.value;
|
|
41
|
+
descriptor.value = async function (...args) {
|
|
42
|
+
const { principal, tenants, sources } = (0, registration_1.activeRegistration)();
|
|
43
|
+
const context = (0, parse_job_context_1.parseJobContext)(readPath(args[0], segments), tenants, sources.correlationId.accepts);
|
|
44
|
+
return sources.tenantId.run(context.tenantId, () => sources.correlationId.run(context.correlationId, () => principal.restore(context.principal, async () => original.apply(this, args))));
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(descriptor.value, 'name', {
|
|
47
|
+
value: original.name,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
return descriptor;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=in-job-context.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-job-context.decorator.js","sourceRoot":"","sources":["../../src/decorators/in-job-context.decorator.ts"],"names":[],"mappings":";AAAA,sEAAsE;;AA6CtE,oCA6BC;AAxED,oEAA+D;AAC/D,0DAA6D;AAY7D,SAAS,QAAQ,CAAC,KAAc,EAAE,QAA2B;IAC3D,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QACtE,OAAO,GAAI,OAAmC,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,YAAY,CAC1B,UAA+B,EAAE;IAEjC,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,iBAAiB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEhE,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,UAA8B,EAAE,EAAE;QAC/D,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAwC,CAAC;QAErE,UAAU,CAAC,KAAK,GAAG,KAAK,WAA0B,GAAG,IAAe;YAClE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAA,iCAAkB,GAAE,CAAC;YAC7D,MAAM,OAAO,GAAG,IAAA,mCAAe,EAC7B,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAC3B,OAAO,EACP,OAAO,CAAC,aAAa,CAAC,OAAO,CAC9B,CAAC;YACF,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,CACjD,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,EAAE,CACpD,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE,CAC9C,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAC3B,CACF,CACF,CAAC;QACJ,CAAC,CAAC;QACF,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE;YAC9C,KAAK,EAAE,QAAQ,CAAC,IAAI;YACpB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when a job payload's context is malformed, names an unconfigured
|
|
3
|
+
* tenant, or carries fields a principal reference must not have, such as
|
|
4
|
+
* grants. A payload is data written by whoever can write to the queue.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* throw new InvalidJobContextError('tenantId is not a configured tenant');
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare class InvalidJobContextError extends Error {
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
constructor(reason: string);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=invalid-job-context.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid-job-context.error.d.ts","sourceRoot":"","sources":["../../src/errors/invalid-job-context.error.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAIpC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.InvalidJobContextError = void 0;
|
|
5
|
+
/**
|
|
6
|
+
* Raised when a job payload's context is malformed, names an unconfigured
|
|
7
|
+
* tenant, or carries fields a principal reference must not have, such as
|
|
8
|
+
* grants. A payload is data written by whoever can write to the queue.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* throw new InvalidJobContextError('tenantId is not a configured tenant');
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
class InvalidJobContextError extends Error {
|
|
16
|
+
reason;
|
|
17
|
+
constructor(reason) {
|
|
18
|
+
super(`Invalid job context: ${reason}.`);
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
this.name = InvalidJobContextError.name;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.InvalidJobContextError = InvalidJobContextError;
|
|
24
|
+
//# sourceMappingURL=invalid-job-context.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid-job-context.error.js","sourceRoot":"","sources":["../../src/errors/invalid-job-context.error.ts"],"names":[],"mappings":";AAAA,sEAAsE;;;AAEtE;;;;;;;;;GASG;AACH,MAAa,sBAAuB,SAAQ,KAAK;IAC1B;IAArB,YAAqB,MAAc;QACjC,KAAK,CAAC,wBAAwB,MAAM,GAAG,CAAC,CAAC;QADtB,WAAM,GAAN,MAAM,CAAQ;QAEjC,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;IAC1C,CAAC;CACF;AALD,wDAKC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when a job would run, or be enqueued, without part of its execution
|
|
3
|
+
* context. A job never falls back to a default tenant or an anonymous principal.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* throw new MissingJobContextError('principal');
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare class MissingJobContextError extends Error {
|
|
11
|
+
readonly missing: string;
|
|
12
|
+
constructor(missing: string);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=missing-job-context.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-job-context.error.d.ts","sourceRoot":"","sources":["../../src/errors/missing-job-context.error.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAMrC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MissingJobContextError = void 0;
|
|
5
|
+
/**
|
|
6
|
+
* Raised when a job would run, or be enqueued, without part of its execution
|
|
7
|
+
* context. A job never falls back to a default tenant or an anonymous principal.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* throw new MissingJobContextError('principal');
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
class MissingJobContextError extends Error {
|
|
15
|
+
missing;
|
|
16
|
+
constructor(missing) {
|
|
17
|
+
super(`Missing job context: ${missing}. A job never runs without it and never falls back to a default.`);
|
|
18
|
+
this.missing = missing;
|
|
19
|
+
this.name = MissingJobContextError.name;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.MissingJobContextError = MissingJobContextError;
|
|
23
|
+
//# sourceMappingURL=missing-job-context.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-job-context.error.js","sourceRoot":"","sources":["../../src/errors/missing-job-context.error.ts"],"names":[],"mappings":";AAAA,sEAAsE;;;AAEtE;;;;;;;;GAQG;AACH,MAAa,sBAAuB,SAAQ,KAAK;IAC1B;IAArB,YAAqB,OAAe;QAClC,KAAK,CACH,wBAAwB,OAAO,kEAAkE,CAClG,CAAC;QAHiB,YAAO,GAAP,OAAO,CAAQ;QAIlC,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;IAC1C,CAAC;CACF;AAPD,wDAOC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JobContext } from '../interfaces/job-context.interface';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a job payload's context before anything runs with it. Every field
|
|
4
|
+
* is checked, unknown fields are refused, the tenant must be configured, and
|
|
5
|
+
* the correlation id must be one `acceptsCorrelationId` accepts.
|
|
6
|
+
*
|
|
7
|
+
* @param value - The payload's `jobContext` value, as read from the queue.
|
|
8
|
+
* @param tenants - The configured tenants.
|
|
9
|
+
* @param acceptsCorrelationId - The correlation source's `accepts`.
|
|
10
|
+
* @returns A context holding only the validated fields.
|
|
11
|
+
* @throws {MissingJobContextError} When `value` is `undefined`.
|
|
12
|
+
* @throws {InvalidJobContextError} For any other value that is not a valid context.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const context = parseJobContext(job.data.jobContext, ['tenant_a'], sources.correlationId.accepts);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseJobContext(value: unknown, tenants: readonly string[], acceptsCorrelationId: (id: string) => boolean): JobContext;
|
|
20
|
+
//# sourceMappingURL=parse-job-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-job-context.d.ts","sourceRoot":"","sources":["../../src/helpers/parse-job-context.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAyBtE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,oBAAoB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAC5C,UAAU,CAmCZ"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.parseJobContext = parseJobContext;
|
|
5
|
+
const invalid_job_context_error_1 = require("../errors/invalid-job-context.error");
|
|
6
|
+
const missing_job_context_error_1 = require("../errors/missing-job-context.error");
|
|
7
|
+
const principal_reference_1 = require("./principal-reference");
|
|
8
|
+
const CONTEXT_FIELDS = new Set(['tenantId', 'correlationId', 'principal']);
|
|
9
|
+
const PRINCIPAL_FIELDS = new Set(['id', 'type', 'sessionId']);
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function isText(value) {
|
|
14
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
15
|
+
}
|
|
16
|
+
function assertFields(value, allowed, name) {
|
|
17
|
+
const extra = Object.keys(value).find((key) => !allowed.has(key));
|
|
18
|
+
if (extra !== undefined) {
|
|
19
|
+
throw new invalid_job_context_error_1.InvalidJobContextError(`${name} carries the field "${extra}"`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validates a job payload's context before anything runs with it. Every field
|
|
24
|
+
* is checked, unknown fields are refused, the tenant must be configured, and
|
|
25
|
+
* the correlation id must be one `acceptsCorrelationId` accepts.
|
|
26
|
+
*
|
|
27
|
+
* @param value - The payload's `jobContext` value, as read from the queue.
|
|
28
|
+
* @param tenants - The configured tenants.
|
|
29
|
+
* @param acceptsCorrelationId - The correlation source's `accepts`.
|
|
30
|
+
* @returns A context holding only the validated fields.
|
|
31
|
+
* @throws {MissingJobContextError} When `value` is `undefined`.
|
|
32
|
+
* @throws {InvalidJobContextError} For any other value that is not a valid context.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const context = parseJobContext(job.data.jobContext, ['tenant_a'], sources.correlationId.accepts);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function parseJobContext(value, tenants, acceptsCorrelationId) {
|
|
40
|
+
if (value === undefined) {
|
|
41
|
+
throw new missing_job_context_error_1.MissingJobContextError('the payload has no jobContext');
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(value)) {
|
|
44
|
+
throw new invalid_job_context_error_1.InvalidJobContextError('jobContext is not an object');
|
|
45
|
+
}
|
|
46
|
+
assertFields(value, CONTEXT_FIELDS, 'jobContext');
|
|
47
|
+
const { tenantId, correlationId, principal } = value;
|
|
48
|
+
if (typeof tenantId !== 'string' || !tenants.includes(tenantId)) {
|
|
49
|
+
throw new invalid_job_context_error_1.InvalidJobContextError('tenantId is not a configured tenant');
|
|
50
|
+
}
|
|
51
|
+
if (typeof correlationId !== 'string' ||
|
|
52
|
+
!acceptsCorrelationId(correlationId)) {
|
|
53
|
+
throw new invalid_job_context_error_1.InvalidJobContextError('correlationId is malformed');
|
|
54
|
+
}
|
|
55
|
+
if (!isRecord(principal)) {
|
|
56
|
+
throw new invalid_job_context_error_1.InvalidJobContextError('principal is not an object');
|
|
57
|
+
}
|
|
58
|
+
assertFields(principal, PRINCIPAL_FIELDS, 'principal');
|
|
59
|
+
const { id, type, sessionId } = principal;
|
|
60
|
+
if (!isText(id) ||
|
|
61
|
+
!isText(type) ||
|
|
62
|
+
(sessionId !== undefined && !isText(sessionId))) {
|
|
63
|
+
throw new invalid_job_context_error_1.InvalidJobContextError('principal is malformed');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
tenantId,
|
|
67
|
+
correlationId,
|
|
68
|
+
principal: (0, principal_reference_1.toReference)({ id, type, sessionId }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=parse-job-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-job-context.js","sourceRoot":"","sources":["../../src/helpers/parse-job-context.ts"],"names":[],"mappings":";AAAA,sEAAsE;;AA8CtE,0CAuCC;AAnFD,mFAA6E;AAC7E,mFAA6E;AAE7E,+DAAoD;AAEpD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC;AAC3E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAE9D,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,YAAY,CACnB,KAA8B,EAC9B,OAA4B,EAC5B,IAAY;IAEZ,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAClE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,kDAAsB,CAAC,GAAG,IAAI,uBAAuB,KAAK,GAAG,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,eAAe,CAC7B,KAAc,EACd,OAA0B,EAC1B,oBAA6C;IAE7C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,kDAAsB,CAAC,+BAA+B,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,kDAAsB,CAAC,6BAA6B,CAAC,CAAC;IAClE,CAAC;IACD,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;IAClD,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACrD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,kDAAsB,CAAC,qCAAqC,CAAC,CAAC;IAC1E,CAAC;IACD,IACE,OAAO,aAAa,KAAK,QAAQ;QACjC,CAAC,oBAAoB,CAAC,aAAa,CAAC,EACpC,CAAC;QACD,MAAM,IAAI,kDAAsB,CAAC,4BAA4B,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,kDAAsB,CAAC,4BAA4B,CAAC,CAAC;IACjE,CAAC;IACD,YAAY,CAAC,SAAS,EAAE,gBAAgB,EAAE,WAAW,CAAC,CAAC;IACvD,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,SAAS,CAAC;IAC1C,IACE,CAAC,MAAM,CAAC,EAAE,CAAC;QACX,CAAC,MAAM,CAAC,IAAI,CAAC;QACb,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAC/C,CAAC;QACD,MAAM,IAAI,kDAAsB,CAAC,wBAAwB,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO;QACL,QAAQ;QACR,aAAa;QACb,SAAS,EAAE,IAAA,iCAAW,EAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;KAChD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PrincipalReference } from '../interfaces/principal-reference.interface';
|
|
2
|
+
/**
|
|
3
|
+
* Copies only the identity fields of `principal`, so grants or session data an
|
|
4
|
+
* application object also holds never reach a payload or a restore call.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* toReference({ id: 'u-1', type: 'user', sid: 's-1', grants: [] }); // { id: 'u-1', type: 'user' }
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function toReference(principal: PrincipalReference): PrincipalReference;
|
|
12
|
+
//# sourceMappingURL=principal-reference.d.ts.map
|