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
@@ -15,7 +15,7 @@ import { encrypt, decrypt } from "./concerns/crypto.js";
15
15
  import { ValidatorManager } from "./validator.class.js";
16
16
  import { tryFn, tryFnSync } from "./concerns/try-fn.js";
17
17
  import { SchemaError } from "./errors.js";
18
- import { encode as toBase62, decode as fromBase62, encodeDecimal, decodeDecimal } from "./concerns/base62.js";
18
+ import { encode as toBase62, decode as fromBase62, encodeDecimal, decodeDecimal, encodeFixedPoint, decodeFixedPoint } from "./concerns/base62.js";
19
19
 
20
20
  /**
21
21
  * Generate base62 mapping for attributes
@@ -274,6 +274,60 @@ export const SchemaActions = {
274
274
  return NaN;
275
275
  });
276
276
  },
277
+ fromArrayOfEmbeddings: (value, { separator, precision = 6 }) => {
278
+ if (value === null || value === undefined || !Array.isArray(value)) {
279
+ return value;
280
+ }
281
+ if (value.length === 0) {
282
+ return '';
283
+ }
284
+ const encodedItems = value.map(item => {
285
+ if (typeof item === 'number' && !isNaN(item)) {
286
+ return encodeFixedPoint(item, precision);
287
+ }
288
+ // fallback: try to parse as number, else keep as is
289
+ const n = Number(item);
290
+ return isNaN(n) ? '' : encodeFixedPoint(n, precision);
291
+ });
292
+ return encodedItems.join(separator);
293
+ },
294
+ toArrayOfEmbeddings: (value, { separator, precision = 6 }) => {
295
+ if (Array.isArray(value)) {
296
+ return value.map(v => (typeof v === 'number' ? v : decodeFixedPoint(v, precision)));
297
+ }
298
+ if (value === null || value === undefined) {
299
+ return value;
300
+ }
301
+ if (value === '') {
302
+ return [];
303
+ }
304
+ const str = String(value);
305
+ const items = [];
306
+ let current = '';
307
+ let i = 0;
308
+ while (i < str.length) {
309
+ if (str[i] === '\\' && i + 1 < str.length) {
310
+ current += str[i + 1];
311
+ i += 2;
312
+ } else if (str[i] === separator) {
313
+ items.push(current);
314
+ current = '';
315
+ i++;
316
+ } else {
317
+ current += str[i];
318
+ i++;
319
+ }
320
+ }
321
+ items.push(current);
322
+ return items.map(v => {
323
+ if (typeof v === 'number') return v;
324
+ if (typeof v === 'string' && v !== '') {
325
+ const n = decodeFixedPoint(v, precision);
326
+ return isNaN(n) ? NaN : n;
327
+ }
328
+ return NaN;
329
+ });
330
+ },
277
331
 
278
332
  }
279
333
 
@@ -351,16 +405,16 @@ export class Schema {
351
405
 
352
406
  extractObjectKeys(obj, prefix = '') {
353
407
  const objectKeys = [];
354
-
408
+
355
409
  for (const [key, value] of Object.entries(obj)) {
356
410
  if (key.startsWith('$$')) continue; // Skip schema metadata
357
-
411
+
358
412
  const fullKey = prefix ? `${prefix}.${key}` : key;
359
-
413
+
360
414
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
361
415
  // This is an object, add its key
362
416
  objectKeys.push(fullKey);
363
-
417
+
364
418
  // Check if it has nested objects
365
419
  if (value.$$type === 'object') {
366
420
  // Recursively extract nested object keys
@@ -368,31 +422,137 @@ export class Schema {
368
422
  }
369
423
  }
370
424
  }
371
-
425
+
372
426
  return objectKeys;
373
427
  }
374
428
 
429
+ _generateHooksFromOriginalAttributes(attributes, prefix = '') {
430
+ for (const [key, value] of Object.entries(attributes)) {
431
+ if (key.startsWith('$$')) continue;
432
+
433
+ const fullKey = prefix ? `${prefix}.${key}` : key;
434
+
435
+ // Check if this is an object notation type definition (has 'type' property)
436
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && value.type) {
437
+ if (value.type === 'array' && value.items) {
438
+ // Handle array with object notation
439
+ const itemsType = value.items;
440
+ const arrayLength = typeof value.length === 'number' ? value.length : null;
441
+
442
+ if (itemsType === 'string' || (typeof itemsType === 'string' && itemsType.includes('string'))) {
443
+ this.addHook("beforeMap", fullKey, "fromArray");
444
+ this.addHook("afterUnmap", fullKey, "toArray");
445
+ } else if (itemsType === 'number' || (typeof itemsType === 'string' && itemsType.includes('number'))) {
446
+ const isIntegerArray = typeof itemsType === 'string' && itemsType.includes('integer');
447
+ const isEmbedding = !isIntegerArray && arrayLength !== null && arrayLength >= 256;
448
+
449
+ if (isIntegerArray) {
450
+ this.addHook("beforeMap", fullKey, "fromArrayOfNumbers");
451
+ this.addHook("afterUnmap", fullKey, "toArrayOfNumbers");
452
+ } else if (isEmbedding) {
453
+ this.addHook("beforeMap", fullKey, "fromArrayOfEmbeddings");
454
+ this.addHook("afterUnmap", fullKey, "toArrayOfEmbeddings");
455
+ } else {
456
+ this.addHook("beforeMap", fullKey, "fromArrayOfDecimals");
457
+ this.addHook("afterUnmap", fullKey, "toArrayOfDecimals");
458
+ }
459
+ }
460
+ }
461
+ // For other types with object notation, they'll be handled by the flattened processing
462
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value) && !value.type) {
463
+ // This is a nested object, recurse
464
+ this._generateHooksFromOriginalAttributes(value, fullKey);
465
+ }
466
+ }
467
+ }
468
+
375
469
  generateAutoHooks() {
470
+ // First, process the original attributes to find arrays with object notation
471
+ // This handles cases like: { type: 'array', items: 'number', length: 768 }
472
+ this._generateHooksFromOriginalAttributes(this.attributes);
473
+
474
+ // Then process the flattened schema for other types
376
475
  const schema = flatten(cloneDeep(this.attributes), { safe: true });
377
476
 
378
477
  for (const [name, definition] of Object.entries(schema)) {
379
- // Handle arrays first to avoid conflicts
380
- if (definition.includes("array")) {
381
- if (definition.includes('items:string')) {
478
+ // Skip metadata fields
479
+ if (name.includes('$$')) continue;
480
+
481
+ // Skip if hooks already exist (from object notation processing)
482
+ if (this.options.hooks.beforeMap[name] || this.options.hooks.afterUnmap[name]) {
483
+ continue;
484
+ }
485
+
486
+ // Normalize definition - can be a string or value from flattened object
487
+ const defStr = typeof definition === 'string' ? definition : '';
488
+ const defType = typeof definition === 'object' && definition !== null ? definition.type : null;
489
+
490
+ // Check if this is an embedding type (custom shorthand)
491
+ const isEmbeddingType = defStr.includes("embedding") || defType === 'embedding';
492
+
493
+ if (isEmbeddingType) {
494
+ // Extract length from embedding:1536 or embedding|length:1536
495
+ let embeddingLength = null;
496
+ const lengthMatch = defStr.match(/embedding:(\d+)/);
497
+ if (lengthMatch) {
498
+ embeddingLength = parseInt(lengthMatch[1], 10);
499
+ } else if (defStr.includes('length:')) {
500
+ const match = defStr.match(/length:(\d+)/);
501
+ if (match) embeddingLength = parseInt(match[1], 10);
502
+ }
503
+
504
+ // Embeddings always use fixed-point encoding
505
+ this.addHook("beforeMap", name, "fromArrayOfEmbeddings");
506
+ this.addHook("afterUnmap", name, "toArrayOfEmbeddings");
507
+ continue;
508
+ }
509
+
510
+ // Check if this is an array type
511
+ const isArray = defStr.includes("array") || defType === 'array';
512
+
513
+ if (isArray) {
514
+ // Determine item type for arrays
515
+ let itemsType = null;
516
+ if (typeof definition === 'object' && definition !== null && definition.items) {
517
+ itemsType = definition.items;
518
+ } else if (defStr.includes('items:string')) {
519
+ itemsType = 'string';
520
+ } else if (defStr.includes('items:number')) {
521
+ itemsType = 'number';
522
+ }
523
+
524
+ if (itemsType === 'string' || (typeof itemsType === 'string' && itemsType.includes('string'))) {
382
525
  this.addHook("beforeMap", name, "fromArray");
383
526
  this.addHook("afterUnmap", name, "toArray");
384
- } else if (definition.includes('items:number')) {
527
+ } else if (itemsType === 'number' || (typeof itemsType === 'string' && itemsType.includes('number'))) {
385
528
  // Check if the array items should be treated as integers
386
- const isIntegerArray = definition.includes("integer:true") ||
387
- definition.includes("|integer:") ||
388
- definition.includes("|integer");
389
-
529
+ const isIntegerArray = defStr.includes("integer:true") ||
530
+ defStr.includes("|integer:") ||
531
+ defStr.includes("|integer") ||
532
+ (typeof itemsType === 'string' && itemsType.includes('integer'));
533
+
534
+ // Check if this is an embedding array (large arrays of decimals)
535
+ // Common embedding dimensions: 256, 384, 512, 768, 1024, 1536, 2048, 3072
536
+ let arrayLength = null;
537
+ if (typeof definition === 'object' && definition !== null && typeof definition.length === 'number') {
538
+ arrayLength = definition.length;
539
+ } else if (defStr.includes('length:')) {
540
+ const match = defStr.match(/length:(\d+)/);
541
+ if (match) arrayLength = parseInt(match[1], 10);
542
+ }
543
+
544
+ const isEmbedding = !isIntegerArray && arrayLength !== null && arrayLength >= 256;
545
+
390
546
  if (isIntegerArray) {
391
547
  // Use standard base62 for arrays of integers
392
548
  this.addHook("beforeMap", name, "fromArrayOfNumbers");
393
549
  this.addHook("afterUnmap", name, "toArrayOfNumbers");
550
+ } else if (isEmbedding) {
551
+ // Use fixed-point encoding for embedding vectors (77% compression)
552
+ this.addHook("beforeMap", name, "fromArrayOfEmbeddings");
553
+ this.addHook("afterUnmap", name, "toArrayOfEmbeddings");
394
554
  } else {
395
- // Use decimal-aware base62 for arrays of decimals
555
+ // Use decimal-aware base62 for regular arrays of decimals
396
556
  this.addHook("beforeMap", name, "fromArrayOfDecimals");
397
557
  this.addHook("afterUnmap", name, "toArrayOfDecimals");
398
558
  }
@@ -402,7 +562,7 @@ export class Schema {
402
562
  }
403
563
 
404
564
  // Handle secrets
405
- if (definition.includes("secret")) {
565
+ if (defStr.includes("secret") || defType === 'secret') {
406
566
  if (this.options.autoEncrypt) {
407
567
  this.addHook("beforeMap", name, "encrypt");
408
568
  }
@@ -414,12 +574,12 @@ export class Schema {
414
574
  }
415
575
 
416
576
  // Handle numbers (only for non-array fields)
417
- if (definition.includes("number")) {
577
+ if (defStr.includes("number") || defType === 'number') {
418
578
  // Check if it's specifically an integer field
419
- const isInteger = definition.includes("integer:true") ||
420
- definition.includes("|integer:") ||
421
- definition.includes("|integer");
422
-
579
+ const isInteger = defStr.includes("integer:true") ||
580
+ defStr.includes("|integer:") ||
581
+ defStr.includes("|integer");
582
+
423
583
  if (isInteger) {
424
584
  // Use standard base62 for integers
425
585
  this.addHook("beforeMap", name, "toBase62");
@@ -433,21 +593,21 @@ export class Schema {
433
593
  }
434
594
 
435
595
  // Handle booleans
436
- if (definition.includes("boolean")) {
596
+ if (defStr.includes("boolean") || defType === 'boolean') {
437
597
  this.addHook("beforeMap", name, "fromBool");
438
598
  this.addHook("afterUnmap", name, "toBool");
439
599
  continue;
440
600
  }
441
601
 
442
602
  // Handle JSON fields
443
- if (definition.includes("json")) {
603
+ if (defStr.includes("json") || defType === 'json') {
444
604
  this.addHook("beforeMap", name, "toJSON");
445
605
  this.addHook("afterUnmap", name, "fromJSON");
446
606
  continue;
447
607
  }
448
608
 
449
609
  // Handle object fields - add JSON serialization hooks
450
- if (definition === "object" || definition.includes("object")) {
610
+ if (definition === "object" || defStr.includes("object") || defType === 'object') {
451
611
  this.addHook("beforeMap", name, "toJSON");
452
612
  this.addHook("afterUnmap", name, "fromJSON");
453
613
  continue;
@@ -604,8 +764,11 @@ export class Schema {
604
764
  const originalKey = reversedMap && reversedMap[key] ? reversedMap[key] : key;
605
765
  let parsedValue = value;
606
766
  const attrDef = this.getAttributeDefinition(originalKey);
767
+ const hasAfterUnmapHook = this.options.hooks?.afterUnmap?.[originalKey];
768
+
607
769
  // Always unmap base62 strings to numbers for number fields (but not array fields or decimal fields)
608
- if (typeof attrDef === 'string' && attrDef.includes('number') && !attrDef.includes('array') && !attrDef.includes('decimal')) {
770
+ // Skip if there are afterUnmap hooks that will handle the conversion
771
+ if (!hasAfterUnmapHook && typeof attrDef === 'string' && attrDef.includes('number') && !attrDef.includes('array') && !attrDef.includes('decimal')) {
609
772
  if (typeof parsedValue === 'string' && parsedValue !== '') {
610
773
  parsedValue = fromBase62(parsedValue);
611
774
  } else if (typeof parsedValue === 'number') {
@@ -677,28 +840,56 @@ export class Schema {
677
840
  */
