s3db.js 11.2.3 → 11.2.5

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 (52) hide show
  1. package/dist/s3db-cli.js +588 -74
  2. package/dist/s3db.cjs.js +2472 -150
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +2464 -151
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/behaviors/enforce-limits.js +28 -4
  8. package/src/behaviors/index.js +6 -1
  9. package/src/client.class.js +11 -1
  10. package/src/concerns/base62.js +70 -0
  11. package/src/concerns/partition-queue.js +7 -1
  12. package/src/concerns/plugin-storage.js +75 -13
  13. package/src/database.class.js +19 -4
  14. package/src/errors.js +306 -27
  15. package/src/partition-drivers/base-partition-driver.js +12 -2
  16. package/src/partition-drivers/index.js +7 -1
  17. package/src/partition-drivers/memory-partition-driver.js +20 -5
  18. package/src/partition-drivers/sqs-partition-driver.js +6 -1
  19. package/src/plugins/audit.errors.js +46 -0
  20. package/src/plugins/backup/base-backup-driver.class.js +36 -6
  21. package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
  22. package/src/plugins/backup/index.js +40 -9
  23. package/src/plugins/backup/multi-backup-driver.class.js +69 -9
  24. package/src/plugins/backup/s3-backup-driver.class.js +48 -6
  25. package/src/plugins/backup.errors.js +45 -0
  26. package/src/plugins/cache/cache.class.js +8 -1
  27. package/src/plugins/cache.errors.js +47 -0
  28. package/src/plugins/cache.plugin.js +8 -1
  29. package/src/plugins/fulltext.errors.js +46 -0
  30. package/src/plugins/fulltext.plugin.js +15 -3
  31. package/src/plugins/index.js +1 -0
  32. package/src/plugins/metrics.errors.js +46 -0
  33. package/src/plugins/queue-consumer.plugin.js +31 -4
  34. package/src/plugins/queue.errors.js +46 -0
  35. package/src/plugins/replicator.errors.js +46 -0
  36. package/src/plugins/replicator.plugin.js +40 -5
  37. package/src/plugins/replicators/base-replicator.class.js +19 -3
  38. package/src/plugins/replicators/index.js +9 -3
  39. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  40. package/src/plugins/scheduler.errors.js +46 -0
  41. package/src/plugins/scheduler.plugin.js +79 -19
  42. package/src/plugins/state-machine.errors.js +47 -0
  43. package/src/plugins/state-machine.plugin.js +86 -17
  44. package/src/plugins/vector/distances.js +173 -0
  45. package/src/plugins/vector/kmeans.js +367 -0
  46. package/src/plugins/vector/metrics.js +369 -0
  47. package/src/plugins/vector/vector-error.js +43 -0
  48. package/src/plugins/vector.plugin.js +687 -0
  49. package/src/schema.class.js +232 -41
  50. package/src/stream/index.js +6 -1
  51. package/src/stream/resource-reader.class.js +6 -1
  52. package/src/validator.class.js +8 -0
package/dist/s3db-cli.js CHANGED
@@ -44419,14 +44419,14 @@ function tryFnSync(fn) {
44419
44419
  }
44420
44420
 
44421
44421
  class BaseError extends Error {
44422
- constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, suggestion, ...rest }) {
44422
+ constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, description, ...rest }) {
44423
44423
  if (verbose) message = message + `\n\nVerbose:\n\n${JSON.stringify(rest, null, 2)}`;
44424
44424
  super(message);
44425
44425
 
44426
44426
  if (typeof Error.captureStackTrace === 'function') {
44427
44427
  Error.captureStackTrace(this, this.constructor);
44428
- } else {
44429
- this.stack = (new Error(message)).stack;
44428
+ } else {
44429
+ this.stack = (new Error(message)).stack;
44430
44430
  }
44431
44431
 
44432
44432
  super.name = this.constructor.name;
@@ -44442,7 +44442,7 @@ class BaseError extends Error {
44442
44442
  this.commandName = commandName;
44443
44443
  this.commandInput = commandInput;
44444
44444
  this.metadata = metadata;
44445
- this.suggestion = suggestion;
44445
+ this.description = description;
44446
44446
  this.data = { bucket, key, ...rest, verbose, message };
44447
44447
  }
44448
44448
 
@@ -44460,7 +44460,7 @@ class BaseError extends Error {
44460
44460
  commandName: this.commandName,
44461
44461
  commandInput: this.commandInput,
44462
44462
  metadata: this.metadata,
44463
- suggestion: this.suggestion,
44463
+ description: this.description,
44464
44464
  data: this.data,
44465
44465
  original: this.original,
44466
44466
  stack: this.stack,
@@ -44489,6 +44489,14 @@ class S3dbError extends BaseError {
44489
44489
  }
44490
44490
  }
44491
44491
 
44492
+ // Database operation errors
44493
+ class DatabaseError extends S3dbError {
44494
+ constructor(message, details = {}) {
44495
+ super(message, details);
44496
+ Object.assign(this, details);
44497
+ }
44498
+ }
44499
+
44492
44500
  // Validation errors
44493
44501
  class ValidationError extends S3dbError {
44494
44502
  constructor(message, details = {}) {
@@ -44580,26 +44588,26 @@ function mapAwsError(err, context = {}) {
44580
44588
  const metadata = err.$metadata ? { ...err.$metadata } : undefined;
44581
44589
  const commandName = context.commandName;
44582
44590
  const commandInput = context.commandInput;
44583
- let suggestion;
44591
+ let description;
44584
44592
  if (code === 'NoSuchKey' || code === 'NotFound') {
44585
- suggestion = 'Check if the key exists in the specified bucket and if your credentials have permission.';
44586
- return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, suggestion });
44593
+ description = 'The specified key does not exist in the bucket. Check if the key exists and if your credentials have permission to access it.';
44594
+ return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, description });
44587
44595
  }
