s3db.js 12.1.0 → 12.2.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 (42) hide show
  1. package/README.md +212 -196
  2. package/dist/s3db.cjs.js +1286 -226
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +1284 -226
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +6 -1
  7. package/src/cli/index.js +954 -43
  8. package/src/cli/migration-manager.js +270 -0
  9. package/src/concerns/calculator.js +0 -4
  10. package/src/concerns/metadata-encoding.js +1 -21
  11. package/src/concerns/plugin-storage.js +17 -4
  12. package/src/concerns/typescript-generator.d.ts +171 -0
  13. package/src/concerns/typescript-generator.js +275 -0
  14. package/src/database.class.js +171 -28
  15. package/src/index.js +15 -9
  16. package/src/plugins/api/index.js +0 -1
  17. package/src/plugins/api/routes/resource-routes.js +86 -1
  18. package/src/plugins/api/server.js +79 -3
  19. package/src/plugins/api/utils/openapi-generator.js +195 -5
  20. package/src/plugins/backup/multi-backup-driver.class.js +0 -1
  21. package/src/plugins/backup.plugin.js +7 -14
  22. package/src/plugins/concerns/plugin-dependencies.js +73 -19
  23. package/src/plugins/eventual-consistency/analytics.js +0 -2
  24. package/src/plugins/eventual-consistency/consolidation.js +2 -13
  25. package/src/plugins/eventual-consistency/index.js +0 -1
  26. package/src/plugins/eventual-consistency/install.js +1 -1
  27. package/src/plugins/geo.plugin.js +5 -6
  28. package/src/plugins/importer/index.js +1 -1
  29. package/src/plugins/relation.plugin.js +11 -11
  30. package/src/plugins/replicator.plugin.js +12 -21
  31. package/src/plugins/s3-queue.plugin.js +4 -4
  32. package/src/plugins/scheduler.plugin.js +10 -12
  33. package/src/plugins/state-machine.plugin.js +8 -12
  34. package/src/plugins/tfstate/README.md +1 -1
  35. package/src/plugins/tfstate/errors.js +3 -3
  36. package/src/plugins/tfstate/index.js +41 -67
  37. package/src/plugins/ttl.plugin.js +3 -3
  38. package/src/resource.class.js +263 -61
  39. package/src/schema.class.js +0 -2
  40. package/src/testing/factory.class.js +286 -0
  41. package/src/testing/index.js +15 -0
  42. package/src/testing/seeder.class.js +183 -0