678
841
  preprocessAttributesForValidation(attributes) {
679
842
  const processed = {};
680
-
843
+
681
844
  for (const [key, value] of Object.entries(attributes)) {
682
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
683
- const isExplicitRequired = value.$$type && value.$$type.includes('required');
684
- const isExplicitOptional = value.$$type && value.$$type.includes('optional');
685
- const objectConfig = {
686
- type: 'object',
687
- properties: this.preprocessAttributesForValidation(value),
688
- strict: false
689
- };
690
- // If explicitly required, don't mark as optional
691
- if (isExplicitRequired) {
692
- // nothing
693
- } else if (isExplicitOptional || this.allNestedObjectsOptional) {
694
- objectConfig.optional = true;
845
+ if (typeof value === 'string') {
846
+ // Expand embedding:XXX shorthand to array|items:number|length:XXX
847
+ if (value.startsWith('embedding:')) {
848
+ const lengthMatch = value.match(/embedding:(\d+)/);
849
+ if (lengthMatch) {
850
+ const length = lengthMatch[1];
851
+ // Extract any additional modifiers after the length
852
+ const rest = value.substring(`embedding:${length}`.length);
853
+ processed[key] = `array|items:number|length:${length}|empty:false${rest}`;
854
+ continue;
855
+ }
856
+ }
857
+ // Expand embedding|... to array|items:number|...
858
+ if (value.startsWith('embedding|') || value === 'embedding') {
859
+ processed[key] = value.replace(/^embedding/, 'array|items:number|empty:false');
860
+ continue;
861
+ }
862
+ processed[key] = value;
863
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
864
+ // Check if this is a validator type definition (has 'type' property that is NOT '$$type')
865
+ // vs a nested object structure
866
+ const hasValidatorType = value.type !== undefined && key !== '$$type';
867
+
868
+ if (hasValidatorType) {
869
+ // This is a validator type definition (e.g., { type: 'array', items: 'number' }), pass it through
870
+ processed[key] = value;
871
+ } else {
872
+ // This is a nested object structure, wrap it for validation
873
+ const isExplicitRequired = value.$$type && value.$$type.includes('required');
874
+ const isExplicitOptional = value.$$type && value.$$type.includes('optional');
875
+ const objectConfig = {
876
+ type: 'object',
877
+ properties: this.preprocessAttributesForValidation(value),
878
+ strict: false
879
+ };
880
+ // If explicitly required, don't mark as optional
881
+ if (isExplicitRequired) {
882
+ // nothing
883
+ } else if (isExplicitOptional || this.allNestedObjectsOptional) {
884
+ objectConfig.optional = true;
885
+ }
886
+ processed[key] = objectConfig;
695
887
  }
696
- processed[key] = objectConfig;
697
888
  } else {
698
889
  processed[key] = value;
699
890
  }
700
891
  }
701
-
892
+
702
893
  return processed;
703
894
  }
704
895
  }