44588
44596
  if (code === 'NoSuchBucket') {
44589
- suggestion = 'Check if the bucket exists and if your credentials have permission.';
44590
- return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, suggestion });
44597
+ description = 'The specified bucket does not exist. Check if the bucket name is correct and if your credentials have permission to access it.';
44598
+ return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, description });
44591
44599
  }
44592
44600
  if (code === 'AccessDenied' || (err.statusCode === 403) || code === 'Forbidden') {
44593
- suggestion = 'Check your credentials and bucket policy.';
44594
- return new PermissionError('Access denied', { ...context, original: err, metadata, commandName, commandInput, suggestion });
44601
+ description = 'Access denied. Check your AWS credentials, IAM permissions, and bucket policy.';
44602
+ return new PermissionError('Access denied', { ...context, original: err, metadata, commandName, commandInput, description });
44595
44603
  }
44596
44604
  if (code === 'ValidationError' || (err.statusCode === 400)) {
44597
- suggestion = 'Check the request parameters and payload.';
44598
- return new ValidationError('Validation error', { ...context, original: err, metadata, commandName, commandInput, suggestion });
44605
+ description = 'Validation error. Check the request parameters and payload format.';
44606
+ return new ValidationError('Validation error', { ...context, original: err, metadata, commandName, commandInput, description });
44599
44607
  }
44600
44608
  if (code === 'MissingMetadata') {
44601
- suggestion = 'Check if the object metadata is present and valid.';
44602
- return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, suggestion });
44609
+ description = 'Object metadata is missing or invalid. Check if the object was uploaded correctly.';
44610
+ return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, description });
44603
44611
  }
44604
44612
  // Outros mapeamentos podem ser adicionados aqui
44605
44613
  // Incluir detalhes do erro original para facilitar debug
@@ -44609,39 +44617,209 @@ function mapAwsError(err, context = {}) {
44609
44617
  err.statusCode && `Status: ${err.statusCode}`,
44610
44618
  err.stack && `Stack: ${err.stack.split('\n')[0]}`,
44611
44619
  ].filter(Boolean).join(' | ');
44612
-
44613
- suggestion = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
44614
- return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, suggestion });
44620
+
44621
+ description = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
44622
+ return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, description });
44615
44623
  }
44616
44624
 
44617
44625
  class ConnectionStringError extends S3dbError {
44618
44626
  constructor(message, details = {}) {
44619
- super(message, { ...details, suggestion: 'Check the connection string format and credentials.' });
44627
+ const description = details.description || 'Invalid connection string format. Check the connection string syntax and credentials.';
44628
+ super(message, { ...details, description });
44620
44629
  }
44621
44630
  }
44622
44631
 
44623
44632
  class CryptoError extends S3dbError {
44624
44633
  constructor(message, details = {}) {
44625
- super(message, { ...details, suggestion: 'Check if the crypto library is available and input is valid.' });
44634
+ const description = details.description || 'Cryptography operation failed. Check if the crypto library is available and input is valid.';
44635
+ super(message, { ...details, description });
44626
44636
  }
44627
44637
  }
44628
44638
 
44629
44639
  class SchemaError extends S3dbError {
44630
44640
  constructor(message, details = {}) {
44631
- super(message, { ...details, suggestion: 'Check schema definition and input data.' });
44641
+ const description = details.description || 'Schema validation failed. Check schema definition and input data format.';
44642
+ super(message, { ...details, description });
44632
44643
  }
44633
44644
  }
44634
44645
 
44635
44646
  class ResourceError extends S3dbError {
44636
44647
  constructor(message, details = {}) {
44637
- super(message, { ...details, suggestion: details.suggestion || 'Check resource configuration, attributes, and operation context.' });
44648
+ const description = details.description || 'Resource operation failed. Check resource configuration, attributes, and operation context.';
44649
+ super(message, { ...details, description });
44638
44650
  Object.assign(this, details);
44639
44651
  }
44640
44652
  }
44641
44653
 
