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
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
66
|
-
this.costs.
|
|
114
|
+
|
|
115
|
+
// Track request counts
|
|
116
|
+
this.costs.requests.totalEvents++;
|
|
67
117
|
this.costs.requests.total++;
|
|
68
|
-
this.costs.requests[
|
|
69
|
-
this.costs.
|
|
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.
|
|
73
|
-
this.client.costs.events.total++;
|
|
161
|
+
this.client.costs.requests.totalEvents++;
|
|
74
162
|
this.client.costs.requests.total++;
|
|
75
|
-
this.client.costs.requests[
|
|
76
|
-
this.client.costs.
|
|
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) => {
|