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.
- package/README.md +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- 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 +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- 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 +39 -19
- 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 +539 -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 +350 -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/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 +14 -10
- package/src/s3db.d.ts +57 -0
- 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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money Encoding/Decoding - Integer-based (Banking Standard)
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: Money should NEVER use floats/decimals due to precision errors.
|
|
5
|
+
* Always store as integers in smallest currency unit (cents, satoshis, etc).
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* $19.99 USD → 1999 cents → encoded as "$w7"
|
|
9
|
+
* 0.00012345 BTC → 12345 satoshis → encoded as "$3d9"
|
|
10
|
+
*
|
|
11
|
+
* Benefits:
|
|
12
|
+
* - Zero precision loss (no 0.1 + 0.2 = 0.30000004 bugs)
|
|
13
|
+
* - Faster integer arithmetic
|
|
14
|
+
* - Banking industry standard
|
|
15
|
+
* - 40-67% compression vs JSON floats
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { encode, decode } from './base62.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Currency decimal places (number of decimals in smallest unit)
|
|
22
|
+
*
|
|
23
|
+
* Fiat currencies:
|
|
24
|
+
* - Most: 2 decimals (cents)
|
|
25
|
+
* - Some: 0 decimals (yen, won)
|
|
26
|
+
*
|
|
27
|
+
* Cryptocurrencies:
|
|
28
|
+
* - BTC: 8 decimals (satoshis)
|
|
29
|
+
* - ETH: 18 decimals (wei) - but commonly use 9 (gwei)
|
|
30
|
+
* - Stablecoins: 6-8 decimals
|
|
31
|
+
*/
|
|
32
|
+
export const CURRENCY_DECIMALS = {
|
|
33
|
+
// Fiat with cents (2 decimals)
|
|
34
|
+
'USD': 2, 'BRL': 2, 'EUR': 2, 'GBP': 2, 'CAD': 2, 'AUD': 2,
|
|
35
|
+
'MXN': 2, 'ARS': 2, 'COP': 2, 'PEN': 2, 'UYU': 2,
|
|
36
|
+
'CHF': 2, 'SEK': 2, 'NOK': 2, 'DKK': 2, 'PLN': 2, 'CZK': 2,
|
|
37
|
+
'HUF': 2, 'RON': 2, 'BGN': 2, 'HRK': 2, 'RSD': 2, 'TRY': 2,
|
|
38
|
+
'ZAR': 2, 'EGP': 2, 'NGN': 2, 'KES': 2, 'GHS': 2,
|
|
39
|
+
'INR': 2, 'PKR': 2, 'BDT': 2, 'LKR': 2, 'NPR': 2,
|
|
40
|
+
'THB': 2, 'MYR': 2, 'SGD': 2, 'PHP': 2, 'IDR': 2,
|
|
41
|
+
'CNY': 2, 'HKD': 2, 'TWD': 2,
|
|
42
|
+
'ILS': 2, 'SAR': 2, 'AED': 2, 'QAR': 2, 'KWD': 3,
|
|
43
|
+
'RUB': 2, 'UAH': 2, 'KZT': 2,
|
|
44
|
+
|
|
45
|
+
// Fiat without decimals
|
|
46
|
+
'JPY': 0, // Japanese Yen
|
|
47
|
+
'KRW': 0, // Korean Won
|
|
48
|
+
'VND': 0, // Vietnamese Dong
|
|
49
|
+
'CLP': 0, // Chilean Peso
|
|
50
|
+
'ISK': 0, // Icelandic Króna
|
|
51
|
+
'PYG': 0, // Paraguayan Guaraní
|
|
52
|
+
|
|
53
|
+
// Cryptocurrencies
|
|
54
|
+
'BTC': 8, // Bitcoin (satoshis)
|
|
55
|
+
'ETH': 18, // Ethereum (wei) - often use 9 for gwei
|
|
56
|
+
'GWEI': 9, // Ethereum gwei (common unit)
|
|
57
|
+
'USDT': 6, // Tether
|
|
58
|
+
'USDC': 6, // USD Coin
|
|
59
|
+
'BUSD': 18, // Binance USD
|
|
60
|
+
'DAI': 18, // Dai
|
|
61
|
+
'BNB': 18, // Binance Coin
|
|
62
|
+
'XRP': 6, // Ripple
|
|
63
|
+
'ADA': 6, // Cardano
|
|
64
|
+
'SOL': 9, // Solana
|
|
65
|
+
'MATIC': 18, // Polygon
|
|
66
|
+
'AVAX': 18, // Avalanche
|
|
67
|
+
'DOT': 10, // Polkadot
|
|
68
|
+
'LINK': 18, // Chainlink
|
|
69
|
+
'UNI': 18, // Uniswap
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get decimal places for a currency
|
|
74
|
+
* @param {string} currency - Currency code (e.g., 'USD', 'BTC')
|
|
75
|
+
* @returns {number} Number of decimal places
|
|
76
|
+
*/
|
|
77
|
+
export function getCurrencyDecimals(currency) {
|
|
78
|
+
const normalized = currency.toUpperCase();
|
|
79
|
+
return CURRENCY_DECIMALS[normalized] ?? 2; // Default to 2 (cents)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Encode money value to integer-based base62
|
|
84
|
+
*
|
|
85
|
+
* @param {number} value - Decimal value (e.g., 19.99)
|
|
86
|
+
* @param {string} currency - Currency code (default: 'USD')
|
|
87
|
+
* @returns {string} Encoded string with '$' prefix
|
|
88
|
+
*
|
|
89
|
+
* @throws {Error} If value is negative
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* encodeMoney(19.99, 'USD') // → "$w7" (1999 cents)
|
|
93
|
+
* encodeMoney(1000.50, 'BRL') // → "$6Dl" (100050 centavos)
|
|
94
|
+
* encodeMoney(0.00012345, 'BTC') // → "$3d9" (12345 satoshis)
|
|
95
|
+
*/
|
|
96
|
+
export function encodeMoney(value, currency = 'USD') {
|
|
97
|
+
if (value === null || value === undefined) return value;
|
|
98
|
+
if (typeof value !== 'number' || isNaN(value)) return value;
|
|
99
|
+
if (!isFinite(value)) return value;
|
|
100
|
+
|
|
101
|
+
// Money cannot be negative (validation should happen at schema level)
|
|
102
|
+
if (value < 0) {
|
|
103
|
+
throw new Error(`Money value cannot be negative: ${value}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const decimals = getCurrencyDecimals(currency);
|
|
107
|
+
const multiplier = Math.pow(10, decimals);
|
|
108
|
+
|
|
109
|
+
// Convert to smallest unit (cents, satoshis, wei, etc)
|
|
110
|
+
// Use Math.round to handle floating point precision issues
|
|
111
|
+
const integerValue = Math.round(value * multiplier);
|
|
112
|
+
|
|
113
|
+
// Encode as pure integer using base62
|
|
114
|
+
return '$' + encode(integerValue);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decode money from base62 to decimal value
|
|
119
|
+
*
|
|
120
|
+
* @param {string} encoded - Encoded string (must start with '$')
|
|
121
|
+
* @param {string} currency - Currency code (default: 'USD')
|
|
122
|
+
* @returns {number} Decoded decimal value
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* decodeMoney('$w7', 'USD') // → 19.99
|
|
126
|
+
* decodeMoney('$6Dl', 'BRL') // → 1000.50
|
|
127
|
+
* decodeMoney('$3d9', 'BTC') // → 0.00012345
|
|
128
|
+
*/
|
|
129
|
+
export function decodeMoney(encoded, currency = 'USD') {
|
|
130
|
+
if (typeof encoded !== 'string') return encoded;
|
|
131
|
+
if (!encoded.startsWith('$')) return encoded;
|
|
132
|
+
|
|
133
|
+
const integerValue = decode(encoded.slice(1));
|
|
134
|
+
if (isNaN(integerValue)) return NaN;
|
|
135
|
+
|
|
136
|
+
const decimals = getCurrencyDecimals(currency);
|
|
137
|
+
const divisor = Math.pow(10, decimals);
|
|
138
|
+
|
|
139
|
+
// Convert back to decimal
|
|
140
|
+
return integerValue / divisor;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate if a currency code is supported
|
|
145
|
+
* @param {string} currency - Currency code
|
|
146
|
+
* @returns {boolean} True if supported
|
|
147
|
+
*/
|
|
148
|
+
export function isSupportedCurrency(currency) {
|
|
149
|
+
const normalized = currency.toUpperCase();
|
|
150
|
+
return normalized in CURRENCY_DECIMALS;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get list of all supported currencies
|
|
155
|
+
* @returns {string[]} Array of currency codes
|
|
156
|
+
*/
|
|
157
|
+
export function getSupportedCurrencies() {
|
|
158
|
+
return Object.keys(CURRENCY_DECIMALS);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format money value for display
|
|
163
|
+
* @param {number} value - Decimal value
|
|
164
|
+
* @param {string} currency - Currency code
|
|
165
|
+
* @param {string} locale - Locale for formatting (default: 'en-US')
|
|
166
|
+
* @returns {string} Formatted money string
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* formatMoney(19.99, 'USD') // → "$19.99"
|
|
170
|
+
* formatMoney(1000.50, 'BRL', 'pt-BR') // → "R$ 1.000,50"
|
|
171
|
+
* formatMoney(0.00012345, 'BTC') // → "0.00012345 BTC"
|
|
172
|
+
*/
|
|
173
|
+
export function formatMoney(value, currency = 'USD', locale = 'en-US') {
|
|
174
|
+
const decimals = getCurrencyDecimals(currency);
|
|
175
|
+
|
|
176
|
+
// For fiat currencies, use Intl.NumberFormat
|
|
177
|
+
if (decimals <= 3 && currency !== 'BTC' && !currency.includes('USDT')) {
|
|
178
|
+
try {
|
|
179
|
+
return new Intl.NumberFormat(locale, {
|
|
180
|
+
style: 'currency',
|
|
181
|
+
currency: currency,
|
|
182
|
+
minimumFractionDigits: decimals,
|
|
183
|
+
maximumFractionDigits: decimals
|
|
184
|
+
}).format(value);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
// Fallback for unsupported currencies
|
|
187
|
+
return `${value.toFixed(decimals)} ${currency}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// For crypto, just show the value with correct decimals
|
|
192
|
+
return `${value.toFixed(decimals)} ${currency}`;
|
|
193
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { PartitionDriverError } from '../errors.js';
|
|
3
|
+
import tryFn from './try-fn.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Robust partition operation queue with retry and persistence
|
|
@@ -52,17 +53,19 @@ export class PartitionQueue extends EventEmitter {
|
|
|
52
53
|
|
|
53
54
|
while (this.queue.length > 0) {
|
|
54
55
|
const item = this.queue.shift();
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
|
|
57
|
+
const [ok, error] = await tryFn(async () => {
|
|
57
58
|
await this.executeOperation(item);
|
|
58
59
|
item.status = 'completed';
|
|
59
60
|
this.emit('success', item);
|
|
60
|
-
|
|
61
|
+
|
|
61
62
|
// Remove from persistence
|
|
62
63
|
if (this.persistence) {
|
|
63
64
|
await this.persistence.remove(item.id);
|
|
64
65
|
}
|
|
65
|
-
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!ok) {
|
|
66
69
|
item.retries++;
|
|
67
70
|
item.lastError = error;
|
|
68
71
|
|
|
@@ -174,16 +174,19 @@ export class PluginStorage {
|
|
|
174
174
|
|
|
175
175
|
// If has body, merge with metadata
|
|
176
176
|
if (response.Body) {
|
|
177
|
-
|
|
177
|
+
const [ok, parseErr, result] = await tryFn(async () => {
|
|
178
178
|
const bodyContent = await response.Body.transformToString();
|
|
179
179
|
|
|
180
180
|
// Only parse if body has content
|
|
181
181
|
if (bodyContent && bodyContent.trim()) {
|
|
182
182
|
const body = JSON.parse(bodyContent);
|
|
183
183
|
// Body takes precedence over metadata for same keys
|
|
184
|
-
|
|
184
|
+
return { ...parsedMetadata, ...body };
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
return parsedMetadata;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!ok) {
|
|
187
190
|
throw new PluginStorageError(`Failed to parse JSON body`, {
|
|
188
191
|
pluginSlug: this.pluginSlug,
|
|
189
192
|
key,
|
|
@@ -192,6 +195,8 @@ export class PluginStorage {
|
|
|
192
195
|
suggestion: 'Body content may be corrupted. Check S3 object integrity'
|
|
193
196
|
});
|
|
194
197
|
}
|
|
198
|
+
|
|
199
|
+
data = result;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
// Check TTL expiration (S3 lowercases metadata keys)
|
|
@@ -224,12 +229,12 @@ export class PluginStorage {
|
|
|
224
229
|
(value.startsWith('{') && value.endsWith('}')) ||
|
|
225
230
|
(value.startsWith('[') && value.endsWith(']'))
|
|
226
231
|
) {
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
const [ok, err, result] = tryFn(() => JSON.parse(value));
|
|
233
|
+
if (ok) {
|
|
234
|
+
parsed[key] = result;
|
|
229
235
|
continue;
|
|
230
|
-
} catch {
|
|
231
|
-
// Not JSON, keep as string
|
|
232
236
|
}
|
|
237
|
+
// Not JSON, keep as string
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
// Try to parse as number
|
|
@@ -373,15 +378,20 @@ export class PluginStorage {
|
|
|
373
378
|
let data = parsedMetadata;
|
|
374
379
|
|
|
375
380
|
if (response.Body) {
|
|
376
|
-
|
|
381
|
+
const [ok, err, result] = await tryFn(async () => {
|
|
377
382
|
const bodyContent = await response.Body.transformToString();
|
|
378
383
|
if (bodyContent && bodyContent.trim()) {
|
|
379
384
|
const body = JSON.parse(bodyContent);
|
|
380
|
-
|
|
385
|
+
return { ...parsedMetadata, ...body };
|
|
381
386
|
}
|
|
382
|
-
|
|
383
|
-
|
|
387
|
+
return parsedMetadata;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (!ok) {
|
|
391
|
+
return true; // Parse error = expired
|
|
384
392
|
}
|
|
393
|
+
|
|
394
|
+
data = result;
|
|
385
395
|
}
|
|
386
396
|
|
|
387
397
|
// S3 lowercases metadata keys
|
|
@@ -412,15 +422,20 @@ export class PluginStorage {
|
|
|
412
422
|
let data = parsedMetadata;
|
|
413
423
|
|
|
414
424
|
if (response.Body) {
|
|
415
|
-
|
|
425
|
+
const [ok, err, result] = await tryFn(async () => {
|
|
416
426
|
const bodyContent = await response.Body.transformToString();
|
|
417
427
|
if (bodyContent && bodyContent.trim()) {
|
|
418
428
|
const body = JSON.parse(bodyContent);
|
|
419
|
-
|
|
429
|
+
return { ...parsedMetadata, ...body };
|
|
420
430
|
}
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
return parsedMetadata;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (!ok) {
|
|
435
|
+
return null; // Parse error
|
|
423
436
|
}
|
|
437
|
+
|
|
438
|
+
data = result;
|
|
424
439
|
}
|
|
425
440
|
|
|
426
441
|
// S3 lowercases metadata keys
|
|
@@ -453,15 +468,20 @@ export class PluginStorage {
|
|
|
453
468
|
let data = parsedMetadata;
|
|
454
469
|
|
|
455
470
|
if (response.Body) {
|
|
456
|
-
|
|
471
|
+
const [ok, err, result] = await tryFn(async () => {
|
|
457
472
|
const bodyContent = await response.Body.transformToString();
|
|
458
473
|
if (bodyContent && bodyContent.trim()) {
|
|
459
474
|
const body = JSON.parse(bodyContent);
|
|
460
|
-
|
|
475
|
+
return { ...parsedMetadata, ...body };
|
|
461
476
|
}
|
|
462
|
-
|
|
463
|
-
|
|
477
|
+
return parsedMetadata;
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (!ok) {
|
|
481
|
+
return false; // Parse error
|
|
464
482
|
}
|
|
483
|
+
|
|
484
|
+
data = result;
|
|
465
485
|
}
|
|
466
486
|
|
|
467
487
|
// S3 lowercases metadata keys
|