44642
44654
  class PartitionError extends S3dbError {
44643
44655
  constructor(message, details = {}) {
44644
- super(message, { ...details, suggestion: details.suggestion || 'Check partition definition, fields, and input values.' });
44656
+ // Generate description if not provided
44657
+ let description = details.description;
44658
+ if (!description && details.resourceName && details.partitionName && details.fieldName) {
44659
+ const { resourceName, partitionName, fieldName, availableFields = [] } = details;
44660
+ description = `
44661
+ Partition Field Validation Error
44662
+
44663
+ Resource: ${resourceName}
44664
+ Partition: ${partitionName}
44665
+ Missing Field: ${fieldName}
44666
+
44667
+ Available fields in schema:
44668
+ ${availableFields.map(f => ` • ${f}`).join('\n') || ' (no fields defined)'}
44669
+
44670
+ Possible causes:
44671
+ 1. Field was removed from schema but partition still references it
44672
+ 2. Typo in partition field name
44673
+ 3. Nested field path is incorrect (use dot notation like 'utm.source')
44674
+
44675
+ Solution:
44676
+ ${details.strictValidation === false
44677
+ ? ' • Update partition definition to use existing fields'
44678
+ : ` • Add missing field to schema, OR
44679
+ • Update partition definition to use existing fields, OR
44680
+ • Use strictValidation: false to skip this check during testing`}
44681
+
44682
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partitions
44683
+ `.trim();
44684
+ }
44685
+
44686
+ super(message, {
44687
+ ...details,
44688
+ description
44689
+ });
44690
+ }
44691
+ }
44692
+
44693
+ // Behavior errors
44694
+ class BehaviorError extends S3dbError {
44695
+ constructor(message, details = {}) {
44696
+ const {
44697
+ behavior = 'unknown',
44698
+ availableBehaviors = [],
44699
+ ...rest
44700
+ } = details;
44701
+
44702
+ let description = details.description;
44703
+ if (!description) {
44704
+ description = `
44705
+ Behavior Error
44706
+
44707
+ Requested: ${behavior}
44708
+ Available: ${availableBehaviors.join(', ') || 'body-overflow, body-only, truncate-data, enforce-limits, user-managed'}
44709
+
44710
+ Possible causes:
44711
+ 1. Behavior name misspelled
44712
+ 2. Custom behavior not registered
44713
+
44714
+ Solution:
44715
+ Use one of the available behaviors or register custom behavior.
44716
+
44717
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#behaviors
44718
+ `.trim();
44719
+ }
44720
+
44721
+ super(message, {
44722
+ ...rest,
44723
+ behavior,
44724
+ availableBehaviors,
44725
+ description
44726
+ });
44727
+ }
44728
+ }
44729
+
44730
+ // Stream errors
44731
+ class StreamError extends S3dbError {
44732
+ constructor(message, details = {}) {
44733
+ const {
44734
+ operation = 'unknown',
44735
+ resource,
44736
+ ...rest
44737
+ } = details;
44738
+
44739
+ let description = details.description;
44740
+ if (!description) {
44741
+ description = `
44742
+ Stream Error
44743
+
44744
+ Operation: ${operation}
44745
+ ${resource ? `Resource: ${resource}` : ''}
44746
+
44747
+ Possible causes:
44748
+ 1. Stream not properly initialized
44749
+ 2. Resource not available
44750
+ 3. Network error during streaming
44751
+
44752
+ Solution:
44753
+ Check stream configuration and resource availability.
44754
+
44755
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#streaming
44756
+ `.trim();
44757
+ }
44758
+
44759
+ super(message, {
44760
+ ...rest,
44761
+ operation,
44762
+ resource,
44763
+ description
44764
+ });
44765
+ }
44766
+ }
44767
+
44768
+ // Metadata limit errors (specific for 2KB S3 limit)
44769
+ class MetadataLimitError extends S3dbError {
44770
+ constructor(message, details = {}) {
44771
+ const {
44772
+ totalSize,
44773
+ effectiveLimit,
44774
+ absoluteLimit = 2047,
44775
+ excess,
44776
+ resourceName,
44777
+ operation,
44778
+ ...rest
44779
+ } = details;
44780
+
44781
+ let description = details.description;
44782
+ if (!description && totalSize && effectiveLimit) {
44783
+ description = `
44784
+ S3 Metadata Size Limit Exceeded
44785
+
44786
+ Current Size: ${totalSize} bytes
44787
+ Effective Limit: ${effectiveLimit} bytes
44788
+ Absolute Limit: ${absoluteLimit} bytes
44789
+ ${excess ? `Excess: ${excess} bytes` : ''}
44790
+ ${resourceName ? `Resource: ${resourceName}` : ''}
44791
+ ${operation ? `Operation: ${operation}` : ''}
44792
+
44793
+ S3 has a hard limit of 2KB (2047 bytes) for object metadata.
44794
+
44795
+ Solutions:
44796
+ 1. Use 'body-overflow' behavior to store excess in body
44797
+ 2. Use 'body-only' behavior to store everything in body
44798
+ 3. Reduce number of fields
44799
+ 4. Use shorter field values
44800
+ 5. Enable advanced metadata encoding
44801
+
44802
+ Example:
44803
+ await db.createResource({
44804
+ name: '${resourceName || 'myResource'}',
44805
+ behavior: 'body-overflow', // Automatically handles overflow
44806
+ attributes: { ... }
44807
+ });
44808
+
44809
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#metadata-size-limits
44810
+ `.trim();
44811
+ }
44812
+
44813
+ super(message, {
44814
+ ...rest,
44815
+ totalSize,
44816
+ effectiveLimit,
44817
+ absoluteLimit,
44818
+ excess,
44819
+ resourceName,
44820
+ operation,
44821
+ description
44822
+ });
44645
44823
  }
44646
44824
  }
44647
44825
 
@@ -45621,7 +45799,17 @@ class Client extends EventEmitter {
45621
45799
  });
45622
45800
  this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
45623
45801
  if (errors.length > 0) {
45624
- throw new Error("Some objects could not be moved");
45802
+ throw new UnknownError("Some objects could not be moved", {
45803
+ bucket: this.config.bucket,
45804
+ operation: 'moveAllObjects',
45805
+ prefixFrom,
45806
+ prefixTo,
45807
+ totalKeys: keys.length,
45808
+ failedCount: errors.length,
45809
+ successCount: results.length,
45810
+ errors: errors.map(e => ({ message: e.message, raw: e.raw })),
45811
+ suggestion: 'Check S3 permissions and retry failed objects individually'
45812
+ });
45625
45813
  }
45626
45814
  return results;
45627
45815
  }
@@ -48287,6 +48475,14 @@ class Validator extends FastestValidator {
48287
48475
  type: "any",
48288
48476
  custom: this.autoEncrypt ? jsonHandler : undefined,
48289
48477
  });
48478
+
48479
+ // Embedding type - shorthand for arrays of numbers optimized for embeddings
48480
+ // Usage: 'embedding:1536' or 'embedding|length:768'
48481
+ this.alias('embedding', {
48482
+ type: "array",
48483
+ items: "number",
48484
+ empty: false,
48485
+ });
48290
48486
  }
48291
48487
  }
48292
48488
 
@@ -48361,6 +48557,76 @@ const decodeDecimal = s => {
48361
48557
  return negative ? -num : num;
48362
48558
  };
48363
48559
 
