@pauldeng/node-red-contrib-bullmq 1.0.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 +22 -0
- package/README.md +124 -0
- package/bull-queue.html +530 -0
- package/bull-queue.js +523 -0
- package/docs/ARCHITECTURE.md +52 -0
- package/docs/CHANGE_WORKFLOW.md +27 -0
- package/docs/COMMANDS.md +89 -0
- package/docs/CONNECTIONS.md +54 -0
- package/docs/MIGRATION.md +37 -0
- package/docs/NODE_GUIDE.md +83 -0
- package/docs/REFERENCE_MAP.md +51 -0
- package/docs/RELEASE.md +108 -0
- package/docs/TESTING.md +88 -0
- package/docs/TROUBLESHOOTING.md +28 -0
- package/examples/README.md +35 -0
- package/examples/bullmq_features.json +331 -0
- package/examples/example_flow.json +277 -0
- package/examples/repeatable_jobs.json +245 -0
- package/icons/bull_icon.png +0 -0
- package/lib/acknowledgements.js +110 -0
- package/lib/commands.js +230 -0
- package/lib/connections.js +300 -0
- package/lib/scheduler.js +114 -0
- package/lib/serialization.js +63 -0
- package/package.json +70 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_REDIS_HOST = "localhost";
|
|
4
|
+
const DEFAULT_REDIS_PORT = 6379;
|
|
5
|
+
const CLUSTER_PREFIX = "{bull}";
|
|
6
|
+
|
|
7
|
+
function isPresent(value) {
|
|
8
|
+
return value !== undefined && value !== null && value !== "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toBoolean(value, defaultValue = false) {
|
|
12
|
+
if (value === undefined || value === null || value === "") {
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "boolean") {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "number") {
|
|
19
|
+
return value !== 0;
|
|
20
|
+
}
|
|
21
|
+
return ["true", "1", "yes", "on"].includes(String(value).toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toPort(value, defaultValue = DEFAULT_REDIS_PORT) {
|
|
25
|
+
if (!isPresent(value)) {
|
|
26
|
+
return defaultValue;
|
|
27
|
+
}
|
|
28
|
+
const port = Number(value);
|
|
29
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
30
|
+
throw new Error(`Invalid Redis port: ${value}`);
|
|
31
|
+
}
|
|
32
|
+
return port;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseEndpoint(endpoint, defaultPort = DEFAULT_REDIS_PORT) {
|
|
36
|
+
if (typeof endpoint === "object" && endpoint !== null) {
|
|
37
|
+
const host = endpoint.host || endpoint.address;
|
|
38
|
+
if (!isPresent(host)) {
|
|
39
|
+
throw new Error("Redis endpoint requires a host");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
host: String(host).trim(),
|
|
43
|
+
port: toPort(endpoint.port, defaultPort),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const text = String(endpoint || "").trim();
|
|
48
|
+
if (!text) {
|
|
49
|
+
throw new Error("Redis endpoint cannot be empty");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let host = text;
|
|
53
|
+
let port = defaultPort;
|
|
54
|
+
const urlMatch = text.match(
|
|
55
|
+
/^(?:redis|rediss):\/\/(?:[^@]+@)?([^/:]+)(?::(\d+))?/i,
|
|
56
|
+
);
|
|
57
|
+
if (urlMatch) {
|
|
58
|
+
host = urlMatch[1];
|
|
59
|
+
port = toPort(urlMatch[2], defaultPort);
|
|
60
|
+
} else if (text.includes(":")) {
|
|
61
|
+
const parts = text.split(":");
|
|
62
|
+
port = toPort(parts.pop(), defaultPort);
|
|
63
|
+
host = parts.join(":");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!host.trim()) {
|
|
67
|
+
throw new Error(`Redis endpoint requires a host: ${text}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { host: host.trim(), port };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseEndpointList(value, defaultPort = DEFAULT_REDIS_PORT) {
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value
|
|
76
|
+
.flatMap((item) =>
|
|
77
|
+
typeof item === "string" ? item.split(/[\n,]+/) : [item],
|
|
78
|
+
)
|
|
79
|
+
.filter((item) => isPresent(item))
|
|
80
|
+
.map((item) => parseEndpoint(item, defaultPort));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isPresent(value)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return String(value)
|
|
88
|
+
.split(/[\n,]+/)
|
|
89
|
+
.map((item) => item.trim())
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((item) => parseEndpoint(item, defaultPort));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readSecret(config, credentials, name) {
|
|
95
|
+
if (credentials && isPresent(credentials[name])) {
|
|
96
|
+
return credentials[name];
|
|
97
|
+
}
|
|
98
|
+
return config[name];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
102
|
+
const deploymentInput = String(
|
|
103
|
+
config.deployment || config.mode || config.redisMode || "single",
|
|
104
|
+
).toLowerCase();
|
|
105
|
+
const deployment =
|
|
106
|
+
deploymentInput === "memorydb" ? "cluster" : deploymentInput;
|
|
107
|
+
|
|
108
|
+
if (!["single", "cluster", "sentinel"].includes(deployment)) {
|
|
109
|
+
throw new Error(`Unsupported Redis deployment mode: ${deploymentInput}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const host = config.host || config.address || DEFAULT_REDIS_HOST;
|
|
113
|
+
const port = toPort(config.port, DEFAULT_REDIS_PORT);
|
|
114
|
+
const queueName = String(config.queueName || config.name || "").trim();
|
|
115
|
+
if (!queueName) {
|
|
116
|
+
throw new Error("BullMQ queue name is required");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalized = {
|
|
120
|
+
queueName,
|
|
121
|
+
deployment,
|
|
122
|
+
host: String(host).trim(),
|
|
123
|
+
port,
|
|
124
|
+
db: isPresent(config.db) ? Number(config.db) : undefined,
|
|
125
|
+
username: config.username || undefined,
|
|
126
|
+
password: readSecret(config, credentials, "password") || undefined,
|
|
127
|
+
tls: toBoolean(config.tls, false),
|
|
128
|
+
tlsRejectUnauthorized: toBoolean(config.tlsRejectUnauthorized, true),
|
|
129
|
+
tlsCa: readSecret(config, credentials, "tlsCa") || undefined,
|
|
130
|
+
tlsCert: readSecret(config, credentials, "tlsCert") || undefined,
|
|
131
|
+
tlsKey: readSecret(config, credentials, "tlsKey") || undefined,
|
|
132
|
+
tlsServerName: config.tlsServerName || undefined,
|
|
133
|
+
prefix: config.prefix || undefined,
|
|
134
|
+
clusterNodes: parseEndpointList(config.clusterNodes || config.startupNodes),
|
|
135
|
+
sentinelMasterName:
|
|
136
|
+
config.sentinelMasterName || config.masterName || config.nameOfMaster,
|
|
137
|
+
sentinels: parseEndpointList(config.sentinels, 26379),
|
|
138
|
+
sentinelUsername: config.sentinelUsername || undefined,
|
|
139
|
+
sentinelPassword:
|
|
140
|
+
readSecret(config, credentials, "sentinelPassword") || undefined,
|
|
141
|
+
sentinelTls: toBoolean(config.sentinelTls, false),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (deployment === "cluster") {
|
|
145
|
+
if (normalized.clusterNodes.length === 0) {
|
|
146
|
+
normalized.clusterNodes = [
|
|
147
|
+
{ host: normalized.host, port: normalized.port },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
normalized.prefix = normalized.prefix || CLUSTER_PREFIX;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (deployment === "sentinel") {
|
|
154
|
+
if (!normalized.sentinelMasterName) {
|
|
155
|
+
throw new Error("Sentinel deployment requires a master name");
|
|
156
|
+
}
|
|
157
|
+
if (normalized.sentinels.length === 0) {
|
|
158
|
+
normalized.sentinels = [{ host: normalized.host, port: 26379 }];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildTlsOptions(config, enabled) {
|
|
166
|
+
if (!enabled) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const tls = {
|
|
171
|
+
rejectUnauthorized: config.tlsRejectUnauthorized,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (config.tlsServerName) {
|
|
175
|
+
tls.servername = config.tlsServerName;
|
|
176
|
+
}
|
|
177
|
+
if (config.tlsCa) {
|
|
178
|
+
tls.ca = config.tlsCa;
|
|
179
|
+
}
|
|
180
|
+
if (config.tlsCert) {
|
|
181
|
+
tls.cert = config.tlsCert;
|
|
182
|
+
}
|
|
183
|
+
if (config.tlsKey) {
|
|
184
|
+
tls.key = config.tlsKey;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return tls;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function retryValueForRole(role) {
|
|
191
|
+
return role === "worker" || role === "events" ? null : 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildStandaloneOptions(config, role) {
|
|
195
|
+
const options = {
|
|
196
|
+
host: config.host,
|
|
197
|
+
port: config.port,
|
|
198
|
+
maxRetriesPerRequest: retryValueForRole(role),
|
|
199
|
+
enableReadyCheck: true,
|
|
200
|
+
connectTimeout: 10000,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (config.db !== undefined && !Number.isNaN(config.db)) {
|
|
204
|
+
options.db = config.db;
|
|
205
|
+
}
|
|
206
|
+
if (config.username) {
|
|
207
|
+
options.username = config.username;
|
|
208
|
+
}
|
|
209
|
+
if (config.password) {
|
|
210
|
+
options.password = config.password;
|
|
211
|
+
}
|
|
212
|
+
const tls = buildTlsOptions(config, config.tls);
|
|
213
|
+
if (tls) {
|
|
214
|
+
options.tls = tls;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return options;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildRedisDescriptor(config, role = "producer") {
|
|
221
|
+
if (config.deployment === "cluster") {
|
|
222
|
+
const redisOptions = {
|
|
223
|
+
maxRetriesPerRequest: retryValueForRole(role),
|
|
224
|
+
enableReadyCheck: true,
|
|
225
|
+
connectTimeout: 10000,
|
|
226
|
+
};
|
|
227
|
+
if (config.username) {
|
|
228
|
+
redisOptions.username = config.username;
|
|
229
|
+
}
|
|
230
|
+
if (config.password) {
|
|
231
|
+
redisOptions.password = config.password;
|
|
232
|
+
}
|
|
233
|
+
const tls = buildTlsOptions(config, config.tls);
|
|
234
|
+
if (tls) {
|
|
235
|
+
redisOptions.tls = tls;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
kind: "cluster",
|
|
240
|
+
startupNodes: config.clusterNodes,
|
|
241
|
+
options: {
|
|
242
|
+
redisOptions,
|
|
243
|
+
slotsRefreshTimeout: 2000,
|
|
244
|
+
maxRedirections: 16,
|
|
245
|
+
dnsLookup: (address, callback) => callback(null, address),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (config.deployment === "sentinel") {
|
|
251
|
+
const options = buildStandaloneOptions(config, role);
|
|
252
|
+
delete options.host;
|
|
253
|
+
delete options.port;
|
|
254
|
+
if (config.db !== undefined && !Number.isNaN(config.db)) {
|
|
255
|
+
options.db = config.db;
|
|
256
|
+
}
|
|
257
|
+
options.sentinels = config.sentinels;
|
|
258
|
+
options.name = config.sentinelMasterName;
|
|
259
|
+
if (config.sentinelUsername) {
|
|
260
|
+
options.sentinelUsername = config.sentinelUsername;
|
|
261
|
+
}
|
|
262
|
+
if (config.sentinelPassword) {
|
|
263
|
+
options.sentinelPassword = config.sentinelPassword;
|
|
264
|
+
}
|
|
265
|
+
if (config.sentinelTls) {
|
|
266
|
+
options.enableTLSForSentinelMode = true;
|
|
267
|
+
options.sentinelTLS = buildTlsOptions(config, true);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { kind: "single", options };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { kind: "single", options: buildStandaloneOptions(config, role) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function createRedisConnection(descriptor, IORedis) {
|
|
277
|
+
if (descriptor.kind === "cluster") {
|
|
278
|
+
return new IORedis.Cluster(descriptor.startupNodes, descriptor.options);
|
|
279
|
+
}
|
|
280
|
+
return new IORedis(descriptor.options);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildBullMQOptions(config, connection) {
|
|
284
|
+
const options = {};
|
|
285
|
+
if (connection) {
|
|
286
|
+
options.connection = connection;
|
|
287
|
+
}
|
|
288
|
+
if (config.prefix) {
|
|
289
|
+
options.prefix = config.prefix;
|
|
290
|
+
}
|
|
291
|
+
return options;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
buildBullMQOptions,
|
|
296
|
+
buildRedisDescriptor,
|
|
297
|
+
createRedisConnection,
|
|
298
|
+
normalizeQueueConfig,
|
|
299
|
+
parseEndpointList,
|
|
300
|
+
};
|
package/lib/scheduler.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function hasOwn(object, key) {
|
|
4
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function clonePlain(value) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
return { ...value };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeRepeatOptions(repeat) {
|
|
15
|
+
const normalized = clonePlain(repeat) || {};
|
|
16
|
+
if (
|
|
17
|
+
normalized.cron &&
|
|
18
|
+
normalized.pattern &&
|
|
19
|
+
normalized.cron !== normalized.pattern
|
|
20
|
+
) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"repeat.cron and repeat.pattern must match when both are supplied",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (normalized.cron) {
|
|
26
|
+
normalized.pattern = normalized.cron;
|
|
27
|
+
delete normalized.cron;
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getJobData(msg) {
|
|
33
|
+
if (hasOwn(msg, "jobData")) {
|
|
34
|
+
return msg.jobData;
|
|
35
|
+
}
|
|
36
|
+
return { payload: msg.payload };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeAddRequest(msg = {}) {
|
|
40
|
+
const jobopts = clonePlain(msg.jobopts) || {};
|
|
41
|
+
const name = msg.jobName || "default";
|
|
42
|
+
|
|
43
|
+
if (jobopts.repeat) {
|
|
44
|
+
const schedulerId = String(msg.schedulerId || jobopts.jobId || "").trim();
|
|
45
|
+
if (!schedulerId) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"scheduled jobs require msg.schedulerId or msg.jobopts.jobId",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const repeat = normalizeRepeatOptions(jobopts.repeat);
|
|
52
|
+
delete jobopts.repeat;
|
|
53
|
+
delete jobopts.jobId;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
kind: "scheduler",
|
|
57
|
+
schedulerId,
|
|
58
|
+
repeat,
|
|
59
|
+
template: {
|
|
60
|
+
name,
|
|
61
|
+
data: getJobData(msg),
|
|
62
|
+
opts: jobopts,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
kind: "job",
|
|
69
|
+
name,
|
|
70
|
+
data: getJobData(msg),
|
|
71
|
+
opts: jobopts,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getLegacySchedulerId(msg = {}) {
|
|
76
|
+
const schedulerId = String(
|
|
77
|
+
msg.schedulerId || msg.jobid || msg.jobId || "",
|
|
78
|
+
).trim();
|
|
79
|
+
if (!schedulerId) {
|
|
80
|
+
throw new Error("A scheduler id is required");
|
|
81
|
+
}
|
|
82
|
+
return schedulerId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function serializeScheduler(scheduler) {
|
|
86
|
+
if (!scheduler) {
|
|
87
|
+
return scheduler;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const key of [
|
|
92
|
+
"id",
|
|
93
|
+
"key",
|
|
94
|
+
"name",
|
|
95
|
+
"next",
|
|
96
|
+
"pattern",
|
|
97
|
+
"every",
|
|
98
|
+
"limit",
|
|
99
|
+
"offset",
|
|
100
|
+
"tz",
|
|
101
|
+
"endDate",
|
|
102
|
+
]) {
|
|
103
|
+
if (hasOwn(scheduler, key)) {
|
|
104
|
+
result[key] = scheduler[key];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
getLegacySchedulerId,
|
|
112
|
+
normalizeAddRequest,
|
|
113
|
+
serializeScheduler,
|
|
114
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function hasOwn(object, key) {
|
|
4
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function copyKnownFields(source, fields) {
|
|
8
|
+
if (!source) {
|
|
9
|
+
return source;
|
|
10
|
+
}
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const field of fields) {
|
|
13
|
+
const value = source[field];
|
|
14
|
+
if (value !== undefined) {
|
|
15
|
+
result[field] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function serializeJob(job) {
|
|
22
|
+
if (!job) {
|
|
23
|
+
return job;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return copyKnownFields(job, [
|
|
27
|
+
"id",
|
|
28
|
+
"name",
|
|
29
|
+
"queueName",
|
|
30
|
+
"data",
|
|
31
|
+
"opts",
|
|
32
|
+
"progress",
|
|
33
|
+
"attemptsMade",
|
|
34
|
+
"failedReason",
|
|
35
|
+
"stacktrace",
|
|
36
|
+
"returnvalue",
|
|
37
|
+
"timestamp",
|
|
38
|
+
"processedOn",
|
|
39
|
+
"finishedOn",
|
|
40
|
+
"delay",
|
|
41
|
+
"priority",
|
|
42
|
+
"parentKey",
|
|
43
|
+
"deduplicationId",
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function serializeFlowJob(flowJob) {
|
|
48
|
+
if (!flowJob) {
|
|
49
|
+
return flowJob;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
job: serializeJob(flowJob.job),
|
|
54
|
+
children: Array.isArray(flowJob.children)
|
|
55
|
+
? flowJob.children.map(serializeFlowJob)
|
|
56
|
+
: undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
serializeFlowJob,
|
|
62
|
+
serializeJob,
|
|
63
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pauldeng/node-red-contrib-bullmq",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BullMQ-backed Redis job queue nodes for Node-RED",
|
|
5
|
+
"main": "bull-queue.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"bull-queue.js",
|
|
8
|
+
"bull-queue.html",
|
|
9
|
+
"lib/",
|
|
10
|
+
"icons/",
|
|
11
|
+
"examples/",
|
|
12
|
+
"docs/*.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.js",
|
|
19
|
+
"test:unit": "node --test test/*.test.js",
|
|
20
|
+
"test:integration": "BULLMQ_INTEGRATION=1 node --test test/integration-standalone.test.js",
|
|
21
|
+
"test:deployments": "node scripts/run-deployment-tests.js",
|
|
22
|
+
"test:playwright": "playwright test",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"validate": "npx --yes node-red-dev validate"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"bullmq": "5.78.0",
|
|
29
|
+
"ioredis": "5.10.1"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"node-red",
|
|
34
|
+
"bullmq",
|
|
35
|
+
"job",
|
|
36
|
+
"queue",
|
|
37
|
+
"bull",
|
|
38
|
+
"redis",
|
|
39
|
+
"worker",
|
|
40
|
+
"jobs"
|
|
41
|
+
],
|
|
42
|
+
"node-red": {
|
|
43
|
+
"version": ">=4.1.0 <5",
|
|
44
|
+
"nodes": {
|
|
45
|
+
"bull-queue": "bull-queue.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/pauldeng/node-red-contrib-bullmq.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/pauldeng/node-red-contrib-bullmq#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/pauldeng/node-red-contrib-bullmq/issues"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"registry": "https://registry.npmjs.org/",
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"author": {
|
|
61
|
+
"name": "Paul Deng",
|
|
62
|
+
"email": "dengpeng.cn@gmail.com"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@playwright/test": "1.60.0",
|
|
66
|
+
"node-red": "4.1.11",
|
|
67
|
+
"node-red-node-test-helper": "^0.3.6",
|
|
68
|
+
"prettier": "3.8.3"
|
|
69
|
+
}
|
|
70
|
+
}
|