@tsed/agenda 7.52.0 → 7.53.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.
@@ -1,14 +1,20 @@
1
1
  import { AfterListen, Logger, OnDestroy } from "@tsed/common";
2
2
  import { InjectorService, Provider } from "@tsed/di";
3
+ import { Job } from "agenda";
3
4
  import { AgendaService } from "./services/AgendaFactory";
4
5
  export declare class AgendaModule implements OnDestroy, AfterListen {
5
6
  protected logger: Logger;
6
7
  protected injector: InjectorService;
7
8
  protected agenda: AgendaService;
8
- private loadAgenda;
9
+ private enabled;
9
10
  private disableJobProcessing;
10
11
  $afterListen(): Promise<any>;
11
12
  $onDestroy(): Promise<any>;
13
+ define(name: string, options?: any, processor?: any): void;
14
+ every(interval: string, name: string, data?: any, options?: any): Promise<any>;
15
+ schedule(when: Date | string, name: string, data?: any): Promise<Job<import("agenda").JobAttributesData>>;
16
+ now(name: string, data?: any): Promise<Job<import("agenda").JobAttributesData>>;
17
+ create(name: string, data?: any): Job<import("agenda").JobAttributesData>;
12
18
  protected getProviders(): Provider<any>[];
13
19
  protected addAgendaDefinitionsForProvider(provider: Provider): void;
14
20
  protected scheduleJobsForProvider(provider: Provider<any>): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsed/agenda",
3
- "version": "7.52.0",
3
+ "version": "7.53.0",
4
4
  "description": "Agenda package for Ts.ED framework",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/cjs/index.js",
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "private": false,
27
27
  "devDependencies": {
28
- "@tsed/common": "7.52.0",
29
- "@tsed/core": "7.52.0",
30
- "@tsed/di": "7.52.0",
28
+ "@tsed/common": "7.53.0",
29
+ "@tsed/core": "7.53.0",
30
+ "@tsed/di": "7.53.0",
31
31
  "@tsed/eslint": "7.44.1",
32
32
  "@tsed/typescript": "7.44.1",
33
33
  "agenda": "^4.1.3",
package/readme.md CHANGED
@@ -82,7 +82,7 @@ method name is used as job name.
82
82
  Use the `@Define` decorator on methods that you would like to schedule
83
83
  programmatically via the AgendaService and Agenda instance access.
84
84
 
85
- ```typescript
85
+ ```ts
86
86
  import {Agenda, Every, Define} from "@tsed/agenda";
87
87
  import {Job} from "agenda";
88
88
 
@@ -111,6 +111,52 @@ export class EmailJobService {
111
111
  }
112
112
  ```
113
113
 
114
+ ## Define a job processor manually
115
+
116
+ Since Ts.ED 7.53.0, AgendaModule exposes methods to manually define a job processor. It can be useful to define a job processor when you need to fetch data beforehand and dynamically build job name / options.
117
+
118
+ ```typescript
119
+ import {Agenda, AgendaModule, Define} from "@tsed/agenda";
120
+
121
+ @Agenda({namespace: "email"})
122
+ export class EmailJobService {
123
+ @Inject()
124
+ agenda: AgendaModule;
125
+
126
+ @Inject()
127
+ httpClient: HttpClient;
128
+
129
+ cache: Map<string, Job[]> = new Map();
130
+
131
+ @Define({
132
+ name: "sendWelcomeEmail",
133
+ concurrency: 3
134
+ /* ... and any option you would normally pass to agenda.define(...) */
135
+ })
136
+ async sendWelcomeEmail(job: Job) {
137
+ // implement something here
138
+ console.log(job.attrs.data.locale);
139
+ }
140
+
141
+ async $beforeAgendaStart() {
142
+ const locales = await this.httpClient.get("/locales");
143
+
144
+ this.cache.set(
145
+ "sendWelcomeEmail",
146
+ locales.map((locale) => {
147
+ return this.agenda.create("sendWelcomeEmail", {locale});
148
+ })
149
+ );
150
+ }
151
+
152
+ async $afterAgendaStart() {
153
+ const jobs = this.cache.get("sendWelcomeEmail");
154
+
155
+ await Promise.all(jobs.map((job) => job.repeatEvery("1 week").save()));
156
+ }
157
+ }
158
+ ```
159
+
114
160
  ## Inject Agenda
115
161
 
116
162
  Inject the AgendaService instance to interact with it directly, e.g. to schedule
@@ -118,12 +164,12 @@ a job manually.
118
164
 
119
165
  ```typescript
120
166
  import {Service, AfterRoutesInit} from "@tsed/common";
121
- import {AgendaService} from "@tsed/agenda";
167
+ import {AgendaModule} from "@tsed/agenda";
122
168
 
123
169
  @Service()
124
- export class UsersService implements AfterRoutesInit {
170
+ export class UsersService {
125
171
  @Inject()
126
- private agenda: AgendaService;
172
+ private agenda: AgendaModule;
127
173
 
128
174
  async create(user: User): Promise<User> {
129
175
  // do something
@@ -137,6 +183,49 @@ export class UsersService implements AfterRoutesInit {
137
183
  }
138
184
  ```
139
185
 
186
+ ## Using Agendash
187
+
188
+ [Agendash](https://github.com/agenda/agendash) provides a job overview dashboard that makes it easy to manage, create and
189
+ schedule your jobs.
190
+
191
+ ::: tip Note
192
+ This is an optional feature and is not required to use agenda.
193
+ :::
194
+
195
+ Install the additional dependency.
196
+
197
+ ```shell
198
+ npm install --save agendash
199
+ ```
200
+
201
+ Afterward create the module `agendash.module.ts` in src/modules so that the dashboard can be exposed using middleware.
202
+
203
+ ```typescript
204
+ import {AfterRoutesInit, Inject, PlatformApplication} from "@tsed/common";
205
+ import {Configuration, Module} from "@tsed/di";
206
+ import {Agenda} from "agenda";
207
+
208
+ const Agendash = require("agendash");
209
+
210
+ @Module()
211
+ export class AgendashModule implements AfterRoutesInit {
212
+ @Configuration()
213
+ config: Configuration;
214
+
215
+ @Inject()
216
+ agenda: Agenda;
217
+
218
+ @Inject()
219
+ app: PlatformApplication;
220
+
221
+ $afterRoutesInit() {
222
+ if (this.config.agenda?.enabled) {
223
+ this.app.use("/agendash", Agendash(this.agenda));
224
+ }
225
+ }
226
+ }
227
+ ```
228
+
140
229
  ## Contributors
141
230
 
142
231
  Please read [contributing guidelines here](https://tsed.io/contributing.html)
@@ -0,0 +1,198 @@
1
+ import {PlatformTest} from "@tsed/common";
2
+ import {Inject} from "@tsed/di";
3
+ import {Job} from "agenda";
4
+ import {AgendaModule} from "./AgendaModule";
5
+ import {Agenda} from "./decorators/agenda";
6
+ import {Define} from "./decorators/define";
7
+ import {Every} from "./decorators/every";
8
+
9
+ jest.mock("agenda", () => {
10
+ return {
11
+ Agenda: class {
12
+ close = jest.fn();
13
+ stop = jest.fn();
14
+ define = jest.fn();
15
+ every = jest.fn();
16
+ schedule = jest.fn();
17
+ now = jest.fn();
18
+ create = jest.fn().mockReturnValue({
19
+ repeatEvery: jest.fn().mockReturnThis(),
20
+ save: jest.fn()
21
+ });
22
+ start = jest.fn();
23
+ }
24
+ };
25
+ });
26
+
27
+ @Agenda({namespace: "test-nsp"})
28
+ class CustomCampaign {
29
+ @Inject()
30
+ agenda: AgendaModule;
31
+
32
+ job: Job;
33
+
34
+ $beforeAgendaStart() {
35
+ this.job = this.agenda.create("customName2", {
36
+ locale: "fr-FR"
37
+ });
38
+ }
39
+
40
+ $afterAgendaStart() {
41
+ return this.job.repeatEvery("1 week").save();
42
+ }
43
+
44
+ @Every("60 seconds")
45
+ test(job: Job) {
46
+ // test
47
+ return "hello " + job.attrs.name;
48
+ }
49
+
50
+ @Define({name: "customName"})
51
+ test2(job: Job) {
52
+ // test
53
+ return "hello " + job.attrs.name;
54
+ }
55
+
56
+ @Define({name: "customName2"})
57
+ test3() {
58
+ // test
59
+ }
60
+ }
61
+
62
+ @Agenda({namespace: "test-nsp-2"})
63
+ class CustomCampaign2 {
64
+ @Define({name: "customName"})
65
+ test(job: Job) {
66
+ // test
67
+ return "hello " + job.attrs.name;
68
+ }
69
+ }
70
+
71
+ @Agenda({namespace: "test-nsp-3"})
72
+ class CustomCampaign3 {}
73
+
74
+ describe("AgendaModule", () => {
75
+ describe("when agenda is enabled", () => {
76
+ beforeEach(() =>
77
+ PlatformTest.create({
78
+ agenda: {
79
+ enabled: true
80
+ }
81
+ })
82
+ );
83
+ afterEach(() => PlatformTest.reset());
84
+
85
+ describe("$afterListen()", () => {
86
+ it("should load all jobs", async () => {
87
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
88
+ const campaign = PlatformTest.get<CustomCampaign>(CustomCampaign)!;
89
+
90
+ await agendaModule.$afterListen();
91
+
92
+ expect(agendaModule.agenda.define).toHaveBeenCalledWith("test-nsp.test", {}, expect.any(Function));
93
+ expect(agendaModule.agenda.every).toHaveBeenCalledWith("60 seconds", "test-nsp.test", {}, {});
94
+ expect(agendaModule.agenda.start).toHaveBeenCalledWith();
95
+ expect(agendaModule.agenda.create).toHaveBeenCalledWith("customName2", {locale: "fr-FR"});
96
+
97
+ expect(campaign.job.repeatEvery).toHaveBeenCalledWith("1 week");
98
+ expect(campaign.job.save).toHaveBeenCalledWith();
99
+
100
+ const result = await agendaModule.agenda.define.mock.calls[0][2]({
101
+ attrs: {
102
+ name: "test-nsp.test"
103
+ }
104
+ });
105
+
106
+ expect(result).toEqual("hello test-nsp.test");
107
+ });
108
+ });
109
+ describe("schedule()", () => {
110
+ it("should schedule a job", async () => {
111
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
112
+
113
+ await agendaModule.schedule("now", "test-nsp.test", {});
114
+
115
+ expect(agendaModule.agenda.schedule).toHaveBeenCalledWith("now", "test-nsp.test", {});
116
+ });
117
+ });
118
+
119
+ describe("now()", () => {
120
+ it("should schedule a job", async () => {
121
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
122
+
123
+ await agendaModule.now("test-nsp.test", {});
124
+
125
+ expect(agendaModule.agenda.now).toHaveBeenCalledWith("test-nsp.test", {});
126
+ });
127
+ });
128
+ describe("$onDestroy()", () => {
129
+ it("should close agenda", async () => {
130
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
131
+
132
+ await agendaModule.$onDestroy();
133
+
134
+ expect(agendaModule.agenda.close).toHaveBeenCalledWith({force: true});
135
+ });
136
+ });
137
+ });
138
+
139
+ describe("when agenda is enabled but disableJobProcessing = true", () => {
140
+ beforeEach(() =>
141
+ PlatformTest.create({
142
+ agenda: {
143
+ enabled: true,
144
+ disableJobProcessing: true
145
+ }
146
+ })
147
+ );
148
+ afterEach(() => PlatformTest.reset());
149
+
150
+ describe("$afterListen()", () => {
151
+ it("should load all jobs", async () => {
152
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
153
+ const campaign = PlatformTest.get<CustomCampaign>(CustomCampaign)!;
154
+
155
+ await agendaModule.$afterListen();
156
+
157
+ expect(agendaModule.agenda.define).not.toHaveBeenCalled();
158
+ expect(agendaModule.agenda.every).not.toHaveBeenCalled();
159
+ expect(agendaModule.agenda.start).toHaveBeenCalledWith();
160
+ expect(agendaModule.agenda.create).not.toHaveBeenCalled();
161
+
162
+ expect(campaign.job).toBeUndefined();
163
+ });
164
+ });
165
+ });
166
+
167
+ describe("when agenda is disabled", () => {
168
+ beforeEach(() =>
169
+ PlatformTest.create({
170
+ agenda: {
171
+ enabled: false
172
+ }
173
+ })
174
+ );
175
+ afterEach(() => PlatformTest.reset());
176
+
177
+ describe("$afterListen()", () => {
178
+ it("should load all jobs", async () => {
179
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
180
+ const campaign = PlatformTest.get<CustomCampaign>(CustomCampaign)!;
181
+
182
+ await agendaModule.$afterListen();
183
+
184
+ expect(agendaModule.agenda.define).toBeUndefined();
185
+
186
+ expect(campaign.job).toBeUndefined();
187
+ });
188
+ });
189
+
190
+ describe("$onDestroy()", () => {
191
+ it("should do nothing", async () => {
192
+ const agendaModule = PlatformTest.get<any>(AgendaModule)!;
193
+
194
+ await agendaModule.$onDestroy();
195
+ });
196
+ });
197
+ });
198
+ });
@@ -1,6 +1,7 @@
1
- import {AfterListen, Logger, OnDestroy} from "@tsed/common";
1
+ import {AfterListen, DIContext, Logger, OnDestroy, runInContext} from "@tsed/common";
2
2
  import {Constant, Inject, InjectorService, Module, Provider} from "@tsed/di";
3
- import {Processor} from "agenda";
3
+ import {Job, Processor} from "agenda";
4
+ import {v4 as uuid} from "uuid";
4
5
  import {PROVIDER_TYPE_AGENDA} from "./constants/constants";
5
6
  import {AgendaStore} from "./interfaces/AgendaStore";
6
7
  import {AgendaService} from "./services/AgendaFactory";
@@ -17,18 +18,20 @@ export class AgendaModule implements OnDestroy, AfterListen {
17
18
  protected agenda: AgendaService;
18
19
 
19
20
  @Constant("agenda.enabled", false)
20
- private loadAgenda: boolean;
21
+ private enabled: boolean;
21
22
 
22
23
  @Constant("agenda.disableJobProcessing", false)
23
24
  private disableJobProcessing: boolean;
24
25
 
25
26
  async $afterListen(): Promise<any> {
26
- if (this.loadAgenda) {
27
+ if (this.enabled) {
27
28
  const providers = this.getProviders();
28
29
 
29
30
  if (!this.disableJobProcessing) {
30
31
  this.logger.info("Agenda add definitions...");
31
32
  providers.forEach((provider) => this.addAgendaDefinitionsForProvider(provider));
33
+
34
+ await this.injector.emit("$beforeAgendaStart");
32
35
  }
33
36
 
34
37
  await this.agenda.start();
@@ -36,6 +39,8 @@ export class AgendaModule implements OnDestroy, AfterListen {
36
39
  if (!this.disableJobProcessing) {
37
40
  this.logger.info("Agenda add scheduled jobs...");
38
41
  await Promise.all(providers.map((provider) => this.scheduleJobsForProvider(provider)));
42
+
43
+ await this.injector.emit("$afterAgendaStart");
39
44
  }
40
45
  } else {
41
46
  this.logger.info("Agenda disabled...");
@@ -43,7 +48,7 @@ export class AgendaModule implements OnDestroy, AfterListen {
43
48
  }
44
49
 
45
50
  async $onDestroy(): Promise<any> {
46
- if (this.loadAgenda) {
51
+ if (this.enabled) {
47
52
  await this.agenda.stop();
48
53
  await this.agenda.close({force: true});
49
54
 
@@ -51,6 +56,36 @@ export class AgendaModule implements OnDestroy, AfterListen {
51
56
  }
52
57
  }
53
58
 
59
+ define(name: string, options?: any, processor?: any) {
60
+ return this.agenda.define(name, options, (job: Job) => {
61
+ const $ctx = new DIContext({
62
+ injector: this.injector,
63
+ id: uuid(),
64
+ logger: this.injector.logger
65
+ });
66
+
67
+ $ctx.set("job", job);
68
+
69
+ return runInContext($ctx, () => processor(job));
70
+ });
71
+ }
72
+
73
+ every(interval: string, name: string, data?: any, options?: any) {
74
+ return this.agenda.every(interval, name, data, options);
75
+ }
76
+
77
+ schedule(when: Date | string, name: string, data?: any) {
78
+ return this.agenda.schedule(when, name, data);
79
+ }
80
+
81
+ now(name: string, data?: any) {
82
+ return this.agenda.now(name, data);
83
+ }
84
+
85
+ create(name: string, data?: any) {
86
+ return this.agenda.create(name, data);
87
+ }
88
+
54
89
  protected getProviders(): Provider<any>[] {
55
90
  return this.injector.getProviders(PROVIDER_TYPE_AGENDA);
56
91
  }
@@ -67,7 +102,8 @@ export class AgendaModule implements OnDestroy, AfterListen {
67
102
 
68
103
  const jobProcessor: Processor = instance[propertyKey].bind(instance) as Processor;
69
104
  const jobName = this.getNameForJob(propertyKey, store.namespace, name);
70
- this.agenda.define(jobName, options, jobProcessor);
105
+
106
+ this.define(jobName, options, jobProcessor);
71
107
  });
72
108
  }
73
109
 
@@ -81,7 +117,7 @@ export class AgendaModule implements OnDestroy, AfterListen {
81
117
  const promises = Object.entries(store.every).map(([propertyKey, {interval, name, ...options}]) => {
82
118
  const jobName = this.getNameForJob(propertyKey, store.namespace, name);
83
119
 
84
- return this.agenda.every(interval, jobName, {}, options);
120
+ return this.every(interval, jobName, {}, options);
85
121
  });
86
122
 
87
123
  await Promise.all(promises);