48560
+ /**
48561
+ * Fixed-point encoding optimized for normalized values (typically -1 to 1)
48562
+ * Common in embeddings, similarity scores, probabilities, etc.
48563
+ *
48564
+ * Achieves ~77% compression vs encodeDecimal for embedding vectors.
48565
+ *
48566
+ * @param {number} n - Number to encode (works for any range, optimized for [-1, 1])
48567
+ * @param {number} precision - Decimal places to preserve (default: 6)
48568
+ * @returns {string} Base62-encoded string with '^' prefix to indicate fixed-point encoding
48569
+ *
48570
+ * Examples:
48571
+ * 0.123456 → "^w7f" (4 bytes vs 8 bytes with encodeDecimal)
48572
+ * -0.8234567 → "^-3sdz" (6 bytes vs 10 bytes)
48573
+ * 1.5 → "^98v9" (for values outside [-1,1], still works but less optimal)
48574
+ */
48575
+ const encodeFixedPoint = (n, precision = 6) => {
48576
+ if (typeof n !== 'number' || isNaN(n)) return 'undefined';
48577
+ if (!isFinite(n)) return 'undefined';
48578
+
48579
+ const scale = Math.pow(10, precision);
48580
+ const scaled = Math.round(n * scale);
48581
+
48582
+ if (scaled === 0) return '^0';
48583
+
48584
+ const negative = scaled < 0;
48585
+ let num = Math.abs(scaled);
48586
+ let s = '';
48587
+
48588
+ while (num > 0) {
48589
+ s = alphabet[num % base] + s;
48590
+ num = Math.floor(num / base);
48591
+ }
48592
+
48593
+ // Prefix with ^ to distinguish from regular base62
48594
+ return '^' + (negative ? '-' : '') + s;
48595
+ };
48596
+
48597
+ /**
48598
+ * Decodes fixed-point encoded values
48599
+ *
48600
+ * @param {string} s - Encoded string (must start with '^')
48601
+ * @param {number} precision - Decimal places used in encoding (default: 6)
48602
+ * @returns {number} Decoded number
48603
+ */
48604
+ const decodeFixedPoint = (s, precision = 6) => {
48605
+ if (typeof s !== 'string') return NaN;
48606
+ if (!s.startsWith('^')) return NaN; // Safety check
48607
+
48608
+ s = s.slice(1); // Remove ^ prefix
48609
+
48610
+ if (s === '0') return 0;
48611
+
48612
+ let negative = false;
48613
+ if (s[0] === '-') {
48614
+ negative = true;
48615
+ s = s.slice(1);
48616
+ }
48617
+
48618
+ let r = 0;
48619
+ for (let i = 0; i < s.length; i++) {
48620
+ const idx = charToValue[s[i]];
48621
+ if (idx === undefined) return NaN;
48622
+ r = r * base + idx;
48623
+ }
48624
+
48625
+ const scale = Math.pow(10, precision);
48626
+ const scaled = negative ? -r : r;
48627
+ return scaled / scale;
48628
+ };
48629
+
48364
48630
  /**
48365
48631
  * Generate base62 mapping for attributes
48366
48632
  * @param {string[]} keys - Array of attribute keys
@@ -48618,6 +48884,60 @@ const SchemaActions = {
48618
48884
  return NaN;
48619
48885
  });
48620
48886
  },
48887
+ fromArrayOfEmbeddings: (value, { separator, precision = 6 }) => {
48888
+ if (value === null || value === undefined || !Array.isArray(value)) {
48889
+ return value;
48890
+ }
48891
+ if (value.length === 0) {
48892
+ return '';
48893
+ }
48894
+ const encodedItems = value.map(item => {
48895
+ if (typeof item === 'number' && !isNaN(item)) {
48896
+ return encodeFixedPoint(item, precision);
48897
+ }
48898
+ // fallback: try to parse as number, else keep as is
48899
+ const n = Number(item);
48900
+ return isNaN(n) ? '' : encodeFixedPoint(n, precision);
48901
+ });
48902
+ return encodedItems.join(separator);
48903
+ },
48904
+ toArrayOfEmbeddings: (value, { separator, precision = 6 }) => {
48905
+ if (Array.isArray(value)) {
48906
+ return value.map(v => (typeof v === 'number' ? v : decodeFixedPoint(v, precision)));
48907
+ }
48908
+ if (value === null || value === undefined) {
48909
+ return value;
48910
+ }
48911
+ if (value === '') {
48912
+ return [];
48913
+ }
48914
+ const str = String(value);
48915
+ const items = [];
48916
+ let current = '';
48917
+ let i = 0;
48918
+ while (i < str.length) {
48919
+ if (str[i] === '\\' && i + 1 < str.length) {
48920
+ current += str[i + 1];
48921
+ i += 2;
48922
+ } else if (str[i] === separator) {
48923
+ items.push(current);
48924
+ current = '';
48925
+ i++;
48926
+ } else {
48927
+ current += str[i];
48928
+ i++;
48929
+ }
48930
+ }
48931
+ items.push(current);
48932
+ return items.map(v => {
48933
+ if (typeof v === 'number') return v;
48934
+ if (typeof v === 'string' && v !== '') {
48935
+ const n = decodeFixedPoint(v, precision);
48936
+ return isNaN(n) ? NaN : n;
48937
+ }
48938
+ return NaN;
48939
+ });
48940
+ },
48621
48941
 
48622
48942
  };
48623
48943
 
@@ -48695,16 +49015,16 @@ class Schema {
48695
49015
 
48696
49016
  extractObjectKeys(obj, prefix = '') {
48697
49017
  const objectKeys = [];
48698
-
49018
+
48699
49019
  for (const [key, value] of Object.entries(obj)) {
48700
49020
  if (key.startsWith('$$')) continue; // Skip schema metadata
48701
-
49021
+
48702
49022
  const fullKey = prefix ? `${prefix}.${key}` : key;
48703
-
49023
+
48704
49024
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
48705
49025
  // This is an object, add its key
48706
49026
  objectKeys.push(fullKey);
48707
-
49027
+
48708
49028
  // Check if it has nested objects
48709
49029
  if (value.$$type === 'object') {
48710
49030
  // Recursively extract nested object keys
@@ -48712,31 +49032,135 @@ class Schema {
48712
49032
  }
48713
49033
  }
48714
49034
  }
48715
-
49035
+
48716
49036
  return objectKeys;
48717
49037
  }
48718
49038
 
49039
+ _generateHooksFromOriginalAttributes(attributes, prefix = '') {
49040
+ for (const [key, value] of Object.entries(attributes)) {
49041
+ if (key.startsWith('$$')) continue;
49042
+
49043
+ const fullKey = prefix ? `${prefix}.${key}` : key;
49044
+
49045
+ // Check if this is an object notation type definition (has 'type' property)
49046
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && value.type) {
49047
+ if (value.type === 'array' && value.items) {
49048
+ // Handle array with object notation
49049
+ const itemsType = value.items;
49050
+ const arrayLength = typeof value.length === 'number' ? value.length : null;
49051
+
49052
+ if (itemsType === 'string' || (typeof itemsType === 'string' && itemsType.includes('string'))) {
49053
+ this.addHook("beforeMap", fullKey, "fromArray");
49054
+ this.addHook("afterUnmap", fullKey, "toArray");
49055
+ } else if (itemsType === 'number' || (typeof itemsType === 'string' && itemsType.includes('number'))) {
49056
+ const isIntegerArray = typeof itemsType === 'string' && itemsType.includes('integer');
49057
+ const isEmbedding = !isIntegerArray && arrayLength !== null && arrayLength >= 256;
49058
+
49059
+ if (isIntegerArray) {
49060
+ this.addHook("beforeMap", fullKey, "fromArrayOfNumbers");
49061
+ this.addHook("afterUnmap", fullKey, "toArrayOfNumbers");
49062
+ } else if (isEmbedding) {
49063
+ this.addHook("beforeMap", fullKey, "fromArrayOfEmbeddings");
49064
+ this.addHook("afterUnmap", fullKey, "toArrayOfEmbeddings");
49065
+ } else {
49066
+ this.addHook("beforeMap", fullKey, "fromArrayOfDecimals");
49067
+ this.addHook("afterUnmap", fullKey, "toArrayOfDecimals");
49068
+ }
49069
+ }
49070
+ }
49071
+ // For other types with object notation, they'll be handled by the flattened processing
49072
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value) && !value.type) {
49073
+ // This is a nested object, recurse
49074
+ this._generateHooksFromOriginalAttributes(value, fullKey);
49075
+ }
49076
+ }
49077
+ }
49078
+
48719
49079
  generateAutoHooks() {
49080
+ // First, process the original attributes to find arrays with object notation
49081
+ // This handles cases like: { type: 'array', items: 'number', length: 768 }
49082
+ this._generateHooksFromOriginalAttributes(this.attributes);
49083
+
49084
+ // Then process the flattened schema for other types
48720
49085
  const schema = flatten(cloneDeep(this.attributes), { safe: true });
48721
49086
 
48722
49087
  for (const [name, definition] of Object.entries(schema)) {
48723
- // Handle arrays first to avoid conflicts
48724
- if (definition.includes("array")) {
48725
- if (definition.includes('items:string')) {
49088
+ // Skip metadata fields
49089
+ if (name.includes('$$')) continue;
49090
+
49091
+ // Skip if hooks already exist (from object notation processing)
49092
+ if (this.options.hooks.beforeMap[name] || this.options.hooks.afterUnmap[name]) {
49093
+ continue;
49094
+ }
49095
+
49096
+ // Normalize definition - can be a string or value from flattened object
49097
+ const defStr = typeof definition === 'string' ? definition : '';
49098
+ const defType = typeof definition === 'object' && definition !== null ? definition.type : null;
49099
+
49100
+ // Check if this is an embedding type (custom shorthand)
49101
+ const isEmbeddingType = defStr.includes("embedding") || defType === 'embedding';
49102
+
49103
+ if (isEmbeddingType) {
49104
+ const lengthMatch = defStr.match(/embedding:(\d+)/);
49105
+ if (lengthMatch) {
49106
+ parseInt(lengthMatch[1], 10);
49107
+ } else if (defStr.includes('length:')) {
49108
+ const match = defStr.match(/length:(\d+)/);
49109
+ if (match) parseInt(match[1], 10);
49110
+ }
49111
+
49112
+ // Embeddings always use fixed-point encoding
49113
+ this.addHook("beforeMap", name, "fromArrayOfEmbeddings");
49114
+ this.addHook("afterUnmap", name, "toArrayOfEmbeddings");
49115
+ continue;
49116
+ }
49117
+
49118
+ // Check if this is an array type
49119
+ const isArray = defStr.includes("array") || defType === 'array';
49120
+
49121
+ if (isArray) {
49122
+ // Determine item type for arrays
49123
+ let itemsType = null;
49124
+ if (typeof definition === 'object' && definition !== null && definition.items) {
49125
+ itemsType = definition.items;
49126
+ } else if (defStr.includes('items:string')) {
49127
+ itemsType = 'string';
49128
+ } else if (defStr.includes('items:number')) {
49129
+ itemsType = 'number';
49130
+ }
49131
+
49132
+ if (itemsType === 'string' || (typeof itemsType === 'string' && itemsType.includes('string'))) {
48726
49133
  this.addHook("beforeMap", name, "fromArray");
48727
49134
  this.addHook("afterUnmap", name, "toArray");
48728
- } else if (definition.includes('items:number')) {
49135
+ } else if (itemsType === 'number' || (typeof itemsType === 'string' && itemsType.includes('number'))) {
48729
49136
  // Check if the array items should be treated as integers
48730
- const isIntegerArray = definition.includes("integer:true") ||
48731
- definition.includes("|integer:") ||
48732
- definition.includes("|integer");
48733
-
49137
+ const isIntegerArray = defStr.includes("integer:true") ||
49138
+ defStr.includes("|integer:") ||
49139
+ defStr.includes("|integer") ||
49140
+ (typeof itemsType === 'string' && itemsType.includes('integer'));
49141
+
49142
+ // Check if this is an embedding array (large arrays of decimals)
49143
+ // Common embedding dimensions: 256, 384, 512, 768, 1024, 1536, 2048, 3072
49144
+ let arrayLength = null;
49145
+ if (typeof definition === 'object' && definition !== null && typeof definition.length === 'number') {
49146
+ arrayLength = definition.length;
49147
+ } else if (defStr.includes('length:')) {
49148
+ const match = defStr.match(/length:(\d+)/);
49149
+ if (match) arrayLength = parseInt(match[1], 10);
49150
+ }
49151
+
49152
+ const isEmbedding = !isIntegerArray && arrayLength !== null && arrayLength >= 256;
49153
+
48734
49154
  if (isIntegerArray) {
48735
49155
  // Use standard base62 for arrays of integers
48736
49156
  this.addHook("beforeMap", name, "fromArrayOfNumbers");
48737
49157
  this.addHook("afterUnmap", name, "toArrayOfNumbers");
49158
+ } else if (isEmbedding) {
49159
+ // Use fixed-point encoding for embedding vectors (77% compression)
49160
+ this.addHook("beforeMap", name, "fromArrayOfEmbeddings");
49161
+ this.addHook("afterUnmap", name, "toArrayOfEmbeddings");
48738
49162
  } else {
48739
- // Use decimal-aware base62 for arrays of decimals
49163
+ // Use decimal-aware base62 for regular arrays of decimals
48740
49164
  this.addHook("beforeMap", name, "fromArrayOfDecimals");
48741
49165
  this.addHook("afterUnmap", name, "toArrayOfDecimals");
48742
49166
  }
@@ -48746,7 +49170,7 @@ class Schema {
48746
49170
  }
48747
49171
 
48748
49172
  // Handle secrets
48749
- if (definition.includes("secret")) {
49173
+ if (defStr.includes("secret") || defType === 'secret') {
48750
49174
  if (this.options.autoEncrypt) {
48751
49175
  this.addHook("beforeMap", name, "encrypt");
48752
49176
  }
@@ -48758,12 +49182,12 @@ class Schema {
48758
49182
  }
48759
49183
 
48760
49184
  // Handle numbers (only for non-array fields)
48761
- if (definition.includes("number")) {
49185
+ if (defStr.includes("number") || defType === 'number') {
48762
49186
  // Check if it's specifically an integer field
48763
- const isInteger = definition.includes("integer:true") ||
48764
- definition.includes("|integer:") ||
48765
- definition.includes("|integer");
48766
-
49187
+ const isInteger = defStr.includes("integer:true") ||
49188
+ defStr.includes("|integer:") ||
49189
+ defStr.includes("|integer");
49190
+
48767
49191
  if (isInteger) {
48768
49192
  // Use standard base62 for integers
48769
49193
  this.addHook("beforeMap", name, "toBase62");
@@ -48777,21 +49201,21 @@ class Schema {
48777
49201
  }
48778
49202
 
48779
49203
  // Handle booleans
48780
- if (definition.includes("boolean")) {
49204
+ if (defStr.includes("boolean") || defType === 'boolean') {
48781
49205
  this.addHook("beforeMap", name, "fromBool");
48782
49206
  this.addHook("afterUnmap", name, "toBool");
48783
49207
  continue;
48784
49208
  }
48785
49209
 
48786
49210
  // Handle JSON fields
48787
- if (definition.includes("json")) {
49211
+ if (defStr.includes("json") || defType === 'json') {
48788
49212
  this.addHook("beforeMap", name, "toJSON");
48789
49213
  this.addHook("afterUnmap", name, "fromJSON");
48790
49214
  continue;
48791
49215
  }
48792
49216
 
48793
49217
  // Handle object fields - add JSON serialization hooks
48794
- if (definition === "object" || definition.includes("object")) {
49218
+ if (definition === "object" || defStr.includes("object") || defType === 'object') {
48795
49219
  this.addHook("beforeMap", name, "toJSON");
48796
49220
  this.addHook("afterUnmap", name, "fromJSON");
48797
49221
  continue;
@@ -48948,8 +49372,11 @@ class Schema {
48948
49372
  const originalKey = reversedMap && reversedMap[key] ? reversedMap[key] : key;
48949
49373
  let parsedValue = value;
48950
49374
  const attrDef = this.getAttributeDefinition(originalKey);
49375
+ const hasAfterUnmapHook = this.options.hooks?.afterUnmap?.[originalKey];
49376
+
48951
49377
  // Always unmap base62 strings to numbers for number fields (but not array fields or decimal fields)
48952
- if (typeof attrDef === 'string' && attrDef.includes('number') && !attrDef.includes('array') && !attrDef.includes('decimal')) {
49378
+ // Skip if there are afterUnmap hooks that will handle the conversion
49379
+ if (!hasAfterUnmapHook && typeof attrDef === 'string' && attrDef.includes('number') && !attrDef.includes('array') && !attrDef.includes('decimal')) {
48953
49380
  if (typeof parsedValue === 'string' && parsedValue !== '') {
48954
49381
  parsedValue = decode(parsedValue);
48955
49382
  } else if (typeof parsedValue === 'number') ; else {
@@ -49017,26 +49444,54 @@ class Schema {
49017
49444
  */
