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,416 @@
1
+ import tryFn from "#src/concerns/try-fn.js";
2
+ import requirePluginDependency from "#src/plugins/concerns/plugin-dependencies.js";
3
+ import BaseReplicator from './base-replicator.class.js';
4
+ import { ReplicationError } from '../replicator.errors.js';
5
+ import {
6
+ generateSQLiteCreateTable,
7
+ generateSQLiteAlterTable
8
+ } from './schema-sync.helper.js';
9
+
10
+ /**
11
+ * Turso Replicator - Replicate data to Turso (SQLite edge database)
12
+ *
13
+ * ⚠️ REQUIRED DEPENDENCY: You must install the Turso client library:
14
+ * ```bash
15
+ * pnpm add @libsql/client
16
+ * ```
17
+ *
18
+ * Configuration:
19
+ * @param {string} url - Turso database URL (required) - e.g., 'libsql://your-db.turso.io'
20
+ * @param {string} authToken - Turso authentication token (required)
21
+ * @param {Object} schemaSync - Schema synchronization configuration
22
+ * @param {boolean} schemaSync.enabled - Enable automatic schema management (default: false)
23
+ * @param {string} schemaSync.strategy - Sync strategy: 'alter' | 'drop-create' | 'validate-only' (default: 'alter')
24
+ * @param {string} schemaSync.onMismatch - Action on schema mismatch: 'error' | 'warn' | 'ignore' (default: 'error')
25
+ * @param {boolean} schemaSync.autoCreateTable - Auto-create table if not exists (default: true)
26
+ * @param {boolean} schemaSync.autoCreateColumns - Auto-add missing columns (default: true, only with strategy: 'alter')
27
+ *
28
+ * @example
29
+ * new TursoReplicator({
30
+ * url: 'libsql://my-db-user.turso.io',
31
+ * authToken: process.env.TURSO_AUTH_TOKEN,
32
+ * schemaSync: {
33
+ * enabled: true,
34
+ * strategy: 'alter',
35
+ * onMismatch: 'error'
36
+ * }
37
+ * }, {
38
+ * users: [{ actions: ['insert', 'update'], table: 'users_table' }],
39
+ * orders: 'orders_table'
40
+ * })
41
+ *
42
+ * See docs/plugins/replicator.md for comprehensive configuration documentation.
43
+ */
44
+ class TursoReplicator extends BaseReplicator {
45
+ constructor(config = {}, resources = {}) {
46
+ super(config);
47
+ this.url = config.url;
48
+ this.authToken = config.authToken;
49
+ this.client = null;
50
+
51
+ // Schema sync configuration
52
+ this.schemaSync = {
53
+ enabled: config.schemaSync?.enabled || false,
54
+ strategy: config.schemaSync?.strategy || 'alter',
55
+ onMismatch: config.schemaSync?.onMismatch || 'error',
56
+ autoCreateTable: config.schemaSync?.autoCreateTable !== false,
57
+ autoCreateColumns: config.schemaSync?.autoCreateColumns !== false
58
+ };
59
+
60
+ // Parse resources configuration
61
+ this.resources = this.parseResourcesConfig(resources);
62
+ }
63
+
64
+ parseResourcesConfig(resources) {
65
+ const parsed = {};
66
+
67
+ for (const [resourceName, config] of Object.entries(resources)) {
68
+ if (typeof config === 'string') {
69
+ parsed[resourceName] = [{
70
+ table: config,
71
+ actions: ['insert']
72
+ }];
73
+ } else if (Array.isArray(config)) {
74
+ parsed[resourceName] = config.map(item => {
75
+ if (typeof item === 'string') {
76
+ return { table: item, actions: ['insert'] };
77
+ }
78
+ return {
79
+ table: item.table,
80
+ actions: item.actions || ['insert']
81
+ };
82
+ });
83
+ } else if (typeof config === 'object') {
84
+ parsed[resourceName] = [{
85
+ table: config.table,
86
+ actions: config.actions || ['insert']
87
+ }];
88
+ }
89
+ }
90
+
91
+ return parsed;
92
+ }
93
+
94
+ validateConfig() {
95
+ const errors = [];
96
+ if (!this.url) errors.push('URL is required');
97
+ if (!this.authToken) errors.push('Auth token is required');
98
+ if (Object.keys(this.resources).length === 0) {
99
+ errors.push('At least one resource must be configured');
100
+ }
101
+
102
+ for (const [resourceName, tables] of Object.entries(this.resources)) {
103
+ for (const tableConfig of tables) {
104
+ if (!tableConfig.table) {
105
+ errors.push(`Table name is required for resource '${resourceName}'`);
106
+ }
107
+ if (!Array.isArray(tableConfig.actions) || tableConfig.actions.length === 0) {
108
+ errors.push(`Actions array is required for resource '${resourceName}'`);
109
+ }
110
+ }
111
+ }
112
+
113
+ return { isValid: errors.length === 0, errors };
114
+ }
115
+
116
+ async initialize(database) {
117
+ await super.initialize(database);
118
+
119
+ // Validate plugin dependencies are installed
120
+ await requirePluginDependency('turso-replicator');
121
+
122
+ const [ok, err, sdk] = await tryFn(() => import('@libsql/client'));
123
+ if (!ok) {
124
+ throw new ReplicationError('Failed to import Turso SDK', {
125
+ operation: 'initialize',
126
+ replicatorClass: 'TursoReplicator',
127
+ original: err,
128
+ suggestion: 'Install @libsql/client: pnpm add @libsql/client'
129
+ });
130
+ }
131
+
132
+ const { createClient } = sdk;
133
+ this.client = createClient({
134
+ url: this.url,
135
+ authToken: this.authToken
136
+ });
137
+
138
+ // Test connection
139
+ const [okTest, errTest] = await tryFn(async () => {
140
+ await this.client.execute('SELECT 1');
141
+ });
142
+
143
+ if (!okTest) {
144
+ throw new ReplicationError('Failed to connect to Turso database', {
145
+ operation: 'initialize',
146
+ replicatorClass: 'TursoReplicator',
147
+ url: this.url,
148
+ original: errTest,
149
+ suggestion: 'Check Turso URL and auth token'
150
+ });
151
+ }
152
+
153
+ // Sync schemas if enabled
154
+ if (this.schemaSync.enabled) {
155
+ await this.syncSchemas(database);
156
+ }
157
+
158
+ this.emit('connected', {
159
+ replicator: 'TursoReplicator',
160
+ url: this.url
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Sync table schemas based on S3DB resource definitions
166
+ */
167
+ async syncSchemas(database) {
168
+ for (const [resourceName, tableConfigs] of Object.entries(this.resources)) {
169
+ const [okRes, errRes, resource] = await tryFn(async () => {
170
+ return await database.getResource(resourceName);
171
+ });
172
+
173
+ if (!okRes) {
174
+ if (this.config.verbose) {
175
+ console.warn(`[TursoReplicator] Could not get resource ${resourceName} for schema sync: ${errRes.message}`);
176
+ }
177
+ continue;
178
+ }
179
+
180
+ const attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
181
+
182
+ for (const tableConfig of tableConfigs) {
183
+ const tableName = tableConfig.table;
184
+
185
+ const [okSync, errSync] = await tryFn(async () => {
186
+ await this.syncTableSchema(tableName, attributes);
187
+ });
188
+
189
+ if (!okSync) {
190
+ const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
191
+
192
+ if (this.schemaSync.onMismatch === 'error') {
193
+ throw new Error(message);
194
+ } else if (this.schemaSync.onMismatch === 'warn') {
195
+ console.warn(`[TursoReplicator] ${message}`);
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ this.emit('schema_sync_completed', {
202
+ replicator: this.name,
203
+ resources: Object.keys(this.resources)
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Sync a single table schema
209
+ */
210
+ async syncTableSchema(tableName, attributes) {
211
+ // Check if table exists
212
+ const [okCheck, errCheck, result] = await tryFn(async () => {
213
+ return await this.client.execute({
214
+ sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
215
+ args: [tableName]
216
+ });
217
+ });
218
+
219
+ const tableExists = okCheck && result.rows.length > 0;
220
+
221
+ if (!tableExists) {
222
+ if (!this.schemaSync.autoCreateTable) {
223
+ throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
224
+ }
225
+
226
+ if (this.schemaSync.strategy === 'validate-only') {
227
+ throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
228
+ }
229
+
230
+ // Create table
231
+ const createSQL = generateSQLiteCreateTable(tableName, attributes);
232
+
233
+ if (this.config.verbose) {
234
+ console.log(`[TursoReplicator] Creating table ${tableName}:\n${createSQL}`);
235
+ }
236
+
237
+ await this.client.execute(createSQL);
238
+
239
+ this.emit('table_created', {
240
+ replicator: this.name,
241
+ tableName,
242
+ attributes: Object.keys(attributes)
243
+ });
244
+
245
+ return;
246
+ }
247
+
248
+ // Table exists - check for schema changes
249
+ if (this.schemaSync.strategy === 'drop-create') {
250
+ if (this.config.verbose) {
251
+ console.warn(`[TursoReplicator] Dropping and recreating table ${tableName}`);
252
+ }
253
+
254
+ await this.client.execute(`DROP TABLE IF EXISTS ${tableName}`);
255
+ const createSQL = generateSQLiteCreateTable(tableName, attributes);
256
+ await this.client.execute(createSQL);
257
+
258
+ this.emit('table_recreated', {
259
+ replicator: this.name,
260
+ tableName,
261
+ attributes: Object.keys(attributes)
262
+ });
263
+
264
+ return;
265
+ }
266
+
267
+ if (this.schemaSync.strategy === 'alter' && this.schemaSync.autoCreateColumns) {
268
+ // Get existing columns
269
+ const [okPragma, errPragma, pragmaResult] = await tryFn(async () => {
270
+ return await this.client.execute(`PRAGMA table_info(${tableName})`);
271
+ });
272
+
273
+ if (okPragma) {
274
+ const existingSchema = {};
275
+ for (const row of pragmaResult.rows) {
276
+ existingSchema[row.name] = { type: row.type };
277
+ }
278
+
279
+ const alterStatements = generateSQLiteAlterTable(tableName, attributes, existingSchema);
280
+
281
+ if (alterStatements.length > 0) {
282
+ if (this.config.verbose) {
283
+ console.log(`[TursoReplicator] Altering table ${tableName}:`, alterStatements);
284
+ }
285
+
286
+ for (const stmt of alterStatements) {
287
+ await this.client.execute(stmt);
288
+ }
289
+
290
+ this.emit('table_altered', {
291
+ replicator: this.name,
292
+ tableName,
293
+ addedColumns: alterStatements.length
294
+ });
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ shouldReplicateResource(resourceName) {
301
+ return this.resources.hasOwnProperty(resourceName);
302
+ }
303
+
304
+ shouldReplicateAction(resourceName, operation) {
305
+ if (!this.resources[resourceName]) return false;
306
+
307
+ return this.resources[resourceName].some(tableConfig =>
308
+ tableConfig.actions.includes(operation)
309
+ );
310
+ }
311
+
312
+ getTablesForResource(resourceName, operation) {
313
+ if (!this.resources[resourceName]) return [];
314
+
315
+ return this.resources[resourceName]
316
+ .filter(tableConfig => tableConfig.actions.includes(operation))
317
+ .map(tableConfig => tableConfig.table);
318
+ }
319
+
320
+ async replicate(resourceName, operation, data, id, beforeData = null) {
321
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
322
+ return { skipped: true, reason: 'resource_not_included' };
323
+ }
324
+
325
+ if (!this.shouldReplicateAction(resourceName, operation)) {
326
+ return { skipped: true, reason: 'action_not_included' };
327
+ }
328
+
329
+ const tables = this.getTablesForResource(resourceName, operation);
330
+ if (tables.length === 0) {
331
+ return { skipped: true, reason: 'no_tables_for_action' };
332
+ }
333
+
334
+ const results = [];
335
+ const errors = [];
336
+
337
+ for (const table of tables) {
338
+ const [okTable, errTable] = await tryFn(async () => {
339
+ if (operation === 'insert') {
340
+ const cleanData = this._cleanInternalFields(data);
341
+ const keys = Object.keys(cleanData);
342
+ const values = keys.map(k => cleanData[k]);
343
+ const placeholders = keys.map((_, i) => `?`).join(', ');
344
+ const sql = `INSERT OR IGNORE INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`;
345
+ await this.client.execute({ sql, args: values });
346
+ } else if (operation === 'update') {
347
+ const cleanData = this._cleanInternalFields(data);
348
+ const keys = Object.keys(cleanData).filter(k => k !== 'id');
349
+ const setClause = keys.map(k => `${k}=?`).join(', ');
350
+ const values = keys.map(k => cleanData[k]);
351
+ values.push(id);
352
+ const sql = `UPDATE ${table} SET ${setClause} WHERE id=?`;
353
+ await this.client.execute({ sql, args: values });
354
+ } else if (operation === 'delete') {
355
+ const sql = `DELETE FROM ${table} WHERE id=?`;
356
+ await this.client.execute({ sql, args: [id] });
357
+ }
358
+
359
+ results.push({ table, success: true });
360
+ });
361
+
362
+ if (!okTable) {
363
+ errors.push({ table, error: errTable.message });
364
+ }
365
+ }
366
+
367
+ const success = errors.length === 0;
368
+
369
+ this.emit('replicated', {
370
+ replicator: this.name,
371
+ resourceName,
372
+ operation,
373
+ id,
374
+ tables,
375
+ results,
376
+ errors,
377
+ success
378
+ });
379
+
380
+ return { success, results, errors, tables };
381
+ }
382
+
383
+ _cleanInternalFields(data) {
384
+ if (!data || typeof data !== 'object') return data;
385
+
386
+ const cleanData = { ...data };
387
+
388
+ Object.keys(cleanData).forEach(key => {
389
+ if (key.startsWith('$') || key.startsWith('_')) {
390
+ delete cleanData[key];
391
+ }
392
+ });
393
+
394
+ return cleanData;
395
+ }
396
+
397
+ async cleanup() {
398
+ if (this.client) {
399
+ this.client.close();
400
+ this.client = null;
401
+ }
402
+ }
403
+
404
+ async getStatus() {
405
+ const baseStatus = await super.getStatus();
406
+ return {
407
+ ...baseStatus,
408
+ connected: !!this.client,
409
+ url: this.url,
410
+ resources: Object.keys(this.resources),
411
+ schemaSync: this.schemaSync
412
+ };
413
+ }
414
+ }
415
+
416
+ export default TursoReplicator;