s3db.js 11.3.2 → 12.0.1

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 (83) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36945 -15510
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +66 -1
  5. package/dist/s3db.es.js +36914 -15534
  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 +35 -15
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +79 -49
  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 +97 -47
  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 +544 -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 +354 -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/replicator.plugin.js +2 -1
  55. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  56. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  57. package/src/plugins/replicators/index.js +28 -3
  58. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  59. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  60. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  61. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  62. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  63. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  64. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  65. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  66. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  67. package/src/plugins/state-machine.plugin.js +122 -68
  68. package/src/plugins/tfstate/README.md +745 -0
  69. package/src/plugins/tfstate/base-driver.js +80 -0
  70. package/src/plugins/tfstate/errors.js +112 -0
  71. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  72. package/src/plugins/tfstate/index.js +2660 -0
  73. package/src/plugins/tfstate/s3-driver.js +192 -0
  74. package/src/plugins/ttl.plugin.js +536 -0
  75. package/src/resource.class.js +315 -36
  76. package/src/s3db.d.ts +66 -1
  77. package/src/schema.class.js +366 -32
  78. package/SECURITY.md +0 -76
  79. package/src/partition-drivers/base-partition-driver.js +0 -106
  80. package/src/partition-drivers/index.js +0 -66
  81. package/src/partition-drivers/memory-partition-driver.js +0 -289
  82. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  83. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -105,72 +105,117 @@
105
105
  * - TTL is enforced by checking object creation time
106
106
  */
107
107
  import zlib from "node:zlib";
108
- import { join } from "path";
109
-
110
- import { Cache } from "./cache.class.js"
111
- import { streamToString } from "../../stream/index.js";
112
- import tryFn from "../../concerns/try-fn.js";
108
+ import { PluginStorage } from "../../concerns/plugin-storage.js";
109
+ import { Cache } from "./cache.class.js";
113
110
 