49018
49445
  preprocessAttributesForValidation(attributes) {
49019
49446
  const processed = {};
49020
-
49447
+
49021
49448
  for (const [key, value] of Object.entries(attributes)) {
49022
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
49023
- const isExplicitRequired = value.$$type && value.$$type.includes('required');
49024
- const isExplicitOptional = value.$$type && value.$$type.includes('optional');
49025
- const objectConfig = {
49026
- type: 'object',
49027
- properties: this.preprocessAttributesForValidation(value),
49028
- strict: false
49029
- };
49030
- // If explicitly required, don't mark as optional
49031
- if (isExplicitRequired) ; else if (isExplicitOptional || this.allNestedObjectsOptional) {
49032
- objectConfig.optional = true;
49449
+ if (typeof value === 'string') {
49450
+ // Expand embedding:XXX shorthand to array|items:number|length:XXX
49451
+ if (value.startsWith('embedding:')) {
49452
+ const lengthMatch = value.match(/embedding:(\d+)/);
49453
+ if (lengthMatch) {
49454
+ const length = lengthMatch[1];
49455
+ // Extract any additional modifiers after the length
49456
+ const rest = value.substring(`embedding:${length}`.length);
49457
+ processed[key] = `array|items:number|length:${length}|empty:false${rest}`;
49458
+ continue;
49459
+ }
49460
+ }
49461
+ // Expand embedding|... to array|items:number|...
49462
+ if (value.startsWith('embedding|') || value === 'embedding') {
49463
+ processed[key] = value.replace(/^embedding/, 'array|items:number|empty:false');
49464
+ continue;
49465
+ }
49466
+ processed[key] = value;
49467
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
49468
+ // Check if this is a validator type definition (has 'type' property that is NOT '$$type')
49469
+ // vs a nested object structure
49470
+ const hasValidatorType = value.type !== undefined && key !== '$$type';
49471
+
49472
+ if (hasValidatorType) {
49473
+ // This is a validator type definition (e.g., { type: 'array', items: 'number' }), pass it through
49474
+ processed[key] = value;
49475
+ } else {
49476
+ // This is a nested object structure, wrap it for validation
49477
+ const isExplicitRequired = value.$$type && value.$$type.includes('required');
49478
+ const isExplicitOptional = value.$$type && value.$$type.includes('optional');
49479
+ const objectConfig = {
49480
+ type: 'object',
49481
+ properties: this.preprocessAttributesForValidation(value),
49482
+ strict: false
49483
+ };
49484
+ // If explicitly required, don't mark as optional
49485
+ if (isExplicitRequired) ; else if (isExplicitOptional || this.allNestedObjectsOptional) {
49486
+ objectConfig.optional = true;
49487
+ }
49488
+ processed[key] = objectConfig;
49033
49489
  }
49034
- processed[key] = objectConfig;
49035
49490
  } else {
49036
49491
  processed[key] = value;
49037
49492
  }
49038
49493
  }
49039
-
49494
+
49040
49495
  return processed;
49041
49496
  }
49042
49497
  }
