s3db.js 10.0.5 → 10.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "10.0.5",
3
+ "version": "10.0.7",
4
4
  "description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
5
5
  "main": "dist/s3db.cjs.js",
6
6
  "module": "dist/s3db.es.js",
@@ -79,16 +79,16 @@
79
79
  "validate:types": "pnpm run test:ts && echo 'TypeScript definitions are valid!'"
80
80
  },
81
81
  "dependencies": {
82
- "@aws-sdk/client-s3": "^3.873.0",
83
- "@modelcontextprotocol/sdk": "^1.17.4",
84
- "@smithy/node-http-handler": "^4.1.1",
82
+ "@aws-sdk/client-s3": "^3.906.0",
83
+ "@modelcontextprotocol/sdk": "^1.19.1",
84
+ "@smithy/node-http-handler": "^4.3.0",
85
85
  "@supercharge/promise-pool": "^3.2.0",
86
- "dotenv": "^17.2.1",
86
+ "dotenv": "^17.2.3",
87
87
  "fastest-validator": "^1.19.1",
88
88
  "flat": "^6.0.1",
89
89
  "json-stable-stringify": "^1.3.0",
90
90
  "lodash-es": "^4.17.21",
91
- "nanoid": "5.1.5"
91
+ "nanoid": "5.1.6"
92
92
  },
93
93
  "peerDependencies": {
94
94
  "@aws-sdk/client-sqs": "^3.0.0",
@@ -115,32 +115,32 @@
115
115
  }
116
116
  },
117
117
  "devDependencies": {
118
- "@babel/core": "^7.28.3",
118
+ "@babel/core": "^7.28.4",
119
119
  "@babel/preset-env": "^7.28.3",
120
120
  "@rollup/plugin-commonjs": "^28.0.6",
121
121
  "@rollup/plugin-json": "^6.1.0",
122
- "@rollup/plugin-node-resolve": "^16.0.1",
122
+ "@rollup/plugin-node-resolve": "^16.0.2",
123
123
  "@rollup/plugin-replace": "^6.0.2",
124
124
  "@rollup/plugin-terser": "^0.4.4",
125
- "@types/node": "24.3.0",
125
+ "@types/node": "24.7.0",
126
126
  "babel-loader": "^10.0.0",
127
- "chalk": "^5.6.0",
127
+ "chalk": "^5.6.2",
128
128
  "cli-table3": "^0.6.5",
129
- "commander": "^14.0.0",
130
- "esbuild": "^0.25.9",
131
- "inquirer": "^12.9.3",
132
- "jest": "^30.0.5",
129
+ "commander": "^14.0.1",
130
+ "esbuild": "^0.25.10",
131
+ "inquirer": "^12.9.6",
132
+ "jest": "^30.2.0",
133
133
  "node-loader": "^2.1.0",
134
- "ora": "^8.2.0",
134
+ "ora": "^9.0.0",
135
135
  "pkg": "^5.8.1",
136
- "rollup": "^4.48.0",
136
+ "rollup": "^4.52.4",
137
137
  "rollup-plugin-copy": "^3.5.0",
138
138
  "rollup-plugin-esbuild": "^6.2.1",
139
139
  "rollup-plugin-polyfill-node": "^0.13.0",
140
140
  "rollup-plugin-shebang-bin": "^0.1.0",
141
141
  "rollup-plugin-terser": "^7.0.2",
142
- "typescript": "5.9.2",
143
- "webpack": "^5.101.3",
142
+ "typescript": "5.9.3",
143
+ "webpack": "^5.102.1",
144
144
  "webpack-cli": "^6.0.1"
145
145
  },
