s3db.js 11.3.2 → 12.0.1
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/README.md +102 -8
- package/dist/s3db.cjs.js +36945 -15510
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +66 -1
- package/dist/s3db.es.js +36914 -15534
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +35 -15
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +79 -49
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +97 -47
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +544 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +354 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicator.plugin.js +2 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +315 -36
- package/src/s3db.d.ts +66 -1
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
package/src/schema.class.js
CHANGED
|
@@ -15,7 +15,10 @@ 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, encodeFixedPoint, decodeFixedPoint } from "./concerns/base62.js";
|
|
18
|
+
import { encode as toBase62, decode as fromBase62, encodeDecimal, decodeDecimal, encodeFixedPoint, decodeFixedPoint, encodeFixedPointBatch, decodeFixedPointBatch } from "./concerns/base62.js";
|
|
19
|
+
import { encodeIPv4, decodeIPv4, encodeIPv6, decodeIPv6, isValidIPv4, isValidIPv6 } from "./concerns/ip.js";
|
|
20
|
+
import { encodeMoney, decodeMoney, getCurrencyDecimals } from "./concerns/money.js";
|
|
21
|
+
import { encodeGeoLat, decodeGeoLat, encodeGeoLon, decodeGeoLon, encodeGeoPoint, decodeGeoPoint } from "./concerns/geo-encoding.js";
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Generate base62 mapping for attributes
|
|
@@ -279,29 +282,33 @@ export const SchemaActions = {
|
|
|
279
282
|
return value;
|
|
280
283
|
}
|
|
281
284
|
if (value.length === 0) {
|
|
282
|
-
return '';
|
|
285
|
+
return '^[]';
|
|
283
286
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
287
|
+
// Use batch encoding for massive compression (17% additional savings)
|
|
288
|
+
// Format: ^[val1,val2,val3,...] instead of ^val1,^val2,^val3,...
|
|
289
|
+
return encodeFixedPointBatch(value, precision);
|
|
293
290
|
},
|
|
294
291
|
toArrayOfEmbeddings: (value, { separator, precision = 6 }) => {
|
|
295
292
|
if (Array.isArray(value)) {
|
|
296
|
-
|
|
293
|
+
// Already an array, return as-is
|
|
294
|
+
return value;
|
|
297
295
|
}
|
|
298
296
|
if (value === null || value === undefined) {
|
|
299
297
|
return value;
|
|
300
298
|
}
|
|
301
|
-
if (value === '') {
|
|
299
|
+
if (value === '' || value === '^[]') {
|
|
302
300
|
return [];
|
|
303
301
|
}
|
|
302
|
+
|
|
304
303
|
const str = String(value);
|
|
304
|
+
|
|
305
|
+
// Check if this is batch-encoded (^[...])
|
|
306
|
+
if (str.startsWith('^[')) {
|
|
307
|
+
return decodeFixedPointBatch(str, precision);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Fallback: Legacy format with individual prefixes (^val,^val,^val)
|
|
311
|
+
// This maintains backwards compatibility with data encoded before batch optimization
|
|
305
312
|
const items = [];
|
|
306
313
|
let current = '';
|
|
307
314
|
let i = 0;
|
|
@@ -329,6 +336,129 @@ export const SchemaActions = {
|
|
|
329
336
|
});
|
|
330
337
|
},
|
|
331
338
|
|
|
339
|
+
encodeIPv4: (value) => {
|
|
340
|
+
if (value === null || value === undefined) return value;
|
|
341
|
+
if (typeof value !== 'string') return value;
|
|
342
|
+
if (!isValidIPv4(value)) return value;
|
|
343
|
+
const [ok, err, encoded] = tryFnSync(() => encodeIPv4(value));
|
|
344
|
+
return ok ? encoded : value;
|
|
345
|
+
},
|
|
346
|
+
decodeIPv4: (value) => {
|
|
347
|
+
if (value === null || value === undefined) return value;
|
|
348
|
+
if (typeof value !== 'string') return value;
|
|
349
|
+
const [ok, err, decoded] = tryFnSync(() => decodeIPv4(value));
|
|
350
|
+
return ok ? decoded : value;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
encodeIPv6: (value) => {
|
|
354
|
+
if (value === null || value === undefined) return value;
|
|
355
|
+
if (typeof value !== 'string') return value;
|
|
356
|
+
if (!isValidIPv6(value)) return value;
|
|
357
|
+
const [ok, err, encoded] = tryFnSync(() => encodeIPv6(value));
|
|
358
|
+
return ok ? encoded : value;
|
|
359
|
+
},
|
|
360
|
+
decodeIPv6: (value) => {
|
|
361
|
+
if (value === null || value === undefined) return value;
|
|
362
|
+
if (typeof value !== 'string') return value;
|
|
363
|
+
const [ok, err, decoded] = tryFnSync(() => decodeIPv6(value));
|
|
364
|
+
return ok ? decoded : value;
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
// Money type - Integer-based (banking standard)
|
|
368
|
+
// Simplified approach: decimals instead of currency
|
|
369
|
+
encodeMoney: (value, { decimals = 2 } = {}) => {
|
|
370
|
+
if (value === null || value === undefined) return value;
|
|
371
|
+
if (typeof value !== 'number') return value;
|
|
372
|
+
|
|
373
|
+
// Use decimal places directly instead of currency lookup
|
|
374
|
+
const multiplier = Math.pow(10, decimals);
|
|
375
|
+
const integerValue = Math.round(value * multiplier);
|
|
376
|
+
|
|
377
|
+
// Encode as base62 with $ prefix
|
|
378
|
+
const [ok, err, encoded] = tryFnSync(() => '$' + toBase62(integerValue));
|
|
379
|
+
return ok ? encoded : value;
|
|
380
|
+
},
|
|
381
|
+
decodeMoney: (value, { decimals = 2 } = {}) => {
|
|
382
|
+
if (value === null || value === undefined) return value;
|
|
383
|
+
if (typeof value !== 'string') return value;
|
|
384
|
+
if (!value.startsWith('$')) return value;
|
|
385
|
+
|
|
386
|
+
// Decode base62 and convert back to decimal
|
|
387
|
+
const [ok, err, integerValue] = tryFnSync(() => fromBase62(value.slice(1)));
|
|
388
|
+
if (!ok || isNaN(integerValue)) return value;
|
|
389
|
+
|
|
390
|
+
const divisor = Math.pow(10, decimals);
|
|
391
|
+
return integerValue / divisor;
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// Decimal type - Fixed-point for non-monetary decimals
|
|
395
|
+
encodeDecimalFixed: (value, { precision = 2 } = {}) => {
|
|
396
|
+
if (value === null || value === undefined) return value;
|
|
397
|
+
if (typeof value !== 'number') return value;
|
|
398
|
+
const [ok, err, encoded] = tryFnSync(() => encodeFixedPoint(value, precision));
|
|
399
|
+
return ok ? encoded : value;
|
|
400
|
+
},
|
|
401
|
+
decodeDecimalFixed: (value, { precision = 2 } = {}) => {
|
|
402
|
+
if (value === null || value === undefined) return value;
|
|
403
|
+
if (typeof value !== 'string') return value;
|
|
404
|
+
const [ok, err, decoded] = tryFnSync(() => decodeFixedPoint(value, precision));
|
|
405
|
+
return ok ? decoded : value;
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
// Geo types - Latitude
|
|
409
|
+
encodeGeoLatitude: (value, { precision = 6 } = {}) => {
|
|
410
|
+
if (value === null || value === undefined) return value;
|
|
411
|
+
if (typeof value !== 'number') return value;
|
|
412
|
+
const [ok, err, encoded] = tryFnSync(() => encodeGeoLat(value, precision));
|
|
413
|
+
return ok ? encoded : value;
|
|
414
|
+
},
|
|
415
|
+
decodeGeoLatitude: (value, { precision = 6 } = {}) => {
|
|
416
|
+
if (value === null || value === undefined) return value;
|
|
417
|
+
if (typeof value !== 'string') return value;
|
|
418
|
+
const [ok, err, decoded] = tryFnSync(() => decodeGeoLat(value, precision));
|
|
419
|
+
return ok ? decoded : value;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// Geo types - Longitude
|
|
423
|
+
encodeGeoLongitude: (value, { precision = 6 } = {}) => {
|
|
424
|
+
if (value === null || value === undefined) return value;
|
|
425
|
+
if (typeof value !== 'number') return value;
|
|
426
|
+
const [ok, err, encoded] = tryFnSync(() => encodeGeoLon(value, precision));
|
|
427
|
+
return ok ? encoded : value;
|
|
428
|
+
},
|
|
429
|
+
decodeGeoLongitude: (value, { precision = 6 } = {}) => {
|
|
430
|
+
if (value === null || value === undefined) return value;
|
|
431
|
+
if (typeof value !== 'string') return value;
|
|
432
|
+
const [ok, err, decoded] = tryFnSync(() => decodeGeoLon(value, precision));
|
|
433
|
+
return ok ? decoded : value;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
// Geo types - Point (lat+lon pair)
|
|
437
|
+
encodeGeoPointPair: (value, { precision = 6 } = {}) => {
|
|
438
|
+
if (value === null || value === undefined) return value;
|
|
439
|
+
// Accept object with lat/lon or array [lat, lon]
|
|
440
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
441
|
+
const [ok, err, encoded] = tryFnSync(() => encodeGeoPoint(value[0], value[1], precision));
|
|
442
|
+
return ok ? encoded : value;
|
|
443
|
+
}
|
|
444
|
+
if (typeof value === 'object' && value.lat !== undefined && value.lon !== undefined) {
|
|
445
|
+
const [ok, err, encoded] = tryFnSync(() => encodeGeoPoint(value.lat, value.lon, precision));
|
|
446
|
+
return ok ? encoded : value;
|
|
447
|
+
}
|
|
448
|
+
if (typeof value === 'object' && value.latitude !== undefined && value.longitude !== undefined) {
|
|
449
|
+
const [ok, err, encoded] = tryFnSync(() => encodeGeoPoint(value.latitude, value.longitude, precision));
|
|
450
|
+
return ok ? encoded : value;
|
|
451
|
+
}
|
|
452
|
+
return value;
|
|
453
|
+
},
|
|
454
|
+
decodeGeoPointPair: (value, { precision = 6 } = {}) => {
|
|
455
|
+
if (value === null || value === undefined) return value;
|
|
456
|
+
if (typeof value !== 'string') return value;
|
|
457
|
+
const [ok, err, decoded] = tryFnSync(() => decodeGeoPoint(value, precision));
|
|
458
|
+
// Return as object { latitude, longitude }
|
|
459
|
+
return ok ? decoded : value;
|
|
460
|
+
},
|
|
461
|
+
|
|
332
462
|
}
|
|
333
463
|
|
|
334
464
|
export class Schema {
|
|
@@ -353,7 +483,7 @@ export class Schema {
|
|
|
353
483
|
const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
|
|
354
484
|
|
|
355
485
|
this.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
|
|
356
|
-
{ $$async: true },
|
|
486
|
+
{ $$async: true, $$strict: false },
|
|
357
487
|
processedAttributes,
|
|
358
488
|
))
|
|
359
489
|
|
|
@@ -398,9 +528,11 @@ export class Schema {
|
|
|
398
528
|
}
|
|
399
529
|
}
|
|
400
530
|
|
|
401
|
-
addHook(hook, attribute, action) {
|
|
531
|
+
addHook(hook, attribute, action, params = {}) {
|
|
402
532
|
if (!this.options.hooks[hook][attribute]) this.options.hooks[hook][attribute] = [];
|
|
403
|
-
|
|
533
|
+
// Store action with parameters if provided
|
|
534
|
+
const hookEntry = Object.keys(params).length > 0 ? { action, params } : action;
|
|
535
|
+
this.options.hooks[hook][attribute] = uniq([...this.options.hooks[hook][attribute], hookEntry])
|
|
404
536
|
}
|
|
405
537
|
|
|
406
538
|
extractObjectKeys(obj, prefix = '') {
|
|
@@ -573,6 +705,97 @@ export class Schema {
|
|
|
573
705
|
continue;
|
|
574
706
|
}
|
|
575
707
|
|
|
708
|
+
// Handle ip4 type
|
|
709
|
+
if (defStr.includes("ip4") || defType === 'ip4') {
|
|
710
|
+
this.addHook("beforeMap", name, "encodeIPv4");
|
|
711
|
+
this.addHook("afterUnmap", name, "decodeIPv4");
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Handle ip6 type
|
|
716
|
+
if (defStr.includes("ip6") || defType === 'ip6') {
|
|
717
|
+
this.addHook("beforeMap", name, "encodeIPv6");
|
|
718
|
+
this.addHook("afterUnmap", name, "decodeIPv6");
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Handle money type (integer-based, decimal-aware)
|
|
723
|
+
if (defStr.includes("money") || defType === 'money' || defStr.includes("crypto") || defType === 'crypto') {
|
|
724
|
+
// Extract decimals from money:8 or crypto:8 notation
|
|
725
|
+
let decimals = 2; // Default for fiat money (2 decimal places)
|
|
726
|
+
|
|
727
|
+
// If it's crypto, default to 8 decimals (satoshi/standard crypto)
|
|
728
|
+
if (defStr.includes("crypto") || defType === 'crypto') {
|
|
729
|
+
decimals = 8;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Override with explicit decimals if provided: money:8, crypto:18
|
|
733
|
+
const decimalsMatch = defStr.match(/(?:money|crypto):(\d+)/i);
|
|
734
|
+
if (decimalsMatch) {
|
|
735
|
+
decimals = parseInt(decimalsMatch[1], 10);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
this.addHook("beforeMap", name, "encodeMoney", { decimals });
|
|
739
|
+
this.addHook("afterUnmap", name, "decodeMoney", { decimals });
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Handle decimal type (fixed-point for non-monetary decimals)
|
|
744
|
+
if (defStr.includes("decimal") || defType === 'decimal') {
|
|
745
|
+
// Extract precision from decimal:4 notation
|
|
746
|
+
let precision = 2; // Default precision
|
|
747
|
+
const precisionMatch = defStr.match(/decimal:(\d+)/);
|
|
748
|
+
if (precisionMatch) {
|
|
749
|
+
precision = parseInt(precisionMatch[1], 10);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
this.addHook("beforeMap", name, "encodeDecimalFixed", { precision });
|
|
753
|
+
this.addHook("afterUnmap", name, "decodeDecimalFixed", { precision });
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Handle geo:lat type (latitude)
|
|
758
|
+
if (defStr.includes("geo:lat") || (defType === 'geo' && defStr.includes('lat'))) {
|
|
759
|
+
// Extract precision from geo:lat:6 notation
|
|
760
|
+
let precision = 6; // Default precision (GPS standard)
|
|
761
|
+
const precisionMatch = defStr.match(/geo:lat:(\d+)/);
|
|
762
|
+
if (precisionMatch) {
|
|
763
|
+
precision = parseInt(precisionMatch[1], 10);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
this.addHook("beforeMap", name, "encodeGeoLatitude", { precision });
|
|
767
|
+
this.addHook("afterUnmap", name, "decodeGeoLatitude", { precision });
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Handle geo:lon type (longitude)
|
|
772
|
+
if (defStr.includes("geo:lon") || (defType === 'geo' && defStr.includes('lon'))) {
|
|
773
|
+
// Extract precision from geo:lon:6 notation
|
|
774
|
+
let precision = 6; // Default precision
|
|
775
|
+
const precisionMatch = defStr.match(/geo:lon:(\d+)/);
|
|
776
|
+
if (precisionMatch) {
|
|
777
|
+
precision = parseInt(precisionMatch[1], 10);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this.addHook("beforeMap", name, "encodeGeoLongitude", { precision });
|
|
781
|
+
this.addHook("afterUnmap", name, "decodeGeoLongitude", { precision });
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Handle geo:point type (lat+lon pair)
|
|
786
|
+
if (defStr.includes("geo:point") || defType === 'geo:point') {
|
|
787
|
+
// Extract precision from geo:point:6 notation
|
|
788
|
+
let precision = 6; // Default precision
|
|
789
|
+
const precisionMatch = defStr.match(/geo:point:(\d+)/);
|
|
790
|
+
if (precisionMatch) {
|
|
791
|
+
precision = parseInt(precisionMatch[1], 10);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.addHook("beforeMap", name, "encodeGeoPointPair", { precision });
|
|
795
|
+
this.addHook("afterUnmap", name, "decodeGeoPointPair", { precision });
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
576
799
|
// Handle numbers (only for non-array fields)
|
|
577
800
|
if (defStr.includes("number") || defType === 'number') {
|
|
578
801
|
// Check if it's specifically an integer field
|
|
@@ -704,12 +927,17 @@ export class Schema {
|
|
|
704
927
|
async applyHooksActions(resourceItem, hook) {
|
|
705
928
|
const cloned = cloneDeep(resourceItem);
|
|
706
929
|
for (const [attribute, actions] of Object.entries(this.options.hooks[hook])) {
|
|
707
|
-
for (const
|
|
930
|
+
for (const actionEntry of actions) {
|
|
931
|
+
// Support both string actions and {action, params} objects
|
|
932
|
+
const actionName = typeof actionEntry === 'string' ? actionEntry : actionEntry.action;
|
|
933
|
+
const actionParams = typeof actionEntry === 'object' ? actionEntry.params : {};
|
|
934
|
+
|
|
708
935
|
const value = get(cloned, attribute)
|
|
709
|
-
if (value !== undefined && typeof SchemaActions[
|
|
710
|
-
set(cloned, attribute, await SchemaActions[
|
|
936
|
+
if (value !== undefined && typeof SchemaActions[actionName] === 'function') {
|
|
937
|
+
set(cloned, attribute, await SchemaActions[actionName](value, {
|
|
711
938
|
passphrase: this.passphrase,
|
|
712
939
|
separator: this.options.arraySeparator,
|
|
940
|
+
...actionParams // Merge custom parameters (currency, precision, etc.)
|
|
713
941
|
}))
|
|
714
942
|
}
|
|
715
943
|
}
|
|
@@ -785,27 +1013,35 @@ export class Schema {
|
|
|
785
1013
|
}
|
|
786
1014
|
}
|
|
787
1015
|
// PATCH: ensure arrays are always arrays
|
|
1016
|
+
// Skip automatic array conversion if there's an afterUnmap hook that will handle it
|
|
788
1017
|
if (this.attributes) {
|
|
789
1018
|
if (typeof attrDef === 'string' && attrDef.includes('array')) {
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1019
|
+
if (!hasAfterUnmapHook) {
|
|
1020
|
+
if (Array.isArray(parsedValue)) {
|
|
1021
|
+
// Already an array
|
|
1022
|
+
} else if (typeof parsedValue === 'string' && parsedValue.trim().startsWith('[')) {
|
|
1023
|
+
const [okArr, errArr, arr] = tryFnSync(() => JSON.parse(parsedValue));
|
|
1024
|
+
if (okArr && Array.isArray(arr)) {
|
|
1025
|
+
parsedValue = arr;
|
|
1026
|
+
}
|
|
1027
|
+
} else {
|
|
1028
|
+
parsedValue = SchemaActions.toArray(parsedValue, { separator: this.options.arraySeparator });
|
|
796
1029
|
}
|
|
797
|
-
} else {
|
|
798
|
-
parsedValue = SchemaActions.toArray(parsedValue, { separator: this.options.arraySeparator });
|
|
799
1030
|
}
|
|
800
1031
|
}
|
|
801
1032
|
}
|
|
802
1033
|
// PATCH: apply afterUnmap hooks for type restoration
|
|
803
1034
|
if (this.options.hooks && this.options.hooks.afterUnmap && this.options.hooks.afterUnmap[originalKey]) {
|
|
804
|
-
for (const
|
|
805
|
-
|
|
806
|
-
|
|
1035
|
+
for (const actionEntry of this.options.hooks.afterUnmap[originalKey]) {
|
|
1036
|
+
// Support both string actions and {action, params} objects
|
|
1037
|
+
const actionName = typeof actionEntry === 'string' ? actionEntry : actionEntry.action;
|
|
1038
|
+
const actionParams = typeof actionEntry === 'object' ? actionEntry.params : {};
|
|
1039
|
+
|
|
1040
|
+
if (typeof SchemaActions[actionName] === 'function') {
|
|
1041
|
+
parsedValue = await SchemaActions[actionName](parsedValue, {
|
|
807
1042
|
passphrase: this.passphrase,
|
|
808
1043
|
separator: this.options.arraySeparator,
|
|
1044
|
+
...actionParams // Merge custom parameters (currency, precision, etc.)
|
|
809
1045
|
});
|
|
810
1046
|
}
|
|
811
1047
|
}
|
|
@@ -843,6 +1079,66 @@ export class Schema {
|
|
|
843
1079
|
|
|
844
1080
|
for (const [key, value] of Object.entries(attributes)) {
|
|
845
1081
|
if (typeof value === 'string') {
|
|
1082
|
+
// Expand ip4 shorthand to string type with custom validation
|
|
1083
|
+
if (value === 'ip4' || value.startsWith('ip4|')) {
|
|
1084
|
+
processed[key] = value.replace(/^ip4/, 'string');
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
// Expand ip6 shorthand to string type with custom validation
|
|
1088
|
+
if (value === 'ip6' || value.startsWith('ip6|')) {
|
|
1089
|
+
processed[key] = value.replace(/^ip6/, 'string');
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
// Expand money/crypto shorthand to number type with min validation
|
|
1093
|
+
if (value === 'money' || value.startsWith('money:') || value.startsWith('money|') ||
|
|
1094
|
+
value === 'crypto' || value.startsWith('crypto:') || value.startsWith('crypto|')) {
|
|
1095
|
+
// Extract any modifiers after money:N or crypto:N
|
|
1096
|
+
const rest = value.replace(/^(?:money|crypto)(?::\d+)?/, '');
|
|
1097
|
+
// Money must be non-negative
|
|
1098
|
+
const hasMin = rest.includes('min:');
|
|
1099
|
+
processed[key] = hasMin ? `number${rest}` : `number|min:0${rest}`;
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
// Expand decimal shorthand to number type
|
|
1103
|
+
if (value === 'decimal' || value.startsWith('decimal:') || value.startsWith('decimal|')) {
|
|
1104
|
+
// Extract any modifiers after decimal:PRECISION
|
|
1105
|
+
const rest = value.replace(/^decimal(:\d+)?/, '');
|
|
1106
|
+
processed[key] = `number${rest}`;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
// Expand geo:lat shorthand to number type with range validation
|
|
1110
|
+
if (value.startsWith('geo:lat')) {
|
|
1111
|
+
// Extract any modifiers after geo:lat:PRECISION
|
|
1112
|
+
const rest = value.replace(/^geo:lat(:\d+)?/, '');
|
|
1113
|
+
// Latitude range: -90 to 90
|
|
1114
|
+
const hasMin = rest.includes('min:');
|
|
1115
|
+
const hasMax = rest.includes('max:');
|
|
1116
|
+
let validation = 'number';
|
|
1117
|
+
if (!hasMin) validation += '|min:-90';
|
|
1118
|
+
if (!hasMax) validation += '|max:90';
|
|
1119
|
+
processed[key] = validation + rest;
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
// Expand geo:lon shorthand to number type with range validation
|
|
1123
|
+
if (value.startsWith('geo:lon')) {
|
|
1124
|
+
// Extract any modifiers after geo:lon:PRECISION
|
|
1125
|
+
const rest = value.replace(/^geo:lon(:\d+)?/, '');
|
|
1126
|
+
// Longitude range: -180 to 180
|
|
1127
|
+
const hasMin = rest.includes('min:');
|
|
1128
|
+
const hasMax = rest.includes('max:');
|
|
1129
|
+
let validation = 'number';
|
|
1130
|
+
if (!hasMin) validation += '|min:-180';
|
|
1131
|
+
if (!hasMax) validation += '|max:180';
|
|
1132
|
+
processed[key] = validation + rest;
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
// Expand geo:point shorthand to object with lat/lon
|
|
1136
|
+
if (value.startsWith('geo:point')) {
|
|
1137
|
+
// geo:point is an object or array with lat/lon
|
|
1138
|
+
// For simplicity, allow it as any type (will be validated in hooks)
|
|
1139
|
+
processed[key] = 'any';
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
846
1142
|
// Expand embedding:XXX shorthand to array|items:number|length:XXX
|
|
847
1143
|
if (value.startsWith('embedding:')) {
|
|
848
1144
|
const lengthMatch = value.match(/embedding:(\d+)/);
|
|
@@ -866,8 +1162,46 @@ export class Schema {
|
|
|
866
1162
|
const hasValidatorType = value.type !== undefined && key !== '$$type';
|
|
867
1163
|
|
|
868
1164
|
if (hasValidatorType) {
|
|
869
|
-
//
|
|
870
|
-
|
|
1165
|
+
// Handle ip4 and ip6 object notation
|
|
1166
|
+
if (value.type === 'ip4') {
|
|
1167
|
+
processed[key] = { ...value, type: 'string' };
|
|
1168
|
+
} else if (value.type === 'ip6') {
|
|
1169
|
+
processed[key] = { ...value, type: 'string' };
|
|
1170
|
+
} else if (value.type === 'money' || value.type === 'crypto') {
|
|
1171
|
+
// Money/crypto type → number with min:0
|
|
1172
|
+
processed[key] = { ...value, type: 'number', min: value.min !== undefined ? value.min : 0 };
|
|
1173
|
+
} else if (value.type === 'decimal') {
|
|
1174
|
+
// Decimal type → number
|
|
1175
|
+
processed[key] = { ...value, type: 'number' };
|
|
1176
|
+
} else if (value.type === 'geo:lat' || value.type === 'geo-lat') {
|
|
1177
|
+
// Geo latitude → number with range [-90, 90]
|
|
1178
|
+
processed[key] = {
|
|
1179
|
+
...value,
|
|
1180
|
+
type: 'number',
|
|
1181
|
+
min: value.min !== undefined ? value.min : -90,
|
|
1182
|
+
max: value.max !== undefined ? value.max : 90
|
|
1183
|
+
};
|
|
1184
|
+
} else if (value.type === 'geo:lon' || value.type === 'geo-lon') {
|
|
1185
|
+
// Geo longitude → number with range [-180, 180]
|
|
1186
|
+
processed[key] = {
|
|
1187
|
+
...value,
|
|
1188
|
+
type: 'number',
|
|
1189
|
+
min: value.min !== undefined ? value.min : -180,
|
|
1190
|
+
max: value.max !== undefined ? value.max : 180
|
|
1191
|
+
};
|
|
1192
|
+
} else if (value.type === 'geo:point' || value.type === 'geo-point') {
|
|
1193
|
+
// Geo point → any (will be validated in hooks)
|
|
1194
|
+
processed[key] = { ...value, type: 'any' };
|
|
1195
|
+
} else if (value.type === 'object' && value.properties) {
|
|
1196
|
+
// Recursively process nested object properties
|
|
1197
|
+
processed[key] = {
|
|
1198
|
+
...value,
|
|
1199
|
+
properties: this.preprocessAttributesForValidation(value.properties)
|
|
1200
|
+
};
|
|
1201
|
+
} else {
|
|
1202
|
+
// This is a validator type definition (e.g., { type: 'array', items: 'number' }), pass it through
|
|
1203
|
+
processed[key] = value;
|
|
1204
|
+
}
|
|
871
1205
|
} else {
|
|
872
1206
|
// This is a nested object structure, wrap it for validation
|
|
873
1207
|
const isExplicitRequired = value.$$type && value.$$type.includes('required');
|
package/SECURITY.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Security Policy
|
|
2
|
-
|
|
3
|
-
## Supported Versions
|
|
4
|
-
|
|
5
|
-
| Version | Supported |
|
|
6
|
-
| ------- | ------------------ |
|
|
7
|
-
| 11.x.x | :white_check_mark: |
|
|
8
|
-
| < 11.0 | :x: |
|
|
9
|
-
|
|
10
|
-
## Known Security Advisories
|
|
11
|
-
|
|
12
|
-
### Development Dependencies
|
|
13
|
-
|
|
14
|
-
The following vulnerabilities exist in **development-only** dependencies and **do not affect** the published npm package or runtime security:
|
|
15
|
-
|
|
16
|
-
#### pkg (GHSA-22r3-9w55-cj54) - MODERATE
|
|
17
|
-
- **Status**: Acknowledged, monitored
|
|
18
|
-
- **Impact**: Local privilege escalation
|
|
19
|
-
- **Scope**: Only affects developers running `pnpm run build:binaries`
|
|
20
|
-
- **Mitigation**: pkg is deprecated and archived. No patched version available (`<0.0.0`).
|
|
21
|
-
- **Risk Assessment**: LOW - Only used for creating standalone binaries during release process
|
|
22
|
-
- **Future Plans**: Migrate to Node.js Single Executable Applications (SEA) when stable
|
|
23
|
-
|
|
24
|
-
#### tar-fs - HIGH
|
|
25
|
-
- **Status**: RESOLVED in v11.1.1+
|
|
26
|
-
- **Fix**: Updated to patched version 2.1.4+
|
|
27
|
-
|
|
28
|
-
## Reporting a Vulnerability
|
|
29
|
-
|
|
30
|
-
If you discover a security vulnerability in the **runtime code** (not dev dependencies), please report it by:
|
|
31
|
-
|
|
32
|
-
1. **DO NOT** open a public issue
|
|
33
|
-
2. Email: [security contact - update this]
|
|
34
|
-
3. Include:
|
|
35
|
-
- Description of the vulnerability
|
|
36
|
-
- Steps to reproduce
|
|
37
|
-
- Potential impact
|
|
38
|
-
- Suggested fix (if any)
|
|
39
|
-
|
|
40
|
-
### Response Timeline
|
|
41
|
-
|
|
42
|
-
- **Initial Response**: Within 48 hours
|
|
43
|
-
- **Status Update**: Within 7 days
|
|
44
|
-
- **Fix Timeline**: Depends on severity
|
|
45
|
-
- Critical: 7 days
|
|
46
|
-
- High: 14 days
|
|
47
|
-
- Medium: 30 days
|
|
48
|
-
- Low: 60 days
|
|
49
|
-
|
|
50
|
-
## Security Best Practices
|
|
51
|
-
|
|
52
|
-
### For Users
|
|
53
|
-
|
|
54
|
-
1. **Always encrypt sensitive data**: Use `secret` field type for passwords, tokens, etc.
|
|
55
|
-
2. **Validate credentials**: Never commit AWS credentials to version control
|
|
56
|
-
3. **Use IAM policies**: Implement least-privilege access for S3 buckets
|
|
57
|
-
4. **Enable paranoid mode**: For production, use `paranoid: true` for soft deletes
|
|
58
|
-
5. **Audit hooks**: Review serialized functions before deploying to production
|
|
59
|
-
|
|
60
|
-
### For Contributors
|
|
61
|
-
|
|
62
|
-
1. **No secrets in tests**: Use environment variables or LocalStack
|
|
63
|
-
2. **Validate input**: All user input should be validated before S3 operations
|
|
64
|
-
3. **Handle errors safely**: Never expose AWS error details to end users
|
|
65
|
-
4. **Review dependencies**: Run `pnpm audit` before submitting PRs
|
|
66
|
-
5. **Test encryption**: Verify `secret` fields are actually encrypted in S3
|
|
67
|
-
|
|
68
|
-
## Audit Configuration
|
|
69
|
-
|
|
70
|
-
This project uses `audit-level=high` in `.npmrc` to focus on critical vulnerabilities affecting production. Moderate/low severity issues in dev-only dependencies are monitored but may not block releases if:
|
|
71
|
-
|
|
72
|
-
- They only affect development tools
|
|
73
|
-
- No patch is available
|
|
74
|
-
- The risk is assessed as acceptable
|
|
75
|
-
|
|
76
|
-
Current audit threshold: **HIGH** (ignores moderate/low in dev dependencies)
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { PartitionDriverError } from '../errors.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Base class for all partition drivers
|
|
6
|
-
* Defines the interface that all drivers must implement
|
|
7
|
-
*/
|
|
8
|
-
export class BasePartitionDriver extends EventEmitter {
|
|
9
|
-
constructor(options = {}) {
|
|
10
|
-
super();
|
|
11
|
-
this.options = options;
|
|
12
|
-
this.stats = {
|
|
13
|
-
queued: 0,
|
|
14
|
-
processed: 0,
|
|
15
|
-
failed: 0,
|
|
16
|
-
processing: 0
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Initialize the driver
|
|
22
|
-
*/
|
|
23
|
-
async initialize() {
|
|
24
|
-
// Override in subclasses if needed
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Queue partition operations for processing
|
|
29
|
-
* @param {Object} operation - The partition operation to queue
|
|
30
|
-
* @param {string} operation.type - 'create', 'update', or 'delete'
|
|
31
|
-
* @param {Object} operation.resource - The resource instance
|
|
32
|
-
* @param {Object} operation.data - The data for the operation
|
|
33
|
-
*/
|
|
34
|
-
async queue(operation) {
|
|
35
|
-
throw new PartitionDriverError('queue() must be implemented by subclass', {
|
|
36
|
-
driver: this.name || 'BasePartitionDriver',
|
|
37
|
-
operation: 'queue',
|
|
38
|
-
suggestion: 'Extend BasePartitionDriver and implement the queue() method'
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Process a single partition operation
|
|
44
|
-
*/
|
|
45
|
-
async processOperation(operation) {
|
|
46
|
-
const { type, resource, data } = operation;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
this.stats.processing++;
|
|
50
|
-
|
|
51
|
-
switch (type) {
|
|
52
|
-
case 'create':
|
|
53
|
-
await resource.createPartitionReferences(data.object);
|
|
54
|
-
break;
|
|
55
|
-
|
|
56
|
-
case 'update':
|
|
57
|
-
await resource.handlePartitionReferenceUpdates(data.original, data.updated);
|
|
58
|
-
break;
|
|
59
|
-
|
|
60
|
-
case 'delete':
|
|
61
|
-
await resource.deletePartitionReferences(data.object);
|
|
62
|
-
break;
|
|
63
|
-
|
|
64
|
-
default:
|
|
65
|
-
throw new PartitionDriverError(`Unknown partition operation type: ${type}`, {
|
|
66
|
-
driver: this.name || 'BasePartitionDriver',
|
|
67
|
-
operation: type,
|
|
68
|
-
availableOperations: ['create', 'update', 'delete'],
|
|
69
|
-
suggestion: 'Use one of the supported partition operations: create, update, or delete'
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.stats.processed++;
|
|
74
|
-
this.emit('processed', operation);
|
|
75
|
-
|
|
76
|
-
} catch (error) {
|
|
77
|
-
this.stats.failed++;
|
|
78
|
-
this.emit('error', { operation, error });
|
|
79
|
-
throw error;
|
|
80
|
-
} finally {
|
|
81
|
-
this.stats.processing--;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Flush any pending operations
|
|
87
|
-
*/
|
|
88
|
-
async flush() {
|
|
89
|
-
// Override in subclasses if needed
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get driver statistics
|
|
94
|
-
*/
|
|
95
|
-
getStats() {
|
|
96
|
-
return { ...this.stats };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Shutdown the driver
|
|
101
|
-
*/
|
|
102
|
-
async shutdown() {
|
|
103
|
-
await this.flush();
|
|
104
|
-
this.removeAllListeners();
|
|
105
|
-
}
|
|
106
|
-
}
|