@@ -49112,7 +49567,11 @@ class ResourceReader extends EventEmitter {
49112
49567
  super();
49113
49568
 
49114
49569
  if (!resource) {
49115
- throw new Error("Resource is required for ResourceReader");
49570
+ throw new StreamError('Resource is required for ResourceReader', {
49571
+ operation: 'constructor',
49572
+ resource: resource?.name,
49573
+ suggestion: 'Pass a valid Resource instance when creating ResourceReader'
49574
+ });
49116
49575
  }
49117
49576
 
49118
49577
  this.resource = resource;
@@ -49269,7 +49728,10 @@ class ResourceWriter extends EventEmitter {
49269
49728
  function streamToString(stream) {
49270
49729
  return new Promise((resolve, reject) => {
49271
49730
  if (!stream) {
49272
- return reject(new Error('streamToString: stream is undefined'));
49731
+ return reject(new StreamError('Stream is undefined', {
49732
+ operation: 'streamToString',
49733
+ suggestion: 'Ensure a valid stream is passed to streamToString()'
49734
+ }));
49273
49735
  }
49274
49736
  const chunks = [];
49275
49737
  stream.on('data', (chunk) => chunks.push(chunk));
@@ -49617,9 +50079,16 @@ async function handleInsert$4({ resource, data, mappedData, originalData }) {
49617
50079
  });
49618
50080
 
49619
50081
  if (totalSize > effectiveLimit) {
49620
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
50082
+ throw new MetadataLimitError('Metadata size exceeds 2KB limit on insert', {
50083
+ totalSize,
50084
+ effectiveLimit,
50085
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
50086
+ excess: totalSize - effectiveLimit,
50087
+ resourceName: resource.name,
50088
+ operation: 'insert'
50089
+ });
49621
50090
  }
49622
-
50091
+
49623
50092
  // If data fits in metadata, store only in metadata
49624
50093
  return { mappedData, body: "" };
49625
50094
  }
@@ -49638,7 +50107,15 @@ async function handleUpdate$4({ resource, id, data, mappedData, originalData })
49638
50107
  });
49639
50108
 
49640
50109
  if (totalSize > effectiveLimit) {
49641
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
50110
+ throw new MetadataLimitError('Metadata size exceeds 2KB limit on update', {
50111
+ totalSize,
50112
+ effectiveLimit,
50113
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
50114
+ excess: totalSize - effectiveLimit,
50115
+ resourceName: resource.name,
50116
+ operation: 'update',
50117
+ id
50118
+ });
49642
50119
  }
49643
50120
  return { mappedData, body: JSON.stringify(mappedData) };
49644
50121
  }