146
146
  "funding": [
@@ -475,6 +475,7 @@ export class Database extends EventEmitter {
475
475
  metadata.resources[name] = {
476
476
  currentVersion: version,
477
477
  partitions: resource.config.partitions || {},
478
+ createdBy: existingResource?.createdBy || resource.config.createdBy || 'user',
478
479
  versions: {
479
480
  ...existingResource?.versions, // Preserve previous versions
480
481
  [version]: {
@@ -906,6 +907,7 @@ export class Database extends EventEmitter {
906
907
  * @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
907
908
  * @param {Function|number} [config.idGenerator] - Custom ID generator or size
908
909
  * @param {number} [config.idSize=22] - Size for auto-generated IDs
910
+ * @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
909
911
  * @returns {Promise<Resource>} The created or updated resource
910
912
  */
911
913
  async createResource({ name, attributes, behavior = 'user-managed', hooks, ...config }) {
@@ -968,7 +970,8 @@ export class Database extends EventEmitter {
968
970
  idGenerator: config.idGenerator,
969
971
  idSize: config.idSize,
970
972
  asyncEvents: config.asyncEvents,
971
- events: config.events || {}
973
+ events: config.events || {},
974
+ createdBy: config.createdBy || 'user'
972
975
  });
973
976
  resource.database = this;
974
977
  this.resources[name] = resource;
@@ -113,7 +113,15 @@ export class CachePlugin extends Plugin {
113
113
  }
114
114
 
115
115
  shouldCacheResource(resourceName) {
116
- // Skip plugin resources by default (unless explicitly included)
116
+ // Get resource metadata to check createdBy
117
+ const resourceMetadata = this.database.savedMetadata?.resources?.[resourceName];
118
+
119
+ // Skip plugin-created resources by default (unless explicitly included)
120
+ if (resourceMetadata?.createdBy && resourceMetadata.createdBy !== 'user' && !this.config.include) {
121
+ return false;
122
+ }
123
+
124
+ // Legacy: Skip plugin resources by name pattern (unless explicitly included)
117
125
  if (resourceName.startsWith('plg_') && !this.config.include) {
118
126
  return false;
119
127
  }
@@ -144,7 +144,8 @@ export class EventualConsistencyPlugin extends Plugin {
144
144
  behavior: 'body-overflow',
145
145
  timestamps: true,
146
146
  partitions: partitionConfig,
147
- asyncPartitions: true // Use async partitions for better performance
147
+ asyncPartitions: true, // Use async partitions for better performance
148
+ createdBy: 'EventualConsistencyPlugin'
148
149
  })
149
150
  );
150
151
 
@@ -165,7 +166,8 @@ export class EventualConsistencyPlugin extends Plugin {
165
166
  workerId: 'string|optional'
166
167
  },
167
168
  behavior: 'body-only',
168
- timestamps: false
169
+ timestamps: false,
170
+ createdBy: 'EventualConsistencyPlugin'
169
171
  })
170
172
  );
171
173
 
@@ -186,10 +188,33 @@ export class EventualConsistencyPlugin extends Plugin {
186
188
  // Setup consolidation if enabled
187
189
  if (this.config.autoConsolidate) {
188
190
  this.startConsolidationTimer();
191
+ if (this.config.verbose) {
192
+ console.log(
193
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
194
+ `Auto-consolidation ENABLED (interval: ${this.config.consolidationInterval}s, ` +
195
+ `window: ${this.config.consolidationWindow}h, mode: ${this.config.mode})`
196
+ );
197
+ }
198
+ } else {
199
+ if (this.config.verbose) {
200
+ console.log(
201
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
202
+ `Auto-consolidation DISABLED (manual consolidation only)`
203
+ );
204
+ }
189
205
  }
190
206
 
191
207
  // Setup garbage collection timer
192
208
  this.startGarbageCollectionTimer();
209
+
210
+ if (this.config.verbose) {
211
+ console.log(
212
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
213
+ `Setup complete. Resources: ${this.config.resource}_transactions_${this.config.field}, ` +
214
+ `${this.config.resource}_consolidation_locks_${this.config.field}` +
215
+ `${this.config.enableAnalytics ? `, ${this.config.resource}_analytics_${this.config.field}` : ''}`
216
+ );
217
+ }
193
218
  }
194
219
 
195
220
  async onStart() {
@@ -286,7 +311,8 @@ export class EventualConsistencyPlugin extends Plugin {
286
311
  byCohort: {
287
312
  fields: { cohort: 'string' }
288
313
  }
289
- }
314
+ },
315
+ createdBy: 'EventualConsistencyPlugin'
290
316
  })
