@agendajs/mongo-backend 1.0.0-alpha.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/LICENSE.md ADDED
@@ -0,0 +1,25 @@
1
+ ## License
2
+ (The MIT License)
3
+
4
+ Copyright (c) 2013 Ryan Schmukler <ryan@slingingcode.com>
5
+
6
+ Copyright (c) 2022-2026 Simon Tretter <s.tretter@gmail.com>
7
+
8
+ Contributors: Mikael Korpela, Alexis Tyler, Vasyl Boroviak, Haris Sulaiman, Loris Guignard
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ this software and associated documentation files (the 'Software'), to deal in
12
+ the Software without restriction, including without limitation the rights to
13
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ the Software, and to permit persons to whom the Software is furnished to do so,
15
+ subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # @agendajs/mongo-backend
2
+
3
+ MongoDB backend for [Agenda](https://www.npmjs.com/package/agenda) job scheduler.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install agenda @agendajs/mongo-backend
9
+ # or
10
+ pnpm add agenda @agendajs/mongo-backend
11
+ # or
12
+ yarn add agenda @agendajs/mongo-backend
13
+ ```
14
+
15
+ **Requirements:**
16
+ - Node.js 18+
17
+ - MongoDB 4+
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import { Agenda } from 'agenda';
23
+ import { MongoBackend } from '@agendajs/mongo-backend';
24
+
25
+ // Via connection string
26
+ const agenda = new Agenda({
27
+ backend: new MongoBackend({
28
+ address: 'mongodb://localhost/agenda'
29
+ })
30
+ });
31
+
32
+ // Or via existing MongoDB connection
33
+ import { MongoClient } from 'mongodb';
34
+
35
+ const client = await MongoClient.connect('mongodb://localhost');
36
+ const db = client.db('agenda');
37
+
38
+ const agenda = new Agenda({
39
+ backend: new MongoBackend({ mongo: db })
40
+ });
41
+
42
+ // Define jobs
43
+ agenda.define('send-email', async (job) => {
44
+ const { to, subject } = job.attrs.data;
45
+ await sendEmail(to, subject);
46
+ });
47
+
48
+ // Start processing
49
+ await agenda.start();
50
+
51
+ // Schedule jobs
52
+ await agenda.every('1 hour', 'send-email', { to: 'user@example.com', subject: 'Hello' });
53
+ ```
54
+
55
+ ## Configuration Options
56
+
57
+ ```typescript
58
+ interface IMongoBackendConfig {
59
+ /** MongoDB connection string */
60
+ address?: string;
61
+
62
+ /** Existing MongoDB database instance */
63
+ mongo?: Db;
64
+
65
+ /** Collection name for jobs (default: 'agendaJobs') */
66
+ collection?: string;
67
+
68
+ /** MongoDB client options */
69
+ options?: MongoClientOptions;
70
+
71
+ /** Name to identify this Agenda instance (stored as lastModifiedBy) */
72
+ name?: string;
73
+
74
+ /** Whether to create indexes on connect (default: true) */
75
+ ensureIndex?: boolean;
76
+
77
+ /** Sort order for job queries */
78
+ sort?: { [key: string]: SortDirection };
79
+ }
80
+ ```
81
+
82
+ ### Full Example with Options
83
+
84
+ ```typescript
85
+ const agenda = new Agenda({
86
+ backend: new MongoBackend({
87
+ address: 'mongodb://localhost/agenda',
88
+ collection: 'jobs', // Custom collection name
89
+ sort: { nextRunAt: 'asc', priority: 'desc' }
90
+ }),
91
+ processEvery: '30 seconds', // Job polling interval
92
+ maxConcurrency: 20, // Max concurrent jobs
93
+ defaultConcurrency: 5 // Default per job type
94
+ });
95
+ ```
96
+
97
+ ## Real-Time Notifications
98
+
99
+ MongoBackend provides storage only and uses polling to check for new jobs. For real-time job processing across distributed systems, combine it with a notification channel:
100
+
101
+ ```typescript
102
+ import { Agenda, InMemoryNotificationChannel } from 'agenda';
103
+ import { MongoBackend } from '@agendajs/mongo-backend';
104
+
105
+ // For single-process apps (testing/development)
106
+ const agenda = new Agenda({
107
+ backend: new MongoBackend({ mongo: db }),
108
+ notificationChannel: new InMemoryNotificationChannel()
109
+ });
110
+
111
+ // For production: use Redis for notifications
112
+ import { RedisBackend } from '@agendajs/redis-backend';
113
+
114
+ const redisBackend = new RedisBackend({ connectionString: 'redis://localhost:6379' });
115
+ const agenda = new Agenda({
116
+ backend: new MongoBackend({ mongo: db }),
117
+ notificationChannel: redisBackend.notificationChannel
118
+ });
119
+ ```
120
+
121
+ ## Database Index
122
+
123
+ By default, MongoBackend automatically creates the `findAndLockNextJobIndex` index on connect for optimal job query performance. The index includes:
124
+
125
+ ```javascript
126
+ {
127
+ "name": 1,
128
+ "nextRunAt": 1,
129
+ "priority": -1,
130
+ "lockedAt": 1,
131
+ "disabled": 1
132
+ }
133
+ ```
134
+
135
+ To disable automatic index creation (e.g., if you manage indexes separately):
136
+
137
+ ```typescript
138
+ const backend = new MongoBackend({
139
+ mongo: db,
140
+ ensureIndex: false // Skip automatic index creation
141
+ });
142
+ ```
143
+
144
+ ### Additional Indexes
145
+
146
+ If you use **Agendash**, add this index for better dashboard performance:
147
+
148
+ ```javascript
149
+ db.agendaJobs.createIndex({
150
+ "nextRunAt": -1,
151
+ "lastRunAt": -1,
152
+ "lastFinishedAt": -1
153
+ }, { name: "agendash" })
154
+ ```
155
+
156
+ If you have job definitions with thousands of instances, this index can improve lock queries:
157
+
158
+ ```javascript
159
+ db.agendaJobs.createIndex({
160
+ "name": 1,
161
+ "disabled": 1,
162
+ "lockedAt": 1
163
+ }, { name: "findAndLockDeadJobs" })
164
+ ```
165
+
166
+ ## Related Packages
167
+
168
+ - [agenda](https://www.npmjs.com/package/agenda) - Core scheduler
169
+ - [@agendajs/postgres-backend](https://www.npmjs.com/package/@agendajs/postgres-backend) - PostgreSQL backend with LISTEN/NOTIFY
170
+ - [@agendajs/redis-backend](https://www.npmjs.com/package/@agendajs/redis-backend) - Redis backend with Pub/Sub
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,48 @@
1
+ import type { AgendaBackend, JobRepository, NotificationChannel } from 'agenda';
2
+ import type { MongoBackendConfig } from './types.js';
3
+ /**
4
+ * MongoDB backend for Agenda.
5
+ * Provides storage via MongoDB. Does not provide notifications (uses polling).
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { Agenda } from 'agenda';
10
+ * import { MongoBackend } from '@agendajs/mongo-backend';
11
+ *
12
+ * // Via connection string
13
+ * const backend = new MongoBackend({ address: 'mongodb://localhost/agenda' });
14
+ *
15
+ * // Via existing connection
16
+ * const backend = new MongoBackend({ mongo: existingDb });
17
+ *
18
+ * const agenda = new Agenda({ backend });
19
+ * ```
20
+ */
21
+ export declare class MongoBackend implements AgendaBackend {
22
+ private config;
23
+ private _repository;
24
+ private _ownsConnection;
25
+ /**
26
+ * MongoDB does not provide a notification channel.
27
+ * For real-time notifications with MongoDB, use a separate notification
28
+ * channel like Redis pub/sub.
29
+ */
30
+ readonly notificationChannel: NotificationChannel | undefined;
31
+ constructor(config: MongoBackendConfig);
32
+ get repository(): JobRepository;
33
+ /**
34
+ * Whether this backend owns its database connection.
35
+ * True if created from address (connection string), false if mongo Db was passed in.
36
+ */
37
+ get ownsConnection(): boolean;
38
+ /**
39
+ * Connect to MongoDB and initialize the collection.
40
+ */
41
+ connect(): Promise<void>;
42
+ /**
43
+ * Disconnect from MongoDB.
44
+ * Only closes the connection if we created it (i.e., from connection string).
45
+ * If an existing Db instance was passed in, this is a no-op.
46
+ */
47
+ disconnect(): Promise<void>;
48
+ }
@@ -0,0 +1,69 @@
1
+ import { MongoJobRepository } from './MongoJobRepository.js';
2
+ /**
3
+ * MongoDB backend for Agenda.
4
+ * Provides storage via MongoDB. Does not provide notifications (uses polling).
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Agenda } from 'agenda';
9
+ * import { MongoBackend } from '@agendajs/mongo-backend';
10
+ *
11
+ * // Via connection string
12
+ * const backend = new MongoBackend({ address: 'mongodb://localhost/agenda' });
13
+ *
14
+ * // Via existing connection
15
+ * const backend = new MongoBackend({ mongo: existingDb });
16
+ *
17
+ * const agenda = new Agenda({ backend });
18
+ * ```
19
+ */
20
+ export class MongoBackend {
21
+ config;
22
+ _repository;
23
+ _ownsConnection;
24
+ /**
25
+ * MongoDB does not provide a notification channel.
26
+ * For real-time notifications with MongoDB, use a separate notification
27
+ * channel like Redis pub/sub.
28
+ */
29
+ notificationChannel = undefined;
30
+ constructor(config) {
31
+ this.config = config;
32
+ // Determine if we own the connection (not passed in by user)
33
+ this._ownsConnection = !('mongo' in config);
34
+ this._repository = new MongoJobRepository({
35
+ ...('mongo' in config
36
+ ? { mongo: config.mongo, db: { collection: config.collection } }
37
+ : {
38
+ db: { address: config.address, collection: config.collection, options: config.options }
39
+ }),
40
+ ensureIndex: config.ensureIndex,
41
+ sort: config.sort
42
+ });
43
+ }
44
+ get repository() {
45
+ return this._repository;
46
+ }
47
+ /**
48
+ * Whether this backend owns its database connection.
49
+ * True if created from address (connection string), false if mongo Db was passed in.
50
+ */
51
+ get ownsConnection() {
52
+ return this._ownsConnection;
53
+ }
54
+ /**
55
+ * Connect to MongoDB and initialize the collection.
56
+ */
57
+ async connect() {
58
+ await this._repository.connect();
59
+ }
60
+ /**
61
+ * Disconnect from MongoDB.
62
+ * Only closes the connection if we created it (i.e., from connection string).
63
+ * If an existing Db instance was passed in, this is a no-op.
64
+ */
65
+ async disconnect() {
66
+ await this._repository.disconnect();
67
+ }
68
+ }
69
+ //# sourceMappingURL=MongoBackend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoBackend.js","sourceRoot":"","sources":["../src/MongoBackend.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,YAAY;IAWJ;IAVZ,WAAW,CAAqB;IAChC,eAAe,CAAU;IAEjC;;;;OAIG;IACM,mBAAmB,GAAoC,SAAS,CAAC;IAE1E,YAAoB,MAA0B;QAA1B,WAAM,GAAN,MAAM,CAAoB;QAC7C,6DAA6D;QAC7D,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,WAAW,GAAG,IAAI,kBAAkB,CAAC;YACzC,GAAG,CAAC,OAAO,IAAI,MAAM;gBACpB,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE;gBAChE,CAAC,CAAC;oBACA,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;iBACvF,CAAC;YACJ,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,IAAI,EAAE,MAAM,CAAC,IAAI;SACjB,CAAC,CAAC;IACJ,CAAC;IAED,IAAI,UAAU;QACb,OAAO,IAAI,CAAC,WAAW,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,IAAI,cAAc;QACjB,OAAO,IAAI,CAAC,eAAe,CAAC;IAC7B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO;QACZ,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU;QACf,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;IACrC,CAAC;CACD"}
@@ -0,0 +1,65 @@
1
+ import { Collection, ObjectId } from 'mongodb';
2
+ import type { JobParameters, JobId, JobsQueryOptions, JobsResult, JobsOverview, JobRepository, JobRepositoryOptions, RemoveJobsOptions } from 'agenda';
3
+ import type { MongoJobRepositoryConfig } from './types.js';
4
+ /**
5
+ * Internal MongoDB document type with ObjectId for _id
6
+ * This is what's actually stored in MongoDB, separate from the public JobParameters interface
7
+ */
8
+ type MongoJobDocument = Omit<JobParameters, '_id'> & {
9
+ _id?: ObjectId;
10
+ };
11
+ /**
12
+ * @class
13
+ * MongoDB implementation of JobRepository
14
+ */
15
+ export declare class MongoJobRepository implements JobRepository {
16
+ private connectOptions;
17
+ collection: Collection<MongoJobDocument>;
18
+ private mongoClient?;
19
+ private ownsConnection;
20
+ constructor(connectOptions: MongoJobRepositoryConfig);
21
+ private createConnection;
22
+ getJobById(id: string): Promise<JobParameters | null>;
23
+ /**
24
+ * Convert a SortDirection value to MongoDB's numeric sort direction
25
+ */
26
+ private toMongoSortDirection;
27
+ /**
28
+ * Convert generic sort options to MongoDB sort
29
+ */
30
+ private toMongoSort;
31
+ /**
32
+ * Query jobs with database-agnostic options.
33
+ * Handles state computation and filtering internally.
34
+ */
35
+ queryJobs(options?: JobsQueryOptions): Promise<JobsResult>;
36
+ /**
37
+ * Get overview statistics for jobs grouped by name.
38
+ * Returns counts of jobs in each state for each job name.
39
+ */
40
+ getJobsOverview(): Promise<JobsOverview[]>;
41
+ /**
42
+ * Get all distinct job names
43
+ */
44
+ getDistinctJobNames(): Promise<string[]>;
45
+ getQueueSize(): Promise<number>;
46
+ removeJobs(options: RemoveJobsOptions): Promise<number>;
47
+ unlockJob(job: JobParameters): Promise<void>;
48
+ /**
49
+ * Internal method to unlock jobs so that they can be re-run
50
+ */
51
+ unlockJobs(jobIds: (JobId | string)[]): Promise<void>;
52
+ lockJob(job: JobParameters, options: JobRepositoryOptions | undefined): Promise<JobParameters | undefined>;
53
+ getNextJobToRun(jobName: string, nextScanAt: Date, lockDeadline: Date, now: Date | undefined, options: JobRepositoryOptions | undefined): Promise<JobParameters | undefined>;
54
+ connect(): Promise<void>;
55
+ private database;
56
+ disconnect(): Promise<void>;
57
+ saveJobState(job: JobParameters, options: JobRepositoryOptions | undefined): Promise<void>;
58
+ /**
59
+ * Save a job to the database (insert or update)
60
+ * @param job Job parameters to save
61
+ * @returns The saved job parameters with ID
62
+ */
63
+ saveJob<DATA = unknown>(job: JobParameters<DATA>, options: JobRepositoryOptions | undefined): Promise<JobParameters<DATA>>;
64
+ }
65
+ export {};
@@ -0,0 +1,492 @@
1
+ import assert from 'node:assert';
2
+ import debug from 'debug';
3
+ import { MongoClient, ObjectId } from 'mongodb';
4
+ import { toJobId, computeJobState } from 'agenda';
5
+ import { hasMongoProtocol } from './hasMongoProtocol.js';
6
+ const log = debug('agenda:mongo:repository');
7
+ /**
8
+ * Escape special regex characters in a string to treat it as literal text
9
+ */
10
+ function escapeRegex(str) {
11
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+ }
13
+ function computeJobObj(job) {
14
+ return {
15
+ _id: toJobId(job._id.toHexString()),
16
+ name: job.name,
17
+ priority: job.priority,
18
+ nextRunAt: job.nextRunAt,
19
+ type: job.type,
20
+ data: job.data,
21
+ lockedAt: job.lockedAt,
22
+ lastFinishedAt: job.lastFinishedAt,
23
+ failedAt: job.failedAt || undefined,
24
+ failCount: job.failCount || undefined,
25
+ failReason: job.failReason || undefined,
26
+ repeatTimezone: job.repeatTimezone,
27
+ lastRunAt: job.lastRunAt,
28
+ repeatInterval: job.repeatInterval,
29
+ repeatAt: job.repeatAt,
30
+ disabled: job.disabled,
31
+ progress: job.progress,
32
+ unique: job.unique,
33
+ uniqueOpts: job.uniqueOpts,
34
+ lastModifiedBy: job.lastModifiedBy,
35
+ fork: job.fork
36
+ };
37
+ }
38
+ /**
39
+ * @class
40
+ * MongoDB implementation of JobRepository
41
+ */
42
+ export class MongoJobRepository {
43
+ connectOptions;
44
+ collection;
45
+ mongoClient;
46
+ ownsConnection = false;
47
+ constructor(connectOptions) {
48
+ this.connectOptions = connectOptions;
49
+ this.connectOptions.sort = this.connectOptions.sort || { nextRunAt: 'asc', priority: 'desc' };
50
+ // Track if we own the connection (i.e., we created it from a connection string)
51
+ this.ownsConnection = !('mongo' in connectOptions);
52
+ }
53
+ async createConnection() {
54
+ const { connectOptions } = this;
55
+ if ('mongo' in connectOptions) {
56
+ log('using passed in mongo connection');
57
+ return connectOptions.mongo;
58
+ }
59
+ if ('db' in connectOptions && connectOptions.db) {
60
+ log('using database config', connectOptions);
61
+ return this.database(connectOptions.db.address, connectOptions.db.options);
62
+ }
63
+ throw new Error('invalid db config, or db config not found');
64
+ }
65
+ async getJobById(id) {
66
+ const doc = await this.collection.findOne({ _id: new ObjectId(id) });
67
+ if (!doc)
68
+ return null;
69
+ // Convert MongoDB ObjectId to JobId
70
+ return computeJobObj(doc);
71
+ }
72
+ /**
73
+ * Convert a SortDirection value to MongoDB's numeric sort direction
74
+ */
75
+ toMongoSortDirection(dir) {
76
+ return dir === 'asc' ? 1 : -1;
77
+ }
78
+ /**
79
+ * Convert generic sort options to MongoDB sort
80
+ */
81
+ toMongoSort(sort) {
82
+ if (!sort) {
83
+ return { nextRunAt: -1, lastRunAt: -1 };
84
+ }
85
+ const mongoSort = {};
86
+ if (sort.nextRunAt !== undefined)
87
+ mongoSort.nextRunAt = this.toMongoSortDirection(sort.nextRunAt);
88
+ if (sort.lastRunAt !== undefined)
89
+ mongoSort.lastRunAt = this.toMongoSortDirection(sort.lastRunAt);
90
+ if (sort.lastFinishedAt !== undefined)
91
+ mongoSort.lastFinishedAt = this.toMongoSortDirection(sort.lastFinishedAt);
92
+ if (sort.priority !== undefined)
93
+ mongoSort.priority = this.toMongoSortDirection(sort.priority);
94
+ if (sort.name !== undefined)
95
+ mongoSort.name = this.toMongoSortDirection(sort.name);
96
+ if (sort.data !== undefined)
97
+ mongoSort.data = this.toMongoSortDirection(sort.data);
98
+ return Object.keys(mongoSort).length > 0 ? mongoSort : { nextRunAt: -1, lastRunAt: -1 };
99
+ }
100
+ /**
101
+ * Query jobs with database-agnostic options.
102
+ * Handles state computation and filtering internally.
103
+ */
104
+ async queryJobs(options = {}) {
105
+ const { name, names, state, id, ids, search, data, includeDisabled = true, sort, skip = 0, limit = 0 } = options;
106
+ const now = new Date();
107
+ // Build MongoDB query from options
108
+ const query = {};
109
+ // Validate name is a string to prevent query operator injection
110
+ if (name && typeof name === 'string') {
111
+ query.name = name;
112
+ }
113
+ else if (names && Array.isArray(names) && names.length > 0) {
114
+ // Filter to only valid strings
115
+ const validNames = names.filter((n) => typeof n === 'string');
116
+ if (validNames.length > 0) {
117
+ query.name = { $in: validNames };
118
+ }
119
+ }
120
+ if (id && typeof id === 'string') {
121
+ try {
122
+ query._id = new ObjectId(id);
123
+ }
124
+ catch {
125
+ return { jobs: [], total: 0 };
126
+ }
127
+ }
128
+ else if (ids && Array.isArray(ids) && ids.length > 0) {
129
+ try {
130
+ const validIds = ids.filter((i) => typeof i === 'string');
131
+ if (validIds.length > 0) {
132
+ query._id = { $in: validIds.map(i => new ObjectId(i)) };
133
+ }
134
+ }
135
+ catch {
136
+ return { jobs: [], total: 0 };
137
+ }
138
+ }
139
+ // Validate search is a string and escape regex metacharacters
140
+ if (search && typeof search === 'string' && search.length > 0) {
141
+ query.name = { $regex: escapeRegex(search), $options: 'i' };
142
+ }
143
+ if (data !== undefined) {
144
+ query.data = data;
145
+ }
146
+ if (!includeDisabled) {
147
+ query.disabled = { $ne: true };
148
+ }
149
+ // Fetch jobs
150
+ const allJobs = await this.collection.find(query).sort(this.toMongoSort(sort)).toArray();
151
+ // Compute states and filter by state if specified
152
+ let jobsWithState = allJobs
153
+ .map(job => {
154
+ const jobOb = computeJobObj(job);
155
+ return {
156
+ ...jobOb,
157
+ state: computeJobState(jobOb, now)
158
+ };
159
+ })
160
+ .filter(job => !state || job.state === state);
161
+ // Apply pagination after state filtering
162
+ const total = jobsWithState.length;
163
+ if (limit > 0) {
164
+ jobsWithState = jobsWithState.slice(skip, skip + limit);
165
+ }
166
+ else if (skip > 0) {
167
+ jobsWithState = jobsWithState.slice(skip);
168
+ }
169
+ return { jobs: jobsWithState, total };
170
+ }
171
+ /**
172
+ * Get overview statistics for jobs grouped by name.
173
+ * Returns counts of jobs in each state for each job name.
174
+ */
175
+ async getJobsOverview() {
176
+ const now = new Date();
177
+ const names = await this.getDistinctJobNames();
178
+ const overviews = await Promise.all(names.map(async (name) => {
179
+ const jobs = await this.collection.find({ name }).toArray();
180
+ const overview = {
181
+ name,
182
+ total: jobs.length,
183
+ running: 0,
184
+ scheduled: 0,
185
+ queued: 0,
186
+ completed: 0,
187
+ failed: 0,
188
+ repeating: 0
189
+ };
190
+ for (const job of jobs) {
191
+ const state = computeJobState(job, now);
192
+ overview[state]++;
193
+ }
194
+ return overview;
195
+ }));
196
+ return overviews;
197
+ }
198
+ /**
199
+ * Get all distinct job names
200
+ */
201
+ async getDistinctJobNames() {
202
+ return this.collection.distinct('name');
203
+ }
204
+ async getQueueSize() {
205
+ return this.collection.countDocuments({ nextRunAt: { $lt: new Date() } });
206
+ }
207
+ async removeJobs(options) {
208
+ const query = {};
209
+ if (options.id) {
210
+ const idStr = String(options.id);
211
+ try {
212
+ query._id = new ObjectId(idStr);
213
+ }
214
+ catch {
215
+ return 0;
216
+ }
217
+ }
218
+ else if (options.ids && Array.isArray(options.ids) && options.ids.length > 0) {
219
+ try {
220
+ query._id = { $in: options.ids.map(id => new ObjectId(String(id))) };
221
+ }
222
+ catch {
223
+ return 0;
224
+ }
225
+ }
226
+ // Validate name is a string to prevent query operator injection
227
+ if (options.name && typeof options.name === 'string') {
228
+ query.name = options.name;
229
+ }
230
+ else if (options.names && Array.isArray(options.names) && options.names.length > 0) {
231
+ const validNames = options.names.filter((n) => typeof n === 'string');
232
+ if (validNames.length > 0) {
233
+ query.name = { $in: validNames };
234
+ }
235
+ }
236
+ else if (options.notNames && Array.isArray(options.notNames) && options.notNames.length > 0) {
237
+ const validNotNames = options.notNames.filter((n) => typeof n === 'string');
238
+ if (validNotNames.length > 0) {
239
+ query.name = { $nin: validNotNames };
240
+ }
241
+ }
242
+ if (options.data !== undefined) {
243
+ query.data = options.data;
244
+ }
245
+ // If no criteria provided, don't delete anything
246
+ if (Object.keys(query).length === 0) {
247
+ log('removeJobs: skipping deleteMany without query', query);
248
+ return 0;
249
+ }
250
+ const result = await this.collection.deleteMany(query);
251
+ return result.deletedCount;
252
+ }
253
+ async unlockJob(job) {
254
+ if (!job._id)
255
+ return;
256
+ // only unlock jobs which are not currently processed (nextRunAt is not null)
257
+ await this.collection.updateOne({ _id: new ObjectId(job._id.toString()), nextRunAt: { $ne: null } }, { $unset: { lockedAt: true } });
258
+ }
259
+ /**
260
+ * Internal method to unlock jobs so that they can be re-run
261
+ */
262
+ async unlockJobs(jobIds) {
263
+ if (jobIds.length === 0)
264
+ return;
265
+ const objectIds = jobIds.map(id => new ObjectId(id.toString()));
266
+ // Unlock jobs by ID regardless of nextRunAt status.
267
+ // When a one-time job starts running, nextRunAt becomes null,
268
+ // but we still want to unlock it when stop() is called.
269
+ await this.collection.updateMany({ _id: { $in: objectIds } }, { $unset: { lockedAt: true } });
270
+ }
271
+ async lockJob(job, options) {
272
+ if (!job._id)
273
+ return undefined;
274
+ // Query to run against collection to see if we need to lock it
275
+ const criteria = {
276
+ _id: new ObjectId(job._id.toString()),
277
+ name: job.name,
278
+ lockedAt: null,
279
+ nextRunAt: job.nextRunAt,
280
+ disabled: { $ne: true }
281
+ };
282
+ // Update / options for the MongoDB query
283
+ const update = {
284
+ $set: { lockedAt: new Date(), lastModifiedBy: options?.lastModifiedBy }
285
+ };
286
+ const findOptions = {
287
+ returnDocument: 'after',
288
+ sort: this.connectOptions.sort
289
+ };
290
+ // Lock the job in MongoDB!
291
+ const result = await this.collection.findOneAndUpdate(criteria, update, findOptions);
292
+ if (!result)
293
+ return undefined;
294
+ // Convert MongoDB ObjectId to JobId
295
+ return computeJobObj(result);
296
+ }
297
+ async getNextJobToRun(jobName, nextScanAt, lockDeadline, now, options) {
298
+ const lockTime = now ?? new Date();
299
+ /**
300
+ * Query used to find job to run
301
+ */
302
+ const JOB_PROCESS_WHERE_QUERY = {
303
+ name: jobName,
304
+ disabled: { $ne: true },
305
+ $or: [
306
+ {
307
+ lockedAt: { $eq: null },
308
+ nextRunAt: { $lte: nextScanAt }
309
+ },
310
+ {
311
+ lockedAt: { $lte: lockDeadline }
312
+ }
313
+ ]
314
+ };
315
+ /**
316
+ * Query used to set a job as locked
317
+ */
318
+ const JOB_PROCESS_SET_QUERY = {
319
+ $set: { lockedAt: lockTime, lastModifiedBy: options?.lastModifiedBy }
320
+ };
321
+ /**
322
+ * Query used to affect what gets returned
323
+ */
324
+ const JOB_RETURN_QUERY = {
325
+ returnDocument: 'after',
326
+ sort: this.connectOptions.sort
327
+ };
328
+ // Find ONE and ONLY ONE job and set the 'lockedAt' time so that job begins to be processed
329
+ const result = await this.collection.findOneAndUpdate(JOB_PROCESS_WHERE_QUERY, JOB_PROCESS_SET_QUERY, JOB_RETURN_QUERY);
330
+ if (!result)
331
+ return undefined;
332
+ // Convert MongoDB ObjectId to JobId
333
+ return computeJobObj(result);
334
+ }
335
+ async connect() {
336
+ const db = await this.createConnection();
337
+ log('successful connection to MongoDB', db.options);
338
+ const collection = this.connectOptions.db?.collection || 'agendaJobs';
339
+ this.collection = db.collection(collection);
340
+ if (log.enabled) {
341
+ log(`connected with collection: ${collection}, collection size: ${typeof this.collection.estimatedDocumentCount === 'function'
342
+ ? await this.collection.estimatedDocumentCount()
343
+ : '?'}`);
344
+ }
345
+ if (this.connectOptions.ensureIndex ?? true) {
346
+ log('attempting index creation');
347
+ try {
348
+ const result = await this.collection.createIndex({
349
+ name: 1,
350
+ ...this.connectOptions.sort,
351
+ priority: -1,
352
+ lockedAt: 1,
353
+ nextRunAt: 1,
354
+ disabled: 1
355
+ }, { name: 'findAndLockNextJobIndex' });
356
+ log('index succesfully created', result);
357
+ }
358
+ catch (error) {
359
+ log('db index creation failed', error);
360
+ throw error;
361
+ }
362
+ }
363
+ }
364
+ async database(url, options) {
365
+ let connectionString = url;
366
+ if (!hasMongoProtocol(connectionString)) {
367
+ connectionString = `mongodb://${connectionString}`;
368
+ }
369
+ this.mongoClient = await MongoClient.connect(connectionString, {
370
+ ...options
371
+ });
372
+ return this.mongoClient.db();
373
+ }
374
+ async disconnect() {
375
+ if (this.ownsConnection && this.mongoClient) {
376
+ log('closing owned MongoDB connection');
377
+ await this.mongoClient.close();
378
+ this.mongoClient = undefined;
379
+ }
380
+ }
381
+ async saveJobState(job, options) {
382
+ if (!job._id) {
383
+ throw new Error('Cannot save job state without job ID');
384
+ }
385
+ const id = new ObjectId(job._id.toString());
386
+ const $set = {
387
+ lockedAt: (job.lockedAt && new Date(job.lockedAt)) || undefined,
388
+ nextRunAt: (job.nextRunAt && new Date(job.nextRunAt)) || undefined,
389
+ lastRunAt: (job.lastRunAt && new Date(job.lastRunAt)) || undefined,
390
+ progress: job.progress,
391
+ failReason: job.failReason,
392
+ failCount: job.failCount,
393
+ failedAt: job.failedAt && new Date(job.failedAt),
394
+ lastFinishedAt: (job.lastFinishedAt && new Date(job.lastFinishedAt)) || undefined,
395
+ lastModifiedBy: options?.lastModifiedBy
396
+ };
397
+ log('[job %s] save job state: \n%O', job._id, $set);
398
+ const result = await this.collection.updateOne({ _id: id, name: job.name }, {
399
+ $set
400
+ });
401
+ if (!result.acknowledged || result.matchedCount !== 1) {
402
+ throw new Error(`job ${job._id} (name: ${job.name}) cannot be updated in the database, maybe it does not exist anymore?`);
403
+ }
404
+ }
405
+ /**
406
+ * Save a job to the database (insert or update)
407
+ * @param job Job parameters to save
408
+ * @returns The saved job parameters with ID
409
+ */
410
+ async saveJob(job, options) {
411
+ try {
412
+ log('attempting to save a job');
413
+ // Extract the ID and unique fields (not persisted directly)
414
+ const { _id, unique, uniqueOpts, ...props } = job;
415
+ // Add lastModifiedBy
416
+ const propsWithModifier = {
417
+ ...props,
418
+ lastModifiedBy: options?.lastModifiedBy
419
+ };
420
+ log('[job %s] set job props: \n%O', _id, propsWithModifier);
421
+ // Grab current time and set default query options for MongoDB
422
+ const now = new Date();
423
+ const protect = {};
424
+ let update = { $set: propsWithModifier };
425
+ log('current time stored as %s', now.toISOString());
426
+ // If the job already had an ID, then update the properties of the job
427
+ if (_id) {
428
+ log('job already has _id, calling findOneAndUpdate() using _id as query');
429
+ const result = await this.collection.findOneAndUpdate({ _id: new ObjectId(_id.toString()), name: props.name }, update, { returnDocument: 'after' });
430
+ if (!result) {
431
+ // Job was removed, return original data unchanged
432
+ log('job %s was not found for update, returning original data', _id);
433
+ return job;
434
+ }
435
+ return computeJobObj(result);
436
+ }
437
+ if (props.type === 'single') {
438
+ // Job type set to 'single' so...
439
+ log('job with type of "single" found');
440
+ // If the nextRunAt time is older than the current time, "protect" that property
441
+ if (props.nextRunAt && props.nextRunAt <= now) {
442
+ log('job has a scheduled nextRunAt time, protecting that field from upsert');
443
+ protect.nextRunAt = props.nextRunAt;
444
+ delete propsWithModifier.nextRunAt;
445
+ }
446
+ // If we have things to protect, set them in MongoDB using $setOnInsert
447
+ if (Object.keys(protect).length > 0) {
448
+ update.$setOnInsert = protect;
449
+ }
450
+ // Try an upsert - ensures only one job of this name exists
451
+ log(`calling findOneAndUpdate(${props.name}) with job name and type of "single" as query`);
452
+ const result = await this.collection.findOneAndUpdate({
453
+ name: props.name,
454
+ type: 'single'
455
+ }, update, {
456
+ upsert: true,
457
+ returnDocument: 'after'
458
+ });
459
+ log(`findOneAndUpdate(${props.name}) with type "single" completed`);
460
+ assert(result, 'findOneAndUpdate with upsert should always return a document');
461
+ return computeJobObj(result);
462
+ }
463
+ if (unique) {
464
+ // Upsert based on the 'unique' query object
465
+ const query = { ...unique };
466
+ query.name = props.name;
467
+ if (uniqueOpts?.insertOnly) {
468
+ update = { $setOnInsert: propsWithModifier };
469
+ }
470
+ log('calling findOneAndUpdate() with unique object as query: \n%O', query);
471
+ const result = await this.collection.findOneAndUpdate(query, update, {
472
+ upsert: true,
473
+ returnDocument: 'after'
474
+ });
475
+ assert(result, 'findOneAndUpdate with upsert should always return a document');
476
+ return computeJobObj(result);
477
+ }
478
+ // Insert new job
479
+ log('using default behavior, inserting new job via insertOne() with props that were set: \n%O', propsWithModifier);
480
+ const result = await this.collection.insertOne(propsWithModifier);
481
+ return computeJobObj({
482
+ _id: result.insertedId,
483
+ ...propsWithModifier
484
+ });
485
+ }
486
+ catch (error) {
487
+ log('saveJob() received an error, job was not updated/created');
488
+ throw error;
489
+ }
490
+ }
491
+ }
492
+ //# sourceMappingURL=MongoJobRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoJobRepository.js","sourceRoot":"","sources":["../src/MongoJobRepository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const hasMongoProtocol: (url: string) => boolean;
@@ -0,0 +1,2 @@
1
+ export const hasMongoProtocol = (url) => /mongodb(?:\+srv)?:\/\/.*/.test(url);
2
+ //# sourceMappingURL=hasMongoProtocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hasMongoProtocol.js","sourceRoot":"","sources":["../src/hasMongoProtocol.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @agendajs/mongo-backend
3
+ *
4
+ * MongoDB backend for Agenda job scheduler.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Agenda } from 'agenda';
9
+ * import { MongoBackend } from '@agendajs/mongo-backend';
10
+ *
11
+ * // Create agenda with MongoDB backend
12
+ * const agenda = new Agenda({
13
+ * backend: new MongoBackend({
14
+ * address: 'mongodb://localhost/agenda'
15
+ * })
16
+ * });
17
+ *
18
+ * // Or with existing connection
19
+ * const agenda = new Agenda({
20
+ * backend: new MongoBackend({ mongo: existingDb })
21
+ * });
22
+ *
23
+ * // Define and run jobs
24
+ * agenda.define('myJob', async (job) => {
25
+ * console.log('Running job:', job.attrs.name);
26
+ * });
27
+ *
28
+ * await agenda.start();
29
+ * await agenda.every('5 minutes', 'myJob');
30
+ * ```
31
+ */
32
+ export { MongoBackend } from './MongoBackend.js';
33
+ export { MongoJobRepository } from './MongoJobRepository.js';
34
+ export type { MongoBackendConfig, MongoJobRepositoryConfig, MongoDbConfig } from './types.js';
35
+ export type { Db, MongoClientOptions } from 'mongodb';
36
+ export { MongoClient, ObjectId } from 'mongodb';
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @agendajs/mongo-backend
3
+ *
4
+ * MongoDB backend for Agenda job scheduler.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Agenda } from 'agenda';
9
+ * import { MongoBackend } from '@agendajs/mongo-backend';
10
+ *
11
+ * // Create agenda with MongoDB backend
12
+ * const agenda = new Agenda({
13
+ * backend: new MongoBackend({
14
+ * address: 'mongodb://localhost/agenda'
15
+ * })
16
+ * });
17
+ *
18
+ * // Or with existing connection
19
+ * const agenda = new Agenda({
20
+ * backend: new MongoBackend({ mongo: existingDb })
21
+ * });
22
+ *
23
+ * // Define and run jobs
24
+ * agenda.define('myJob', async (job) => {
25
+ * console.log('Running job:', job.attrs.name);
26
+ * });
27
+ *
28
+ * await agenda.start();
29
+ * await agenda.every('5 minutes', 'myJob');
30
+ * ```
31
+ */
32
+ export { MongoBackend } from './MongoBackend.js';
33
+ export { MongoJobRepository } from './MongoJobRepository.js';
34
+ export { MongoClient, ObjectId } from 'mongodb';
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAM7D,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Test utilities for @agendajs/mongo-backend
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { mockMongo } from '@agendajs/mongo-backend/testing';
7
+ *
8
+ * const { db, disconnect } = await mockMongo();
9
+ * // Use db for testing
10
+ * await disconnect();
11
+ * ```
12
+ */
13
+ import { Db, MongoClient } from 'mongodb';
14
+ export interface IMockMongo {
15
+ disconnect: () => Promise<void>;
16
+ mongo: MongoClient;
17
+ uri: string;
18
+ db: Db;
19
+ }
20
+ /**
21
+ * Create a mock MongoDB connection for testing.
22
+ * Requires MONGO_URI environment variable to be set (usually by mongodb-memory-server).
23
+ */
24
+ export declare function mockMongo(): Promise<IMockMongo>;
25
+ export type { Db, MongoClient } from 'mongodb';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Test utilities for @agendajs/mongo-backend
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { mockMongo } from '@agendajs/mongo-backend/testing';
7
+ *
8
+ * const { db, disconnect } = await mockMongo();
9
+ * // Use db for testing
10
+ * await disconnect();
11
+ * ```
12
+ */
13
+ import { MongoClient } from 'mongodb';
14
+ import debug from 'debug';
15
+ import { randomUUID } from 'crypto';
16
+ const log = debug('agenda:mock-mongodb');
17
+ /**
18
+ * Create a mock MongoDB connection for testing.
19
+ * Requires MONGO_URI environment variable to be set (usually by mongodb-memory-server).
20
+ */
21
+ export async function mockMongo() {
22
+ const baseUri = process.env.MONGO_URI;
23
+ if (!baseUri) {
24
+ throw new Error('MONGO_URI not set. Ensure global setup is configured.');
25
+ }
26
+ // Use unique database per test suite to avoid interference
27
+ const dbName = `agenda_test_${randomUUID().replace(/-/g, '')}`;
28
+ // Build URI with database name (ensure single slash before db name)
29
+ const uri = `${baseUri.replace(/\/$/, '')}/${dbName}`;
30
+ log('connecting to mongod with db', dbName);
31
+ const mongo = await MongoClient.connect(uri);
32
+ const db = mongo.db(dbName);
33
+ return {
34
+ uri,
35
+ db,
36
+ mongo,
37
+ disconnect: async () => {
38
+ // Drop the test database to clean up
39
+ await db.dropDatabase();
40
+ await mongo.close();
41
+ log('mongo client closed, db dropped:', dbName);
42
+ }
43
+ };
44
+ }
45
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAM,WAAW,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC,MAAM,GAAG,GAAG,KAAK,CAAC,qBAAqB,CAAC,CAAC;AASzC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IACtC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC1E,CAAC;IAED,2DAA2D;IAC3D,MAAM,MAAM,GAAG,eAAe,UAAU,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;IAC/D,oEAAoE;IACpE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC;IACtD,GAAG,CAAC,8BAA8B,EAAE,MAAM,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;IAE5B,OAAO;QACN,GAAG;QACH,EAAE;QACF,KAAK;QACL,UAAU,EAAE,KAAK,IAAI,EAAE;YACtB,qCAAqC;YACrC,MAAM,EAAE,CAAC,YAAY,EAAE,CAAC;YACxB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;YACpB,GAAG,CAAC,kCAAkC,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;KACD,CAAC;AACH,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { Db, MongoClientOptions } from 'mongodb';
2
+ import type { SortDirection } from 'agenda';
3
+ /**
4
+ * Configuration options for MongoBackend
5
+ */
6
+ export type MongoConnectionConfig = {
7
+ /** Existing MongoDB database instance */
8
+ mongo: Db;
9
+ } | {
10
+ /** MongoDB connection string */
11
+ address: string;
12
+ /** MongoDB client options */
13
+ options?: MongoClientOptions;
14
+ };
15
+ export type MongoBackendConfig = MongoConnectionConfig & {
16
+ /** Collection name for jobs (default: 'agendaJobs') */
17
+ collection?: string;
18
+ /** Whether to create indexes on connect (default: true) */
19
+ ensureIndex?: boolean;
20
+ /** Sort order for job queries */
21
+ sort?: {
22
+ [key: string]: SortDirection;
23
+ };
24
+ };
25
+ /**
26
+ * Database configuration options used internally by the repository
27
+ */
28
+ export interface MongoDbConfig {
29
+ ensureIndex?: boolean;
30
+ sort?: {
31
+ [key: string]: SortDirection;
32
+ };
33
+ }
34
+ /**
35
+ * Configuration options for MongoJobRepository
36
+ */
37
+ export type MongoJobRepositoryConfig = MongoDbConfig & ({
38
+ /** MongoDB connection string */
39
+ db: {
40
+ address: string;
41
+ collection?: string;
42
+ options?: MongoClientOptions;
43
+ };
44
+ } | {
45
+ /** Existing MongoDB database instance */
46
+ mongo: Db;
47
+ db?: {
48
+ collection?: string;
49
+ };
50
+ });
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@agendajs/mongo-backend",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "MongoDB backend for Agenda job scheduler",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./testing": {
14
+ "types": "./dist/testing.d.ts",
15
+ "import": "./dist/testing.js"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git://github.com/agenda/agenda"
30
+ },
31
+ "keywords": [
32
+ "agenda",
33
+ "job",
34
+ "scheduler",
35
+ "mongodb",
36
+ "mongo"
37
+ ],
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/agenda/agenda/issues"
41
+ },
42
+ "dependencies": {
43
+ "debug": "^4.4.0",
44
+ "mongodb": "^6.12.0"
45
+ },
46
+ "peerDependencies": {
47
+ "agenda": "6.0.0-alpha.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/debug": "^4.1.12",
51
+ "@types/node": "^22.0.0",
52
+ "@vitest/coverage-v8": "^3.1.0",
53
+ "mongodb-memory-server": "^10.0.0",
54
+ "tsx": "^4.21.0",
55
+ "vitest": "^3.1.0",
56
+ "agenda": "6.0.0-alpha.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -b",
60
+ "typecheck": "tsc -b && tsc -p tsconfig.test.json",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "test:coverage": "vitest run --coverage",
64
+ "test:debug": "DEBUG=agenda:**,-agenda:internal:** vitest run"
65
+ }
66
+ }