@@ -49657,7 +50134,15 @@ async function handleUpsert$4({ resource, id, data, mappedData }) {
49657
50134
  });
49658
50135
 
49659
50136
  if (totalSize > effectiveLimit) {
49660
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
50137
+ throw new MetadataLimitError('Metadata size exceeds 2KB limit on upsert', {
50138
+ totalSize,
50139
+ effectiveLimit,
50140
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
50141
+ excess: totalSize - effectiveLimit,
50142
+ resourceName: resource.name,
50143
+ operation: 'upsert',
50144
+ id
50145
+ });
49661
50146
  }
49662
50147
  return { mappedData, body: "" };
49663
50148
  }
@@ -50350,7 +50835,11 @@ const behaviors = {
50350
50835
  function getBehavior(behaviorName) {
50351
50836
  const behavior = behaviors[behaviorName];
50352
50837
  if (!behavior) {
50353
- throw new Error(`Unknown behavior: ${behaviorName}. Available behaviors: ${Object.keys(behaviors).join(', ')}`);
50838
+ throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
50839
+ behavior: behaviorName,
50840
+ availableBehaviors: Object.keys(behaviors),
50841
+ operation: 'getBehavior'
50842
+ });
50354
50843
  }
50355
50844
  return behavior;
50356
50845
  }
