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.
- package/README.md +102 -8
- package/dist/s3db.cjs.js +36945 -15510
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +66 -1
- package/dist/s3db.es.js +36914 -15534
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +35 -15
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +79 -49
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +97 -47
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +544 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +354 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicator.plugin.js +2 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +315 -36
- package/src/s3db.d.ts +66 -1
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- 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 {
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
body
|
|
133
|
-
|
|
134
|
-
return this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
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)
|
|
337
|
-
|
|
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)
|
|
355
|
-
|
|
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;
|