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
@@ -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
- try {
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
- } catch (error) {
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
- try {
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
- data = { ...parsedMetadata, ...body };
184
+ return { ...parsedMetadata, ...body };
185
185
  }
186
- } catch (parseErr) {
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
- try {
228
- parsed[key] = JSON.parse(value);
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
- try {
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
- data = { ...parsedMetadata, ...body };
385
+ return { ...parsedMetadata, ...body };
381
386
  }
382
- } catch {
383
- return true;
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
- try {
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
- data = { ...parsedMetadata, ...body };
429
+ return { ...parsedMetadata, ...body };
420
430
  }
421
- } catch {
422
- return null;
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
- try {
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
- data = { ...parsedMetadata, ...body };
475
+ return { ...parsedMetadata, ...body };
461
476
  }
462
- } catch {
463
- return false;
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