@@ -16,81 +16,97 @@
16
16
  export const PLUGIN_DEPENDENCIES = {
17
17
  'postgresql-replicator': {
18
18
  name: 'PostgreSQL Replicator',
19
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md',
19
20
  dependencies: {
20
21
  'pg': {
21
22
  version: '^8.0.0',
22
23
  description: 'PostgreSQL client for Node.js',
23
- installCommand: 'pnpm add pg'
24
+ installCommand: 'pnpm add pg',
25
+ npmUrl: 'https://www.npmjs.com/package/pg'
24
26
  }
25
27
  }
26
28
  },
27
29
  'bigquery-replicator': {
28
30
  name: 'BigQuery Replicator',
31
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md',
29
32
  dependencies: {
30
33
  '@google-cloud/bigquery': {
31
34
  version: '^7.0.0',
32
35
  description: 'Google Cloud BigQuery SDK',
33
- installCommand: 'pnpm add @google-cloud/bigquery'
36
+ installCommand: 'pnpm add @google-cloud/bigquery',
37
+ npmUrl: 'https://www.npmjs.com/package/@google-cloud/bigquery'
34
38
  }
35
39
  }
36
40
  },
37
41
  'sqs-replicator': {
38
42
  name: 'SQS Replicator',
43
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md',
39
44
  dependencies: {
40
45
  '@aws-sdk/client-sqs': {
41
46
  version: '^3.0.0',
42
47
  description: 'AWS SDK for SQS',
43
- installCommand: 'pnpm add @aws-sdk/client-sqs'
48
+ installCommand: 'pnpm add @aws-sdk/client-sqs',
49
+ npmUrl: 'https://www.npmjs.com/package/@aws-sdk/client-sqs'
44
50
  }
45
51
  }
46
52
  },
47
53
  'sqs-consumer': {
48
54
  name: 'SQS Queue Consumer',
55
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md',
49
56
  dependencies: {
50
57
  '@aws-sdk/client-sqs': {
51
58
  version: '^3.0.0',
52
59
  description: 'AWS SDK for SQS',
53
- installCommand: 'pnpm add @aws-sdk/client-sqs'
60
+ installCommand: 'pnpm add @aws-sdk/client-sqs',
61
+ npmUrl: 'https://www.npmjs.com/package/@aws-sdk/client-sqs'
54
62
  }
55
63
  }
56
64
  },
57
65
  'rabbitmq-consumer': {
58
66
  name: 'RabbitMQ Queue Consumer',
67
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md',
59
68
  dependencies: {
60
69
  'amqplib': {
61
70
  version: '^0.10.0',
62
71
  description: 'AMQP 0-9-1 library for RabbitMQ',
63
- installCommand: 'pnpm add amqplib'
72
+ installCommand: 'pnpm add amqplib',
73
+ npmUrl: 'https://www.npmjs.com/package/amqplib'
64
74
  }
65
75
  }
66
76
  },
67
77
  'tfstate-plugin': {
68
- name: 'Terraform State Plugin',
78
+ name: 'Tfstate Plugin',
79
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/tfstate.md',
69
80
  dependencies: {
70
81
  'node-cron': {
71
82
  version: '^4.0.0',
72
83
  description: 'Cron job scheduler for auto-sync functionality',
73
- installCommand: 'pnpm add node-cron'
84
+ installCommand: 'pnpm add node-cron',
85
+ npmUrl: 'https://www.npmjs.com/package/node-cron'
74
86
  }
75
87
  }
76
88
  },
77
89
  'api-plugin': {
78
90
  name: 'API Plugin',
91
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/api.md',
79
92
  dependencies: {
80
93
  'hono': {
81
94
  version: '^4.0.0',
82
95
  description: 'Ultra-light HTTP server framework',
83
- installCommand: 'pnpm add hono'
96
+ installCommand: 'pnpm add hono',
97
+ npmUrl: 'https://www.npmjs.com/package/hono'
84
98
  },
85
99
  '@hono/node-server': {
86
100
  version: '^1.0.0',
87
101
  description: 'Node.js adapter for Hono',
88
- installCommand: 'pnpm add @hono/node-server'
102
+ installCommand: 'pnpm add @hono/node-server',
103
+ npmUrl: 'https://www.npmjs.com/package/@hono/node-server'
89
104
  },
90
105
  '@hono/swagger-ui': {
91
106
  version: '^0.4.0',
92
107
  description: 'Swagger UI integration for Hono',
93
- installCommand: 'pnpm add @hono/swagger-ui'
108
+ installCommand: 'pnpm add @hono/swagger-ui',
109
+ npmUrl: 'https://www.npmjs.com/package/@hono/swagger-ui'
94
110
  }
95
111
  }
96
112
  }
@@ -229,22 +245,60 @@ export async function requirePluginDependency(pluginId, options = {}) {
229
245
 
230
246
  // Throw comprehensive error if validation failed
231
247
  if (!valid && throwOnError) {
248
+ const depCount = Object.keys(pluginDef.dependencies).length;
249
+ const missingCount = missing.length;
250
+ const incompatCount = incompatible.length;
251
+
232
252
  const errorMsg = [
233
- `\n${pluginDef.name} - Missing dependencies detected!\n`,
234
- `Plugin ID: ${pluginId}`,
235
253
  '',
254
+ '╔══════════════════════════════════════════════════════════════════════╗',
255
+ `║ ❌ ${pluginDef.name} - Missing Dependencies ║`,
256
+ '╚══════════════════════════════════════════════════════════════════════╝',
257
+ '',
258
+ `📦 Plugin: ${pluginId}`,
259
+ `📊 Status: ${depCount - missingCount - incompatCount}/${depCount} dependencies satisfied`,
260
+ '',
261
+ '🔍 Dependency Status:',
262
+ '─────────────────────────────────────────────────────────────────────',
236
263
  ...messages,
237
264
  '',
238
- 'Quick fix - Run all install commands:',
239
- Object.values(pluginDef.dependencies)
240
- .map(dep => ` ${dep.installCommand}`)
241
- .join('\n'),
265
+ '🚀 Quick Fix - Install Missing Dependencies:',
266
+ '─────────────────────────────────────────────────────────────────────',
267
+ '',
268
+ ' Option 1: Install individually',
269
+ ...Object.entries(pluginDef.dependencies)
270
+ .filter(([pkg]) => missing.includes(pkg) || incompatible.includes(pkg))
271
+ .map(([pkg, info]) => ` ${info.installCommand}`),
272
+ '',
273
+ ' Option 2: Install all at once',
274
+ ` pnpm add ${Object.keys(pluginDef.dependencies).join(' ')}`,
242
275
  '',
243
- 'Or install all peer dependencies at once:',
244
- ` pnpm add ${Object.keys(pluginDef.dependencies).join(' ')}`
276
+ '📚 Documentation:',
277
+ ` ${pluginDef.docsUrl}`,
278
+ '',
279
+ '💡 Troubleshooting:',
280
+ ' • If packages are installed but not detected, try:',
281
+ ' 1. Delete node_modules and reinstall: rm -rf node_modules && pnpm install',
282
+ ' 2. Check Node.js version: node --version (requires Node 18+)',
283
+ ' 3. Verify pnpm version: pnpm --version (requires pnpm 8+)',
284
+ '',
285
+ ' • Still having issues? Check:',
286
+ ' - Package.json has correct dependencies listed',
287
+ ' - No conflicting versions in pnpm-lock.yaml',
288
+ ' - File permissions (especially in node_modules/)',
289
+ '',
290
+ '═══════════════════════════════════════════════════════════════════════',
291
+ ''
245
292
  ].join('\n');
246
293
 
247
- throw new Error(errorMsg);
294
+ const error = new Error(errorMsg);
295
+ error.pluginId = pluginId;
296
+ error.pluginName = pluginDef.name;
297
+ error.missing = missing;
298
+ error.incompatible = incompatible;
299
+ error.docsUrl = pluginDef.docsUrl;
300
+
301
+ throw error;
248
302
  }
249
303
 
250
304
  return { valid, missing, incompatible, messages };
@@ -559,8 +559,6 @@ async function getAnalyticsForRecord(resourceName, field, recordId, options, han
559
559
  return [];
560
560
  }
561
561
 
562
- // ✅ FIX BUG #2: Ensure all transactions have cohortHour calculated
563
- // This handles legacy data that may be missing cohortHour
564
562
  allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || 'UTC', false);
565
563
 
566
564
  // Filter transactions by temporal range
@@ -286,9 +286,7 @@ export async function consolidateRecord(
286
286
 
287
287
  if (!hasSetInApplied) {
288
288
  // No 'set' operation in applied transactions means we're missing the base value
289
- // This can only happen if:
290
- // 1. Record had an initial value before first transaction
291
- // 2. First consolidation didn't create an anchor transaction (legacy behavior)
289
+ // This can happen if record had an initial value before first transaction
292
290
  // Solution: Get the current record value and create an anchor transaction now
293
291
  const recordValue = recordExists[config.field] || 0;
294
292
 
@@ -525,7 +523,6 @@ export async function consolidateRecord(
525
523
  updateResult = result[2];
526
524
  }
527
525
 
528
- // For backward compatibility, return the value of the main field
529
526
  const consolidatedValue = consolidatedValues[config.field] ||
530
527
  (record ? lodash.get(record, config.field, 0) : 0);
531
528
 
@@ -610,13 +607,10 @@ export async function consolidateRecord(
610
607
 
611
608
  const { results, errors } = await PromisePool
612
609
  .for(transactionsToUpdate)
613
- .withConcurrency(markAppliedConcurrency) // ✅ Configurável e maior!
610
+ .withConcurrency(markAppliedConcurrency)
614
611
  .process(async (txn) => {
615
- // ✅ FIX BUG #3: Ensure cohort fields exist before marking as applied
616
- // This handles legacy transactions missing cohortHour, cohortDate, etc.
617
612
  const txnWithCohorts = ensureCohortHour(txn, config.cohort.timezone, false);
618
613
 
619
- // Build update data with applied flag
620
614
  const updateData = { applied: true };
621
615
 
622
616
  // Add missing cohort fields if they were calculated
@@ -633,11 +627,6 @@ export async function consolidateRecord(
633
627
  updateData.cohortMonth = txnWithCohorts.cohortMonth;
634
628
  }
635
629
 
636
- // Handle null value field (legacy data might have null)
637
- if (txn.value === null || txn.value === undefined) {
638
- updateData.value = 1; // Default to 1 for backward compatibility
639
- }
640
-
641
630
  const [ok, err] = await tryFn(() =>
642
631
  transactionResource.update(txn.id, updateData)
643
632
  );
@@ -183,7 +183,6 @@ export class EventualConsistencyPlugin extends Plugin {
183
183
  * @private
184
184
  */
185
185
  async _syncModeConsolidate(handler, id, field) {
186
- // Temporarily set config for legacy methods
187
186
  const oldResource = this.config.resource;
188
187
  const oldField = this.config.field;
189
188
  const oldTransactionResource = this.transactionResource;
@@ -100,7 +100,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
100
100
  operation: 'string|required',
101
101
  timestamp: 'string|required',
102
102
  cohortDate: 'string|required',
103
- cohortHour: 'string|optional', // ✅ FIX BUG #2: Changed from required to optional for migration compatibility
103
+ cohortHour: 'string|required',
104
104
  cohortWeek: 'string|optional',
105
105
  cohortMonth: 'string|optional',
106
106
  source: 'string|optional',
@@ -127,7 +127,7 @@ class GeoPlugin extends Plugin {
127
127
  return;
128
128
  }
129
129
 
130
- const resource = this.database.resource(resourceName);
130
+ const resource = this.database.resources[resourceName];
131
131
  if (!resource || typeof resource.addHook !== 'function') {
132
132
  if (this.verbose) {
133
133
  console.warn(`[GeoPlugin] Resource "${resourceName}" not found or invalid`);
@@ -325,14 +325,13 @@ class GeoPlugin extends Plugin {
325
325
  * Find nearby locations within radius
326
326
  * Automatically selects optimal zoom level if multi-zoom enabled
327
327
  */
328
- resource.findNearby = async function({ lat, lon, lng, radius = 10, limit = 100 }) {
329
- // Support both 'lon' and 'lng' for backward compatibility
330
- const longitude = lon !== undefined ? lon : lng;
331
-
332
- if (lat === undefined || longitude === undefined) {
328
+ resource.findNearby = async function({ lat, lon, radius = 10, limit = 100 }) {
329
+ if (lat === undefined || lon === undefined) {
333
330
  throw new Error('lat and lon are required for findNearby');
334
331
  }
335
332
 
333
+ const longitude = lon; // Alias for internal use
334
+
336
335
  let allRecords = [];
337
336
 
338
337
  // Use partitions if enabled for efficient queries
@@ -739,7 +739,7 @@ export class ImporterPlugin extends Plugin {
739
739
  async onInstall() {
740
740
  // Get resource - database.resource() returns a rejected Promise if not found
741
741
  try {
742
- this.resource = this.database.resource(this.resourceName);
742
+ this.resource = this.database.resources[this.resourceName];
743
743
  // If resource() returns a Promise, await it
744
744
  if (this.resource && typeof this.resource.then === 'function') {
745
745
  this.resource = await this.resource;
@@ -416,7 +416,7 @@ class RelationPlugin extends Plugin {
416
416
  * @private
417
417
  */
418
418
  async _setupResourceRelations(resourceName, relationsDef) {
419
- const resource = this.database.resource(resourceName);
419
+ const resource = this.database.resources[resourceName];
420
420
  if (!resource) {
421
421
  if (this.verbose) {
422
422
  console.warn(`[RelationPlugin] Resource "${resourceName}" not found, will setup when created`);
@@ -572,7 +572,7 @@ class RelationPlugin extends Plugin {
572
572
  for (const record of records) {
573
573
  const relatedData = record[relationName];
574
574
  if (relatedData) {
575
- const relatedResource = this.database.resource(config.resource);
575
+ const relatedResource = this.database.resources[config.resource];
576
576
  const relatedArray = Array.isArray(relatedData) ? relatedData : [relatedData];
577
577
 
578
578
  if (relatedArray.length > 0) {
@@ -635,7 +635,7 @@ class RelationPlugin extends Plugin {
635
635
  * @private
636
636
  */
637
637
  async _loadHasOne(records, relationName, config, sourceResource) {
638
- const relatedResource = this.database.resource(config.resource);
638
+ const relatedResource = this.database.resources[config.resource];
639
639
  if (!relatedResource) {
640
640
  throw new RelatedResourceNotFoundError(config.resource, {
641
641
  sourceResource: sourceResource.name,
@@ -689,7 +689,7 @@ class RelationPlugin extends Plugin {
689
689
  * @private
690
690
  */
691
691
  async _loadHasMany(records, relationName, config, sourceResource) {
692
- const relatedResource = this.database.resource(config.resource);
692
+ const relatedResource = this.database.resources[config.resource];
693
693
  if (!relatedResource) {
694
694
  throw new RelatedResourceNotFoundError(config.resource, {
695
695
  sourceResource: sourceResource.name,
@@ -751,7 +751,7 @@ class RelationPlugin extends Plugin {
751
751
  * @private
752
752
  */
753
753
  async _loadBelongsTo(records, relationName, config, sourceResource) {
754
- const relatedResource = this.database.resource(config.resource);
754
+ const relatedResource = this.database.resources[config.resource];
755
755
  if (!relatedResource) {
756
756
  throw new RelatedResourceNotFoundError(config.resource, {
757
757
  sourceResource: sourceResource.name,
@@ -818,7 +818,7 @@ class RelationPlugin extends Plugin {
818
818
  * @private
819
819
  */
820
820
  async _loadBelongsToMany(records, relationName, config, sourceResource) {
821
- const relatedResource = this.database.resource(config.resource);
821
+ const relatedResource = this.database.resources[config.resource];
822
822
  if (!relatedResource) {
823
823
  throw new RelatedResourceNotFoundError(config.resource, {
824
824
  sourceResource: sourceResource.name,
@@ -826,7 +826,7 @@ class RelationPlugin extends Plugin {
826
826
  });
827
827
  }
828
828
 
829
- const junctionResource = this.database.resource(config.through);
829
+ const junctionResource = this.database.resources[config.through];
830
830
  if (!junctionResource) {
831
831
  throw new JunctionTableNotFoundError(config.through, {
832
832
  sourceResource: sourceResource.name,
@@ -1077,7 +1077,7 @@ class RelationPlugin extends Plugin {
1077
1077
  async _cascadeDelete(record, resource, relationName, config) {
1078
1078
  this.stats.cascadeOperations++;
1079
1079
 
1080
- const relatedResource = this.database.resource(config.resource);
1080
+ const relatedResource = this.database.resources[config.resource];
1081
1081
  if (!relatedResource) {
1082
1082
  throw new RelatedResourceNotFoundError(config.resource, {
1083
1083
  sourceResource: resource.name,
@@ -1087,7 +1087,7 @@ class RelationPlugin extends Plugin {
1087
1087
 
1088
1088
  // Track deleted records for rollback (if transactions enabled)
1089
1089
  const deletedRecords = [];
1090
- const junctionResource = config.type === 'belongsToMany' ? this.database.resource(config.through) : null;
1090
+ const junctionResource = config.type === 'belongsToMany' ? this.database.resources[config.through] : null;
1091
1091
 
1092
1092
  try {
1093
1093
  if (config.type === 'hasMany') {
@@ -1156,7 +1156,7 @@ class RelationPlugin extends Plugin {
1156
1156
  }
1157
1157
  } else if (config.type === 'belongsToMany') {
1158
1158
  // Delete junction table entries - use partition if available
1159
- const junctionResource = this.database.resource(config.through);
1159
+ const junctionResource = this.database.resources[config.through];
1160
1160
  if (junctionResource) {
1161
1161
  let junctionRecords;
1162
1162
  const partitionName = this._findPartitionByField(junctionResource, config.foreignKey);
@@ -1240,7 +1240,7 @@ class RelationPlugin extends Plugin {
1240
1240
  async _cascadeUpdate(record, changes, resource, relationName, config) {
1241
1241
  this.stats.cascadeOperations++;
1242
1242
 
1243
- const relatedResource = this.database.resource(config.resource);
1243
+ const relatedResource = this.database.resources[config.resource];
1244
1244
  if (!relatedResource) {
1245
1245
  return;
1246
1246
  }
@@ -306,18 +306,6 @@ export class ReplicatorPlugin extends Plugin {
306
306
  // Plugin is ready
307
307
  }
308
308
 
309
- async stop() {
310
- // Stop all replicators
311
- for (const replicator of this.replicators || []) {
312
- if (replicator && typeof replicator.cleanup === 'function') {
313
- await replicator.cleanup();
314
- }
315
- }
316
-
317
- // Remove database hooks
318
- this.removeDatabaseHooks();
319
- }
320
-
321
309
  installDatabaseHooks() {
322
310
  // Store hook reference for later removal
323
311
  this._afterCreateResourceHook = (resource) => {
@@ -730,21 +718,21 @@ export class ReplicatorPlugin extends Plugin {
730
718
  this.emit('replicator.sync.completed', { replicatorId, stats: this.stats });
731
719
  }
732
720
 
733
- async cleanup() {
721
+ async stop() {
734
722
  const [ok, error] = await tryFn(async () => {
735
723
  if (this.replicators && this.replicators.length > 0) {
736
724
  const cleanupPromises = this.replicators.map(async (replicator) => {
737
725
  const [replicatorOk, replicatorError] = await tryFn(async () => {
738
- if (replicator && typeof replicator.cleanup === 'function') {
739
- await replicator.cleanup();
726
+ if (replicator && typeof replicator.stop === 'function') {
727
+ await replicator.stop();
740
728
  }
741
729
  });
742
-
730
+
743
731
  if (!replicatorOk) {
744
732
  if (this.config.verbose) {
745
- console.warn(`[ReplicatorPlugin] Failed to cleanup replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
733
+ console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
746
734
  }
747
- this.emit('replicator_cleanup_error', {
735
+ this.emit('replicator_stop_error', {
748
736
  replicator: replicator.name || replicator.id || 'unknown',
749
737
  driver: replicator.driver || 'unknown',
750
738
  error: replicatorError.message
@@ -754,7 +742,10 @@ export class ReplicatorPlugin extends Plugin {
754
742
 
755
743
  await Promise.allSettled(cleanupPromises);
756
744
  }
757
-
745
+
746
+ // Remove database hooks
747
+ this.removeDatabaseHooks();
748
+
758
749
  // Remove event listeners from resources to prevent memory leaks
759
750
  if (this.database && this.database.resources) {
760
751
  for (const resourceName of this.eventListenersInstalled) {
@@ -779,9 +770,9 @@ export class ReplicatorPlugin extends Plugin {
779
770
 
780
771
  if (!ok) {
781
772
  if (this.config.verbose) {
782
- console.warn(`[ReplicatorPlugin] Failed to cleanup plugin: ${error.message}`);
773
+ console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
783
774
  }
784
- this.emit('replicator_plugin_cleanup_error', {
775
+ this.emit('replicator_plugin_stop_error', {
785
776
  error: error.message
786
777
  });
787
778
  }
@@ -42,22 +42,22 @@ import { idGenerator } from "../concerns/id.js";
42
42
  * === Usage ===
43
43
  *
44
44
  * // Enqueue a message
45
- * await db.resource('emails').enqueue({
45
+ * await db.resources.emails.enqueue({
46
46
  * to: 'user@example.com',
47
47
  * subject: 'Hello',
48
48
  * body: 'World'
49
49
  * });
50
50
  *
51
51
  * // Start processing (if not auto-started)
52
- * await db.resource('emails').startProcessing(async (email) => {
52
+ * await db.resources.emails.startProcessing(async (email) => {
53
53
  * await sendEmail(email);
54
54
  * }, { concurrency: 10 });
55
55
  *
56
56
  * // Stop processing
57
- * await db.resource('emails').stopProcessing();
57
+ * await db.resources.emails.stopProcessing();
58
58
  *
59
59
  * // Get queue statistics
60
- * const stats = await db.resource('emails').queueStats();
60
+ * const stats = await db.resources.emails.queueStats();
61
61
  * // { total: 100, pending: 50, processing: 20, completed: 25, failed: 5, dead: 0 }
62
62
  */
63
63
  export class S3QueuePlugin extends Plugin {
@@ -30,11 +30,11 @@ import { SchedulerError } from "./scheduler.errors.js";
30
30
  * schedule: '0 3 * * *',
31
31
  * description: 'Clean up expired records',
32
32
  * action: async (database, context) => {
33
- * const expired = await this.database.resource('sessions')
33
+ * const expired = await this.database.resources['sessions')
34
34
  * .list({ where: { expiresAt: { $lt: new Date() } } });
35
35
  *
36
36
  * for (const record of expired) {
37
- * await this.database.resource('sessions').delete(record.id);
37
+ * await this.database.resources['sessions').delete(record.id);
38
38
  * }
39
39
  *
40
40
  * return { deleted: expired.length };
@@ -49,8 +49,8 @@ import { SchedulerError } from "./scheduler.errors.js";
49
49
  * schedule: '0 9 * * MON',
50
50
  * description: 'Generate weekly analytics report',
51
51
  * action: async (database, context) => {
52
- * const users = await this.database.resource('users').count();
53
- * const orders = await this.database.resource('orders').count({
52
+ * const users = await this.database.resources['users').count();
53
+ * const orders = await this.database.resources['orders').count({
54
54
  * where: {
55
55
  * createdAt: {
56
56
  * $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
@@ -65,7 +65,7 @@ import { SchedulerError } from "./scheduler.errors.js";
65
65
  * createdAt: new Date().toISOString()
66
66
  * };
67
67
  *
68
- * await this.database.resource('reports').insert(report);
68
+ * await this.database.resources['reports').insert(report);
69
69
  * return report;
70
70
  * }
71
71
  * },
@@ -107,7 +107,7 @@ import { SchedulerError } from "./scheduler.errors.js";
107
107
  * const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
108
108
  *
109
109
  * // Aggregate metrics from the last hour
110
- * const events = await this.database.resource('events').list({
110
+ * const events = await this.database.resources['events').list({
111
111
  * where: {
112
112
  * timestamp: {
113
113
  * $gte: hourAgo.getTime(),
@@ -121,7 +121,7 @@ import { SchedulerError } from "./scheduler.errors.js";
121
121
  * return acc;
122
122
  * }, {});
123
123
  *
124
- * await this.database.resource('hourly_metrics').insert({
124
+ * await this.database.resources['hourly_metrics').insert({
125
125
  * hour: hourAgo.toISOString().slice(0, 13),
126
126
  * metrics: aggregated,
127
127
  * total: events.length,
@@ -577,7 +577,7 @@ export class SchedulerPlugin extends Plugin {
577
577
 
578
578
  async _persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, error, retryCount) {
579
579
  const [ok, err] = await tryFn(() =>
580
- this.database.resource(this.config.jobHistoryResource).insert({
580
+ this.database.resources[this.config.jobHistoryResource].insert({
581
581
  id: executionId,
582
582
  jobName,
583
583
  status,
@@ -741,7 +741,7 @@ export class SchedulerPlugin extends Plugin {
741
741
 
742
742
  // Use query() to leverage partitions instead of list() + filter
743
743
  const [ok, err, history] = await tryFn(() =>
744
- this.database.resource(this.config.jobHistoryResource).query(queryParams)
744
+ this.database.resources[this.config.jobHistoryResource].query(queryParams)
745
745
  );
746
746
 
747
747
  if (!ok) {
@@ -914,10 +914,8 @@ export class SchedulerPlugin extends Plugin {
914
914
  if (this._isTestEnvironment()) {
915
915
  this.activeJobs.clear();
916
916
  }
917
- }
918
917
 
919
- async cleanup() {
920
- await this.stop();
918
+ // Cleanup resources
921
919
  this.jobs.clear();
922
920
  this.statistics.clear();
923
921
  this.activeJobs.clear();
@@ -62,7 +62,7 @@ import { StateMachineError } from "./state-machine.errors.js";
62
62
  *
63
63
  * actions: {
64
64
  * onConfirmed: async (context, event, machine) => {
65
- * await machine.this.database.resource('inventory').update(context.productId, {
65
+ * await machine.this.database.resources['inventory'].update(context.productId, {
66
66
  * quantity: { $decrement: context.quantity }
67
67
  * });
68
68
  * await machine.sendNotification(context.customerEmail, 'order_confirmed');
@@ -74,7 +74,7 @@ import { StateMachineError } from "./state-machine.errors.js";
74
74
  *
75
75
  * guards: {
76
76
  * canShip: async (context, event, machine) => {
77
- * const inventory = await machine.this.database.resource('inventory').get(context.productId);
77
+ * const inventory = await machine.this.database.resources['inventory'].get(context.productId);
78
78
  * return inventory.quantity >= context.quantity;
79
79
  * }
80
80
  * },
@@ -352,7 +352,7 @@ export class StateMachinePlugin extends Plugin {
352
352
 
353
353
  for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
354
354
  const [ok, err] = await tryFn(() =>
355
- this.database.resource(this.config.transitionLogResource).insert({
355
+ this.database.resources[this.config.transitionLogResource].insert({
356
356
  id: transitionId,
357
357
  machineId,
358
358
  entityId,
@@ -395,13 +395,13 @@ export class StateMachinePlugin extends Plugin {
395
395
 
396
396
  // Try update first (most common case), fallback to insert if doesn't exist
397
397
  const [updateOk] = await tryFn(() =>
398
- this.database.resource(this.config.stateResource).update(stateId, stateData)
398
+ this.database.resources[this.config.stateResource].update(stateId, stateData)
399
399
  );
400
400
 
401
401
  if (!updateOk) {
402
402
  // Record doesn't exist, insert it
403
403
  const [insertOk, insertErr] = await tryFn(() =>
404
- this.database.resource(this.config.stateResource).insert({ id: stateId, ...stateData })
404
+ this.database.resources[this.config.stateResource].insert({ id: stateId, ...stateData })
405
405
  );
406
406
 
407
407
  if (!insertOk && this.config.verbose) {
@@ -476,7 +476,7 @@ export class StateMachinePlugin extends Plugin {
476
476
  if (this.config.persistTransitions) {
477
477
  const stateId = `${machineId}_${entityId}`;
478
478
  const [ok, err, stateRecord] = await tryFn(() =>
479
- this.database.resource(this.config.stateResource).get(stateId)
479
+ this.database.resources[this.config.stateResource].get(stateId)
480
480
  );
481
481
 
482
482
  if (ok && stateRecord) {
@@ -530,7 +530,7 @@ export class StateMachinePlugin extends Plugin {
530
530
  const { limit = 50, offset = 0 } = options;
531
531
 
532
532
  const [ok, err, transitions] = await tryFn(() =>
533
- this.database.resource(this.config.transitionLogResource).query({
533
+ this.database.resources[this.config.transitionLogResource].query({
534
534
  machineId,
535
535
  entityId
536
536
  }, {
@@ -581,7 +581,7 @@ export class StateMachinePlugin extends Plugin {
581
581
 
582
582
  // Try to insert, ignore if already exists (idempotent)
583
583
  const [ok, err] = await tryFn(() =>
584
- this.database.resource(this.config.stateResource).insert({
584
+ this.database.resources[this.config.stateResource].insert({
585
585
  id: stateId,
586
586
  machineId,
587
587
  entityId,
@@ -682,10 +682,6 @@ export class StateMachinePlugin extends Plugin {
682
682
 
683
683
  async stop() {
684
684
  this.machines.clear();
685
- }
686
-
687
- async cleanup() {
688
- await this.stop();
689
685
  this.removeAllListeners();
690
686
  }
691
687
  }
@@ -740,6 +740,6 @@ Verificar que partitions são rápidas:
740
740
 
741
741
  ## 📚 References
742
742
 
743
- - [Terraform State Format](https://www.terraform.io/internals/json-format)
743
+ - [Tfstate Format](https://www.terraform.io/internals/json-format)
744
744
  - [s3db Partitioning Guide](../../docs/partitioning.md)
745
745
  - [Plugin Development](../../docs/plugins.md)