114
111
  export class S3Cache extends Cache {
115
- constructor({
116
- client,
112
+ constructor({
113
+ client,
117
114
  keyPrefix = 'cache',
118
115
  ttl = 0,
119
- prefix = undefined
116
+ prefix = undefined,
117
+ enableCompression = true,
118
+ compressionThreshold = 1024
120
119
  }) {
121
120
  super();
122
- this.client = client
121
+ this.client = client;
123
122
  this.keyPrefix = keyPrefix;
124
123
  this.config.ttl = ttl;
125
124
  this.config.client = client;
126
125
  this.config.prefix = prefix !== undefined ? prefix : keyPrefix + (keyPrefix.endsWith('/') ? '' : '/');
126
+ this.config.enableCompression = enableCompression;
127
+ this.config.compressionThreshold = compressionThreshold;
128
+
129
+ // Create PluginStorage instance for consistent storage operations with TTL support
130
+ this.storage = new PluginStorage(client, 'cache');
131
+ }
132
+
133
+ /**
134
+ * Compress data if enabled and above threshold
135
+ * @private
136
+ */
137
+ _compressData(data) {
138
+ const jsonString = JSON.stringify(data);
139
+
140
+ // Don't compress if disabled or below threshold
141
+ if (!this.config.enableCompression || jsonString.length < this.config.compressionThreshold) {
142
+ return {
143
+ data: jsonString,
144
+ compressed: false,
145
+ originalSize: jsonString.length
146
+ };
147
+ }
148
+
149
+ // Compress with gzip
150
+ const compressed = zlib.gzipSync(jsonString).toString('base64');
151
+ return {
152
+ data: compressed,
153
+ compressed: true,
154
+ originalSize: jsonString.length,
155
+ compressedSize: compressed.length,
156
+ compressionRatio: (compressed.length / jsonString.length).toFixed(2)
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Decompress data if needed
162
+ * @private
163
+ */
164
+ _decompressData(storedData) {
165
+ if (!storedData || !storedData.compressed) {
166
+ // Not compressed - parse JSON directly
167
+ return storedData && storedData.data ? JSON.parse(storedData.data) : null;
168
+ }
169
+
170
+ // Decompress gzip data
171
+ const buffer = Buffer.from(storedData.data, 'base64');
172
+ const decompressed = zlib.unzipSync(buffer).toString();
173
+ return JSON.parse(decompressed);
127
174
  }
128
175
 
129
176
  async _set(key, data) {
130
- let body = JSON.stringify(data);
131
- const lengthSerialized = body.length;
132
- body = zlib.gzipSync(body).toString('base64');
133
-
134
- return this.client.putObject({
135
- key: join(this.keyPrefix, key),
136
- body,
137
- contentEncoding: "gzip",
138
- contentType: "application/gzip",
139
- metadata: {
140
- compressor: "zlib",
141
- compressed: 'true',
142
- "client-id": this.client.id,
143
- "length-serialized": String(lengthSerialized),
144
- "length-compressed": String(body.length),
145
- "compression-gain": (body.length/lengthSerialized).toFixed(2),
146
- },
147
- });
177
+ const compressed = this._compressData(data);
178
+
179
+ // Use PluginStorage with body-only behavior (compressed data doesn't benefit from metadata encoding)
180
+ // TTL is handled automatically by PluginStorage
181
+ return this.storage.set(
182
+ this.storage.getPluginKey(null, this.keyPrefix, key),
183
+ compressed,
184
+ {
185
+ ttl: this.config.ttl,
186
+ behavior: 'body-only', // Compressed data is already optimized, skip metadata encoding
187
+ contentType: compressed.compressed ? 'application/gzip' : 'application/json'
188
+ }
189
+ );
148
190
  }
149
191
 
150
192
  async _get(key) {
151
- const [ok, err, result] = await tryFn(async () => {
152
- const { Body } = await this.client.getObject(join(this.keyPrefix, key));
153
- let content = await streamToString(Body);
154
- content = Buffer.from(content, 'base64');
155
- content = zlib.unzipSync(content).toString();
156
- return JSON.parse(content);
157
- });
158
- if (ok) return result;
159
- if (err.name === 'NoSuchKey' || err.name === 'NotFound') return null;
160
- throw err;
193
+ // PluginStorage automatically checks TTL and deletes expired items
194
+ const storedData = await this.storage.get(
195
+ this.storage.getPluginKey(null, this.keyPrefix, key)
196
+ );
197
+
198
+ if (!storedData) return null;
199
+
200
+ return this._decompressData(storedData);
161
201
  }
162
202
 
163
203
  async _del(key) {
164
- await this.client.deleteObject(join(this.keyPrefix, key));
165
- return true
204
+ await this.storage.delete(
205
+ this.storage.getPluginKey(null, this.keyPrefix, key)
206
+ );
207
+ return true;
166
208
  }
167
209
 
168
210
  async _clear() {
169
- const keys = await this.client.getAllKeys({
170
- prefix: this.keyPrefix,
171
- });
211
+ // Get all keys with the cache plugin prefix
212
+ const pluginPrefix = `plugin=cache/${this.keyPrefix}`;
213
+ const allKeys = await this.client.getAllKeys({ prefix: pluginPrefix });
172
214
 
173
- await this.client.deleteObjects(keys);
215
+ // Delete all cache keys
216
+ for (const key of allKeys) {
217
+ await this.storage.delete(key);
218
+ }
174
219
  }
175
220
 
176
221
  async size() {
@@ -179,10 +224,13 @@ export class S3Cache extends Cache {
179
224
  }
180
225
 
181
226
  async keys() {
182
- // Busca todas as chaves com o prefixo do cache e remove o prefixo
183
- const allKeys = await this.client.getAllKeys({ prefix: this.keyPrefix });
184
- const prefix = this.keyPrefix.endsWith('/') ? this.keyPrefix : this.keyPrefix + '/';
185
- return allKeys.map(k => k.startsWith(prefix) ? k.slice(prefix.length) : k);
227
+ // Get all keys with the cache plugin prefix
228
+ const pluginPrefix = `plugin=cache/${this.keyPrefix}`;
229
+ const allKeys = await this.client.getAllKeys({ prefix: pluginPrefix });
230
+
231
+ // Remove the plugin prefix to return just the cache keys
232
+ const prefixToRemove = `plugin=cache/${this.keyPrefix}/`;
233
+ return allKeys.map(k => k.startsWith(prefixToRemove) ? k.slice(prefixToRemove.length) : k);
186
234
  }
187
235
  }
188
236
 
@@ -126,6 +126,16 @@ export class CachePlugin extends Plugin {
126
126
  // Logging
127
127
  verbose: options.verbose || false
128
128
  };
129
+
130
+ // Initialize stats tracking
131
+ this.stats = {
132
+ hits: 0,
133
+ misses: 0,
134
+ writes: 0,
135
+ deletes: 0,
136
+ errors: 0,
137
+ startTime: Date.now()
138
+ };
129
139
  }
130
140
 
131
141
  async onInstall() {
@@ -332,30 +342,46 @@ export class CachePlugin extends Plugin {
332
342
  partition,
333
343
  partitionValues
334
344
  }));
335
-
336
- if (ok && result !== null && result !== undefined) return result;
337
- if (!ok && err.name !== 'NoSuchKey') throw err;
338
-
345
+
346
+ if (ok && result !== null && result !== undefined) {
347
+ this.stats.hits++;
348
+ return result;
349
+ }
350
+ if (!ok && err.name !== 'NoSuchKey') {
351
+ this.stats.errors++;
352
+ throw err;
353
+ }
354
+
339
355
  // Not cached, call next
356
+ this.stats.misses++;
340
357
  const freshResult = await next();
341
-
358
+
342
359
  // Store with partition context
360
+ this.stats.writes++;
343
361
  await resource.cache._set(key, freshResult, {
344
362
  resource: resource.name,
345
363
  action: method,
346
364
  partition,
347
365
  partitionValues
348
366
  });
349
-
367
+
350
368
  return freshResult;
351
369
  } else {
352
370
  // Standard cache behavior
353
371
  const [ok, err, result] = await tryFn(() => resource.cache.get(key));
354
- if (ok && result !== null && result !== undefined) return result;
355
- if (!ok && err.name !== 'NoSuchKey') throw err;
356
-
372
+ if (ok && result !== null && result !== undefined) {
373
+ this.stats.hits++;
374
+ return result;
375
+ }
376
+ if (!ok && err.name !== 'NoSuchKey') {
377
+ this.stats.errors++;
378
+ throw err;
379
+ }
380
+
357
381
  // Not cached, call next
382
+ this.stats.misses++;
358
383
  const freshResult = await next();
384
+ this.stats.writes++;
359
385
  await resource.cache.set(key, freshResult);
360
386
  return freshResult;
361
387
  }
@@ -476,6 +502,7 @@ export class CachePlugin extends Plugin {
476
502
  const [ok, err] = await tryFn(() => cache.clear(key));
477
503
 
478
504
  if (ok) {
505
+ this.stats.deletes++;
479
506
  return [true, null];
480
507
  }
481
508
 
@@ -682,6 +709,77 @@ export class CachePlugin extends Plugin {
682
709
 
683
710
  return analysis;
684
711
  }
712
+
713
+ /**
714
+ * Get cache statistics including hit/miss rates
715
+ * @returns {Object} Stats object with hits, misses, writes, deletes, errors, and calculated metrics
716
+ */
717
+ getStats() {
718
+ const total = this.stats.hits + this.stats.misses;
719
+ const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
720
+ const missRate = total > 0 ? (this.stats.misses / total) * 100 : 0;
721
+ const uptime = Date.now() - this.stats.startTime;
722
+ const uptimeSeconds = Math.floor(uptime / 1000);
723
+
724
+ return {
725
+ // Raw counters
726
+ hits: this.stats.hits,
727
+ misses: this.stats.misses,
728
+ writes: this.stats.writes,
729
+ deletes: this.stats.deletes,
730
+ errors: this.stats.errors,
731
+
732
+ // Calculated metrics
733
+ total,
734
+ hitRate: hitRate.toFixed(2) + '%',
735
+ missRate: missRate.toFixed(2) + '%',
736
+ hitRateDecimal: hitRate / 100,
737
+ missRateDecimal: missRate / 100,
738
+
739
+ // Uptime
740
+ uptime: uptimeSeconds,
741
+ uptimeFormatted: this._formatUptime(uptimeSeconds),
742
+ startTime: new Date(this.stats.startTime).toISOString(),
743
+
744
+ // Rates per second
745
+ hitsPerSecond: uptimeSeconds > 0 ? (this.stats.hits / uptimeSeconds).toFixed(2) : 0,
746
+ missesPerSecond: uptimeSeconds > 0 ? (this.stats.misses / uptimeSeconds).toFixed(2) : 0,
747
+ writesPerSecond: uptimeSeconds > 0 ? (this.stats.writes / uptimeSeconds).toFixed(2) : 0
748
+ };
749
+ }
750
+
751
+ /**
752
+ * Reset cache statistics
753
+ */
754
+ resetStats() {
755
+ this.stats = {
756
+ hits: 0,
757
+ misses: 0,
758
+ writes: 0,
759
+ deletes: 0,
760
+ errors: 0,
761
+ startTime: Date.now()
762
+ };
763
+ }
764
+
765
+ /**
766
+ * Format uptime in human-readable format
767
+ * @private
768
+ */
769
+ _formatUptime(seconds) {
770
+ const days = Math.floor(seconds / 86400);
771
+ const hours = Math.floor((seconds % 86400) / 3600);
772
+ const minutes = Math.floor((seconds % 3600) / 60);
773
+ const secs = seconds % 60;
774
+
775
+ const parts = [];
776
+ if (days > 0) parts.push(`${days}d`);
777
+ if (hours > 0) parts.push(`${hours}h`);
778
+ if (minutes > 0) parts.push(`${minutes}m`);
779
+ if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
780
+
781
+ return parts.join(' ');
782
+ }
685
783
  }
686
784
 
687
785
  export default CachePlugin;
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Plugin Dependency Validation System
3
+ *
4
+ * Validates that optional plugin dependencies are installed and meet version requirements.
5
+ * This keeps the core s3db.js package lightweight while ensuring plugins work correctly.
6
+ *
7
+ * @example
8
+ * // In a plugin constructor:
9
+ * await requirePluginDependency('postgresql-replicator');
10
+ */
11
+
12
+ /**
13
+ * Plugin dependency registry
14
+ * Maps plugin identifiers to their required dependencies
15
+ */
16
+ export const PLUGIN_DEPENDENCIES = {
17
+ 'postgresql-replicator': {
18
+ name: 'PostgreSQL Replicator',
19
+ dependencies: {
20
+ 'pg': {
21
+ version: '^8.0.0',
22
+ description: 'PostgreSQL client for Node.js',
23
+ installCommand: 'pnpm add pg'
24
+ }
25
+ }
26
+ },
27
+ 'bigquery-replicator': {
28
+ name: 'BigQuery Replicator',
29
+ dependencies: {
30
+ '@google-cloud/bigquery': {
31
+ version: '^7.0.0',
32
+ description: 'Google Cloud BigQuery SDK',
33
+ installCommand: 'pnpm add @google-cloud/bigquery'
34
+ }
35
+ }
36
+ },
37
+ 'sqs-replicator': {
38
+ name: 'SQS Replicator',
39
+ dependencies: {
40
+ '@aws-sdk/client-sqs': {
41
+ version: '^3.0.0',
42
+ description: 'AWS SDK for SQS',
43
+ installCommand: 'pnpm add @aws-sdk/client-sqs'
44
+ }
45
+ }
46
+ },
47
+ 'sqs-consumer': {
48
+ name: 'SQS Queue Consumer',
49
+ dependencies: {
50
+ '@aws-sdk/client-sqs': {
51
+ version: '^3.0.0',
52
+ description: 'AWS SDK for SQS',
53
+ installCommand: 'pnpm add @aws-sdk/client-sqs'
54
+ }
55
+ }
56
+ },
57
+ 'rabbitmq-consumer': {
58
+ name: 'RabbitMQ Queue Consumer',
59
+ dependencies: {
60
+ 'amqplib': {
61
+ version: '^0.10.0',
62
+ description: 'AMQP 0-9-1 library for RabbitMQ',
63
+ installCommand: 'pnpm add amqplib'
64
+ }
65
+ }
66
+ },
67
+ 'tfstate-plugin': {
68
+ name: 'Terraform State Plugin',
69
+ dependencies: {
70
+ 'node-cron': {
71
+ version: '^4.0.0',
72
+ description: 'Cron job scheduler for auto-sync functionality',
73
+ installCommand: 'pnpm add node-cron'
74
+ }
75
+ }
76
+ },
77
+ 'api-plugin': {
78
+ name: 'API Plugin',
79
+ dependencies: {
80
+ 'hono': {
81
+ version: '^4.0.0',
82
+ description: 'Ultra-light HTTP server framework',
83
+ installCommand: 'pnpm add hono'
84
+ },
85
+ '@hono/node-server': {
86
+ version: '^1.0.0',
87
+ description: 'Node.js adapter for Hono',
88
+ installCommand: 'pnpm add @hono/node-server'
89
+ },
90
+ '@hono/swagger-ui': {
91
+ version: '^0.4.0',
92
+ description: 'Swagger UI integration for Hono',
93
+ installCommand: 'pnpm add @hono/swagger-ui'
94
+ }
95
+ }
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Simple semver comparison for major version checking
101
+ * @param {string} actual - Actual version (e.g., "8.11.3")
102
+ * @param {string} required - Required version range (e.g., "^8.0.0")
103
+ * @returns {boolean} True if version is compatible
104
+ */
105
+ function isVersionCompatible(actual, required) {
106
+ if (!actual || !required) return false;
107
+
108
+ // Remove ^ and ~ prefixes
109
+ const cleanRequired = required.replace(/^[\^~]/, '');
110
+
111
+ // Extract major versions
112
+ const actualMajor = parseInt(actual.split('.')[0], 10);
113
+ const requiredMajor = parseInt(cleanRequired.split('.')[0], 10);
114
+
115
+ // For ^X.Y.Z, accept any version >= X.Y.Z with same major
116
+ if (required.startsWith('^')) {
117
+ return actualMajor === requiredMajor;
118
+ }
119
+
120
+ // For ~X.Y.Z, accept any version >= X.Y.Z with same major.minor
121
+ if (required.startsWith('~')) {
122
+ const actualMinor = parseInt(actual.split('.')[1] || '0', 10);
123
+ const requiredMinor = parseInt(cleanRequired.split('.')[1] || '0', 10);
124
+ return actualMajor === requiredMajor && actualMinor >= requiredMinor;
125
+ }
126
+
127
+ // Exact match for unspecified ranges
128
+ return actualMajor >= requiredMajor;
129
+ }
130
+
131
+ /**
132
+ * Try to load a package and get its version
133
+ * @param {string} packageName - NPM package name
134
+ * @returns {Promise<{installed: boolean, version: string|null, error: Error|null}>}
135
+ */
136
+ async function tryLoadPackage(packageName) {
137
+ try {
138
+ // Try to import the package
139
+ const pkg = await import(packageName);
140
+
141
+ // Try to get version from package.json
142
+ let version = null;
143
+ try {
144
+ const pkgJson = await import(`${packageName}/package.json`, {
145
+ assert: { type: 'json' }
146
+ });
147
+ version = pkgJson.default?.version || pkgJson.version || null;
148
+ } catch (e) {
149
+ // Package.json not accessible, version unknown but package exists
150
+ version = 'unknown';
151
+ }
152
+
153
+ return { installed: true, version, error: null };
154
+ } catch (error) {
155
+ return { installed: false, version: null, error };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Validate that a plugin's dependencies are installed and meet version requirements
161
+ * @param {string} pluginId - Plugin identifier from PLUGIN_DEPENDENCIES
162
+ * @param {Object} options - Validation options
163
+ * @param {boolean} options.throwOnError - Throw error if validation fails (default: true)
164
+ * @param {boolean} options.checkVersions - Check version compatibility (default: true)
165
+ * @returns {Promise<{valid: boolean, missing: string[], incompatible: string[], messages: string[]}>}
166
+ * @throws {Error} If throwOnError=true and validation fails
167
+ */
168
+ export async function requirePluginDependency(pluginId, options = {}) {
169
+ const {
170
+ throwOnError = true,
171
+ checkVersions = true
172
+ } = options;
173
+
174
+ const pluginDef = PLUGIN_DEPENDENCIES[pluginId];
175
+
176
+ if (!pluginDef) {
177
+ const error = new Error(
178
+ `Unknown plugin identifier: ${pluginId}. ` +
179
+ `Available plugins: ${Object.keys(PLUGIN_DEPENDENCIES).join(', ')}`
180
+ );
181
+ if (throwOnError) throw error;
182
+ return { valid: false, missing: [], incompatible: [], messages: [error.message] };
183
+ }
184
+
185
+ const missing = [];
186
+ const incompatible = [];
187
+ const messages = [];
188
+
189
+ // Check each dependency
190
+ for (const [pkgName, pkgInfo] of Object.entries(pluginDef.dependencies)) {
191
+ const { installed, version, error } = await tryLoadPackage(pkgName);
192
+
193
+ if (!installed) {
194
+ missing.push(pkgName);
195
+ messages.push(
196
+ `❌ Missing dependency: ${pkgName}\n` +
197
+ ` Description: ${pkgInfo.description}\n` +
198
+ ` Required: ${pkgInfo.version}\n` +
199
+ ` Install: ${pkgInfo.installCommand}`
200
+ );
201
+ continue;
202
+ }
203
+
204
+ // Check version compatibility if requested
205
+ if (checkVersions && version && version !== 'unknown') {
206
+ const compatible = isVersionCompatible(version, pkgInfo.version);
207
+
208
+ if (!compatible) {
209
+ incompatible.push(pkgName);
210
+ messages.push(
211
+ `⚠️ Incompatible version: ${pkgName}\n` +
212
+ ` Installed: ${version}\n` +
213
+ ` Required: ${pkgInfo.version}\n` +
214
+ ` Update: ${pkgInfo.installCommand}`
215
+ );
216
+ } else {
217
+ messages.push(
218
+ `✅ ${pkgName}@${version} (compatible with ${pkgInfo.version})`
219
+ );
220
+ }
221
+ } else {
222
+ messages.push(
223
+ `✅ ${pkgName}@${version || 'unknown'} (installed)`
224
+ );
225
+ }
226
+ }
227
+
228
+ const valid = missing.length === 0 && incompatible.length === 0;
229
+
230
+ // Throw comprehensive error if validation failed
231
+ if (!valid && throwOnError) {
232
+ const errorMsg = [
233
+ `\n${pluginDef.name} - Missing dependencies detected!\n`,
234
+ `Plugin ID: ${pluginId}`,
235
+ '',
236
+ ...messages,
237
+ '',
238
+ 'Quick fix - Run all install commands:',
239
+ Object.values(pluginDef.dependencies)
240
+ .map(dep => ` ${dep.installCommand}`)
241
+ .join('\n'),
242
+ '',
243
+ 'Or install all peer dependencies at once:',
244
+ ` pnpm add ${Object.keys(pluginDef.dependencies).join(' ')}`
245
+ ].join('\n');
246
+
247
+ throw new Error(errorMsg);
248
+ }
249
+
250
+ return { valid, missing, incompatible, messages };
251
+ }
252
+
253
+ /**
254
+ * Check multiple plugin dependencies at once
255
+ * @param {string[]} pluginIds - Array of plugin identifiers
256
+ * @param {Object} options - Validation options
257
+ * @returns {Promise<Map<string, {valid: boolean, missing: string[], incompatible: string[], messages: string[]}>>}
258
+ */
259
+ export async function checkPluginDependencies(pluginIds, options = {}) {
260
+ const results = new Map();
261
+
262
+ for (const pluginId of pluginIds) {
263
+ const result = await requirePluginDependency(pluginId, {
264
+ ...options,
265
+ throwOnError: false
266
+ });
267
+ results.set(pluginId, result);
268
+ }
269
+
270
+ return results;
271
+ }
272
+
273
+ /**
274
+ * Get a report of all plugin dependencies and their status
275
+ * @returns {Promise<string>} Formatted report
276
+ */
277
+ export async function getPluginDependencyReport() {
278
+ const pluginIds = Object.keys(PLUGIN_DEPENDENCIES);
279
+ const results = await checkPluginDependencies(pluginIds);
280
+
281
+ const lines = [
282
+ '╔═══════════════════════════════════════════════════════════════╗',
283
+ '║ S3DB.JS - Plugin Dependency Status Report ║',
284
+ '╚═══════════════════════════════════════════════════════════════╝',
285
+ ''
286
+ ];
287
+
288
+ for (const [pluginId, result] of results.entries()) {
289
+ const pluginDef = PLUGIN_DEPENDENCIES[pluginId];
290
+ const status = result.valid ? '✅ READY' : '❌ MISSING';
291
+
292
+ lines.push(`${status} - ${pluginDef.name}`);
293
+
294
+ if (result.messages.length > 0) {
295
+ result.messages.forEach(msg => {
296
+ lines.push(` ${msg.replace(/\n/g, '\n ')}`);
297
+ });
298
+ }
299
+
300
+ lines.push('');
301
+ }
302
+
303
+ const totalPlugins = pluginIds.length;
304
+ const readyPlugins = Array.from(results.values()).filter(r => r.valid).length;
305
+
306
+ lines.push('─────────────────────────────────────────────────────────────────');
307
+ lines.push(`Summary: ${readyPlugins}/${totalPlugins} plugins ready to use`);
308
+ lines.push('─────────────────────────────────────────────────────────────────');
309
+
310
+ return lines.join('\n');
311
+ }
312
+
313
+ export default requirePluginDependency;