s3db.js 11.2.2 → 11.2.3

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/dist/s3db.cjs.js CHANGED
@@ -15,6 +15,7 @@ var jsonStableStringify = require('json-stable-stringify');
15
15
  var stream = require('stream');
16
16
  var promisePool = require('@supercharge/promise-pool');
17
17
  var web = require('node:stream/web');
18
+ var os$1 = require('node:os');
18
19
  var lodashEs = require('lodash-es');
19
20
  var http = require('http');
20
21
  var https = require('https');
@@ -221,7 +222,7 @@ function calculateEffectiveLimit(config = {}) {
221
222
  }
222
223
 
223
224
  class BaseError extends Error {
224
- constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, suggestion, ...rest }) {
225
+ constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, suggestion, description, ...rest }) {
225
226
  if (verbose) message = message + `
226
227
 
227
228
  Verbose:
@@ -247,6 +248,7 @@ ${JSON.stringify(rest, null, 2)}`;
247
248
  this.commandInput = commandInput;
248
249
  this.metadata = metadata;
249
250
  this.suggestion = suggestion;
251
+ this.description = description;
250
252
  this.data = { bucket, key, ...rest, verbose, message };
251
253
  }
252
254
  toJson() {
@@ -264,6 +266,7 @@ ${JSON.stringify(rest, null, 2)}`;
264
266
  commandInput: this.commandInput,
265
267
  metadata: this.metadata,
266
268
  suggestion: this.suggestion,
269
+ description: this.description,
267
270
  data: this.data,
268
271
  original: this.original,
269
272
  stack: this.stack
@@ -456,7 +459,107 @@ class ResourceError extends S3dbError {
456
459
  }
457
460
  class PartitionError extends S3dbError {
458
461
  constructor(message, details = {}) {
459
- super(message, { ...details, suggestion: details.suggestion || "Check partition definition, fields, and input values." });
462
+ let description = details.description;
463
+ if (!description && details.resourceName && details.partitionName && details.fieldName) {
464
+ const { resourceName, partitionName, fieldName, availableFields = [] } = details;
465
+ description = `
466
+ Partition Field Validation Error
467
+
468
+ Resource: ${resourceName}
469
+ Partition: ${partitionName}
470
+ Missing Field: ${fieldName}
471
+
472
+ Available fields in schema:
473
+ ${availableFields.map((f) => ` \u2022 ${f}`).join("\n") || " (no fields defined)"}
474
+
475
+ Possible causes:
476
+ 1. Field was removed from schema but partition still references it
477
+ 2. Typo in partition field name
478
+ 3. Nested field path is incorrect (use dot notation like 'utm.source')
479
+
480
+ Solution:
481
+ ${details.strictValidation === false ? " \u2022 Update partition definition to use existing fields" : ` \u2022 Add missing field to schema, OR
482
+ \u2022 Update partition definition to use existing fields, OR
483
+ \u2022 Use strictValidation: false to skip this check during testing`}
484
+
485
+ Docs: https://docs.s3db.js.org/resources/partitions#validation
486
+ `.trim();
487
+ }
488
+ super(message, {
489
+ ...details,
490
+ description,
491
+ suggestion: details.suggestion || "Check partition definition, fields, and input values."
492
+ });
493
+ }
494
+ }
495
+ class AnalyticsNotEnabledError extends S3dbError {
496
+ constructor(details = {}) {
497
+ const {
498
+ pluginName = "EventualConsistency",
499
+ resourceName = "unknown",
500
+ field = "unknown",
501
+ configuredResources = [],
502
+ registeredResources = [],
503
+ pluginInitialized = false,
504
+ ...rest
505
+ } = details;
506
+ const message = `Analytics not enabled for ${resourceName}.${field}`;
507
+ const description = `
508
+ Analytics Not Enabled
509
+
510
+ Plugin: ${pluginName}
511
+ Resource: ${resourceName}
512
+ Field: ${field}
513
+
514
+ Diagnostics:
515
+ \u2022 Plugin initialized: ${pluginInitialized ? "\u2713 Yes" : "\u2717 No"}
516
+ \u2022 Analytics resources created: ${registeredResources.length}/${configuredResources.length}
517
+ ${configuredResources.map((r) => {
518
+ const exists = registeredResources.includes(r);
519
+ return ` ${exists ? "\u2713" : "\u2717"} ${r}${!exists ? " (missing)" : ""}`;
520
+ }).join("\n")}
521
+
522
+ Possible causes:
523
+ 1. Resource not created yet - Analytics resources are created when db.createResource() is called
524
+ 2. Resource created before plugin initialization - Plugin must be initialized before resources
525
+ 3. Field not configured in analytics.resources config
526
+
527
+ Correct initialization order:
528
+ 1. Create database: const db = new Database({ ... })
529
+ 2. Install plugins: await db.connect() (triggers plugin.install())
530
+ 3. Create resources: await db.createResource({ name: '${resourceName}', ... })
531
+ 4. Analytics resources are auto-created by plugin
532
+
533
+ Example fix:
534
+ const db = new Database({
535
+ bucket: 'my-bucket',
536
+ plugins: [new EventualConsistencyPlugin({
537
+ resources: {
538
+ '${resourceName}': {
539
+ fields: {
540
+ '${field}': { type: 'counter', analytics: true }
541
+ }
542
+ }
543
+ }
544
+ })]
545
+ });
546
+
547
+ await db.connect(); // Plugin initialized here
548
+ await db.createResource({ name: '${resourceName}', ... }); // Analytics resource created here
549
+
550
+ Docs: https://docs.s3db.js.org/plugins/eventual-consistency#troubleshooting
551
+ `.trim();
552
+ super(message, {
553
+ ...rest,
554
+ pluginName,
555
+ resourceName,
556
+ field,
557
+ configuredResources,
558
+ registeredResources,
559
+ pluginInitialized,
560
+ description,
561
+ suggestion: "Ensure resources are created after plugin initialization. Check plugin configuration and resource creation order."
562
+ });
460
563
  }
461
564
  }
462
565
 
@@ -3605,6 +3708,24 @@ class MemoryCache extends Cache {
3605
3708
  this.cache = {};
3606
3709
  this.meta = {};
3607
3710
  this.maxSize = config.maxSize !== void 0 ? config.maxSize : 1e3;
3711
+ if (config.maxMemoryBytes && config.maxMemoryBytes > 0 && config.maxMemoryPercent && config.maxMemoryPercent > 0) {
3712
+ throw new Error(
3713
+ "[MemoryCache] Cannot use both maxMemoryBytes and maxMemoryPercent. Choose one: maxMemoryBytes (absolute) or maxMemoryPercent (0...1 fraction)."
3714
+ );
3715
+ }
3716
+ if (config.maxMemoryPercent && config.maxMemoryPercent > 0) {
3717
+ if (config.maxMemoryPercent > 1) {
3718
+ throw new Error(
3719
+ `[MemoryCache] maxMemoryPercent must be between 0 and 1 (e.g., 0.1 for 10%). Received: ${config.maxMemoryPercent}`
3720
+ );
3721
+ }
3722
+ const totalMemory = os$1.totalmem();
3723
+ this.maxMemoryBytes = Math.floor(totalMemory * config.maxMemoryPercent);
3724
+ this.maxMemoryPercent = config.maxMemoryPercent;
3725
+ } else {
3726
+ this.maxMemoryBytes = config.maxMemoryBytes !== void 0 ? config.maxMemoryBytes : 0;
3727
+ this.maxMemoryPercent = 0;
3728
+ }
3608
3729
  this.ttl = config.ttl !== void 0 ? config.ttl : 3e5;
3609
3730
  this.enableCompression = config.enableCompression !== void 0 ? config.enableCompression : false;
3610
3731
  this.compressionThreshold = config.compressionThreshold !== void 0 ? config.compressionThreshold : 1024;
@@ -3614,23 +3735,18 @@ class MemoryCache extends Cache {
3614
3735
  totalCompressedSize: 0,
3615
3736
  compressionRatio: 0
3616
3737
  };
3738
+ this.currentMemoryBytes = 0;
3739
+ this.evictedDueToMemory = 0;
3617
3740
  }
3618
3741
  async _set(key, data) {
3619
- if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
3620
- const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
3621
- if (oldestKey) {
3622
- delete this.cache[oldestKey];
3623
- delete this.meta[oldestKey];
3624
- }
3625
- }
3626
3742
  let finalData = data;
3627
3743
  let compressed = false;
3628
3744
  let originalSize = 0;
3629
3745
  let compressedSize = 0;
3746
+ const serialized = JSON.stringify(data);
3747
+ originalSize = Buffer.byteLength(serialized, "utf8");
3630
3748
  if (this.enableCompression) {
3631
3749
  try {
3632
- const serialized = JSON.stringify(data);
3633
- originalSize = Buffer.byteLength(serialized, "utf8");
3634
3750
  if (originalSize >= this.compressionThreshold) {
3635
3751
  const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, "utf8"));
3636
3752
  finalData = {
@@ -3649,13 +3765,42 @@ class MemoryCache extends Cache {
3649
3765
  console.warn(`[MemoryCache] Compression failed for key '${key}':`, error.message);
3650
3766
  }
3651
3767
  }
3768
+ const itemSize = compressed ? compressedSize : originalSize;
3769
+ if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
3770
+ const oldSize = this.meta[key]?.compressedSize || 0;
3771
+ this.currentMemoryBytes -= oldSize;
3772
+ }
3773
+ if (this.maxMemoryBytes > 0) {
3774
+ while (this.currentMemoryBytes + itemSize > this.maxMemoryBytes && Object.keys(this.cache).length > 0) {
3775
+ const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
3776
+ if (oldestKey) {
3777
+ const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
3778
+ delete this.cache[oldestKey];
3779
+ delete this.meta[oldestKey];
3780
+ this.currentMemoryBytes -= evictedSize;
3781
+ this.evictedDueToMemory++;
3782
+ } else {
3783
+ break;
3784
+ }
3785
+ }
3786
+ }
3787
+ if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
3788
+ const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
3789
+ if (oldestKey) {
3790
+ const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
3791
+ delete this.cache[oldestKey];
3792
+ delete this.meta[oldestKey];
3793
+ this.currentMemoryBytes -= evictedSize;
3794
+ }
3795
+ }
3652
3796
  this.cache[key] = finalData;
3653
3797
  this.meta[key] = {
3654
3798
  ts: Date.now(),
3655
3799
  compressed,
3656
3800
  originalSize,
3657
- compressedSize: compressed ? compressedSize : originalSize
3801
+ compressedSize: itemSize
3658
3802
  };
3803
+ this.currentMemoryBytes += itemSize;
3659
3804
  return data;
3660
3805
  }
3661
3806
  async _get(key) {
@@ -3663,7 +3808,9 @@ class MemoryCache extends Cache {
3663
3808
  if (this.ttl > 0) {
3664
3809
  const now = Date.now();
3665
3810
  const meta = this.meta[key];
3666
- if (meta && now - meta.ts > this.ttl * 1e3) {
3811
+ if (meta && now - meta.ts > this.ttl) {
3812
+ const itemSize = meta.compressedSize || 0;
3813
+ this.currentMemoryBytes -= itemSize;
3667
3814
  delete this.cache[key];
3668
3815
  delete this.meta[key];
3669
3816
  return null;
@@ -3685,6 +3832,10 @@ class MemoryCache extends Cache {
3685
3832
  return rawData;
3686
3833
  }
3687
3834
  async _del(key) {
3835
+ if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
3836
+ const itemSize = this.meta[key]?.compressedSize || 0;
3837
+ this.currentMemoryBytes -= itemSize;
3838
+ }
3688
3839
  delete this.cache[key];
3689
3840
  delete this.meta[key];
3690
3841
  return true;
@@ -3693,10 +3844,13 @@ class MemoryCache extends Cache {
3693
3844
  if (!prefix) {
3694
3845
  this.cache = {};
3695
3846
  this.meta = {};
3847
+ this.currentMemoryBytes = 0;
3696
3848
  return true;
3697
3849
  }
3698
3850
  for (const key of Object.keys(this.cache)) {
3699
3851
  if (key.startsWith(prefix)) {
3852
+ const itemSize = this.meta[key]?.compressedSize || 0;
3853
+ this.currentMemoryBytes -= itemSize;
3700
3854
  delete this.cache[key];
3701
3855
  delete this.meta[key];
3702
3856
  }
@@ -3734,6 +3888,53 @@ class MemoryCache extends Cache {
3734
3888
  }
3735
3889
  };
3736
3890
  }
3891
+ /**
3892
+ * Get memory usage statistics
3893
+ * @returns {Object} Memory stats including current usage, limits, and eviction counts
3894
+ */
3895
+ getMemoryStats() {
3896
+ const totalItems = Object.keys(this.cache).length;
3897
+ const memoryUsagePercent = this.maxMemoryBytes > 0 ? (this.currentMemoryBytes / this.maxMemoryBytes * 100).toFixed(2) : 0;
3898
+ const systemMemory = {
3899
+ total: os$1.totalmem(),
3900
+ free: os$1.freemem(),
3901
+ used: os$1.totalmem() - os$1.freemem()
3902
+ };
3903
+ const cachePercentOfTotal = systemMemory.total > 0 ? (this.currentMemoryBytes / systemMemory.total * 100).toFixed(2) : 0;
3904
+ return {
3905
+ currentMemoryBytes: this.currentMemoryBytes,
3906
+ maxMemoryBytes: this.maxMemoryBytes,
3907
+ maxMemoryPercent: this.maxMemoryPercent,
3908
+ memoryUsagePercent: parseFloat(memoryUsagePercent),
3909
+ cachePercentOfSystemMemory: parseFloat(cachePercentOfTotal),
3910
+ totalItems,
3911
+ maxSize: this.maxSize,
3912
+ evictedDueToMemory: this.evictedDueToMemory,
3913
+ averageItemSize: totalItems > 0 ? Math.round(this.currentMemoryBytes / totalItems) : 0,
3914
+ memoryUsage: {
3915
+ current: this._formatBytes(this.currentMemoryBytes),
3916
+ max: this.maxMemoryBytes > 0 ? this._formatBytes(this.maxMemoryBytes) : "unlimited",
3917
+ available: this.maxMemoryBytes > 0 ? this._formatBytes(this.maxMemoryBytes - this.currentMemoryBytes) : "unlimited"
3918
+ },
3919
+ systemMemory: {
3920
+ total: this._formatBytes(systemMemory.total),
3921
+ free: this._formatBytes(systemMemory.free),
3922
+ used: this._formatBytes(systemMemory.used),
3923
+ cachePercent: `${cachePercentOfTotal}%`
3924
+ }
3925
+ };
3926
+ }
3927
+ /**
3928
+ * Format bytes to human-readable format
3929
+ * @private
3930
+ */
3931
+ _formatBytes(bytes) {
3932
+ if (bytes === 0) return "0 B";
3933
+ const k = 1024;
3934
+ const sizes = ["B", "KB", "MB", "GB"];
3935
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3936
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
3937
+ }
3737
3938
  }
3738
3939
 
3739
3940
  class FilesystemCache extends Cache {
@@ -4563,8 +4764,10 @@ class CachePlugin extends Plugin {
4563
4764
  config: {
4564
4765
  ttl: options.ttl,
4565
4766
  maxSize: options.maxSize,
4767
+ maxMemoryBytes: options.maxMemoryBytes,
4768
+ maxMemoryPercent: options.maxMemoryPercent,
4566
4769
  ...options.config
4567
- // Driver-specific config (can override ttl/maxSize)
4770
+ // Driver-specific config (can override ttl/maxSize/maxMemoryBytes/maxMemoryPercent)
4568
4771
  },
4569
4772
  // Resource filtering
4570
4773
  include: options.include || null,
@@ -7023,6 +7226,80 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
7023
7226
  }
7024
7227
  return data;
7025
7228
  }
7229
+ async function getRawEvents(resourceName, field, options, fieldHandlers) {
7230
+ const resourceHandlers = fieldHandlers.get(resourceName);
7231
+ if (!resourceHandlers) {
7232
+ throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
7233
+ }
7234
+ const handler = resourceHandlers.get(field);
7235
+ if (!handler) {
7236
+ throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
7237
+ }
7238
+ if (!handler.transactionResource) {
7239
+ throw new Error("Transaction resource not initialized");
7240
+ }
7241
+ const {
7242
+ recordId,
7243
+ startDate,
7244
+ endDate,
7245
+ cohortDate,
7246
+ cohortHour,
7247
+ cohortMonth,
7248
+ applied,
7249
+ operation,
7250
+ limit
7251
+ } = options;
7252
+ const query = {};
7253
+ if (recordId !== void 0) {
7254
+ query.originalId = recordId;
7255
+ }
7256
+ if (applied !== void 0) {
7257
+ query.applied = applied;
7258
+ }
7259
+ const [ok, err, allTransactions] = await tryFn(
7260
+ () => handler.transactionResource.query(query)
7261
+ );
7262
+ if (!ok || !allTransactions) {
7263
+ return [];
7264
+ }
7265
+ let filtered = allTransactions;
7266
+ if (operation !== void 0) {
7267
+ filtered = filtered.filter((t) => t.operation === operation);
7268
+ }
7269
+ if (cohortDate) {
7270
+ filtered = filtered.filter((t) => t.cohortDate === cohortDate);
7271
+ }
7272
+ if (cohortHour) {
7273
+ filtered = filtered.filter((t) => t.cohortHour === cohortHour);
7274
+ }
7275
+ if (cohortMonth) {
7276
+ filtered = filtered.filter((t) => t.cohortMonth === cohortMonth);
7277
+ }
7278
+ if (startDate && endDate) {
7279
+ const isHourly = startDate.length > 10;
7280
+ const cohortField = isHourly ? "cohortHour" : "cohortDate";
7281
+ filtered = filtered.filter(
7282
+ (t) => t[cohortField] && t[cohortField] >= startDate && t[cohortField] <= endDate
7283
+ );
7284
+ } else if (startDate) {
7285
+ const isHourly = startDate.length > 10;
7286
+ const cohortField = isHourly ? "cohortHour" : "cohortDate";
7287
+ filtered = filtered.filter((t) => t[cohortField] && t[cohortField] >= startDate);
7288
+ } else if (endDate) {
7289
+ const isHourly = endDate.length > 10;
7290
+ const cohortField = isHourly ? "cohortHour" : "cohortDate";
7291
+ filtered = filtered.filter((t) => t[cohortField] && t[cohortField] <= endDate);
7292
+ }
7293
+ filtered.sort((a, b) => {
7294
+ const aTime = new Date(a.timestamp || a.createdAt).getTime();
7295
+ const bTime = new Date(b.timestamp || b.createdAt).getTime();
7296
+ return bTime - aTime;
7297
+ });
7298
+ if (limit && limit > 0) {
7299
+ filtered = filtered.slice(0, limit);
7300
+ }
7301
+ return filtered;
7302
+ }
7026
7303
 
7027
7304
  function addHelperMethods(resource, plugin, config) {
7028
7305
  resource.set = async (id, field, value) => {
@@ -7780,6 +8057,185 @@ class EventualConsistencyPlugin extends Plugin {
7780
8057
  async getLastNMonths(resourceName, field, months = 12, options = {}) {
7781
8058
  return await getLastNMonths(resourceName, field, months, options, this.fieldHandlers);
7782
8059
  }
8060
+ /**
8061
+ * Get raw transaction events for custom aggregation
8062
+ *
8063
+ * This method provides direct access to the underlying transaction events,
8064
+ * allowing developers to perform custom aggregations beyond the pre-built analytics.
8065
+ * Useful for complex queries, custom metrics, or when you need the raw event data.
8066
+ *
8067
+ * @param {string} resourceName - Resource name
8068
+ * @param {string} field - Field name
8069
+ * @param {Object} options - Query options
8070
+ * @param {string} options.recordId - Filter by specific record ID
8071
+ * @param {string} options.startDate - Start date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
8072
+ * @param {string} options.endDate - End date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
8073
+ * @param {string} options.cohortDate - Filter by cohort date (YYYY-MM-DD)
8074
+ * @param {string} options.cohortHour - Filter by cohort hour (YYYY-MM-DDTHH)
8075
+ * @param {string} options.cohortMonth - Filter by cohort month (YYYY-MM)
8076
+ * @param {boolean} options.applied - Filter by applied status (true/false/undefined for both)
8077
+ * @param {string} options.operation - Filter by operation type ('add', 'sub', 'set')
8078
+ * @param {number} options.limit - Maximum number of events to return
8079
+ * @returns {Promise<Array>} Raw transaction events
8080
+ *
8081
+ * @example
8082
+ * // Get all events for a specific record
8083
+ * const events = await plugin.getRawEvents('wallets', 'balance', {
8084
+ * recordId: 'wallet1'
8085
+ * });
8086
+ *
8087
+ * @example
8088
+ * // Get events for a specific time range
8089
+ * const events = await plugin.getRawEvents('wallets', 'balance', {
8090
+ * startDate: '2025-10-01',
8091
+ * endDate: '2025-10-31'
8092
+ * });
8093
+ *
8094
+ * @example
8095
+ * // Get only pending (unapplied) transactions
8096
+ * const pending = await plugin.getRawEvents('wallets', 'balance', {
8097
+ * applied: false
8098
+ * });
8099
+ */
8100
+ async getRawEvents(resourceName, field, options = {}) {
8101
+ return await getRawEvents(resourceName, field, options, this.fieldHandlers);
8102
+ }
8103
+ /**
8104
+ * Get diagnostics information about the plugin state
8105
+ *
8106
+ * This method provides comprehensive diagnostic information about the EventualConsistencyPlugin,
8107
+ * including configured resources, field handlers, timers, and overall health status.
8108
+ * Useful for debugging initialization issues, configuration problems, or runtime errors.
8109
+ *
8110
+ * @param {Object} options - Diagnostic options
8111
+ * @param {string} options.resourceName - Optional: limit diagnostics to specific resource
8112
+ * @param {string} options.field - Optional: limit diagnostics to specific field
8113
+ * @param {boolean} options.includeStats - Include transaction statistics (default: false)
8114
+ * @returns {Promise<Object>} Diagnostic information
8115
+ *
8116
+ * @example
8117
+ * // Get overall plugin diagnostics
8118
+ * const diagnostics = await plugin.getDiagnostics();
8119
+ * console.log(diagnostics);
8120
+ *
8121
+ * @example
8122
+ * // Get diagnostics for specific resource/field with stats
8123
+ * const diagnostics = await plugin.getDiagnostics({
8124
+ * resourceName: 'wallets',
8125
+ * field: 'balance',
8126
+ * includeStats: true
8127
+ * });
8128
+ */
8129
+ async getDiagnostics(options = {}) {
8130
+ const { resourceName, field, includeStats = false } = options;
8131
+ const diagnostics = {
8132
+ plugin: {
8133
+ name: "EventualConsistencyPlugin",
8134
+ initialized: this.database !== null && this.database !== void 0,
8135
+ verbose: this.config.verbose || false,
8136
+ timezone: this.config.cohort?.timezone || "UTC",
8137
+ consolidation: {
8138
+ mode: this.config.consolidation?.mode || "timer",
8139
+ interval: this.config.consolidation?.interval || 6e4,
8140
+ batchSize: this.config.consolidation?.batchSize || 100
8141
+ },
8142
+ garbageCollection: {
8143
+ enabled: this.config.garbageCollection?.enabled !== false,
8144
+ retentionDays: this.config.garbageCollection?.retentionDays || 30,
8145
+ interval: this.config.garbageCollection?.interval || 36e5
8146
+ }
8147
+ },
8148
+ resources: [],
8149
+ errors: [],
8150
+ warnings: []
8151
+ };
8152
+ for (const [resName, resourceHandlers] of this.fieldHandlers.entries()) {
8153
+ if (resourceName && resName !== resourceName) {
8154
+ continue;
8155
+ }
8156
+ const resourceDiag = {
8157
+ name: resName,
8158
+ fields: []
8159
+ };
8160
+ for (const [fieldName, handler] of resourceHandlers.entries()) {
8161
+ if (field && fieldName !== field) {
8162
+ continue;
8163
+ }
8164
+ const fieldDiag = {
8165
+ name: fieldName,
8166
+ type: handler.type || "counter",
8167
+ analyticsEnabled: handler.analyticsResource !== null && handler.analyticsResource !== void 0,
8168
+ resources: {
8169
+ transaction: handler.transactionResource?.name || null,
8170
+ target: handler.targetResource?.name || null,
8171
+ analytics: handler.analyticsResource?.name || null
8172
+ },
8173
+ timers: {
8174
+ consolidation: handler.consolidationTimer !== null && handler.consolidationTimer !== void 0,
8175
+ garbageCollection: handler.garbageCollectionTimer !== null && handler.garbageCollectionTimer !== void 0
8176
+ }
8177
+ };
8178
+ if (!handler.transactionResource) {
8179
+ diagnostics.errors.push({
8180
+ resource: resName,
8181
+ field: fieldName,
8182
+ issue: "Missing transaction resource",
8183
+ suggestion: "Ensure plugin is installed and resources are created after plugin installation"
8184
+ });
8185
+ }
8186
+ if (!handler.targetResource) {
8187
+ diagnostics.warnings.push({
8188
+ resource: resName,
8189
+ field: fieldName,
8190
+ issue: "Missing target resource",
8191
+ suggestion: "Target resource may not have been created yet"
8192
+ });
8193
+ }
8194
+ if (handler.analyticsResource && !handler.analyticsResource.name) {
8195
+ diagnostics.errors.push({
8196
+ resource: resName,
8197
+ field: fieldName,
8198
+ issue: "Invalid analytics resource",
8199
+ suggestion: "Analytics resource exists but has no name - possible initialization failure"
8200
+ });
8201
+ }
8202
+ if (includeStats && handler.transactionResource) {
8203
+ try {
8204
+ const [okPending, errPending, pendingTxns] = await handler.transactionResource.query({ applied: false }).catch(() => [false, null, []]);
8205
+ const [okApplied, errApplied, appliedTxns] = await handler.transactionResource.query({ applied: true }).catch(() => [false, null, []]);
8206
+ fieldDiag.stats = {
8207
+ pendingTransactions: okPending ? pendingTxns?.length || 0 : "error",
8208
+ appliedTransactions: okApplied ? appliedTxns?.length || 0 : "error",
8209
+ totalTransactions: okPending && okApplied ? (pendingTxns?.length || 0) + (appliedTxns?.length || 0) : "error"
8210
+ };
8211
+ if (handler.analyticsResource) {
8212
+ const [okAnalytics, errAnalytics, analyticsRecords] = await handler.analyticsResource.list().catch(() => [false, null, []]);
8213
+ fieldDiag.stats.analyticsRecords = okAnalytics ? analyticsRecords?.length || 0 : "error";
8214
+ }
8215
+ } catch (error) {
8216
+ diagnostics.warnings.push({
8217
+ resource: resName,
8218
+ field: fieldName,
8219
+ issue: "Failed to fetch statistics",
8220
+ error: error.message
8221
+ });
8222
+ }
8223
+ }
8224
+ resourceDiag.fields.push(fieldDiag);
8225
+ }
8226
+ if (resourceDiag.fields.length > 0) {
8227
+ diagnostics.resources.push(resourceDiag);
8228
+ }
8229
+ }
8230
+ diagnostics.health = {
8231
+ status: diagnostics.errors.length === 0 ? diagnostics.warnings.length === 0 ? "healthy" : "warning" : "error",
8232
+ totalResources: diagnostics.resources.length,
8233
+ totalFields: diagnostics.resources.reduce((sum, r) => sum + r.fields.length, 0),
8234
+ errorCount: diagnostics.errors.length,
8235
+ warningCount: diagnostics.warnings.length
8236
+ };
8237
+ return diagnostics;
8238
+ }
7783
8239
  }
7784
8240
 
7785
8241
  class FullTextPlugin extends Plugin {
@@ -11401,6 +11857,7 @@ ${errorDetails}`,
11401
11857
  idGenerator: customIdGenerator,
11402
11858
  idSize = 22,
11403
11859
  versioningEnabled = false,
11860
+ strictValidation = true,
11404
11861
  events = {},
11405
11862
  asyncEvents = true,
11406
11863
  asyncPartitions = true,
@@ -11414,6 +11871,7 @@ ${errorDetails}`,
11414
11871
  this.parallelism = parallelism;
11415
11872
  this.passphrase = passphrase ?? "secret";
11416
11873
  this.versioningEnabled = versioningEnabled;
11874
+ this.strictValidation = strictValidation;
11417
11875
  this.setAsyncMode(asyncEvents);
11418
11876
  this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
11419
11877
  if (typeof customIdGenerator === "number" && customIdGenerator > 0) {
@@ -11661,9 +12119,12 @@ ${errorDetails}`,
11661
12119
  }
