s3db.js 11.3.2 → 12.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.
Files changed (82) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36664 -15480
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +57 -0
  5. package/dist/s3db.es.js +36661 -15531
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +27 -6
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +41 -46
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +39 -19
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +539 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +350 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  55. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  56. package/src/plugins/replicators/index.js +28 -3
  57. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  58. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  59. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  60. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  61. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  62. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  63. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  64. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  65. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  66. package/src/plugins/state-machine.plugin.js +122 -68
  67. package/src/plugins/tfstate/README.md +745 -0
  68. package/src/plugins/tfstate/base-driver.js +80 -0
  69. package/src/plugins/tfstate/errors.js +112 -0
  70. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  71. package/src/plugins/tfstate/index.js +2660 -0
  72. package/src/plugins/tfstate/s3-driver.js +192 -0
  73. package/src/plugins/ttl.plugin.js +536 -0
  74. package/src/resource.class.js +14 -10
  75. package/src/s3db.d.ts +57 -0
  76. package/src/schema.class.js +366 -32
  77. package/SECURITY.md +0 -76
  78. package/src/partition-drivers/base-partition-driver.js +0 -106
  79. package/src/partition-drivers/index.js +0 -66
  80. package/src/partition-drivers/memory-partition-driver.js +0 -289
  81. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  82. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -0,0 +1,612 @@
