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
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Prometheus Formatter - Format s3db.js metrics to Prometheus text-based format
3
+ *
4
+ * Generates metrics in Prometheus exposition format:
5
+ * https://prometheus.io/docs/instrumenting/exposition_formats/
6
+ */
7
+
8
+ /**
9
+ * Sanitize label value for Prometheus
10
+ * - Replace invalid characters with underscores
11
+ * - Escape special characters
12
+ * @param {string} value - Label value
13
+ * @returns {string} Sanitized value
14
+ */
15
+ function sanitizeLabel(value) {
16
+ if (typeof value !== 'string') {
17
+ value = String(value);
18
+ }
19
+
20
+ // Escape backslashes and quotes
21
+ return value
22
+ .replace(/\\/g, '\\\\')
23
+ .replace(/"/g, '\\"')
24
+ .replace(/\n/g, '\\n');
25
+ }
26
+
27
+ /**
28
+ * Sanitize metric name for Prometheus
29
+ * - Only alphanumeric and underscores allowed
30
+ * - Must not start with digit
31
+ * @param {string} name - Metric name
32
+ * @returns {string} Sanitized name
33
+ */
34
+ function sanitizeMetricName(name) {
35
+ // Replace invalid characters with underscores
36
+ let sanitized = name.replace(/[^a-zA-Z0-9_]/g, '_');
37
+
38
+ // Ensure doesn't start with digit
39
+ if (/^\d/.test(sanitized)) {
40
+ sanitized = '_' + sanitized;
41
+ }
42
+
43
+ return sanitized;
44
+ }
45
+
46
+ /**
47
+ * Format labels for Prometheus metric line
48
+ * @param {Object} labels - Label key-value pairs
49
+ * @returns {string} Formatted labels string
50
+ */
51
+ function formatLabels(labels) {
52
+ if (!labels || Object.keys(labels).length === 0) {
53
+ return '';
54
+ }
55
+
56
+ const labelPairs = Object.entries(labels)
57
+ .map(([key, value]) => `${key}="${sanitizeLabel(value)}"`)
58
+ .join(',');
59
+
60
+ return `{${labelPairs}}`;
61
+ }
62
+
63
+ /**
64
+ * Format a single Prometheus metric
65
+ * @param {string} name - Metric name
66
+ * @param {string} type - Metric type (counter, gauge, histogram, summary)
67
+ * @param {string} help - Help text
68
+ * @param {Array<{labels: Object, value: number}>} values - Metric values with labels
69
+ * @returns {string} Formatted metric lines
70
+ */
71
+ function formatMetric(name, type, help, values) {
72
+ const lines = [];
73
+
74
+ // HELP line
75
+ lines.push(`# HELP ${name} ${help}`);
76
+
77
+ // TYPE line
78
+ lines.push(`# TYPE ${name} ${type}`);
79
+
80
+ // Value lines
81
+ for (const { labels, value } of values) {
82
+ const labelsStr = formatLabels(labels);
83
+ lines.push(`${name}${labelsStr} ${value}`);
84
+ }
85
+
86
+ return lines.join('\n');
87
+ }
88
+
89
+ /**
90
+ * Format all metrics from MetricsPlugin to Prometheus format
91
+ * @param {MetricsPlugin} metricsPlugin - Instance of MetricsPlugin
92
+ * @returns {string} Complete Prometheus metrics text
93
+ */
94
+ export function formatPrometheusMetrics(metricsPlugin) {
95
+ const lines = [];
96
+ const metrics = metricsPlugin.metrics;
97
+
98
+ // 1. Operations Total (counter)
99
+ const operationsTotalValues = [];
100
+
101
+ // Global operations
102
+ for (const [operation, data] of Object.entries(metrics.operations)) {
103
+ if (data.count > 0) {
104
+ operationsTotalValues.push({
105
+ labels: { operation, resource: '_global' },
106
+ value: data.count
107
+ });
108
+ }
109
+ }
110
+
111
+ // Resource-specific operations
112
+ for (const [resourceName, operations] of Object.entries(metrics.resources)) {
113
+ for (const [operation, data] of Object.entries(operations)) {
114
+ if (data.count > 0) {
115
+ operationsTotalValues.push({
116
+ labels: { operation, resource: sanitizeMetricName(resourceName) },
117
+ value: data.count
118
+ });
119
+ }
120
+ }
121
+ }
122
+
123
+ if (operationsTotalValues.length > 0) {
124
+ lines.push(formatMetric(
125
+ 's3db_operations_total',
126
+ 'counter',
127
+ 'Total number of operations by type and resource',
128
+ operationsTotalValues
129
+ ));
130
+ lines.push('');
131
+ }
132
+
133
+ // 2. Operation Duration (gauge - average)
134
+ const durationValues = [];
135
+
136
+ // Global operations
137
+ for (const [operation, data] of Object.entries(metrics.operations)) {
138
+ if (data.count > 0) {
139
+ const avgSeconds = (data.totalTime / data.count) / 1000; // Convert ms to seconds
140
+ durationValues.push({
141
+ labels: { operation, resource: '_global' },
142
+ value: avgSeconds.toFixed(6)
143
+ });
144
+ }
145
+ }
146
+
147
+ // Resource-specific operations
148
+ for (const [resourceName, operations] of Object.entries(metrics.resources)) {
149
+ for (const [operation, data] of Object.entries(operations)) {
150
+ if (data.count > 0) {
151
+ const avgSeconds = (data.totalTime / data.count) / 1000; // Convert ms to seconds
152
+ durationValues.push({
153
+ labels: { operation, resource: sanitizeMetricName(resourceName) },
154
+ value: avgSeconds.toFixed(6)
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ if (durationValues.length > 0) {
161
+ lines.push(formatMetric(
162
+ 's3db_operation_duration_seconds',
163
+ 'gauge',
164
+ 'Average operation duration in seconds',
165
+ durationValues
166
+ ));
167
+ lines.push('');
168
+ }
169
+
170
+ // 3. Operation Errors Total (counter)
171
+ const errorsValues = [];
172
+
173
+ // Global errors
174
+ for (const [operation, data] of Object.entries(metrics.operations)) {
175
+ if (data.errors > 0) {
176
+ errorsValues.push({
177
+ labels: { operation, resource: '_global' },
178
+ value: data.errors
179
+ });
180
+ }
181
+ }
182
+
183
+ // Resource-specific errors
184
+ for (const [resourceName, operations] of Object.entries(metrics.resources)) {
185
+ for (const [operation, data] of Object.entries(operations)) {
186
+ if (data.errors > 0) {
187
+ errorsValues.push({
188
+ labels: { operation, resource: sanitizeMetricName(resourceName) },
189
+ value: data.errors
190
+ });
191
+ }
192
+ }
193
+ }
194
+
195
+ if (errorsValues.length > 0) {
196
+ lines.push(formatMetric(
197
+ 's3db_operation_errors_total',
198
+ 'counter',
199
+ 'Total number of operation errors',
200
+ errorsValues
201
+ ));
202
+ lines.push('');
203
+ }
204
+
205
+ // 4. Uptime (gauge)
206
+ const startTime = new Date(metrics.startTime);
207
+ const uptimeSeconds = (Date.now() - startTime.getTime()) / 1000;
208
+
209
+ lines.push(formatMetric(
210
+ 's3db_uptime_seconds',
211
+ 'gauge',
212
+ 'Process uptime in seconds',
213
+ [{ labels: {}, value: uptimeSeconds.toFixed(2) }]
214
+ ));
215
+ lines.push('');
216
+
217
+ // 5. Resources Total (gauge)
218
+ const resourcesCount = Object.keys(metrics.resources).length;
219
+
220
+ lines.push(formatMetric(
221
+ 's3db_resources_total',
222
+ 'gauge',
223
+ 'Total number of tracked resources',
224
+ [{ labels: {}, value: resourcesCount }]
225
+ ));
226
+ lines.push('');
227
+
228
+ // 6. Build Info (gauge - always 1)
229
+ const nodeVersion = process.version || 'unknown';
230
+ const s3dbVersion = '1.0.0'; // TODO: Get from package.json
231
+
232
+ lines.push(formatMetric(
233
+ 's3db_info',
234
+ 'gauge',
235
+ 'Build and runtime information',
236
+ [{
237
+ labels: {
238
+ version: s3dbVersion,
239
+ node_version: nodeVersion
240
+ },
241
+ value: 1
242
+ }]
243
+ ));
244
+
245
+ // Join all lines with newline and ensure ends with newline
246
+ return lines.join('\n') + '\n';
247
+ }
248
+
249
+ export default {
250
+ formatPrometheusMetrics,
251
+ formatMetric,
252
+ sanitizeLabel,
253
+ sanitizeMetricName,
254
+ formatLabels
255
+ };
@@ -1,4 +1,5 @@
1
1
  import tryFn from "../../concerns/try-fn.js";
2
+ import requirePluginDependency from "../concerns/plugin-dependencies.js";
2
3
 
3
4
  export class RabbitMqConsumer {
4
5
  constructor({ amqpUrl, queue, prefetch = 10, reconnectInterval = 2000, onMessage, onError, driver = 'rabbitmq' }) {
@@ -15,6 +16,9 @@ export class RabbitMqConsumer {
15
16
  }
16
17
 
17
18
  async start() {
19
+ // Validate plugin dependencies are installed
20
+ await requirePluginDependency('rabbitmq-consumer');
21
+
18
22
  this._stopped = false;
19
23
  await this._connect();
20
24
  }
@@ -1,4 +1,5 @@
1
1
  import tryFn from "../../concerns/try-fn.js";
2
+ import requirePluginDependency from "../concerns/plugin-dependencies.js";
2
3
  // Remove static SDK import
3
4
  // import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
4
5
 
@@ -25,6 +26,9 @@ export class SqsConsumer {
25
26
  }
26
27
 
27
28
  async start() {
29
+ // Validate plugin dependencies are installed
30
+ await requirePluginDependency('sqs-consumer');
31
+
28
32
  // Carregar SDK dinamicamente
29
33
  const [ok, err, sdk] = await tryFn(() => import('@aws-sdk/client-sqs'));
30
34
  if (!ok) throw new Error('SqsConsumer: @aws-sdk/client-sqs is not installed. Please install it to use the SQS consumer.');
@@ -1,14 +1,20 @@
1
1
  export const CostsPlugin = {
2
- async setup (db) {
2
+ async setup (db, options = {}) {
3
3
  if (!db || !db.client) {
4
4
  return; // Handle null/invalid database gracefully
5
5
  }
6
6
 
7
7
  this.client = db.client
8
+ this.options = {
9
+ considerFreeTier: false, // Flag to consider AWS free tier in calculations
10
+ region: 'us-east-1', // AWS region for pricing (future use)
11
+ ...options
12
+ }
8
13
 
9
14
  this.map = {
10
15
  PutObjectCommand: 'put',
11
16
  GetObjectCommand: 'get',
17
+ CopyObjectCommand: 'copy',
12
18
  HeadObjectCommand: 'head',
13
19
  DeleteObjectCommand: 'delete',
14
20
  DeleteObjectsCommand: 'delete',
@@ -17,35 +23,79 @@ export const CostsPlugin = {
17
23
 
18
24
  this.costs = {
19
25
  total: 0,
20
- prices: {
21
- put: 0.005 / 1000,
22
- copy: 0.005 / 1000,
23
- list: 0.005 / 1000,
24
- post: 0.005 / 1000,
25
- get: 0.0004 / 1000,
26
- select: 0.0004 / 1000,
27
- delete: 0.0004 / 1000,
28
- head: 0.0004 / 1000,
29
- },
26
+
27
+ // === REQUESTS PRICING ===
30
28
  requests: {
29
+ prices: {
30
+ put: 0.005 / 1000,
31
+ copy: 0.005 / 1000,
32
+ list: 0.005 / 1000,
33
+ post: 0.005 / 1000,
34
+ get: 0.0004 / 1000,
35
+ select: 0.0004 / 1000,
36
+ delete: 0.0004 / 1000,
37
+ head: 0.0004 / 1000,
38
+ },
31
39
  total: 0,
32
- put: 0,
33
- post: 0,
34
- copy: 0,
35
- list: 0,
36
- get: 0,
37
- select: 0,
38
- delete: 0,
39
- head: 0,
40
+ counts: {
41
+ put: 0,
42
+ post: 0,
43
+ copy: 0,
44
+ list: 0,
45
+ get: 0,
46
+ select: 0,
47
+ delete: 0,
48
+ head: 0,
49
+ },
50
+ totalEvents: 0,
51
+ events: {
52
+ PutObjectCommand: 0,
53
+ GetObjectCommand: 0,
54
+ CopyObjectCommand: 0,
55
+ HeadObjectCommand: 0,
56
+ DeleteObjectCommand: 0,
57
+ DeleteObjectsCommand: 0,
58
+ ListObjectsV2Command: 0,
59
+ },
60
+ subtotal: 0,
40
61
  },
41
- events: {
42
- total: 0,
43
- PutObjectCommand: 0,
44
- GetObjectCommand: 0,
45
- HeadObjectCommand: 0,
46
- DeleteObjectCommand: 0,
47
- DeleteObjectsCommand: 0,
48
- ListObjectsV2Command: 0,
62
+
63
+ // === STORAGE PRICING ===
64
+ storage: {
65
+ totalBytes: 0,
66
+ totalGB: 0,
67
+ // Tiered pricing (S3 Standard - us-east-1)
68
+ tiers: [
69
+ { limit: 50 * 1024, pricePerGB: 0.023 }, // First 50 TB
70
+ { limit: 500 * 1024, pricePerGB: 0.022 }, // Next 450 TB
71
+ { limit: 999999999, pricePerGB: 0.021 } // Over 500 TB (effectively unlimited)
72
+ ],
73
+ currentTier: 0,
74
+ subtotal: 0 // Monthly storage cost estimate
75
+ },
76
+
77
+ // === DATA TRANSFER PRICING ===
78
+ dataTransfer: {
79
+ // Upload (always free)
80
+ inBytes: 0,
81
+ inGB: 0,
82
+ inCost: 0, // Always $0
83
+
84
+ // Download (charged with tiers)
85
+ outBytes: 0,
86
+ outGB: 0,
87
+ // Tiered pricing (out to internet)
88
+ tiers: [
89
+ { limit: 10 * 1024, pricePerGB: 0.09 }, // First 10 TB
90
+ { limit: 50 * 1024, pricePerGB: 0.085 }, // Next 40 TB
91
+ { limit: 150 * 1024, pricePerGB: 0.07 }, // Next 100 TB
92
+ { limit: 999999999, pricePerGB: 0.05 } // Over 150 TB (effectively unlimited)
93
+ ],
94
+ // Free tier (100GB/month aggregated across AWS)
95
+ freeTierGB: 100,
96
+ freeTierUsed: 0,
97
+ currentTier: 0,
98
+ subtotal: 0 // Data transfer out cost
49
99
  }
50
100
  }
51
101
 
@@ -54,26 +104,192 @@ export const CostsPlugin = {
54
104
 
55
105
  async start () {
56
106
  if (this.client) {
57
- this.client.on("command.response", (name) => this.addRequest(name, this.map[name]));
58
- this.client.on("command.error", (name) => this.addRequest(name, this.map[name]));
107
+ this.client.on("command.response", (name, response, input) => this.addRequest(name, this.map[name], response, input));
108
+ this.client.on("command.error", (name, response, input) => this.addRequest(name, this.map[name], response, input));
59
109
  }
60
110
  },
61
111
 
62
- addRequest (name, method) {
112
+ addRequest (name, method, response = {}, input = {}) {
63
113
  if (!method) return; // Skip if no mapping found
64
-
65
- this.costs.events[name]++;
66
- this.costs.events.total++;
114
+
115
+ // Track request counts
116
+ this.costs.requests.totalEvents++;
67
117
  this.costs.requests.total++;
68
- this.costs.requests[method]++;
69
- this.costs.total += this.costs.prices[method];
118
+ this.costs.requests.events[name]++;
119
+ this.costs.requests.counts[method]++;
120
+
121
+ // Calculate request cost
122
+ const requestCost = this.costs.requests.prices[method];
123
+ this.costs.requests.subtotal += requestCost;
124
+
125
+ // Track storage and data transfer based on ContentLength
126
+ let contentLength = 0;
127
+
128
+ if (['put', 'post', 'copy'].includes(method)) {
129
+ // For uploads, get size from input Body (AWS SDK uses capital B)
130
+ const body = input.Body || input.body;
131
+ if (body) {
132
+ if (typeof body === 'string') {
133
+ contentLength = Buffer.byteLength(body, 'utf8');
134
+ } else if (Buffer.isBuffer(body)) {
135
+ contentLength = body.length;
136
+ } else if (body.length !== undefined) {
137
+ contentLength = body.length;
138
+ }
139
+ }
140
+
141
+ if (contentLength > 0) {
142
+ this.trackStorage(contentLength);
143
+ this.trackDataTransferIn(contentLength);
144
+ }
145
+ }
146
+
147
+ if (method === 'get') {
148
+ // For downloads, get size from response
149
+ contentLength = response?.httpResponse?.headers?.['content-length'] ||
150
+ response?.ContentLength ||
151
+ 0;
70
152
 
153
+ if (contentLength > 0) {
154
+ this.trackDataTransferOut(contentLength);
155
+ }
156
+ }
157
+
158
+ // Mirror request-related counters to client.costs BEFORE updateTotal()
159
+ // (Storage and data transfer are mirrored in tracking methods)
71
160
  if (this.client && this.client.costs) {
72
- this.client.costs.events[name]++;
73
- this.client.costs.events.total++;
161
+ this.client.costs.requests.totalEvents++;
74
162
  this.client.costs.requests.total++;
75
- this.client.costs.requests[method]++;
76
- this.client.costs.total += this.client.costs.prices[method];
163
+ this.client.costs.requests.events[name]++;
164
+ this.client.costs.requests.counts[method]++;
165
+ this.client.costs.requests.subtotal += requestCost;
166
+ }
167
+
168
+ // Update total cost (must be after mirroring request counters)
169
+ this.updateTotal();
170
+ },
171
+
172
+ trackStorage (bytes) {
173
+ this.costs.storage.totalBytes += bytes;
174
+ this.costs.storage.totalGB = this.costs.storage.totalBytes / (1024 * 1024 * 1024);
175
+ this.costs.storage.subtotal = this.calculateStorageCost(this.costs.storage);
176
+
177
+ // Mirror to client.costs
178
+ if (this.client && this.client.costs) {
179
+ this.client.costs.storage.totalBytes += bytes;
180
+ this.client.costs.storage.totalGB = this.client.costs.storage.totalBytes / (1024 * 1024 * 1024);
181
+ this.client.costs.storage.subtotal = this.calculateStorageCost(this.client.costs.storage);
182
+ }
183
+
184
+ // Update total cost
185
+ this.updateTotal();
186
+ },
187
+
188
+ trackDataTransferIn (bytes) {
189
+ this.costs.dataTransfer.inBytes += bytes;
190
+ this.costs.dataTransfer.inGB = this.costs.dataTransfer.inBytes / (1024 * 1024 * 1024);
191
+ // inCost is always $0
192
+
193
+ // Mirror to client.costs
194
+ if (this.client && this.client.costs) {
195
+ this.client.costs.dataTransfer.inBytes += bytes;
196
+ this.client.costs.dataTransfer.inGB = this.client.costs.dataTransfer.inBytes / (1024 * 1024 * 1024);
197
+ }
198
+
199
+ // Update total cost
200
+ this.updateTotal();
201
+ },
202
+
203
+ trackDataTransferOut (bytes) {
204
+ this.costs.dataTransfer.outBytes += bytes;
205
+ this.costs.dataTransfer.outGB = this.costs.dataTransfer.outBytes / (1024 * 1024 * 1024);
206
+ this.costs.dataTransfer.subtotal = this.calculateDataTransferCost(this.costs.dataTransfer);
207
+
208
+ // Mirror to client.costs
209
+ if (this.client && this.client.costs) {
210
+ this.client.costs.dataTransfer.outBytes += bytes;
211
+ this.client.costs.dataTransfer.outGB = this.client.costs.dataTransfer.outBytes / (1024 * 1024 * 1024);
212
+ this.client.costs.dataTransfer.subtotal = this.calculateDataTransferCost(this.client.costs.dataTransfer);
213
+ }
214
+
215
+ // Update total cost
216
+ this.updateTotal();
217
+ },
218
+
219
+ calculateStorageCost (storage) {
220
+ const totalGB = storage.totalGB;
221
+ let cost = 0;
222
+ let remaining = totalGB;
223
+
224
+ for (let i = 0; i < storage.tiers.length; i++) {
225
+ const tier = storage.tiers[i];
226
+ const prevLimit = i > 0 ? storage.tiers[i - 1].limit : 0;
227
+ const tierCapacity = tier.limit - prevLimit;
228
+
229
+ if (remaining <= 0) break;
230
+
231
+ const gbInTier = Math.min(remaining, tierCapacity);
232
+ cost += gbInTier * tier.pricePerGB;
233
+ remaining -= gbInTier;
234
+
235
+ if (remaining <= 0) {
236
+ storage.currentTier = i;
237
+ break;
238
+ }
239
+ }
240
+
241
+ return cost;
242
+ },
243
+
244
+ calculateDataTransferCost (dataTransfer) {
245
+ let totalGB = dataTransfer.outGB;
246
+ let cost = 0;
247
+
248
+ // Apply free tier if enabled
249
+ if (this.options && this.options.considerFreeTier) {
250
+ const freeTierRemaining = dataTransfer.freeTierGB - dataTransfer.freeTierUsed;
251
+
252
+ if (freeTierRemaining > 0 && totalGB > 0) {
253
+ const gbToDeduct = Math.min(totalGB, freeTierRemaining);
254
+ totalGB -= gbToDeduct;
255
+ dataTransfer.freeTierUsed += gbToDeduct;
256
+ }
257
+ }
258
+
259
+ // Calculate with tiers
260
+ let remaining = totalGB;
261
+ for (let i = 0; i < dataTransfer.tiers.length; i++) {
262
+ const tier = dataTransfer.tiers[i];
263
+ const prevLimit = i > 0 ? dataTransfer.tiers[i - 1].limit : 0;
264
+ const tierCapacity = tier.limit - prevLimit;
265
+
266
+ if (remaining <= 0) break;
267
+
268
+ const gbInTier = Math.min(remaining, tierCapacity);
269
+ cost += gbInTier * tier.pricePerGB;
270
+ remaining -= gbInTier;
271
+
272
+ if (remaining <= 0) {
273
+ dataTransfer.currentTier = i;
274
+ break;
275
+ }
276
+ }
277
+
278
+ return cost;
279
+ },
280
+
281
+ updateTotal () {
282
+ this.costs.total =
283
+ this.costs.requests.subtotal +
284
+ this.costs.storage.subtotal +
285
+ this.costs.dataTransfer.subtotal;
286
+
287
+ // Mirror to client.costs
288
+ if (this.client && this.client.costs) {
289
+ this.client.costs.total =
290
+ this.client.costs.requests.subtotal +
291
+ this.client.costs.storage.subtotal +
292
+ this.client.costs.dataTransfer.subtotal;
77
293
  }
78
294
  },
79
295
  }
@@ -9,7 +9,7 @@ import { getCohortInfo, resolveFieldAndPlugin } from "./utils.js";
9
9
 
10
10
  /**
11
11
  * Add helper methods to resources
12
- * This adds: set(), add(), sub(), consolidate(), getConsolidatedValue(), recalculate()
12
+ * This adds: set(), add(), sub(), increment(), decrement(), consolidate(), getConsolidatedValue(), recalculate()
13
13
  *
14
14
  * @param {Object} resource - Resource to add methods to
15
15
  * @param {Object} plugin - Plugin instance
@@ -137,6 +137,20 @@ export function addHelperMethods(resource, plugin, config) {
137
137
  return currentValue - amount;
138
138
  };
139
139
 
140
+ // Add method to increment value by 1 (shorthand for add(id, field, 1))
141
+ // Signature: increment(id, field)
142
+ // Supports dot notation: increment(id, 'loginCount')
143
+ resource.increment = async (id, field) => {
144
+ return await resource.add(id, field, 1);
145
+ };
146
+
147
+ // Add method to decrement value by 1 (shorthand for sub(id, field, 1))
148
+ // Signature: decrement(id, field)
149
+ // Supports dot notation: decrement(id, 'remainingAttempts')
150
+ resource.decrement = async (id, field) => {
151
+ return await resource.sub(id, field, 1);
152
+ };
153
+
140
154
  // Add method to manually trigger consolidation
141
155
  // Signature: consolidate(id, field)
142
156
  resource.consolidate = async (id, field) => {