11662
12120
  /**
11663
12121
  * Validate that all partition fields exist in current resource attributes
11664
- * @throws {Error} If partition fields don't exist in current schema
12122
+ * @throws {Error} If partition fields don't exist in current schema (only when strictValidation is true)
11665
12123
  */
11666
12124
  validatePartitions() {
12125
+ if (!this.strictValidation) {
12126
+ return;
12127
+ }
11667
12128
  if (!this.config.partitions) {
11668
12129
  return;
11669
12130
  }
@@ -13798,7 +14259,7 @@ class Database extends EventEmitter {
13798
14259
  this.id = idGenerator(7);
13799
14260
  this.version = "1";
13800
14261
  this.s3dbVersion = (() => {
13801
- const [ok, err, version] = tryFn(() => true ? "11.2.2" : "latest");
14262
+ const [ok, err, version] = tryFn(() => true ? "11.2.3" : "latest");
13802
14263
  return ok ? version : "latest";
13803
14264
  })();
13804
14265
  this.resources = {};
@@ -13813,6 +14274,7 @@ class Database extends EventEmitter {
13813
14274
  this.passphrase = options.passphrase || "secret";
13814
14275
  this.versioningEnabled = options.versioningEnabled || false;
13815
14276
  this.persistHooks = options.persistHooks || false;
14277
+ this.strictValidation = options.strictValidation !== false;
13816
14278
  this._initHooks();
13817
14279
  let connectionString = options.connectionString;
13818
14280
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
@@ -13934,6 +14396,7 @@ class Database extends EventEmitter {
13934
14396
  asyncEvents: versionData.asyncEvents !== void 0 ? versionData.asyncEvents : true,
13935
14397
  hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : versionData.hooks || {},
13936
14398
  versioningEnabled: this.versioningEnabled,
14399
+ strictValidation: this.strictValidation,
13937
14400
  map: versionData.map,
13938
14401
  idGenerator: restoredIdGenerator,
13939
14402
  idSize: restoredIdSize
@@ -14595,6 +15058,7 @@ class Database extends EventEmitter {
14595
15058
  autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
14596
15059
  hooks: hooks || {},
14597
15060
  versioningEnabled: this.versioningEnabled,
15061
+ strictValidation: this.strictValidation,
14598
15062
  map: config.map,
14599
15063
  idGenerator: config.idGenerator,
14600
15064
  idSize: config.idSize,
@@ -17430,6 +17894,7 @@ class StateMachinePlugin extends Plugin {
17430
17894
  }
17431
17895
 
17432
17896
  exports.AVAILABLE_BEHAVIORS = AVAILABLE_BEHAVIORS;
17897
+ exports.AnalyticsNotEnabledError = AnalyticsNotEnabledError;
17433
17898
  exports.AuditPlugin = AuditPlugin;
17434
17899
  exports.AuthenticationError = AuthenticationError;
17435
17900
  exports.BackupPlugin = BackupPlugin;