@@ -3,10 +3,15 @@ export * from "./resource-writer.class.js"
3
3
  export * from "./resource-ids-reader.class.js"
4
4
  export * from "./resource-ids-page-reader.class.js"
5
5
 
6
+ import { StreamError } from '../errors.js';
7
+
6
8
  export function streamToString(stream) {
7
9
  return new Promise((resolve, reject) => {
8
10
  if (!stream) {
9
- return reject(new Error('streamToString: stream is undefined'));
11
+ return reject(new StreamError('Stream is undefined', {
12
+ operation: 'streamToString',
13
+ suggestion: 'Ensure a valid stream is passed to streamToString()'
14
+ }));
10
15
  }
11
16
  const chunks = [];
12
17
  stream.on('data', (chunk) => chunks.push(chunk));
@@ -4,13 +4,18 @@ import { PromisePool } from "@supercharge/promise-pool";
4
4
 
5
5
  import { ResourceIdsPageReader } from "./resource-ids-page-reader.class.js"
6
6
  import tryFn from "../concerns/try-fn.js";
7
+ import { StreamError } from '../errors.js';
7
8
 
8
9
  export class ResourceReader extends EventEmitter {
9
10
  constructor({ resource, batchSize = 10, concurrency = 5 }) {
10
11
  super()
11
12
 
12
13
  if (!resource) {
13
- throw new Error("Resource is required for ResourceReader");
14
+ throw new StreamError('Resource is required for ResourceReader', {
15
+ operation: 'constructor',
16
+ resource: resource?.name,
17
+ suggestion: 'Pass a valid Resource instance when creating ResourceReader'
18
+ });
14
19
  }
15
20
 
16
21
  this.resource = resource;
@@ -82,6 +82,14 @@ export class Validator extends FastestValidator {
82
82
  type: "any",
83
83
  custom: this.autoEncrypt ? jsonHandler : undefined,
84
84
  })
85
+
86
+ // Embedding type - shorthand for arrays of numbers optimized for embeddings
87
+ // Usage: 'embedding:1536' or 'embedding|length:768'
88
+ this.alias('embedding', {
89
+ type: "array",
90
+ items: "number",
91
+ empty: false,
92
+ })
85
93
  }
86
94
  }
87
95