s3db.js 12.2.4 → 12.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/s3db.cjs.js +329 -43
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +329 -43
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/body-only.js +15 -5
- package/src/behaviors/body-overflow.js +9 -0
- package/src/behaviors/user-managed.js +8 -1
- package/src/concerns/typescript-generator.js +12 -2
- package/src/plugins/api/utils/openapi-generator.js +21 -2
- package/src/plugins/replicators/bigquery-replicator.class.js +9 -1
- package/src/plugins/replicators/mysql-replicator.class.js +9 -1
- package/src/plugins/replicators/planetscale-replicator.class.js +9 -1
- package/src/plugins/replicators/postgres-replicator.class.js +9 -1
- package/src/plugins/replicators/schema-sync.helper.js +19 -0
- package/src/plugins/replicators/turso-replicator.class.js +9 -1
- package/src/plugins/vector.plugin.js +3 -3
- package/src/resource.class.js +203 -4
- package/src/schema.class.js +223 -33
package/src/schema.class.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { flatten, unflatten } from "flat";
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
set,
|
|
@@ -36,6 +37,73 @@ function generateBase62Mapping(keys) {
|
|
|
36
37
|
return { mapping, reversedMapping };
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Generate stable hash for plugin attribute ID
|
|
42
|
+
* Uses plugin name + attribute name to create deterministic, stable IDs
|
|
43
|
+
* This ensures IDs don't change when other plugins are added/removed
|
|
44
|
+
*
|
|
45
|
+
* Uses base62 encoding for maximum compactness (3 chars = 238,328 combinations)
|
|
46
|
+
* Perfect for realistic plugin usage (2-15 plugins per resource)
|
|
47
|
+
*
|
|
48
|
+
* @param {string} pluginName - Name of the plugin
|
|
49
|
+
* @param {string} attributeName - Name of the attribute
|
|
50
|
+
* @returns {string} Stable hash ID like 'pX7k' (3 chars base62)
|
|
51
|
+
*/
|
|
52
|
+
function generatePluginAttributeHash(pluginName, attributeName) {
|
|
53
|
+
const input = `${pluginName}:${attributeName}`;
|
|
54
|
+
const hash = createHash('sha256').update(input).digest();
|
|
55
|
+
|
|
56
|
+
// Convert first 4 bytes to integer
|
|
57
|
+
const num = hash.readUInt32BE(0);
|
|
58
|
+
|
|
59
|
+
// Convert to base62 and take first 3 characters
|
|
60
|
+
const base62Hash = toBase62(num);
|
|
61
|
+
|
|
62
|
+
// Pad with zeros if needed and take first 3 chars
|
|
63
|
+
const paddedHash = base62Hash.padStart(3, '0').substring(0, 3);
|
|
64
|
+
|
|
65
|
+
// S3 metadata keys are case-insensitive and stored in lowercase
|
|
66
|
+
// So we must lowercase the hash to ensure consistency
|
|
67
|
+
return 'p' + paddedHash.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate plugin attribute mapping with stable hash-based IDs
|
|
72
|
+
* Each plugin attribute gets a unique, stable ID based on plugin name + attribute name
|
|
73
|
+
* IDs don't change when other plugins are added/removed, preventing data corruption
|
|
74
|
+
*
|
|
75
|
+
* Uses 3-char base62 encoding for maximum compactness:
|
|
76
|
+
* - 62^3 = 238,328 possible combinations
|
|
77
|
+
* - Perfect for realistic usage (2-15 plugins per resource)
|
|
78
|
+
* - Saves ~62.5% metadata space vs 8-char hex format
|
|
79
|
+
*
|
|
80
|
+
* @param {Array<{key: string, pluginName: string}>} attributes - Array of plugin attributes with metadata
|
|
81
|
+
* @returns {Object} Mapping object with stable hash-based keys (e.g., 'pX7k')
|
|
82
|
+
*/
|
|
83
|
+
function generatePluginMapping(attributes) {
|
|
84
|
+
const mapping = {};
|
|
85
|
+
const reversedMapping = {};
|
|
86
|
+
const usedHashes = new Set();
|
|
87
|
+
|
|
88
|
+
for (const { key, pluginName } of attributes) {
|
|
89
|
+
let hash = generatePluginAttributeHash(pluginName, key);
|
|
90
|
+
|
|
91
|
+
// Handle collisions by appending counter
|
|
92
|
+
let counter = 1;
|
|
93
|
+
let finalHash = hash;
|
|
94
|
+
while (usedHashes.has(finalHash)) {
|
|
95
|
+
finalHash = `${hash}${counter}`; // e.g., pX7k1, pX7k2
|
|
96
|
+
counter++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
usedHashes.add(finalHash);
|
|
100
|
+
mapping[key] = finalHash;
|
|
101
|
+
reversedMapping[finalHash] = key;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { mapping, reversedMapping };
|
|
105
|
+
}
|
|
106
|
+
|
|
39
107
|
export const SchemaActions = {
|
|
40
108
|
trim: (value) => value == null ? value : value.trim(),
|
|
41
109
|
|
|
@@ -463,11 +531,14 @@ export class Schema {
|
|
|
463
531
|
constructor(args) {
|
|
464
532
|
const {
|
|
465
533
|
map,
|
|
534
|
+
pluginMap,
|
|
466
535
|
name,
|
|
467
536
|
attributes,
|
|
468
537
|
passphrase,
|
|
469
538
|
version = 1,
|
|
470
|
-
options = {}
|
|
539
|
+
options = {},
|
|
540
|
+
_pluginAttributeMetadata,
|
|
541
|
+
_pluginAttributes
|
|
471
542
|
} = args;
|
|
472
543
|
|
|
473
544
|
this.name = name;
|
|
@@ -477,6 +548,10 @@ export class Schema {
|
|
|
477
548
|
this.options = merge({}, this.defaultOptions(), options);
|
|
478
549
|
this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
|
|
479
550
|
|
|
551
|
+
// Initialize plugin attribute metadata tracking
|
|
552
|
+
this._pluginAttributeMetadata = _pluginAttributeMetadata || {};
|
|
553
|
+
this._pluginAttributes = _pluginAttributes || {};
|
|
554
|
+
|
|
480
555
|
// Preprocess attributes to handle nested objects for validator compilation
|
|
481
556
|
const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
|
|
482
557
|
|
|
@@ -494,19 +569,65 @@ export class Schema {
|
|
|
494
569
|
else {
|
|
495
570
|
const flatAttrs = flatten(this.attributes, { safe: true });
|
|
496
571
|
const leafKeys = Object.keys(flatAttrs).filter(k => !k.includes('$$'));
|
|
497
|
-
|
|
572
|
+
|
|
498
573
|
// Also include parent object keys for objects that can be empty
|
|
499
574
|
const objectKeys = this.extractObjectKeys(this.attributes);
|
|
500
|
-
|
|
575
|
+
|
|
501
576
|
// Combine leaf keys and object keys, removing duplicates
|
|
502
577
|
const allKeys = [...new Set([...leafKeys, ...objectKeys])];
|
|
503
|
-
|
|
504
|
-
//
|
|
505
|
-
const
|
|
578
|
+
|
|
579
|
+
// Separate user attributes from plugin attributes
|
|
580
|
+
const userKeys = [];
|
|
581
|
+
const pluginAttributes = []; // Array of {key, pluginName}
|
|
582
|
+
|
|
583
|
+
for (const key of allKeys) {
|
|
584
|
+
const attrDef = this.getAttributeDefinition(key);
|
|
585
|
+
// Check if it's a plugin attribute (object with __plugin__ OR string with metadata)
|
|
586
|
+
if (typeof attrDef === 'object' && attrDef !== null && attrDef.__plugin__) {
|
|
587
|
+
pluginAttributes.push({ key, pluginName: attrDef.__plugin__ });
|
|
588
|
+
} else if (typeof attrDef === 'string' && this._pluginAttributeMetadata && this._pluginAttributeMetadata[key]) {
|
|
589
|
+
const pluginName = this._pluginAttributeMetadata[key].__plugin__;
|
|
590
|
+
pluginAttributes.push({ key, pluginName });
|
|
591
|
+
} else {
|
|
592
|
+
userKeys.push(key);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Generate base62 mapping for user attributes
|
|
597
|
+
const { mapping, reversedMapping } = generateBase62Mapping(userKeys);
|
|
506
598
|
this.map = mapping;
|
|
507
599
|
this.reversedMap = reversedMapping;
|
|
508
|
-
|
|
509
600
|
|
|
601
|
+
// Generate plugin mapping with stable hash-based IDs
|
|
602
|
+
const { mapping: pMapping, reversedMapping: pReversedMapping } = generatePluginMapping(pluginAttributes);
|
|
603
|
+
this.pluginMap = pMapping;
|
|
604
|
+
this.reversedPluginMap = pReversedMapping;
|
|
605
|
+
|
|
606
|
+
// Build _pluginAttributes reverse mapping (pluginName -> array of attribute names)
|
|
607
|
+
this._pluginAttributes = {};
|
|
608
|
+
for (const { key, pluginName } of pluginAttributes) {
|
|
609
|
+
if (!this._pluginAttributes[pluginName]) {
|
|
610
|
+
this._pluginAttributes[pluginName] = [];
|
|
611
|
+
}
|
|
612
|
+
this._pluginAttributes[pluginName].push(key);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// If pluginMap was provided, use it
|
|
617
|
+
if (!isEmpty(pluginMap)) {
|
|
618
|
+
this.pluginMap = pluginMap;
|
|
619
|
+
this.reversedPluginMap = invert(pluginMap);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Initialize plugin maps if not set
|
|
623
|
+
if (!this.pluginMap) {
|
|
624
|
+
this.pluginMap = {};
|
|
625
|
+
this.reversedPluginMap = {};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Initialize _pluginAttributes if not set
|
|
629
|
+
if (!this._pluginAttributes) {
|
|
630
|
+
this._pluginAttributes = {};
|
|
510
631
|
}
|
|
511
632
|
}
|
|
512
633
|
|
|
@@ -839,6 +960,8 @@ export class Schema {
|
|
|
839
960
|
static import(data) {
|
|
840
961
|
let {
|
|
841
962
|
map,
|
|
963
|
+
pluginMap,
|
|
964
|
+
_pluginAttributeMetadata,
|
|
842
965
|
name,
|
|
843
966
|
options,
|
|
844
967
|
version,
|
|
@@ -852,11 +975,18 @@ export class Schema {
|
|
|
852
975
|
|
|
853
976
|
const schema = new Schema({
|
|
854
977
|
map,
|
|
978
|
+
pluginMap: pluginMap || {},
|
|
855
979
|
name,
|
|
856
980
|
options,
|
|
857
981
|
version,
|
|
858
982
|
attributes
|
|
859
983
|
});
|
|
984
|
+
|
|
985
|
+
// Restore plugin metadata for string definitions
|
|
986
|
+
if (_pluginAttributeMetadata) {
|
|
987
|
+
schema._pluginAttributeMetadata = _pluginAttributeMetadata;
|
|
988
|
+
}
|
|
989
|
+
|
|
860
990
|
return schema;
|
|
861
991
|
}
|
|
862
992
|
|
|
@@ -898,6 +1028,9 @@ export class Schema {
|
|
|
898
1028
|
options: this.options,
|
|
899
1029
|
attributes: this._exportAttributes(this.attributes),
|
|
900
1030
|
map: this.map,
|
|
1031
|
+
pluginMap: this.pluginMap || {},
|
|
1032
|
+
_pluginAttributeMetadata: this._pluginAttributeMetadata || {},
|
|
1033
|
+
_pluginAttributes: this._pluginAttributes || {}
|
|
901
1034
|
};
|
|
902
1035
|
return data;
|
|
903
1036
|
}
|
|
@@ -956,8 +1089,10 @@ export class Schema {
|
|
|
956
1089
|
// Then flatten the object
|
|
957
1090
|
const flattenedObj = flatten(obj, { safe: true });
|
|
958
1091
|
const rest = { '_v': this.version + '' };
|
|
1092
|
+
|
|
959
1093
|
for (const [key, value] of Object.entries(flattenedObj)) {
|
|
960
|
-
|
|
1094
|
+
// Try plugin map first, then user map, then use original key
|
|
1095
|
+
const mappedKey = this.pluginMap[key] || this.map[key] || key;
|
|
961
1096
|
// Always map numbers to base36
|
|
962
1097
|
const attrDef = this.getAttributeDefinition(key);
|
|
963
1098
|
if (typeof value === 'number' && typeof attrDef === 'string' && attrDef.includes('number')) {
|
|
@@ -980,14 +1115,22 @@ export class Schema {
|
|
|
980
1115
|
return rest;
|
|
981
1116
|
}
|
|
982
1117
|
|
|
983
|
-
async unmapper(mappedResourceItem, mapOverride) {
|
|
1118
|
+
async unmapper(mappedResourceItem, mapOverride, pluginMapOverride) {
|
|
984
1119
|
let obj = cloneDeep(mappedResourceItem);
|
|
985
1120
|
delete obj._v;
|
|
986
1121
|
obj = await this.applyHooksActions(obj, "beforeUnmap");
|
|
987
1122
|
const reversedMap = mapOverride ? invert(mapOverride) : this.reversedMap;
|
|
1123
|
+
const reversedPluginMap = pluginMapOverride ? invert(pluginMapOverride) : this.reversedPluginMap;
|
|
988
1124
|
const rest = {};
|
|
989
1125
|
for (const [key, value] of Object.entries(obj)) {
|
|
990
|
-
|
|
1126
|
+
// Try plugin reversed map first, then user reversed map, then use original key
|
|
1127
|
+
let originalKey = reversedPluginMap[key] || reversedMap[key] || key;
|
|
1128
|
+
|
|
1129
|
+
// If key not found in either map, use original key
|
|
1130
|
+
if (!originalKey) {
|
|
1131
|
+
originalKey = key;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
991
1134
|
let parsedValue = value;
|
|
992
1135
|
const attrDef = this.getAttributeDefinition(originalKey);
|
|
993
1136
|
const hasAfterUnmapHook = this.options.hooks?.afterUnmap?.[originalKey];
|
|
@@ -1067,6 +1210,50 @@ export class Schema {
|
|
|
1067
1210
|
return def;
|
|
1068
1211
|
}
|
|
1069
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* Regenerate plugin attribute mapping
|
|
1215
|
+
* Called when plugin attributes are added or removed
|
|
1216
|
+
* @returns {void}
|
|
1217
|
+
*/
|
|
1218
|
+
regeneratePluginMapping() {
|
|
1219
|
+
const flatAttrs = flatten(this.attributes, { safe: true });
|
|
1220
|
+
const leafKeys = Object.keys(flatAttrs).filter(k => !k.includes('$$'));
|
|
1221
|
+
|
|
1222
|
+
// Also include parent object keys for objects that can be empty
|
|
1223
|
+
const objectKeys = this.extractObjectKeys(this.attributes);
|
|
1224
|
+
|
|
1225
|
+
// Combine leaf keys and object keys, removing duplicates
|
|
1226
|
+
const allKeys = [...new Set([...leafKeys, ...objectKeys])];
|
|
1227
|
+
|
|
1228
|
+
// Extract only plugin attributes
|
|
1229
|
+
const pluginAttributes = []; // Array of {key, pluginName}
|
|
1230
|
+
for (const key of allKeys) {
|
|
1231
|
+
const attrDef = this.getAttributeDefinition(key);
|
|
1232
|
+
// Check if it's a plugin attribute (object with __plugin__ OR string with metadata)
|
|
1233
|
+
if (typeof attrDef === 'object' && attrDef !== null && attrDef.__plugin__) {
|
|
1234
|
+
pluginAttributes.push({ key, pluginName: attrDef.__plugin__ });
|
|
1235
|
+
} else if (typeof attrDef === 'string' && this._pluginAttributeMetadata && this._pluginAttributeMetadata[key]) {
|
|
1236
|
+
// String definition with plugin metadata
|
|
1237
|
+
const pluginName = this._pluginAttributeMetadata[key].__plugin__;
|
|
1238
|
+
pluginAttributes.push({ key, pluginName });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Regenerate plugin mapping with stable hash-based IDs
|
|
1243
|
+
const { mapping, reversedMapping } = generatePluginMapping(pluginAttributes);
|
|
1244
|
+
this.pluginMap = mapping;
|
|
1245
|
+
this.reversedPluginMap = reversedMapping;
|
|
1246
|
+
|
|
1247
|
+
// Rebuild _pluginAttributes reverse mapping (pluginName -> array of attribute names)
|
|
1248
|
+
this._pluginAttributes = {};
|
|
1249
|
+
for (const { key, pluginName } of pluginAttributes) {
|
|
1250
|
+
if (!this._pluginAttributes[pluginName]) {
|
|
1251
|
+
this._pluginAttributes[pluginName] = [];
|
|
1252
|
+
}
|
|
1253
|
+
this._pluginAttributes[pluginName].push(key);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1070
1257
|
/**
|
|
1071
1258
|
* Preprocess attributes to convert nested objects into validator-compatible format
|
|
1072
1259
|
* @param {Object} attributes - Original attributes
|
|
@@ -1160,45 +1347,48 @@ export class Schema {
|
|
|
1160
1347
|
const hasValidatorType = value.type !== undefined && key !== '$$type';
|
|
1161
1348
|
|
|
1162
1349
|
if (hasValidatorType) {
|
|
1350
|
+
// Remove plugin metadata from all object definitions
|
|
1351
|
+
const { __plugin__, __pluginCreated__, ...cleanValue } = value;
|
|
1352
|
+
|
|
1163
1353
|
// Handle ip4 and ip6 object notation
|
|
1164
|
-
if (
|
|
1165
|
-
processed[key] = { ...
|
|
1166
|
-
} else if (
|
|
1167
|
-
processed[key] = { ...
|
|
1168
|
-
} else if (
|
|
1354
|
+
if (cleanValue.type === 'ip4') {
|
|
1355
|
+
processed[key] = { ...cleanValue, type: 'string' };
|
|
1356
|
+
} else if (cleanValue.type === 'ip6') {
|
|
1357
|
+
processed[key] = { ...cleanValue, type: 'string' };
|
|
1358
|
+
} else if (cleanValue.type === 'money' || cleanValue.type === 'crypto') {
|
|
1169
1359
|
// Money/crypto type → number with min:0
|
|
1170
|
-
processed[key] = { ...
|
|
1171
|
-
} else if (
|
|
1360
|
+
processed[key] = { ...cleanValue, type: 'number', min: cleanValue.min !== undefined ? cleanValue.min : 0 };
|
|
1361
|
+
} else if (cleanValue.type === 'decimal') {
|
|
1172
1362
|
// Decimal type → number
|
|
1173
|
-
processed[key] = { ...
|
|
1174
|
-
} else if (
|
|
1363
|
+
processed[key] = { ...cleanValue, type: 'number' };
|
|
1364
|
+
} else if (cleanValue.type === 'geo:lat' || cleanValue.type === 'geo-lat') {
|
|
1175
1365
|
// Geo latitude → number with range [-90, 90]
|
|
1176
1366
|
processed[key] = {
|
|
1177
|
-
...
|
|
1367
|
+
...cleanValue,
|
|
1178
1368
|
type: 'number',
|
|
1179
|
-
min:
|
|
1180
|
-
max:
|
|
1369
|
+
min: cleanValue.min !== undefined ? cleanValue.min : -90,
|
|
1370
|
+
max: cleanValue.max !== undefined ? cleanValue.max : 90
|
|
1181
1371
|
};
|
|
1182
|
-
} else if (
|
|
1372
|
+
} else if (cleanValue.type === 'geo:lon' || cleanValue.type === 'geo-lon') {
|
|
1183
1373
|
// Geo longitude → number with range [-180, 180]
|
|
1184
1374
|
processed[key] = {
|
|
1185
|
-
...
|
|
1375
|
+
...cleanValue,
|
|
1186
1376
|
type: 'number',
|
|
1187
|
-
min:
|
|
1188
|
-
max:
|
|
1377
|
+
min: cleanValue.min !== undefined ? cleanValue.min : -180,
|
|
1378
|
+
max: cleanValue.max !== undefined ? cleanValue.max : 180
|
|
1189
1379
|
};
|
|
1190
|
-
} else if (
|
|
1380
|
+
} else if (cleanValue.type === 'geo:point' || cleanValue.type === 'geo-point') {
|
|
1191
1381
|
// Geo point → any (will be validated in hooks)
|
|
1192
|
-
processed[key] = { ...
|
|
1193
|
-
} else if (
|
|
1382
|
+
processed[key] = { ...cleanValue, type: 'any' };
|
|
1383
|
+
} else if (cleanValue.type === 'object' && cleanValue.properties) {
|
|
1194
1384
|
// Recursively process nested object properties
|
|
1195
1385
|
processed[key] = {
|
|
1196
|
-
...
|
|
1197
|
-
properties: this.preprocessAttributesForValidation(
|
|
1386
|
+
...cleanValue,
|
|
1387
|
+
properties: this.preprocessAttributesForValidation(cleanValue.properties)
|
|
1198
1388
|
};
|
|
1199
1389
|
} else {
|
|
1200
|
-
// This is a validator type definition (e.g., { type: 'array', items: 'number' })
|
|
1201
|
-
processed[key] =
|
|
1390
|
+
// This is a validator type definition (e.g., { type: 'array', items: 'number' })
|
|
1391
|
+
processed[key] = cleanValue;
|
|
1202
1392
|
}
|
|
1203
1393
|
} else {
|
|
1204
1394
|
// This is a nested object structure, wrap it for validation
|