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
@@ -3,16 +3,27 @@ export * from './plugin.obj.js'
3
3
  export { default as Plugin } from './plugin.class.js'
4
4
 
5
5
  // plugins:
6
+ export * from './api/index.js'
6
7
  export * from './audit.plugin.js'
7
8
  export * from './backup.plugin.js'
8
9
  export * from './cache.plugin.js'
9
10
  export * from './costs.plugin.js'
10
11
  export * from './eventual-consistency/index.js'
11
12
  export * from './fulltext.plugin.js'
13
+ export * from './geo.plugin.js'
12
14
  export * from './metrics.plugin.js'
13
15
  export * from './queue-consumer.plugin.js'
16
+ export * from './relation.plugin.js'
14
17
  export * from './replicator.plugin.js'
15
18
  export * from './s3-queue.plugin.js'
16
19
  export * from './scheduler.plugin.js'
17
20
  export * from './state-machine.plugin.js'
21
+ export * from './tfstate/index.js'
22
+ export * from './ttl.plugin.js'
18
23
  export * from './vector.plugin.js'
24
+
25
+ // plugin drivers & utilities:
26
+ export * from './backup/index.js'
27
+ export * from './cache/index.js'
28
+ export * from './replicators/index.js'
29
+ export * from './consumers/index.js'
@@ -10,9 +10,19 @@ export class MetricsPlugin extends Plugin {
10
10
  collectUsage: options.collectUsage !== false,
11
11
  retentionDays: options.retentionDays || 30,
12
12
  flushInterval: options.flushInterval || 60000, // 1 minute
13
+
14
+ // Prometheus configuration
15
+ prometheus: {
16
+ enabled: options.prometheus?.enabled !== false, // Enabled by default
17
+ mode: options.prometheus?.mode || 'auto', // 'auto' | 'integrated' | 'standalone'
18
+ port: options.prometheus?.port || 9090, // Standalone server port
19
+ path: options.prometheus?.path || '/metrics', // Metrics endpoint path
20
+ includeResourceLabels: options.prometheus?.includeResourceLabels !== false
21
+ },
22
+
13
23
  ...options
14
24
  };
15
-
25
+
16
26
  this.metrics = {
17
27
  operations: {
18
28
  insert: { count: 0, totalTime: 0, errors: 0 },
@@ -27,8 +37,9 @@ export class MetricsPlugin extends Plugin {
27
37
  performance: [],
28
38
  startTime: new Date().toISOString()
29
39
  };
30
-
40
+
31
41
  this.flushTimer = null;
42
+ this.metricsServer = null; // Standalone HTTP server (if needed)
32
43
  }
33
44
 
34
45
  async onInstall() {
@@ -113,7 +124,8 @@ export class MetricsPlugin extends Plugin {
113
124
  }
114
125
 
115
126
  async start() {
116
- // Plugin is ready
127
+ // Setup Prometheus metrics exporter
128
+ await this._setupPrometheusExporter();
117
129
  }
118
130
 
119
131
  async stop() {
@@ -122,7 +134,18 @@ export class MetricsPlugin extends Plugin {
122
134
  clearInterval(this.flushTimer);
123
135
  this.flushTimer = null;
124
136
  }
125
-
137
+
138
+ // Stop standalone metrics server if running
139
+ if (this.metricsServer) {
140
+ await new Promise((resolve) => {
141
+ this.metricsServer.close(() => {
142
+ console.log('[Metrics Plugin] Standalone metrics server stopped');
143
+ this.metricsServer = null;
144
+ resolve();
145
+ });
146
+ });
147
+ }
148
+
126
149
  // Remove database hooks
127
150
  this.removeDatabaseHooks();
128
151
  }
@@ -672,6 +695,142 @@ export class MetricsPlugin extends Plugin {
672
695
  }
673
696
  }
674
697
  }