1
+ import tryFn from "#src/concerns/try-fn.js";
2
+ import BaseReplicator from './base-replicator.class.js';
3
+
4
+ /**
5
+ * Webhook Replicator - Send data changes to HTTP endpoints
6
+ *
7
+ * Sends database changes to webhook endpoints via HTTP POST requests.
8
+ * Supports multiple authentication methods, custom headers, retries, and transformers.
9
+ *
10
+ * Configuration:
11
+ * @param {string} url - Webhook endpoint URL (required)
12
+ * @param {string} method - HTTP method (default: 'POST')
13
+ * @param {Object} auth - Authentication configuration
14
+ * @param {string} auth.type - Auth type: 'bearer', 'basic', 'apikey'
15
+ * @param {string} auth.token - Bearer token
16
+ * @param {string} auth.username - Basic auth username
17
+ * @param {string} auth.password - Basic auth password
18
+ * @param {string} auth.header - API key header name
19
+ * @param {string} auth.value - API key value
20
+ * @param {Object} headers - Custom headers to send
21
+ * @param {number} timeout - Request timeout in ms (default: 5000)
22
+ * @param {number} retries - Number of retry attempts (default: 3)
23
+ * @param {number} retryDelay - Delay between retries in ms (default: 1000)
24
+ * @param {string} retryStrategy - 'fixed' or 'exponential' (default: 'exponential')
25
+ * @param {Array<number>} retryOnStatus - Status codes to retry (default: [429, 500, 502, 503, 504])
26
+ * @param {boolean} batch - Enable batch mode (default: false)
27
+ * @param {number} batchSize - Max records per batch request (default: 100)
28
+ *
29
+ * @example
30
+ * // Bearer token authentication
31
+ * new WebhookReplicator({
32
+ * url: 'https://api.example.com/webhook',
33
+ * auth: {
34
+ * type: 'bearer',
35
+ * token: 'your-secret-token'
36
+ * },
37
+ * headers: {
38
+ * 'Content-Type': 'application/json',
39
+ * 'X-Custom-Header': 'value'
40
+ * },
41
+ * timeout: 10000,
42
+ * retries: 3
43
+ * }, ['users', 'orders'])
44
+ *
45
+ * @example
46
+ * // Basic authentication
47
+ * new WebhookReplicator({
48
+ * url: 'https://api.example.com/webhook',
49
+ * auth: {
50
+ * type: 'basic',
51
+ * username: 'user',
52
+ * password: 'pass'
53
+ * }
54
+ * })
55
+ *
56
+ * @example
57
+ * // API Key authentication
58
+ * new WebhookReplicator({
59
+ * url: 'https://api.example.com/webhook',
60
+ * auth: {
61
+ * type: 'apikey',
62
+ * header: 'X-API-Key',
63
+ * value: 'your-api-key'
64
+ * }
65
+ * })
66
+ *
67
+ * @example
68
+ * // With resource transformers
69
+ * new WebhookReplicator({
70
+ * url: 'https://api.example.com/webhook',
71
+ * resources: {
72
+ * users: (data) => ({
73
+ * ...data,
74
+ * source: 's3db',
75
+ * transformedAt: new Date().toISOString()
76
+ * })
77
+ * }
78
+ * })
79
+ */
80
+ class WebhookReplicator extends BaseReplicator {
81
+ constructor(config = {}, resources = [], client = null) {
82
+ super(config);
83
+
84
+ // Required
85
+ this.url = config.url;
86
+ if (!this.url) {
87
+ throw new Error('WebhookReplicator requires a "url" configuration');
88
+ }
89
+
90
+ // HTTP settings
91
+ this.method = (config.method || 'POST').toUpperCase();
92
+ this.headers = config.headers || {};
93
+ this.timeout = config.timeout || 5000;
94
+
95
+ // Retry settings
96
+ this.retries = config.retries ?? 3;
97
+ this.retryDelay = config.retryDelay || 1000;
98
+ this.retryStrategy = config.retryStrategy || 'exponential';
99
+ this.retryOnStatus = config.retryOnStatus || [429, 500, 502, 503, 504];
100
+
101
+ // Batch settings
102
+ this.batch = config.batch || false;
103
+ this.batchSize = config.batchSize || 100;
104
+
105
+ // Authentication
106
+ this.auth = config.auth || null;
107
+
108
+ // Resource configuration
109
+ if (Array.isArray(resources)) {
110
+ this.resources = {};
111
+ for (const resource of resources) {
112
+ if (typeof resource === 'string') {
113
+ this.resources[resource] = true;
114
+ } else if (typeof resource === 'object' && resource.name) {
115
+ this.resources[resource.name] = resource;
116
+ }
117
+ }
118
+ } else if (typeof resources === 'object') {
119
+ this.resources = resources;
120
+ } else {
121
+ this.resources = {};
122
+ }
123
+
124
+ // Statistics
125
+ this.stats = {
126
+ totalRequests: 0,
127
+ successfulRequests: 0,
128
+ failedRequests: 0,
129
+ retriedRequests: 0,
130
+ totalRetries: 0
131
+ };
132
+ }
133
+
134
+ validateConfig() {
135
+ const errors = [];
136
+
137
+ if (!this.url) {
138
+ errors.push('URL is required');
139
+ }
140
+
141
+ // Validate URL format
142
+ try {
143
+ new URL(this.url);
144
+ } catch (err) {
145
+ errors.push(`Invalid URL format: ${this.url}`);
146
+ }
147
+
148
+ // Validate auth configuration
149
+ if (this.auth) {
150
+ if (!this.auth.type) {
151
+ errors.push('auth.type is required when auth is configured');
152
+ } else if (!['bearer', 'basic', 'apikey'].includes(this.auth.type)) {
153
+ errors.push('auth.type must be one of: bearer, basic, apikey');
154
+ }
155
+
156
+ if (this.auth.type === 'bearer' && !this.auth.token) {
157
+ errors.push('auth.token is required for bearer authentication');
158
+ }
159
+
160
+ if (this.auth.type === 'basic' && (!this.auth.username || !this.auth.password)) {
161
+ errors.push('auth.username and auth.password are required for basic authentication');
162
+ }
163
+
164
+ if (this.auth.type === 'apikey' && (!this.auth.header || !this.auth.value)) {
165
+ errors.push('auth.header and auth.value are required for API key authentication');
166
+ }
167
+ }
168
+
169
+ return {
170
+ isValid: errors.length === 0,
171
+ errors
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Build headers with authentication
177
+ * @returns {Object} Headers object
178
+ */
179
+ _buildHeaders() {
180
+ const headers = {
181
+ 'Content-Type': 'application/json',
182
+ 'User-Agent': 's3db-webhook-replicator',
183
+ ...this.headers
184
+ };
185
+
186
+ if (this.auth) {
187
+ switch (this.auth.type) {
188
+ case 'bearer':
189
+ headers['Authorization'] = `Bearer ${this.auth.token}`;
190
+ break;
191
+
192
+ case 'basic':
193
+ const credentials = Buffer.from(`${this.auth.username}:${this.auth.password}`).toString('base64');
194
+ headers['Authorization'] = `Basic ${credentials}`;
195
+ break;
196
+
197
+ case 'apikey':
198
+ headers[this.auth.header] = this.auth.value;
199
+ break;
200
+ }
201
+ }
202
+
203
+ return headers;
204
+ }
205
+
206
+ /**
207
+ * Apply resource transformer if configured
208
+ * @param {string} resource - Resource name
209
+ * @param {Object} data - Data to transform
210
+ * @returns {Object} Transformed data
211
+ */
212
+ _applyTransformer(resource, data) {
213
+ // Clean internal fields
214
+ let cleanData = this._cleanInternalFields(data);
215
+
216
+ const entry = this.resources[resource];
217
+ let result = cleanData;
218
+
219
+ if (!entry) return cleanData;
220
+
221
+ // Apply transform function if configured
222
+ if (typeof entry.transform === 'function') {
223
+ result = entry.transform(cleanData);
224
+ }
225
+
226
+ return result || cleanData;
227
+ }
228
+
229
+ /**
230
+ * Remove internal fields from data
231
+ * @param {Object} data - Data object
232
+ * @returns {Object} Cleaned data
233
+ */
234
+ _cleanInternalFields(data) {
235
+ if (!data || typeof data !== 'object') return data;
236
+
237
+ const cleanData = { ...data };
238
+
239
+ // Remove fields starting with $ or _
240
+ Object.keys(cleanData).forEach(key => {
241
+ if (key.startsWith('$') || key.startsWith('_')) {
242
+ delete cleanData[key];
243
+ }
244
+ });
245
+
246
+ return cleanData;
247
+ }
248
+
249
+ /**
250
+ * Create standardized webhook payload
251
+ * @param {string} resource - Resource name
252
+ * @param {string} operation - Operation type
253
+ * @param {Object} data - Record data
254
+ * @param {string} id - Record ID
255
+ * @param {Object} beforeData - Before data (for updates)
256
+ * @returns {Object} Webhook payload
257
+ */
258
+ createPayload(resource, operation, data, id, beforeData = null) {
259
+ const basePayload = {
260
+ resource: resource,
261
+ action: operation,
262
+ timestamp: new Date().toISOString(),
263
+ source: 's3db-webhook-replicator'
264
+ };
265
+
266
+ switch (operation) {
267
+ case 'insert':
268
+ return {
269
+ ...basePayload,
270
+ data: data
271
+ };
272
+ case 'update':
273
+ return {
274
+ ...basePayload,
275
+ before: beforeData,
276
+ data: data
277
+ };
278
+ case 'delete':
279
+ return {
280
+ ...basePayload,
281
+ data: data
282
+ };
283
+ default:
284
+ return {
285
+ ...basePayload,
286
+ data: data
287
+ };
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Make HTTP request with retries
293
+ * @param {Object} payload - Request payload
294
+ * @param {number} attempt - Current attempt number
295
+ * @returns {Promise<Object>} Response
296
+ */
297
+ async _makeRequest(payload, attempt = 0) {
298
+ const controller = new AbortController();
299
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
300
+
301
+ try {
302
+ const response = await fetch(this.url, {
303
+ method: this.method,
304
+ headers: this._buildHeaders(),
305
+ body: JSON.stringify(payload),
306
+ signal: controller.signal
307
+ });
308
+
309
+ clearTimeout(timeoutId);
310
+ this.stats.totalRequests++;
311
+
312
+ // Check if response is OK
313
+ if (response.ok) {
314
+ this.stats.successfulRequests++;
315
+ return {
316
+ success: true,
317
+ status: response.status,
318
+ statusText: response.statusText
319
+ };
320
+ }
321
+
322
+ // Check if we should retry this status code
323
+ if (this.retryOnStatus.includes(response.status) && attempt < this.retries) {
324
+ this.stats.retriedRequests++;
325
+ this.stats.totalRetries++;
326
+
327
+ // Calculate retry delay
328
+ const delay = this.retryStrategy === 'exponential'
329
+ ? this.retryDelay * Math.pow(2, attempt)
330
+ : this.retryDelay;
331
+
332
+ if (this.config.verbose) {
333
+ console.log(`[WebhookReplicator] Retrying request (attempt ${attempt + 1}/${this.retries}) after ${delay}ms - Status: ${response.status}`);
334
+ }
335
+
336
+ await new Promise(resolve => setTimeout(resolve, delay));
337
+ return this._makeRequest(payload, attempt + 1);
338
+ }
339
+
340
+ // Failed without retry
341
+ this.stats.failedRequests++;
342
+ const errorText = await response.text().catch(() => '');
343
+
344
+ return {
345
+ success: false,
346
+ status: response.status,
347
+ statusText: response.statusText,
348
+ error: errorText || `HTTP ${response.status}: ${response.statusText}`
349
+ };
350
+
351
+ } catch (error) {
352
+ clearTimeout(timeoutId);
353
+
354
+ // Retry on network errors
355
+ if (attempt < this.retries) {
356
+ this.stats.retriedRequests++;
357
+ this.stats.totalRetries++;
358
+
359
+ const delay = this.retryStrategy === 'exponential'
360
+ ? this.retryDelay * Math.pow(2, attempt)
361
+ : this.retryDelay;
362
+
363
+ if (this.config.verbose) {
364
+ console.log(`[WebhookReplicator] Retrying request (attempt ${attempt + 1}/${this.retries}) after ${delay}ms - Error: ${error.message}`);
365
+ }
366
+
367
+ await new Promise(resolve => setTimeout(resolve, delay));
368
+ return this._makeRequest(payload, attempt + 1);
369
+ }
370
+
371
+ this.stats.failedRequests++;
372
+ this.stats.totalRequests++;
373
+
374
+ return {
375
+ success: false,
376
+ error: error.message
377
+ };
378
+ }
379
+ }
380
+
381
+ async initialize(database) {
382
+ await super.initialize(database);
383
+
384
+ // Validate configuration
385
+ const validation = this.validateConfig();
386
+ if (!validation.isValid) {
387
+ const error = new Error(`WebhookReplicator configuration is invalid: ${validation.errors.join(', ')}`);
388
+
389
+ if (this.config.verbose) {
390
+ console.error(`[WebhookReplicator] ${error.message}`);
391
+ }
392
+
393
+ this.emit('initialization_error', {
394
+ replicator: this.name,
395
+ error: error.message,
396
+ errors: validation.errors
397
+ });
398
+
399
+ throw error;
400
+ }
401
+
402
+ this.emit('initialized', {
403
+ replicator: this.name,
404
+ url: this.url,
405
+ method: this.method,
406
+ authType: this.auth?.type || 'none',
407
+ resources: Object.keys(this.resources || {})
408
+ });
409
+ }
410
+
411
+ async replicate(resource, operation, data, id, beforeData = null) {
412
+ if (this.enabled === false) {
413
+ return { skipped: true, reason: 'replicator_disabled' };
414
+ }
415
+
416
+ if (!this.shouldReplicateResource(resource)) {
417
+ return { skipped: true, reason: 'resource_not_included' };
418
+ }
419
+
420
+ const [ok, err, result] = await tryFn(async () => {
421
+ // Apply transformation
422
+ const transformedData = this._applyTransformer(resource, data);
423
+
424
+ // Create payload
425
+ const payload = this.createPayload(resource, operation, transformedData, id, beforeData);
426
+
427
+ // Make request
428
+ const response = await this._makeRequest(payload);
429
+
430
+ if (response.success) {
431
+ this.emit('replicated', {
432
+ replicator: this.name,
433
+ resource,
434
+ operation,
435
+ id,
436
+ url: this.url,
437
+ status: response.status,
438
+ success: true
439
+ });
440
+
441
+ return { success: true, status: response.status };
442
+ }
443
+
444
+ throw new Error(response.error || `HTTP ${response.status}: ${response.statusText}`);
445
+ });
446
+
447
+ if (ok) return result;
448
+
449
+ if (this.config.verbose) {
450
+ console.warn(`[WebhookReplicator] Replication failed for ${resource}: ${err.message}`);
451
+ }
452
+
453
+ this.emit('replicator_error', {
454
+ replicator: this.name,
455
+ resource,
456
+ operation,
457
+ id,
458
+ error: err.message
459
+ });
460
+
461
+ return { success: false, error: err.message };
462
+ }
463
+
464
+ async replicateBatch(resource, records) {
465
+ if (this.enabled === false) {
466
+ return { skipped: true, reason: 'replicator_disabled' };
467
+ }
468
+
469
+ if (!this.shouldReplicateResource(resource)) {
470
+ return { skipped: true, reason: 'resource_not_included' };
471
+ }
472
+
473
+ const [ok, err, result] = await tryFn(async () => {
474
+ // If batch mode is enabled, send all records in one request
475
+ if (this.batch) {
476
+ const payloads = records.map(record =>
477
+ this.createPayload(
478
+ resource,
479
+ record.operation,
480
+ this._applyTransformer(resource, record.data),
481
+ record.id,
482
+ record.beforeData
483
+ )
484
+ );
485
+
486
+ const response = await this._makeRequest({ batch: payloads });
487
+
488
+ if (response.success) {
489
+ this.emit('batch_replicated', {
490
+ replicator: this.name,
491
+ resource,
492
+ url: this.url,
493
+ total: records.length,
494
+ successful: records.length,
495
+ errors: 0,
496
+ status: response.status
497
+ });
498
+
499
+ return {
500
+ success: true,
501
+ total: records.length,
502
+ successful: records.length,
503
+ errors: 0,
504
+ status: response.status
505
+ };
506
+ }
507
+
508
+ throw new Error(response.error || `HTTP ${response.status}: ${response.statusText}`);
509
+ }
510
+
511
+ // Otherwise, send individual requests (parallel)
512
+ const results = await Promise.allSettled(
513
+ records.map(record =>
514
+ this.replicate(resource, record.operation, record.data, record.id, record.beforeData)
515
+ )
516
+ );
517
+
518
+ const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
519
+ const failed = results.length - successful;
520
+
521
+ this.emit('batch_replicated', {
522
+ replicator: this.name,
523
+ resource,
524
+ url: this.url,
525
+ total: records.length,
526
+ successful,
527
+ errors: failed
528
+ });
529
+
530
+ return {
531
+ success: failed === 0,
532
+ total: records.length,
533
+ successful,
534
+ errors: failed,
535
+ results
536
+ };
537
+ });
538
+
539
+ if (ok) return result;
540
+
541
+ if (this.config.verbose) {
542
+ console.warn(`[WebhookReplicator] Batch replication failed for ${resource}: ${err.message}`);
543
+ }
544
+
545
+ this.emit('batch_replicator_error', {
546
+ replicator: this.name,
547
+ resource,
548
+ error: err.message
549
+ });
550
+
551
+ return { success: false, error: err.message };
552
+ }
553
+
554
+ async testConnection() {
555
+ const [ok, err] = await tryFn(async () => {
556
+ const testPayload = {
557
+ test: true,
558
+ timestamp: new Date().toISOString(),
559
+ source: 's3db-webhook-replicator'
560
+ };
561
+
562
+ const response = await this._makeRequest(testPayload);
563
+
564
+ if (!response.success) {
565
+ throw new Error(response.error || `HTTP ${response.status}: ${response.statusText}`);
566
+ }
567
+
568
+ return true;
569
+ });
570
+
571
+ if (ok) return true;
572
+
573
+ if (this.config.verbose) {
574
+ console.warn(`[WebhookReplicator] Connection test failed: ${err.message}`);
575
+ }
576
+
577
+ this.emit('connection_error', {
578
+ replicator: this.name,
579
+ error: err.message
580
+ });
581
+
582
+ return false;
583
+ }
584
+
585
+ async getStatus() {
586
+ const baseStatus = await super.getStatus();
587
+ return {
588
+ ...baseStatus,
589
+ url: this.url,
590
+ method: this.method,
591
+ authType: this.auth?.type || 'none',
592
+ timeout: this.timeout,
593
+ retries: this.retries,
594
+ retryStrategy: this.retryStrategy,
595
+ batchMode: this.batch,
596
+ resources: Object.keys(this.resources || {}),
597
+ stats: { ...this.stats }
598
+ };
599
+ }
600
+
601
+ shouldReplicateResource(resource) {
602
+ // If no resources configured, replicate all
603
+ if (!this.resources || Object.keys(this.resources).length === 0) {
604
+ return true;
605
+ }
606
+
607
+ // Check if resource is in the list
608
+ return Object.keys(this.resources).includes(resource);
609
+ }
610
+ }
611
+
612
+ export default WebhookReplicator;