291
317
  );
292
318
 
@@ -539,15 +565,31 @@ export class EventualConsistencyPlugin extends Plugin {
539
565
  // Batch transactions if configured
540
566
  if (this.config.batchTransactions) {
541
567
  this.pendingTransactions.set(transaction.id, transaction);
542
-
568
+
569
+ if (this.config.verbose) {
570
+ console.log(
571
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
572
+ `Transaction batched: ${data.operation} ${data.value} for ${data.originalId} ` +
573
+ `(batch: ${this.pendingTransactions.size}/${this.config.batchSize})`
574
+ );
575
+ }
576
+
543
577
  // Flush if batch size reached
544
578
  if (this.pendingTransactions.size >= this.config.batchSize) {
545
579
  await this.flushPendingTransactions();
546
580
  }
547
581
  } else {
548
582
  await this.transactionResource.insert(transaction);
583
+
584
+ if (this.config.verbose) {
585
+ console.log(
586
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
587
+ `Transaction created: ${data.operation} ${data.value} for ${data.originalId} ` +
588
+ `(cohort: ${cohortInfo.hour}, applied: false)`
589
+ );
590
+ }
549
591
  }
550
-
592
+
551
593
  return transaction;
552
594
  }
553
595
 
@@ -636,12 +678,30 @@ export class EventualConsistencyPlugin extends Plugin {
636
678
  startConsolidationTimer() {
637
679
  const intervalMs = this.config.consolidationInterval * 1000; // Convert seconds to ms
638
680
 
681
+ if (this.config.verbose) {
682
+ const nextRun = new Date(Date.now() + intervalMs);
683
+ console.log(
684
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
685
+ `Consolidation timer started. Next run at ${nextRun.toISOString()} ` +
686
+ `(every ${this.config.consolidationInterval}s)`
687
+ );
688
+ }
689
+
639
690
  this.consolidationTimer = setInterval(async () => {
640
691
  await this.runConsolidation();
641
692
  }, intervalMs);
642
693
  }
643
694
 
644
695
  async runConsolidation() {
696
+ const startTime = Date.now();
697
+
698
+ if (this.config.verbose) {
699
+ console.log(
700
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
701
+ `Starting consolidation run at ${new Date().toISOString()}`
702
+ );
703
+ }
704
+
645
705
  try {
646
706
  // Query unapplied transactions from recent cohorts (last 24 hours by default)
647
707
  // This uses hourly partition for O(1) performance instead of full scan
@@ -655,6 +715,13 @@ export class EventualConsistencyPlugin extends Plugin {
655
715
  cohortHours.push(cohortInfo.hour);
656
716
  }
657
717
 
718
+ if (this.config.verbose) {
719
+ console.log(
720
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
721
+ `Querying ${hoursToCheck} hour partitions for pending transactions...`
722
+ );
723
+ }
724
+
658
725
  // Query transactions by partition for each hour (parallel for speed)
659
726
  const transactionsByHour = await Promise.all(
660
727
  cohortHours.map(async (cohortHour) => {
@@ -673,7 +740,10 @@ export class EventualConsistencyPlugin extends Plugin {
673
740
 
674
741
  if (transactions.length === 0) {
675
742
  if (this.config.verbose) {
676
- console.log(`[EventualConsistency] No pending transactions to consolidate`);
743
+ console.log(
744
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
745
+ `No pending transactions found. Next run in ${this.config.consolidationInterval}s`
746
+ );
677
747
  }
678
748
  return;
679
749
  }
@@ -681,6 +751,14 @@ export class EventualConsistencyPlugin extends Plugin {
681
751
  // Get unique originalIds
682
752
  const uniqueIds = [...new Set(transactions.map(t => t.originalId))];
683
753
 
754
+ if (this.config.verbose) {
755
+ console.log(
756
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
757
+ `Found ${transactions.length} pending transactions for ${uniqueIds.length} records. ` +
758
+ `Consolidating with concurrency=${this.config.consolidationConcurrency}...`
759
+ );
760
+ }
761
+
684
762
  // Consolidate each record in parallel with concurrency limit
685
763
  const { results, errors } = await PromisePool
686
764
  .for(uniqueIds)
@@ -689,8 +767,22 @@ export class EventualConsistencyPlugin extends Plugin {
689
767
  return await this.consolidateRecord(id);
690
768
  });
691
769
 
770
+ const duration = Date.now() - startTime;
771
+
692
772
  if (errors && errors.length > 0) {
693
- console.error(`Consolidation completed with ${errors.length} errors:`, errors);
773
+ console.error(
774
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
775
+ `Consolidation completed with ${errors.length} errors in ${duration}ms:`,
776
+ errors
777
+ );
778
+ }
779
+
780
+ if (this.config.verbose) {
781
+ console.log(
782
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
783
+ `Consolidation complete: ${results.length} records consolidated in ${duration}ms ` +
784
+ `(${errors.length} errors). Next run in ${this.config.consolidationInterval}s`
785
+ );
694
786
  }
695
787
 
696
788
  this.emit('eventual-consistency.consolidated', {
@@ -698,10 +790,16 @@ export class EventualConsistencyPlugin extends Plugin {
698
790
  field: this.config.field,
699
791
  recordCount: uniqueIds.length,
700
792
  successCount: results.length,
701
- errorCount: errors.length
793
+ errorCount: errors.length,
794
+ duration
702
795
  });
703
796
  } catch (error) {
704
- console.error('Consolidation error:', error);
797
+ const duration = Date.now() - startTime;
798
+ console.error(
799
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
800
+ `Consolidation error after ${duration}ms:`,
801
+ error
802
+ );
705
803
  this.emit('eventual-consistency.consolidation-error', error);
706
804
  }
707
805
  }
@@ -749,9 +847,23 @@ export class EventualConsistencyPlugin extends Plugin {
749
847
  );
750
848
 
751
849
  if (!ok || !transactions || transactions.length === 0) {
850
+ if (this.config.verbose) {
851
+ console.log(
852
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
853
+ `No pending transactions for ${originalId}, skipping`
854
+ );
855
+ }
752
856
  return currentValue;
753
857
  }
754
858
 
859
+ if (this.config.verbose) {
860
+ console.log(
861
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
862
+ `Consolidating ${originalId}: ${transactions.length} pending transactions ` +
863
+ `(current: ${currentValue})`
864
+ );
865
+ }
866
+
755
867
  // Sort transactions by timestamp
756
868
  transactions.sort((a, b) =>
757
869
  new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
@@ -766,6 +878,14 @@ export class EventualConsistencyPlugin extends Plugin {
766
878
  // Apply reducer to get consolidated value
767
879
  const consolidatedValue = this.config.reducer(transactions);
768
880
 
881
+ if (this.config.verbose) {
882
+ console.log(
883
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
884
+ `${originalId}: ${currentValue} → ${consolidatedValue} ` +
885
+ `(${consolidatedValue > currentValue ? '+' : ''}${consolidatedValue - currentValue})`
886
+ );
887
+ }
888
+
769
889
  // Update the original record
770
890
  const [updateOk, updateErr] = await tryFn(() =>
771
891
  this.targetResource.update(originalId, {
@@ -800,6 +920,29 @@ export class EventualConsistencyPlugin extends Plugin {
800
920
  if (this.config.enableAnalytics && transactionsToUpdate.length > 0) {
801
921
  await this.updateAnalytics(transactionsToUpdate);
802
922
  }
923
+
924
+ // Invalidate cache for this record after consolidation
925
+ if (this.targetResource.cache && typeof this.targetResource.cache.delete === 'function') {
926
+ try {
927
+ const cacheKey = await this.targetResource.cacheKeyFor({ id: originalId });
928
+ await this.targetResource.cache.delete(cacheKey);
929
+
930
+ if (this.config.verbose) {
931
+ console.log(
932
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
933
+ `Cache invalidated for ${originalId}`
934
+ );
935
+ }
936
+ } catch (cacheErr) {
937
+ // Log but don't fail consolidation if cache invalidation fails
938
+ if (this.config.verbose) {
939
+ console.warn(
940
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
941
+ `Failed to invalidate cache for ${originalId}: ${cacheErr?.message}`
942
+ );
943
+ }
944
+ }
945
+ }
803
946
  }
804
947
 
805
948
  return consolidatedValue;
@@ -1087,9 +1230,24 @@ export class EventualConsistencyPlugin extends Plugin {
1087
1230
  async updateAnalytics(transactions) {
1088
1231
  if (!this.analyticsResource || transactions.length === 0) return;
1089
1232
 
1233
+ if (this.config.verbose) {
1234
+ console.log(
1235
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1236
+ `Updating analytics for ${transactions.length} transactions...`
1237
+ );
1238
+ }
1239
+
1090
1240
  try {
1091
1241
  // Group transactions by cohort hour
1092
1242
  const byHour = this._groupByCohort(transactions, 'cohortHour');
1243
+ const cohortCount = Object.keys(byHour).length;
1244
+
1245
+ if (this.config.verbose) {
1246
+ console.log(
1247
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1248
+ `Updating ${cohortCount} hourly analytics cohorts...`
1249
+ );
1250
+ }
1093
1251
 
1094
1252
  // Update hourly analytics
1095
1253
  for (const [cohort, txns] of Object.entries(byHour)) {
@@ -1099,14 +1257,31 @@ export class EventualConsistencyPlugin extends Plugin {
1099
1257
  // Roll up to daily and monthly if configured
1100
1258
  if (this.config.analyticsConfig.rollupStrategy === 'incremental') {
1101
1259
  const uniqueHours = Object.keys(byHour);
1260
+
1261
+ if (this.config.verbose) {
1262
+ console.log(
1263
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1264
+ `Rolling up ${uniqueHours.length} hours to daily/monthly analytics...`
1265
+ );
1266
+ }
1267
+
1102
1268
  for (const cohortHour of uniqueHours) {
1103
1269
  await this._rollupAnalytics(cohortHour);
1104
1270
  }
1105
1271
  }
1106
- } catch (error) {
1272
+
1107
1273
  if (this.config.verbose) {
1108
- console.warn(`[EventualConsistency] Analytics update error:`, error.message);
1274
+ console.log(
1275
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1276
+ `Analytics update complete for ${cohortCount} cohorts`
1277
+ );
1109
1278
  }
1279
+ } catch (error) {
1280
+ console.warn(
1281
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1282
+ `Analytics update error:`,
1283
+ error.message
1284
+ );
1110
1285
  }
1111
1286
  }
1112
1287
 
@@ -136,7 +136,8 @@ export class Resource extends AsyncEventEmitter {
136
136
  versioningEnabled = false,
137
137
  events = {},
138
138
  asyncEvents = true,
139
- asyncPartitions = true
139
+ asyncPartitions = true,
140
+ createdBy = 'user'
140
141
  } = config;
141
142
 
142
143
  // Set instance properties
@@ -179,6 +180,7 @@ export class Resource extends AsyncEventEmitter {
179
180
  allNestedObjectsOptional,
180
181
  asyncEvents,
181
182
  asyncPartitions,
183
+ createdBy,
182
184
  };
183
185
 
184
186
  // Initialize hooks system