698
+
699
+ /**
700
+ * Get metrics in Prometheus format
701
+ * @returns {Promise<string>} Prometheus metrics text
702
+ */
703
+ async getPrometheusMetrics() {
704
+ const { formatPrometheusMetrics } = await import('./concerns/prometheus-formatter.js');
705
+ return formatPrometheusMetrics(this);
706
+ }
707
+
708
+ /**
709
+ * Setup Prometheus metrics exporter
710
+ * Chooses mode based on configuration and API Plugin availability
711
+ * @private
712
+ */
713
+ async _setupPrometheusExporter() {
714
+ if (!this.config.prometheus.enabled) {
715
+ return; // Prometheus export disabled
716
+ }
717
+
718
+ const mode = this.config.prometheus.mode;
719
+ const apiPlugin = this.database.plugins?.api || this.database.plugins?.ApiPlugin;
720
+
721
+ // AUTO mode: detect API Plugin
722
+ if (mode === 'auto') {
723
+ if (apiPlugin && apiPlugin.server) {
724
+ await this._setupIntegratedMetrics(apiPlugin);
725
+ } else {
726
+ await this._setupStandaloneMetrics();
727
+ }
728
+ }
729
+
730
+ // INTEGRATED mode: requires API Plugin
731
+ else if (mode === 'integrated') {
732
+ if (!apiPlugin || !apiPlugin.server) {
733
+ throw new Error(
734
+ '[Metrics Plugin] prometheus.mode=integrated requires API Plugin to be active'
735
+ );
736
+ }
737
+ await this._setupIntegratedMetrics(apiPlugin);
738
+ }
739
+
740
+ // STANDALONE mode: always separate server
741
+ else if (mode === 'standalone') {
742
+ await this._setupStandaloneMetrics();
743
+ }
744
+
745
+ else {
746
+ console.warn(
747
+ `[Metrics Plugin] Unknown prometheus.mode="${mode}". Valid modes: auto, integrated, standalone`
748
+ );
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Setup integrated metrics (uses API Plugin's server)
754
+ * @param {ApiPlugin} apiPlugin - API Plugin instance
755
+ * @private
756
+ */
757
+ async _setupIntegratedMetrics(apiPlugin) {
758
+ const app = apiPlugin.getApp();
759
+ const path = this.config.prometheus.path;
760
+
761
+ if (!app) {
762
+ console.error('[Metrics Plugin] Failed to get Hono app from API Plugin');
763
+ return;
764
+ }
765
+
766
+ // Add /metrics route to Hono app
767
+ app.get(path, async (c) => {
768
+ try {
769
+ const metrics = await this.getPrometheusMetrics();
770
+ return c.text(metrics, 200, {
771
+ 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8'
772
+ });
773
+ } catch (err) {
774
+ console.error('[Metrics Plugin] Error generating Prometheus metrics:', err);
775
+ return c.text('Internal Server Error', 500);
776
+ }
777
+ });
778
+
779
+ const port = apiPlugin.config?.port || 3000;
780
+ console.log(
781
+ `[Metrics Plugin] Prometheus metrics available at http://localhost:${port}${path} (integrated mode)`
782
+ );
783
+ }
784
+
785
+ /**
786
+ * Setup standalone metrics server (separate HTTP server)
787
+ * @private
788
+ */
789
+ async _setupStandaloneMetrics() {
790
+ const { createServer } = await import('http');
791
+ const port = this.config.prometheus.port;
792
+ const path = this.config.prometheus.path;
793
+
794
+ this.metricsServer = createServer(async (req, res) => {
795
+ // CORS headers to allow scraping from anywhere
796
+ res.setHeader('Access-Control-Allow-Origin', '*');
797
+ res.setHeader('Access-Control-Allow-Methods', 'GET');
798
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
799
+
800
+ if (req.url === path && req.method === 'GET') {
801
+ try {
802
+ const metrics = await this.getPrometheusMetrics();
803
+ res.writeHead(200, {
804
+ 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
805
+ 'Content-Length': Buffer.byteLength(metrics, 'utf8')
806
+ });
807
+ res.end(metrics);
808
+ } catch (err) {
809
+ console.error('[Metrics Plugin] Error generating Prometheus metrics:', err);
810
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
811
+ res.end('Internal Server Error');
812
+ }
813
+ } else if (req.method === 'OPTIONS') {
814
+ // Handle preflight requests
815
+ res.writeHead(204);
816
+ res.end();
817
+ } else {
818
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
819
+ res.end('Not Found');
820
+ }
821
+ });
822
+
823
+ this.metricsServer.listen(port, '0.0.0.0', () => {
824
+ console.log(
825
+ `[Metrics Plugin] Prometheus metrics available at http://0.0.0.0:${port}${path} (standalone mode)`
826
+ );
827
+ });
828
+
829
+ // Handle server errors
830
+ this.metricsServer.on('error', (err) => {
831
+ console.error('[Metrics Plugin] Standalone metrics server error:', err);
832
+ });
833
+ }
675
834
  }
676
835
 
677
836
  export default MetricsPlugin;
@@ -35,42 +35,21 @@ export class QueueConsumerPlugin extends Plugin {
35
35
 
36
36
  for (const driverDef of this.driversConfig) {
37
37
  const { driver, config: driverConfig = {}, consumers: consumerDefs = [] } = driverDef;
38
-
39
- // Handle legacy format where config is mixed with driver definition
40
- if (consumerDefs.length === 0 && driverDef.resources) {
41
- // Legacy format: { driver: 'sqs', resources: 'users', config: {...} }
42
- const { resources, driver: defDriver, config: nestedConfig, ...directConfig } = driverDef;
38
+
39
+ // Structured format: { driver: 'sqs', config: {...}, consumers: [{ resources: 'users', ... }] }
40
+ for (const consumerDef of consumerDefs) {
41
+ const { resources, ...consumerConfig } = consumerDef;
43
42
  const resourceList = Array.isArray(resources) ? resources : [resources];
44
-
45
- // Flatten config - prioritize nested config if it exists, otherwise use direct config
46
- const flatConfig = nestedConfig ? { ...directConfig, ...nestedConfig } : directConfig;
47
-
48
43
  for (const resource of resourceList) {
44
+ const mergedConfig = { ...driverConfig, ...consumerConfig };
49
45
  const consumer = createConsumer(driver, {
50
- ...flatConfig,
46
+ ...mergedConfig,
51
47
  onMessage: (msg) => this._handleMessage(msg, resource),
52
48
  onError: (err, raw) => this._handleError(err, raw, resource)
53
49
  });
54
-
55
50
  await consumer.start();
56
51
  this.consumers.push(consumer);
57
52
  }
58
- } else {
59
- // New format: { driver: 'sqs', config: {...}, consumers: [{ resources: 'users', ... }] }
60
- for (const consumerDef of consumerDefs) {
61
- const { resources, ...consumerConfig } = consumerDef;
62
- const resourceList = Array.isArray(resources) ? resources : [resources];
63
- for (const resource of resourceList) {
64
- const mergedConfig = { ...driverConfig, ...consumerConfig };
65
- const consumer = createConsumer(driver, {
66
- ...mergedConfig,
67
- onMessage: (msg) => this._handleMessage(msg, resource),
68
- onError: (err, raw) => this._handleError(err, raw, resource)
69
- });
70
- await consumer.start();
71
- this.consumers.push(consumer);
72
- }
73
- }
74
53
  }
75
54
  }
76
55
  }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * RelationPlugin Error Classes
3
+ * Custom errors for relation operations
4
+ */
5
+
6
+ /**
7
+ * Base error for all relation operations
8
+ */
9
+ export class RelationError extends Error {
10
+ constructor(message, context = {}) {
11
+ super(message);
12
+ this.name = 'RelationError';
13
+ this.context = context;
14
+ Error.captureStackTrace(this, this.constructor);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Thrown when relation configuration is invalid
20
+ */
21
+ export class RelationConfigError extends RelationError {
22
+ constructor(message, context = {}) {
23
+ super(message, context);
24
+ this.name = 'RelationConfigError';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Thrown when a relation type is not supported
30
+ */
31
+ export class UnsupportedRelationTypeError extends RelationError {
32
+ constructor(type, context = {}) {
33
+ super(`Unsupported relation type: ${type}. Supported types: hasOne, hasMany, belongsTo, belongsToMany`, context);
34
+ this.name = 'UnsupportedRelationTypeError';
35
+ this.relationType = type;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Thrown when a related resource is not found
41
+ */
42
+ export class RelatedResourceNotFoundError extends RelationError {
43
+ constructor(resourceName, context = {}) {
44
+ super(`Related resource "${resourceName}" not found`, context);
45
+ this.name = 'RelatedResourceNotFoundError';
46
+ this.resourceName = resourceName;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Thrown when a junction table is missing for belongsToMany
52
+ */
53
+ export class JunctionTableNotFoundError extends RelationError {
54
+ constructor(junctionTable, context = {}) {
55
+ super(`Junction table "${junctionTable}" not found for belongsToMany relation`, context);
56
+ this.name = 'JunctionTableNotFoundError';
57
+ this.junctionTable = junctionTable;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Thrown when cascade operation fails
63
+ */
64
+ export class CascadeError extends RelationError {
65
+ constructor(operation, resourceName, recordId, originalError, context = {}) {
66
+ super(
67
+ `Cascade ${operation} failed for resource "${resourceName}" record "${recordId}": ${originalError.message}`,
68
+ context
69
+ );
70
+ this.name = 'CascadeError';
71
+ this.operation = operation;
72
+ this.resourceName = resourceName;
73
+ this.recordId = recordId;
74
+ this.originalError = originalError;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Thrown when foreign key is missing
80
+ */
81
+ export class MissingForeignKeyError extends RelationError {
82
+ constructor(foreignKey, resourceName, context = {}) {
83
+ super(`Foreign key "${foreignKey}" not found in resource "${resourceName}"`, context);
84
+ this.name = 'MissingForeignKeyError';
85
+ this.foreignKey = foreignKey;
86
+ this.resourceName = resourceName;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Thrown when trying to load relations on non-existent record
92
+ */
93
+ export class RecordNotFoundError extends RelationError {
94
+ constructor(recordId, resourceName, context = {}) {
95
+ super(`Record "${recordId}" not found in resource "${resourceName}"`, context);
96
+ this.name = 'RecordNotFoundError';
97
+ this.recordId = recordId;
98
+ this.resourceName = resourceName;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Thrown when circular relation is detected
104
+ */
105
+ export class CircularRelationError extends RelationError {
106
+ constructor(path, context = {}) {
107
+ super(`Circular relation detected in path: ${path.join(' -> ')}`, context);
108
+ this.name = 'CircularRelationError';
109
+ this.relationPath = path;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Thrown when include path is invalid
115
+ */
116
+ export class InvalidIncludePathError extends RelationError {
117
+ constructor(path, reason, context = {}) {
118
+ super(`Invalid include path "${path}": ${reason}`, context);
119
+ this.name = 'InvalidIncludePathError';
120
+ this.includePath = path;
121
+ this.reason = reason;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Thrown when batch loading fails
127
+ */
128
+ export class BatchLoadError extends RelationError {
129
+ constructor(relation, batchSize, failedCount, context = {}) {
130
+ super(
131
+ `Batch loading failed for relation "${relation}". Failed ${failedCount} out of ${batchSize} records`,
132
+ context
133
+ );
134
+ this.name = 'BatchLoadError';
135
+ this.relation = relation;
136
+ this.batchSize = batchSize;
137
+ this.failedCount = failedCount;
138
+ }
139
+ }