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,539 @@
1
+ /**
2
+ * API Plugin - RESTful HTTP API for s3db.js resources
3
+ *
4
+ * Transforms s3db.js resources into HTTP REST endpoints with:
5
+ * - Multiple authentication methods (JWT, API Key, Basic Auth, Public)
6
+ * - Automatic versioning based on resource version
7
+ * - Production features (CORS, Rate Limiting, Logging, Compression)
8
+ * - Schema validation middleware
9
+ * - Custom middleware support
10
+ *
11
+ * @example
12
+ * const apiPlugin = new ApiPlugin({
13
+ * port: 3000,
14
+ * docs: { enabled: true },
15
+ * auth: {
16
+ * jwt: { enabled: true, secret: 'my-secret' },
17
+ * apiKey: { enabled: true }
18
+ * },
19
+ * resources: {
20
+ * cars: {
21
+ * auth: ['jwt', 'apiKey'],
22
+ * methods: ['GET', 'POST', 'PUT', 'DELETE']
23
+ * }
24
+ * },
25
+ * cors: { enabled: true },
26
+ * rateLimit: { enabled: true, maxRequests: 100 },
27
+ * logging: { enabled: true },
28
+ * compression: { enabled: true },
29
+ * validation: { enabled: true }
30
+ * });
31
+ *
32
+ * await database.usePlugin(apiPlugin);
33
+ */
34
+
35
+ import { Plugin } from '../plugin.class.js';
36
+ import { ApiServer } from './server.js';
37
+ import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
38
+ import tryFn from '../../concerns/try-fn.js';
39
+
40
+ export class ApiPlugin extends Plugin {
41
+ /**
42
+ * Create API Plugin instance
43
+ * @param {Object} options - Plugin configuration
44
+ */
45
+ constructor(options = {}) {
46
+ super(options);
47
+
48
+ this.config = {
49
+ // Server configuration
50
+ port: options.port || 3000,
51
+ host: options.host || '0.0.0.0',
52
+ verbose: options.verbose || false,
53
+
54
+ // API Documentation (supports both new and legacy formats)
55
+ docs: {
56
+ enabled: options.docs?.enabled !== false && options.docsEnabled !== false, // Enable by default
57
+ ui: options.docs?.ui || 'redoc', // 'swagger' or 'redoc' (redoc is prettier!)
58
+ title: options.docs?.title || options.apiTitle || 's3db.js API',
59
+ version: options.docs?.version || options.apiVersion || '1.0.0',
60
+ description: options.docs?.description || options.apiDescription || 'Auto-generated REST API for s3db.js resources'
61
+ },
62
+
63
+ // Authentication configuration
64
+ auth: {
65
+ jwt: {
66
+ enabled: options.auth?.jwt?.enabled || false,
67
+ secret: options.auth?.jwt?.secret || null,
68
+ expiresIn: options.auth?.jwt?.expiresIn || '7d'
69
+ },
70
+ apiKey: {
71
+ enabled: options.auth?.apiKey?.enabled || false,
72
+ headerName: options.auth?.apiKey?.headerName || 'X-API-Key'
73
+ },
74
+ basic: {
75
+ enabled: options.auth?.basic?.enabled || false,
76
+ realm: options.auth?.basic?.realm || 'API Access'
77
+ }
78
+ },
79
+
80
+ // Resource configuration
81
+ resources: options.resources || {},
82
+
83
+ // CORS configuration
84
+ cors: {
85
+ enabled: options.cors?.enabled || false,
86
+ origin: options.cors?.origin || '*',
87
+ methods: options.cors?.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
88
+ allowedHeaders: options.cors?.allowedHeaders || ['Content-Type', 'Authorization', 'X-API-Key'],
89
+ exposedHeaders: options.cors?.exposedHeaders || ['X-Total-Count', 'X-Page-Count'],
90
+ credentials: options.cors?.credentials !== false,
91
+ maxAge: options.cors?.maxAge || 86400
92
+ },
93
+
94
+ // Rate limiting configuration
95
+ rateLimit: {
96
+ enabled: options.rateLimit?.enabled || false,
97
+ windowMs: options.rateLimit?.windowMs || 60000, // 1 minute
98
+ maxRequests: options.rateLimit?.maxRequests || 100,
99
+ keyGenerator: options.rateLimit?.keyGenerator || null
100
+ },
101
+
102
+ // Logging configuration
103
+ logging: {
104
+ enabled: options.logging?.enabled || false,
105
+ format: options.logging?.format || ':method :path :status :response-time ms',
106
+ verbose: options.logging?.verbose || false
107
+ },
108
+
109
+ // Compression configuration
110
+ compression: {
111
+ enabled: options.compression?.enabled || false,
112
+ threshold: options.compression?.threshold || 1024, // 1KB
113
+ level: options.compression?.level || 6
114
+ },
115
+
116
+ // Validation configuration
117
+ validation: {
118
+ enabled: options.validation?.enabled !== false,
119
+ validateOnInsert: options.validation?.validateOnInsert !== false,
120
+ validateOnUpdate: options.validation?.validateOnUpdate !== false,
121
+ returnValidationErrors: options.validation?.returnValidationErrors !== false
122
+ },
123
+
124
+ // Content Security Policy (CSP) configuration
125
+ csp: {
126
+ enabled: options.csp?.enabled || false,
127
+ // Default CSP that works with Redoc v2.5.1 (allows CDN scripts/styles)
128
+ directives: options.csp?.directives || {
129
+ 'default-src': ["'self'"],
130
+ 'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/'],
131
+ 'style-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/', 'https://fonts.googleapis.com'],
132
+ 'font-src': ["'self'", 'https://fonts.gstatic.com'],
133
+ 'img-src': ["'self'", 'data:', 'https:'],
134
+ 'connect-src': ["'self'"]
135
+ },
136
+ reportOnly: options.csp?.reportOnly || false, // If true, uses Content-Security-Policy-Report-Only
137
+ reportUri: options.csp?.reportUri || null
138
+ },
139
+
140
+ // Custom global middlewares
141
+ middlewares: options.middlewares || []
142
+ };
143
+
144
+ this.server = null;
145
+ this.usersResource = null;
146
+ }
147
+
148
+ /**
149
+ * Validate plugin dependencies
150
+ * @private
151
+ */
152
+ async _validateDependencies() {
153
+ await requirePluginDependency('api-plugin', {
154
+ throwOnError: true,
155
+ checkVersions: true
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Install plugin
161
+ */
162
+ async onInstall() {
163
+ if (this.config.verbose) {
164
+ console.log('[API Plugin] Installing...');
165
+ }
166
+
167
+ // Validate dependencies
168
+ try {
169
+ await this._validateDependencies();
170
+ } catch (err) {
171
+ console.error('[API Plugin] Dependency validation failed:', err.message);
172
+ throw err;
173
+ }
174
+
175
+ // Create users resource if authentication is enabled
176
+ const authEnabled = this.config.auth.jwt.enabled ||
177
+ this.config.auth.apiKey.enabled ||
178
+ this.config.auth.basic.enabled;
179
+
180
+ if (authEnabled) {
181
+ await this._createUsersResource();
182
+ }
183
+
184
+ // Setup middlewares
185
+ await this._setupMiddlewares();
186
+
187
+ if (this.config.verbose) {
188
+ console.log('[API Plugin] Installed successfully');
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Create users resource for authentication
194
+ * @private
195
+ */
196
+ async _createUsersResource() {
197
+ const [ok, err, resource] = await tryFn(() =>
198
+ this.database.createResource({
199
+ name: 'plg_users',
200
+ attributes: {
201
+ id: 'string|required',
202
+ username: 'string|required|minlength:3',
203
+ email: 'string|optional|email',
204
+ password: 'secret|required|minlength:8',
205
+ apiKey: 'string|optional',
206
+ jwtSecret: 'string|optional',
207
+ role: 'string|default:user',
208
+ active: 'boolean|default:true',
209
+ createdAt: 'string|optional',
210
+ lastLoginAt: 'string|optional',
211
+ metadata: 'json|optional'
212
+ },
213
+ behavior: 'body-overflow',
214
+ timestamps: true,
215
+ createdBy: 'ApiPlugin'
216
+ })
217
+ );
218
+
219
+ if (ok) {
220
+ this.usersResource = resource;
221
+ if (this.config.verbose) {
222
+ console.log('[API Plugin] Created plg_users resource for authentication');
223
+ }
224
+ } else if (this.database.resources.plg_users) {
225
+ // Resource already exists
226
+ this.usersResource = this.database.resources.plg_users;
227
+ if (this.config.verbose) {
228
+ console.log('[API Plugin] Using existing plg_users resource');
229
+ }
230
+ } else {
231
+ throw err;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Setup middlewares
237
+ * @private
238
+ */
239
+ async _setupMiddlewares() {
240
+ const middlewares = [];
241
+
242
+ // Add request ID middleware
243
+ middlewares.push(async (c, next) => {
244
+ c.set('requestId', crypto.randomUUID());
245
+ c.set('verbose', this.config.verbose);
246
+ await next();
247
+ });
248
+
249
+ // Add CORS middleware
250
+ if (this.config.cors.enabled) {
251
+ const corsMiddleware = await this._createCorsMiddleware();
252
+ middlewares.push(corsMiddleware);
253
+ }
254
+
255
+ // Add CSP middleware
256
+ if (this.config.csp.enabled) {
257
+ const cspMiddleware = await this._createCSPMiddleware();
258
+ middlewares.push(cspMiddleware);
259
+ }
260
+
261
+ // Add rate limiting middleware
262
+ if (this.config.rateLimit.enabled) {
263
+ const rateLimitMiddleware = await this._createRateLimitMiddleware();
264
+ middlewares.push(rateLimitMiddleware);
265
+ }
266
+
267
+ // Add logging middleware
268
+ if (this.config.logging.enabled) {
269
+ const loggingMiddleware = await this._createLoggingMiddleware();
270
+ middlewares.push(loggingMiddleware);
271
+ }
272
+
273
+ // Add compression middleware
274
+ if (this.config.compression.enabled) {
275
+ const compressionMiddleware = await this._createCompressionMiddleware();
276
+ middlewares.push(compressionMiddleware);
277
+ }
278
+
279
+ // Add custom middlewares
280
+ middlewares.push(...this.config.middlewares);
281
+
282
+ // Store compiled middlewares
283
+ this.compiledMiddlewares = middlewares;
284
+ }
285
+
286
+ /**
287
+ * Create CORS middleware (placeholder)
288
+ * @private
289
+ */
290
+ async _createCorsMiddleware() {
291
+ return async (c, next) => {
292
+ const { origin, methods, allowedHeaders, exposedHeaders, credentials, maxAge } = this.config.cors;
293
+
294
+ // Set CORS headers
295
+ c.header('Access-Control-Allow-Origin', origin);
296
+ c.header('Access-Control-Allow-Methods', methods.join(', '));
297
+ c.header('Access-Control-Allow-Headers', allowedHeaders.join(', '));
298
+ c.header('Access-Control-Expose-Headers', exposedHeaders.join(', '));
299
+
300
+ if (credentials) {
301
+ c.header('Access-Control-Allow-Credentials', 'true');
302
+ }
303
+
304
+ c.header('Access-Control-Max-Age', maxAge.toString());
305
+
306
+ // Handle OPTIONS preflight
307
+ if (c.req.method === 'OPTIONS') {
308
+ return c.body(null, 204);
309
+ }
310
+
311
+ await next();
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Create CSP middleware
317
+ * @private
318
+ */
319
+ async _createCSPMiddleware() {
320
+ return async (c, next) => {
321
+ const { directives, reportOnly, reportUri } = this.config.csp;
322
+
323
+ // Build CSP header value from directives
324
+ const cspParts = [];
325
+ for (const [directive, values] of Object.entries(directives)) {
326
+ if (Array.isArray(values) && values.length > 0) {
327
+ cspParts.push(`${directive} ${values.join(' ')}`);
328
+ } else if (typeof values === 'string') {
329
+ cspParts.push(`${directive} ${values}`);
330
+ }
331
+ }
332
+
333
+ // Add report-uri if specified
334
+ if (reportUri) {
335
+ cspParts.push(`report-uri ${reportUri}`);
336
+ }
337
+
338
+ const cspValue = cspParts.join('; ');
339
+
340
+ // Set appropriate header (report-only or enforced)
341
+ const headerName = reportOnly
342
+ ? 'Content-Security-Policy-Report-Only'
343
+ : 'Content-Security-Policy';
344
+
345
+ c.header(headerName, cspValue);
346
+
347
+ await next();
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Create rate limiting middleware (placeholder)
353
+ * @private
354
+ */
355
+ async _createRateLimitMiddleware() {
356
+ const requests = new Map();
357
+ const { windowMs, maxRequests, keyGenerator } = this.config.rateLimit;
358
+
359
+ return async (c, next) => {
360
+ // Generate key (IP or custom)
361
+ const key = keyGenerator
362
+ ? keyGenerator(c)
363
+ : c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown';
364
+
365
+ // Get or create request count
366
+ if (!requests.has(key)) {
367
+ requests.set(key, { count: 0, resetAt: Date.now() + windowMs });
368
+ }
369
+
370
+ const record = requests.get(key);
371
+
372
+ // Reset if window expired
373
+ if (Date.now() > record.resetAt) {
374
+ record.count = 0;
375
+ record.resetAt = Date.now() + windowMs;
376
+ }
377
+
378
+ // Check limit
379
+ if (record.count >= maxRequests) {
380
+ const retryAfter = Math.ceil((record.resetAt - Date.now()) / 1000);
381
+ c.header('Retry-After', retryAfter.toString());
382
+ c.header('X-RateLimit-Limit', maxRequests.toString());
383
+ c.header('X-RateLimit-Remaining', '0');
384
+ c.header('X-RateLimit-Reset', record.resetAt.toString());
385
+
386
+ return c.json({
387
+ success: false,
388
+ error: {
389
+ message: 'Rate limit exceeded',
390
+ code: 'RATE_LIMIT_EXCEEDED',
391
+ details: { retryAfter }
392
+ }
393
+ }, 429);
394
+ }
395
+
396
+ // Increment count
397
+ record.count++;
398
+
399
+ // Set rate limit headers
400
+ c.header('X-RateLimit-Limit', maxRequests.toString());
401
+ c.header('X-RateLimit-Remaining', (maxRequests - record.count).toString());
402
+ c.header('X-RateLimit-Reset', record.resetAt.toString());
403
+
404
+ await next();
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Create logging middleware (placeholder)
410
+ * @private
411
+ */
412
+ async _createLoggingMiddleware() {
413
+ return async (c, next) => {
414
+ const start = Date.now();
415
+ const method = c.req.method;
416
+ const path = c.req.path;
417
+ const requestId = c.get('requestId');
418
+
419
+ await next();
420
+
421
+ const duration = Date.now() - start;
422
+ const status = c.res.status;
423
+ const user = c.get('user')?.username || 'anonymous';
424
+
425
+ console.log(`[API Plugin] ${requestId} - ${method} ${path} ${status} ${duration}ms - ${user}`);
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Create compression middleware (placeholder)
431
+ * @private
432
+ */
433
+ async _createCompressionMiddleware() {
434
+ return async (c, next) => {
435
+ await next();
436
+
437
+ // Note: Actual compression would require proper streaming support
438
+ // For now, this is a placeholder
439
+ const acceptEncoding = c.req.header('accept-encoding') || '';
440
+
441
+ if (acceptEncoding.includes('gzip')) {
442
+ c.header('Content-Encoding', 'gzip');
443
+ } else if (acceptEncoding.includes('deflate')) {
444
+ c.header('Content-Encoding', 'deflate');
445
+ }
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Start plugin
451
+ */
452
+ async onStart() {
453
+ if (this.config.verbose) {
454
+ console.log('[API Plugin] Starting server...');
455
+ }
456
+
457
+ // Create server instance
458
+ this.server = new ApiServer({
459
+ port: this.config.port,
460
+ host: this.config.host,
461
+ database: this.database,
462
+ resources: this.config.resources,
463
+ middlewares: this.compiledMiddlewares,
464
+ verbose: this.config.verbose,
465
+ auth: this.config.auth,
466
+ docsEnabled: this.config.docs.enabled,
467
+ docsUI: this.config.docs.ui,
468
+ apiTitle: this.config.docs.title,
469
+ apiVersion: this.config.docs.version,
470
+ apiDescription: this.config.docs.description
471
+ });
472
+
473
+ // Start server
474
+ await this.server.start();
475
+
476
+ this.emit('plugin.started', {
477
+ port: this.config.port,
478
+ host: this.config.host
479
+ });
480
+ }
481
+
482
+ /**
483
+ * Stop plugin
484
+ */
485
+ async onStop() {
486
+ if (this.config.verbose) {
487
+ console.log('[API Plugin] Stopping server...');
488
+ }
489
+
490
+ if (this.server) {
491
+ await this.server.stop();
492
+ this.server = null;
493
+ }
494
+
495
+ this.emit('plugin.stopped');
496
+ }
497
+
498
+ /**
499
+ * Uninstall plugin
500
+ */
501
+ async onUninstall(options = {}) {
502
+ const { purgeData = false } = options;
503
+
504
+ // Stop server if running
505
+ await this.onStop();
506
+
507
+ // Optionally delete users resource
508
+ if (purgeData && this.usersResource) {
509
+ // Delete all users (plugin data cleanup happens automatically via base Plugin class)
510
+ const [ok] = await tryFn(() => this.database.deleteResource('plg_users'));
511
+
512
+ if (ok && this.config.verbose) {
513
+ console.log('[API Plugin] Deleted plg_users resource');
514
+ }
515
+ }
516
+
517
+ if (this.config.verbose) {
518
+ console.log('[API Plugin] Uninstalled successfully');
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Get server information
524
+ * @returns {Object} Server info
525
+ */
526
+ getServerInfo() {
527
+ return this.server ? this.server.getInfo() : { isRunning: false };
528
+ }
529
+
530
+ /**
531
+ * Get Hono app instance (for advanced usage)
532
+ * @returns {Hono|null} Hono app
533
+ */
534
+ getApp() {
535
+ return this.server ? this.server.getApp() : null;
536
+ }
537
+ }
538
+
539
+ export default ApiPlugin;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Middlewares - Export all API middlewares
3
+ */
4
+
5
+ export { createValidationMiddleware, createQueryValidation, listQueryValidation } from './validator.js';
6
+
7
+ // Note: CORS, Rate Limiting, Logging, and Compression middlewares
8
+ // are currently implemented in api.plugin.js as inline functions.
9
+ // They can be extracted to separate files if needed for better organization.
10
+
11
+ export default {
12
+ createValidationMiddleware,
13
+ createQueryValidation,
14
+ listQueryValidation
15
+ };