@@ -50478,6 +50967,7 @@ class Resource extends AsyncEventEmitter {
50478
50967
  idGenerator: customIdGenerator,
50479
50968
  idSize = 22,
50480
50969
  versioningEnabled = false,
50970
+ strictValidation = true,
50481
50971
  events = {},
50482
50972
  asyncEvents = true,
50483
50973
  asyncPartitions = true,
@@ -50493,6 +50983,7 @@ class Resource extends AsyncEventEmitter {
50493
50983
  this.parallelism = parallelism;
50494
50984
  this.passphrase = passphrase ?? 'secret';
50495
50985
  this.versioningEnabled = versioningEnabled;
50986
+ this.strictValidation = strictValidation;
50496
50987
 
50497
50988
  // Configure async events mode
50498
50989
  this.setAsyncMode(asyncEvents);
@@ -50816,9 +51307,14 @@ class Resource extends AsyncEventEmitter {
50816
51307
 
50817
51308
  /**
50818
51309
  * Validate that all partition fields exist in current resource attributes
50819
- * @throws {Error} If partition fields don't exist in current schema
51310
+ * @throws {Error} If partition fields don't exist in current schema (only when strictValidation is true)
50820
51311
  */
50821
51312
  validatePartitions() {
51313
+ // Skip validation if strictValidation is disabled
51314
+ if (!this.strictValidation) {
51315
+ return;
51316
+ }
51317
+
50822
51318
  if (!this.config.partitions) {
50823
51319
  return; // No partitions to validate
50824
51320
  }
@@ -53368,6 +53864,7 @@ class Database extends EventEmitter {
53368
53864
  this.passphrase = options.passphrase || "secret";
53369
53865
  this.versioningEnabled = options.versioningEnabled || false;
53370
53866
  this.persistHooks = options.persistHooks || false; // New configuration for hook persistence
53867
+ this.strictValidation = options.strictValidation !== false; // Enable strict validation by default
53371
53868
 
53372
53869
  // Initialize hooks system
53373
53870
  this._initHooks();
@@ -53532,6 +54029,7 @@ class Database extends EventEmitter {
53532
54029
  asyncEvents: versionData.asyncEvents !== undefined ? versionData.asyncEvents : true,
53533
54030
  hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : (versionData.hooks || {}),
53534
54031
  versioningEnabled: this.versioningEnabled,
54032
+ strictValidation: this.strictValidation,
53535
54033
  map: versionData.map,
53536
54034
  idGenerator: restoredIdGenerator,
53537
54035
  idSize: restoredIdSize
@@ -53783,7 +54281,12 @@ class Database extends EventEmitter {
53783
54281
  const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
53784
54282
 
53785
54283
  if (!plugin) {
53786
- throw new Error(`Plugin '${name}' not found`);
54284
+ throw new DatabaseError(`Plugin '${name}' not found`, {
54285
+ operation: 'uninstallPlugin',
54286
+ pluginName: name,
54287
+ availablePlugins: Object.keys(this.pluginRegistry),
54288
+ suggestion: 'Check plugin name or list available plugins using Object.keys(db.pluginRegistry)'
54289
+ });
53787
54290
  }
53788
54291
 
53789
54292
  // Stop the plugin first
@@ -54332,6 +54835,7 @@ class Database extends EventEmitter {
54332
54835
  autoDecrypt: config.autoDecrypt !== undefined ? config.autoDecrypt : true,
54333
54836
  hooks: hooks || {},
54334
54837
  versioningEnabled: this.versioningEnabled,
54838
+ strictValidation: this.strictValidation,
54335
54839
  map: config.map,
54336
54840
  idGenerator: config.idGenerator,
54337
54841
  idSize: config.idSize,
@@ -54548,10 +55052,20 @@ class Database extends EventEmitter {
54548
55052
  addHook(event, fn) {
54549
55053
  if (!this._hooks) this._initHooks();
54550
55054
  if (!this._hooks.has(event)) {
54551
- throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(', ')}`);
55055
+ throw new DatabaseError(`Unknown hook event: ${event}`, {
55056
+ operation: 'addHook',
55057
+ invalidEvent: event,
55058
+ availableEvents: this._hookEvents,
55059
+ suggestion: `Use one of the available hook events: ${this._hookEvents.join(', ')}`
55060
+ });
54552
55061
  }
54553
55062
  if (typeof fn !== 'function') {
54554
- throw new Error('Hook function must be a function');
55063
+ throw new DatabaseError('Hook function must be a function', {
55064
+ operation: 'addHook',
55065
+ event,
55066
+ receivedType: typeof fn,
55067
+ suggestion: 'Provide a function that will be called when the hook event occurs'
55068
+ });
54555
55069
  }
54556
55070
  this._hooks.get(event).push(fn);
54557
55071
  }