mail-time 3.0.0 → 4.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.
@@ -0,0 +1,520 @@
1
+ import {
2
+ debug,
3
+ logError,
4
+ isSendClaimUpdate,
5
+ isSendLeaseGuardedUpdate,
6
+ isAppendMailOptionUpdate,
7
+ stripInternalUpdateMeta,
8
+ isSendLeaseRemove,
9
+ } from '../helpers.js';
10
+
11
+ /**
12
+ * @typedef {object} PostgresQueryResult
13
+ * @property {number | null | undefined} [rowCount]
14
+ * @property {unknown[]} [rows]
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} PostgresClient
19
+ * @property {(queryText: string, values?: unknown[]) => Promise<PostgresQueryResult>} query
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} PostgresQueueOption
24
+ * @property {PostgresClient} client
25
+ * @property {string} [prefix]
26
+ */
27
+
28
+ const DEFAULT_PREFIX = 'default';
29
+ const setupLockId = 93824519;
30
+
31
+ const fieldMap = {
32
+ to: 'to_address',
33
+ tries: 'tries',
34
+ sendAt: 'send_at',
35
+ isSent: 'is_sent',
36
+ isCancelled: 'is_cancelled',
37
+ isFailed: 'is_failed',
38
+ isSending: 'is_sending',
39
+ sendingAt: 'sending_at',
40
+ template: 'template',
41
+ transport: 'transport',
42
+ concatSubject: 'concat_subject',
43
+ mailOptions: 'mail_options',
44
+ };
45
+
46
+ const parseMailOptions = (mailOptions) => {
47
+ if (typeof mailOptions === 'string') {
48
+ return JSON.parse(mailOptions);
49
+ }
50
+ return mailOptions;
51
+ };
52
+
53
+ const normalizeRow = (row) => {
54
+ if (!row) {
55
+ return null;
56
+ }
57
+
58
+ return {
59
+ id: row.id,
60
+ uuid: row.uuid,
61
+ to: row.to_address,
62
+ tries: parseInt(row.tries, 10),
63
+ sendAt: parseInt(row.send_at, 10),
64
+ isSent: row.is_sent,
65
+ isCancelled: row.is_cancelled,
66
+ isFailed: row.is_failed,
67
+ isSending: row.is_sending === true,
68
+ sendingAt: row.sending_at !== null && row.sending_at !== undefined ? parseInt(row.sending_at, 10) : 0,
69
+ template: row.template || false,
70
+ transport: parseInt(row.transport, 10),
71
+ concatSubject: row.concat_subject || false,
72
+ mailOptions: parseMailOptions(row.mail_options),
73
+ };
74
+ };
75
+
76
+ /** Class representing PostgreSQL Queue for MailTime */
77
+ class PostgresQueue {
78
+ /**
79
+ * Create a PostgresQueue instance
80
+ * @param {PostgresQueueOption} opts - configuration object
81
+ */
82
+ constructor(opts) {
83
+ this.name = 'postgres-queue';
84
+ if (!opts || typeof opts !== 'object') {
85
+ throw new TypeError('[mail-time] Configuration object must be passed into PostgresQueue constructor');
86
+ }
87
+
88
+ if (!opts.client || typeof opts.client.query !== 'function') {
89
+ throw new Error('[mail-time] [PostgresQueue] required {client} option is missing or does not expose a `query` method');
90
+ }
91
+
92
+ this.client = opts.client;
93
+ if (typeof opts.prefix === 'string' && opts.prefix.length > 0) {
94
+ this.__applyPrefix(opts.prefix);
95
+ }
96
+ }
97
+
98
+ /** @internal */
99
+ __applyPrefix(prefix) {
100
+ this.prefix = prefix;
101
+ this.__readyPromise = this.__setup();
102
+ this.__readyPromise.catch(() => void 0);
103
+ }
104
+
105
+ /** @internal */
106
+ __ensurePrefix() {
107
+ if (typeof this.prefix !== 'string') {
108
+ this.__applyPrefix(this.mailTimeInstance?.prefix || DEFAULT_PREFIX);
109
+ }
110
+ }
111
+
112
+ /** @internal */
113
+ __debug(...args) {
114
+ debug(this.mailTimeInstance?.debug === true, `[${this.name}]`, ...args);
115
+ }
116
+
117
+ /**
118
+ * @async
119
+ * @memberOf PostgresQueue
120
+ * @name ready
121
+ * @description Wait until PostgreSQL schema is ready
122
+ * @returns {Promise<void 0>}
123
+ */
124
+ async ready() {
125
+ this.__ensurePrefix();
126
+ this.__debug('[ready]');
127
+ await this.__readyPromise;
128
+ }
129
+
130
+ /** @internal */
131
+ async __setup() {
132
+ await this.client.query('SELECT pg_advisory_lock($1)', [setupLockId]);
133
+
134
+ try {
135
+ await this.client.query(`CREATE TABLE IF NOT EXISTS mail_time_queue (
136
+ id BIGSERIAL PRIMARY KEY,
137
+ prefix TEXT NOT NULL DEFAULT 'default',
138
+ uuid TEXT NOT NULL,
139
+ to_address TEXT,
140
+ tries INTEGER NOT NULL DEFAULT 0,
141
+ send_at BIGINT NOT NULL,
142
+ is_sent BOOLEAN NOT NULL DEFAULT false,
143
+ is_cancelled BOOLEAN NOT NULL DEFAULT false,
144
+ is_failed BOOLEAN NOT NULL DEFAULT false,
145
+ is_sending BOOLEAN NOT NULL DEFAULT false,
146
+ sending_at BIGINT NOT NULL DEFAULT 0,
147
+ template TEXT,
148
+ transport INTEGER NOT NULL DEFAULT 0,
149
+ concat_subject TEXT,
150
+ mail_options JSONB NOT NULL,
151
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
152
+ updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
153
+ )`);
154
+ await this.client.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_mail_time_queue_prefix_uuid
155
+ ON mail_time_queue (prefix, uuid)`);
156
+
157
+ await this.client.query(`CREATE INDEX IF NOT EXISTS idx_mail_time_queue_due
158
+ ON mail_time_queue (prefix, is_sent, is_failed, is_cancelled, is_sending, sending_at, send_at, tries)`);
159
+
160
+ await this.client.query(`CREATE INDEX IF NOT EXISTS idx_mail_time_queue_pending_to
161
+ ON mail_time_queue (prefix, to_address, is_sent, is_failed, is_cancelled, send_at)`);
162
+ } finally {
163
+ await this.client.query('SELECT pg_advisory_unlock($1)', [setupLockId]);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * @async
169
+ * @memberOf PostgresQueue
170
+ * @name ping
171
+ * @description Check connection to Storage
172
+ * @returns {Promise<object>}
173
+ */
174
+ async ping() {
175
+ this.__debug('[ping]');
176
+ if (!this.mailTimeInstance) {
177
+ return {
178
+ status: 'Service Unavailable',
179
+ code: 503,
180
+ statusCode: 503,
181
+ error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
182
+ };
183
+ }
184
+
185
+ try {
186
+ await this.ready();
187
+ const ping = await this.client.query('SELECT 1 as ping');
188
+ if (ping?.rows?.[0]?.ping === 1) {
189
+ return {
190
+ status: 'OK',
191
+ code: 200,
192
+ statusCode: 200,
193
+ };
194
+ }
195
+ } catch (pingError) {
196
+ return {
197
+ status: 'Internal Server Error',
198
+ code: 500,
199
+ statusCode: 500,
200
+ error: pingError,
201
+ };
202
+ }
203
+
204
+ return {
205
+ status: 'Service Unavailable',
206
+ code: 503,
207
+ statusCode: 503,
208
+ error: new Error('Service Unavailable'),
209
+ };
210
+ }
211
+
212
+ /**
213
+ * @memberOf PostgresQueue
214
+ * @name iterate
215
+ * @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool). Postgres reads buffer the full result, so each tick is bounded by `opts.limit` (or 1000 when caller passes `Infinity` / no limit) to keep memory predictable; high-throughput deployments should shard prefixes.
216
+ * @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
217
+ * @returns {Promise<void>}
218
+ */
219
+ async iterate(opts) {
220
+ this.__debug('[iterate]', opts);
221
+ if (!this.mailTimeInstance) {
222
+ return;
223
+ }
224
+ await this.ready();
225
+
226
+ const now = Date.now();
227
+ const sendingTimeout = (opts && typeof opts.sendingTimeout === 'number' && opts.sendingTimeout > 0)
228
+ ? opts.sendingTimeout
229
+ : 300000;
230
+ const limit = (opts && typeof opts.limit === 'number' && Number.isFinite(opts.limit) && opts.limit > 0)
231
+ ? Math.max(1, Math.floor(opts.limit))
232
+ : 1000;
233
+
234
+ try {
235
+ const res = await this.client.query(`SELECT id, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
236
+ is_sending, sending_at, template, transport, concat_subject, mail_options
237
+ FROM mail_time_queue
238
+ WHERE prefix = $1
239
+ AND is_sent = false
240
+ AND is_failed = false
241
+ AND is_cancelled = false
242
+ AND send_at <= $2
243
+ AND tries < $3
244
+ AND (is_sending = false OR sending_at <= $4)
245
+ ORDER BY send_at ASC
246
+ LIMIT $5`, [this.prefix, now, this.mailTimeInstance.maxTries, now - sendingTimeout, limit]);
247
+
248
+ for (const row of res.rows || []) {
249
+ await this.mailTimeInstance.___dispatch(normalizeRow(row));
250
+ }
251
+ } catch (iterateError) {
252
+ logError('[PostgresQueue] [iterate] [iterateError]', iterateError);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * @async
258
+ * @memberOf PostgresQueue
259
+ * @name getPendingTo
260
+ * @description get queued task by `to` field (addressee)
261
+ * @param to {string} - email address
262
+ * @param sendAt {number} - timestamp
263
+ * @returns {Promise<object|null>}
264
+ */
265
+ async getPendingTo(to, sendAt) {
266
+ this.__debug('[getPendingTo]', to, sendAt);
267
+ if (typeof to !== 'string' || typeof sendAt !== 'number') {
268
+ return null;
269
+ }
270
+
271
+ await this.ready();
272
+
273
+ const res = await this.client.query(`SELECT id, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
274
+ is_sending, sending_at, template, transport, concat_subject, mail_options
275
+ FROM mail_time_queue
276
+ WHERE prefix = $1
277
+ AND to_address = $2
278
+ AND is_sent = false
279
+ AND is_failed = false
280
+ AND is_cancelled = false
281
+ AND is_sending = false
282
+ AND tries < $4
283
+ AND send_at <= $3
284
+ ORDER BY send_at DESC
285
+ LIMIT 1`, [this.prefix, to, sendAt, this.mailTimeInstance.maxTries]);
286
+
287
+ return normalizeRow(res.rows?.[0]);
288
+ }
289
+
290
+ /**
291
+ * @async
292
+ * @memberOf PostgresQueue
293
+ * @name push
294
+ * @description push task to the queue/storage
295
+ * @param task {object} - task's object
296
+ * @returns {Promise<void 0>}
297
+ */
298
+ async push(task) {
299
+ this.__debug('[push]', task?.uuid);
300
+ if (!task || typeof task !== 'object') {
301
+ return;
302
+ }
303
+
304
+ await this.ready();
305
+
306
+ if (task.sendAt instanceof Date) {
307
+ task.sendAt = +task.sendAt;
308
+ }
309
+
310
+ await this.client.query(`INSERT INTO mail_time_queue (
311
+ prefix, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
312
+ is_sending, sending_at, template, transport, concat_subject, mail_options, created_at, updated_at
313
+ )
314
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
315
+ ON CONFLICT (prefix, uuid) DO UPDATE SET
316
+ to_address = EXCLUDED.to_address,
317
+ tries = EXCLUDED.tries,
318
+ send_at = EXCLUDED.send_at,
319
+ is_sent = EXCLUDED.is_sent,
320
+ is_cancelled = EXCLUDED.is_cancelled,
321
+ is_failed = EXCLUDED.is_failed,
322
+ is_sending = EXCLUDED.is_sending,
323
+ sending_at = EXCLUDED.sending_at,
324
+ template = EXCLUDED.template,
325
+ transport = EXCLUDED.transport,
326
+ concat_subject = EXCLUDED.concat_subject,
327
+ mail_options = EXCLUDED.mail_options,
328
+ updated_at = CURRENT_TIMESTAMP`, [
329
+ this.prefix,
330
+ task.uuid,
331
+ typeof task.to === 'string' ? task.to : null,
332
+ task.tries,
333
+ task.sendAt,
334
+ task.isSent,
335
+ task.isCancelled,
336
+ task.isFailed,
337
+ task.isSending === true,
338
+ typeof task.sendingAt === 'number' ? task.sendingAt : 0,
339
+ task.template || null,
340
+ task.transport,
341
+ task.concatSubject || null,
342
+ JSON.stringify(task.mailOptions || []),
343
+ ]);
344
+ }
345
+
346
+ /**
347
+ * @async
348
+ * @memberOf PostgresQueue
349
+ * @name cancel
350
+ * @description cancel scheduled email
351
+ * @param uuid {string} - email's uuid
352
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
353
+ */
354
+ async cancel(uuid) {
355
+ this.__debug('[cancel]', uuid);
356
+ if (typeof uuid !== 'string') {
357
+ return false;
358
+ }
359
+
360
+ await this.ready();
361
+
362
+ const task = normalizeRow((await this.client.query(`SELECT id, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
363
+ template, transport, concat_subject, mail_options
364
+ FROM mail_time_queue
365
+ WHERE prefix = $1
366
+ AND uuid = $2
367
+ LIMIT 1`, [this.prefix, uuid])).rows?.[0]);
368
+
369
+ if (!task || task.isSent === true || task.isCancelled === true) {
370
+ return false;
371
+ }
372
+
373
+ if (!this.mailTimeInstance.keepHistory) {
374
+ return await this.remove(task);
375
+ }
376
+
377
+ return await this.update(task, {
378
+ isCancelled: true,
379
+ });
380
+ }
381
+
382
+ /**
383
+ * @async
384
+ * @memberOf PostgresQueue
385
+ * @name remove
386
+ * @description remove task from queue
387
+ * @param task {object} - task's object
388
+ * @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
389
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
390
+ */
391
+ async remove(task, opts) {
392
+ this.__debug('[remove]', task?.uuid);
393
+ if (!task || typeof task !== 'object') {
394
+ return false;
395
+ }
396
+
397
+ await this.ready();
398
+
399
+ const where = task.id ? 'id = $2' : 'uuid = $2';
400
+ const value = task.id || task.uuid;
401
+ let leaseWhere = '';
402
+ const params = [this.prefix, value];
403
+ if (isSendLeaseRemove(opts)) {
404
+ params.push(opts.leaseTries, opts.leaseSendingAt);
405
+ leaseWhere = ` AND tries = $3 AND is_sending = true AND sending_at = $4 AND is_cancelled = false AND is_failed = false`;
406
+ }
407
+
408
+ const res = await this.client.query(`DELETE FROM mail_time_queue
409
+ WHERE prefix = $1
410
+ AND ${where}${leaseWhere}`, params);
411
+ return (res.rowCount || 0) >= 1;
412
+ }
413
+
414
+ /**
415
+ * @async
416
+ * @memberOf PostgresQueue
417
+ * @name update
418
+ * @description update task in queue
419
+ * @param task {object} - task's object
420
+ * @param updateObj {object} - fields with new values to update
421
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
422
+ */
423
+ async update(task, updateObj) {
424
+ this.__debug('[update]', task?.uuid);
425
+ if (!task || typeof task !== 'object' || !updateObj || typeof updateObj !== 'object') {
426
+ return false;
427
+ }
428
+
429
+ await this.ready();
430
+
431
+ if (isAppendMailOptionUpdate(updateObj)) {
432
+ const where = task.id ? 'id = $3' : 'uuid = $3';
433
+ const value = task.id || task.uuid;
434
+ const res = await this.client.query(`UPDATE mail_time_queue
435
+ SET mail_options = mail_options || $1::jsonb,
436
+ updated_at = CURRENT_TIMESTAMP
437
+ WHERE prefix = $2
438
+ AND ${where}
439
+ AND is_sent = false
440
+ AND is_failed = false
441
+ AND is_cancelled = false
442
+ AND is_sending = false`, [
443
+ JSON.stringify([updateObj.appendMailOption]),
444
+ this.prefix,
445
+ value,
446
+ ]);
447
+ return (res.rowCount || 0) >= 1;
448
+ }
449
+
450
+ const persistObj = stripInternalUpdateMeta(updateObj);
451
+ const sets = [];
452
+ const values = [];
453
+ for (const key of Object.keys(persistObj)) {
454
+ if (!fieldMap[key]) {
455
+ continue;
456
+ }
457
+
458
+ let value = persistObj[key];
459
+ if (key === 'sendAt' && value instanceof Date) {
460
+ value = +value;
461
+ }
462
+ if (key === 'mailOptions') {
463
+ value = JSON.stringify(value);
464
+ }
465
+
466
+ values.push(value);
467
+ sets.push(`${fieldMap[key]} = $${values.length}`);
468
+ }
469
+
470
+ if (!sets.length) {
471
+ return false;
472
+ }
473
+
474
+ let claimWhere = '';
475
+ if (isSendClaimUpdate(updateObj)) {
476
+ const now = typeof updateObj.sendingAt === 'number' ? updateObj.sendingAt : Date.now();
477
+ const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
478
+ values.push(task.tries);
479
+ const triesIndex = values.length;
480
+ values.push(now - sendingTimeout);
481
+ const staleIndex = values.length;
482
+ claimWhere = `
483
+ AND is_sent = false
484
+ AND is_failed = false
485
+ AND is_cancelled = false
486
+ AND tries = $${triesIndex}
487
+ AND (is_sending = false OR sending_at <= $${staleIndex})
488
+ `;
489
+ } else if (isSendLeaseGuardedUpdate(updateObj)) {
490
+ values.push(updateObj.leaseTries);
491
+ const triesIndex = values.length;
492
+ values.push(updateObj.leaseSendingAt);
493
+ const sendingAtIndex = values.length;
494
+ claimWhere = `
495
+ AND tries = $${triesIndex}
496
+ AND is_sending = true
497
+ AND sending_at = $${sendingAtIndex}
498
+ AND is_cancelled = false
499
+ AND is_failed = false
500
+ `;
501
+ }
502
+
503
+ values.push(this.prefix);
504
+ const prefixIndex = values.length;
505
+ values.push(task.id || task.uuid);
506
+ const taskIndex = values.length;
507
+ const where = task.id ? 'id' : 'uuid';
508
+
509
+ const res = await this.client.query(`UPDATE mail_time_queue
510
+ SET ${sets.join(', ')},
511
+ updated_at = CURRENT_TIMESTAMP
512
+ WHERE prefix = $${prefixIndex}
513
+ AND ${where} = $${taskIndex}
514
+ ${claimWhere}`, values);
515
+
516
+ return (res.rowCount || 0) >= 1;
517
+ }
518
+ }
519
+
520
+ export { PostgresQueue };
@@ -0,0 +1,105 @@
1
+ export type RedisClient = {
2
+ exists: (key: string) => Promise<number>;
3
+ get: (key: string) => Promise<string | null>;
4
+ set: (key: string, value: string, options?: object) => Promise<unknown>;
5
+ del: (key: string | string[]) => Promise<number>;
6
+ ping: () => Promise<string>;
7
+ scanIterator: (options: object) => AsyncIterable<string | string[]>;
8
+ watch?: ((key: string) => Promise<unknown>) | undefined;
9
+ unwatch?: (() => Promise<unknown>) | undefined;
10
+ multi?: (() => object) | undefined;
11
+ };
12
+ export type RedisQueueOption = {
13
+ client: RedisClient;
14
+ prefix?: string | undefined;
15
+ };
16
+ /** Class representing Redis Queue for MailTime */
17
+ export class RedisQueue {
18
+ /**
19
+ * Create a RedisQueue instance
20
+ * @param {RedisQueueOption} opts - configuration object
21
+ */
22
+ constructor(opts: RedisQueueOption);
23
+ name: string;
24
+ client: RedisClient;
25
+ prefix: any;
26
+ uniqueName: string | undefined;
27
+ /**
28
+ * @async
29
+ * @memberOf RedisQueue
30
+ * @name ready
31
+ * @description Storage adapter has no async setup
32
+ * @returns {Promise<void 0>}
33
+ */
34
+ ready(): Promise<void>;
35
+ /**
36
+ * @async
37
+ * @memberOf RedisQueue
38
+ * @name ping
39
+ * @description Check connection to Storage
40
+ * @returns {Promise<object>}
41
+ */
42
+ ping(): Promise<object>;
43
+ /**
44
+ * @memberOf RedisQueue
45
+ * @name iterate
46
+ * @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool)
47
+ * @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
48
+ * @returns {Promise<void>}
49
+ */
50
+ iterate(opts?: {
51
+ limit?: number;
52
+ sendingTimeout?: number;
53
+ }): Promise<void>;
54
+ /**
55
+ * @async
56
+ * @memberOf RedisQueue
57
+ * @name getPendingTo
58
+ * @description get queued task by `to` field (addressee)
59
+ * @param to {string} - email address
60
+ * @param sendAt {number} - timestamp
61
+ * @returns {Promise<object|null>}
62
+ */
63
+ getPendingTo(to: string, sendAt: number): Promise<object | null>;
64
+ /**
65
+ * @async
66
+ * @memberOf RedisQueue
67
+ * @name push
68
+ * @description push task to the queue/storage
69
+ * @param task {object} - task's object
70
+ * @returns {Promise<void 0>}
71
+ */
72
+ push(task: object): Promise<void>;
73
+ /**
74
+ * @async
75
+ * @memberOf RedisQueue
76
+ * @name cancel
77
+ * @description cancel scheduled email
78
+ * @param uuid {string} - email's uuid
79
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
80
+ */
81
+ cancel(uuid: string): Promise<boolean>;
82
+ /**
83
+ * @async
84
+ * @memberOf RedisQueue
85
+ * @name remove
86
+ * @description remove task from queue
87
+ * @param task {object} - task's object
88
+ * @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
89
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
90
+ */
91
+ remove(task: object, opts?: {
92
+ leaseTries: number;
93
+ leaseSendingAt: number;
94
+ }): Promise<boolean>;
95
+ /**
96
+ * @async
97
+ * @memberOf RedisQueue
98
+ * @name update
99
+ * @description update task in queue
100
+ * @param task {object} - task's object
101
+ * @param updateObj {object} - fields with new values to update
102
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
103
+ */
104
+ update(task: object, updateObj: object): Promise<boolean>;
105
+ }