s3db.js 12.2.3 → 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/README.md +915 -1669
- package/dist/s3db.cjs.js +610 -50
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +610 -50
- 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 +361 -9
- package/src/resource.class.js +203 -4
- package/src/schema.class.js +223 -33
package/dist/s3db.es.js
CHANGED
|
@@ -13670,7 +13670,11 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
13670
13670
|
}
|
|
13671
13671
|
continue;
|
|
13672
13672
|
}
|
|
13673
|
-
const
|
|
13673
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
13674
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
13675
|
+
const attributes = Object.fromEntries(
|
|
13676
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
13677
|
+
);
|
|
13674
13678
|
for (const tableConfig of tableConfigs) {
|
|
13675
13679
|
const tableName = tableConfig.table;
|
|
13676
13680
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -14688,7 +14692,11 @@ class MySQLReplicator extends BaseReplicator {
|
|
|
14688
14692
|
}
|
|
14689
14693
|
continue;
|
|
14690
14694
|
}
|
|
14691
|
-
const
|
|
14695
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
14696
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
14697
|
+
const attributes = Object.fromEntries(
|
|
14698
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
14699
|
+
);
|
|
14692
14700
|
for (const tableConfig of tableConfigs) {
|
|
14693
14701
|
const tableName = tableConfig.table;
|
|
14694
14702
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -15067,7 +15075,11 @@ class PlanetScaleReplicator extends BaseReplicator {
|
|
|
15067
15075
|
}
|
|
15068
15076
|
continue;
|
|
15069
15077
|
}
|
|
15070
|
-
const
|
|
15078
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
15079
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
15080
|
+
const attributes = Object.fromEntries(
|
|
15081
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
15082
|
+
);
|
|
15071
15083
|
for (const tableConfig of tableConfigs) {
|
|
15072
15084
|
const tableName = tableConfig.table;
|
|
15073
15085
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -15388,7 +15400,11 @@ class PostgresReplicator extends BaseReplicator {
|
|
|
15388
15400
|
}
|
|
15389
15401
|
continue;
|
|
15390
15402
|
}
|
|
15391
|
-
const
|
|
15403
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
15404
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
15405
|
+
const attributes = Object.fromEntries(
|
|
15406
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
15407
|
+
);
|
|
15392
15408
|
for (const tableConfig of tableConfigs) {
|
|
15393
15409
|
const tableName = tableConfig.table;
|
|
15394
15410
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -16510,6 +16526,32 @@ function generateBase62Mapping(keys) {
|
|
|
16510
16526
|
});
|
|
16511
16527
|
return { mapping, reversedMapping };
|
|
16512
16528
|
}
|
|
16529
|
+
function generatePluginAttributeHash(pluginName, attributeName) {
|
|
16530
|
+
const input = `${pluginName}:${attributeName}`;
|
|
16531
|
+
const hash = createHash("sha256").update(input).digest();
|
|
16532
|
+
const num = hash.readUInt32BE(0);
|
|
16533
|
+
const base62Hash = encode(num);
|
|
16534
|
+
const paddedHash = base62Hash.padStart(3, "0").substring(0, 3);
|
|
16535
|
+
return "p" + paddedHash.toLowerCase();
|
|
16536
|
+
}
|
|
16537
|
+
function generatePluginMapping(attributes) {
|
|
16538
|
+
const mapping = {};
|
|
16539
|
+
const reversedMapping = {};
|
|
16540
|
+
const usedHashes = /* @__PURE__ */ new Set();
|
|
16541
|
+
for (const { key, pluginName } of attributes) {
|
|
16542
|
+
let hash = generatePluginAttributeHash(pluginName, key);
|
|
16543
|
+
let counter = 1;
|
|
16544
|
+
let finalHash = hash;
|
|
16545
|
+
while (usedHashes.has(finalHash)) {
|
|
16546
|
+
finalHash = `${hash}${counter}`;
|
|
16547
|
+
counter++;
|
|
16548
|
+
}
|
|
16549
|
+
usedHashes.add(finalHash);
|
|
16550
|
+
mapping[key] = finalHash;
|
|
16551
|
+
reversedMapping[finalHash] = key;
|
|
16552
|
+
}
|
|
16553
|
+
return { mapping, reversedMapping };
|
|
16554
|
+
}
|
|
16513
16555
|
const SchemaActions = {
|
|
16514
16556
|
trim: (value) => value == null ? value : value.trim(),
|
|
16515
16557
|
encrypt: async (value, { passphrase }) => {
|
|
@@ -16900,11 +16942,14 @@ class Schema {
|
|
|
16900
16942
|
constructor(args) {
|
|
16901
16943
|
const {
|
|
16902
16944
|
map,
|
|
16945
|
+
pluginMap,
|
|
16903
16946
|
name,
|
|
16904
16947
|
attributes,
|
|
16905
16948
|
passphrase,
|
|
16906
16949
|
version = 1,
|
|
16907
|
-
options = {}
|
|
16950
|
+
options = {},
|
|
16951
|
+
_pluginAttributeMetadata,
|
|
16952
|
+
_pluginAttributes
|
|
16908
16953
|
} = args;
|
|
16909
16954
|
this.name = name;
|
|
16910
16955
|
this.version = version;
|
|
@@ -16912,6 +16957,8 @@ class Schema {
|
|
|
16912
16957
|
this.passphrase = passphrase ?? "secret";
|
|
16913
16958
|
this.options = merge({}, this.defaultOptions(), options);
|
|
16914
16959
|
this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
|
|
16960
|
+
this._pluginAttributeMetadata = _pluginAttributeMetadata || {};
|
|
16961
|
+
this._pluginAttributes = _pluginAttributes || {};
|
|
16915
16962
|
const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
|
|
16916
16963
|
this.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
|
|
16917
16964
|
{ $$async: true, $$strict: false },
|
|
@@ -16926,9 +16973,43 @@ class Schema {
|
|
|
16926
16973
|
const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
|
|
16927
16974
|
const objectKeys = this.extractObjectKeys(this.attributes);
|
|
16928
16975
|
const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
|
|
16929
|
-
const
|
|
16976
|
+
const userKeys = [];
|
|
16977
|
+
const pluginAttributes = [];
|
|
16978
|
+
for (const key of allKeys) {
|
|
16979
|
+
const attrDef = this.getAttributeDefinition(key);
|
|
16980
|
+
if (typeof attrDef === "object" && attrDef !== null && attrDef.__plugin__) {
|
|
16981
|
+
pluginAttributes.push({ key, pluginName: attrDef.__plugin__ });
|
|
16982
|
+
} else if (typeof attrDef === "string" && this._pluginAttributeMetadata && this._pluginAttributeMetadata[key]) {
|
|
16983
|
+
const pluginName = this._pluginAttributeMetadata[key].__plugin__;
|
|
16984
|
+
pluginAttributes.push({ key, pluginName });
|
|
16985
|
+
} else {
|
|
16986
|
+
userKeys.push(key);
|
|
16987
|
+
}
|
|
16988
|
+
}
|
|
16989
|
+
const { mapping, reversedMapping } = generateBase62Mapping(userKeys);
|
|
16930
16990
|
this.map = mapping;
|
|
16931
16991
|
this.reversedMap = reversedMapping;
|
|
16992
|
+
const { mapping: pMapping, reversedMapping: pReversedMapping } = generatePluginMapping(pluginAttributes);
|
|
16993
|
+
this.pluginMap = pMapping;
|
|
16994
|
+
this.reversedPluginMap = pReversedMapping;
|
|
16995
|
+
this._pluginAttributes = {};
|
|
16996
|
+
for (const { key, pluginName } of pluginAttributes) {
|
|
16997
|
+
if (!this._pluginAttributes[pluginName]) {
|
|
16998
|
+
this._pluginAttributes[pluginName] = [];
|
|
16999
|
+
}
|
|
17000
|
+
this._pluginAttributes[pluginName].push(key);
|
|
17001
|
+
}
|
|
17002
|
+
}
|
|
17003
|
+
if (!isEmpty(pluginMap)) {
|
|
17004
|
+
this.pluginMap = pluginMap;
|
|
17005
|
+
this.reversedPluginMap = invert(pluginMap);
|
|
17006
|
+
}
|
|
17007
|
+
if (!this.pluginMap) {
|
|
17008
|
+
this.pluginMap = {};
|
|
17009
|
+
this.reversedPluginMap = {};
|
|
17010
|
+
}
|
|
17011
|
+
if (!this._pluginAttributes) {
|
|
17012
|
+
this._pluginAttributes = {};
|
|
16932
17013
|
}
|
|
16933
17014
|
}
|
|
16934
17015
|
defaultOptions() {
|
|
@@ -17157,6 +17238,8 @@ class Schema {
|
|
|
17157
17238
|
static import(data) {
|
|
17158
17239
|
let {
|
|
17159
17240
|
map,
|
|
17241
|
+
pluginMap,
|
|
17242
|
+
_pluginAttributeMetadata,
|
|
17160
17243
|
name,
|
|
17161
17244
|
options,
|
|
17162
17245
|
version,
|
|
@@ -17167,11 +17250,15 @@ class Schema {
|
|
|
17167
17250
|
attributes = attrs;
|
|
17168
17251
|
const schema = new Schema({
|
|
17169
17252
|
map,
|
|
17253
|
+
pluginMap: pluginMap || {},
|
|
17170
17254
|
name,
|
|
17171
17255
|
options,
|
|
17172
17256
|
version,
|
|
17173
17257
|
attributes
|
|
17174
17258
|
});
|
|
17259
|
+
if (_pluginAttributeMetadata) {
|
|
17260
|
+
schema._pluginAttributeMetadata = _pluginAttributeMetadata;
|
|
17261
|
+
}
|
|
17175
17262
|
return schema;
|
|
17176
17263
|
}
|
|
17177
17264
|
/**
|
|
@@ -17209,7 +17296,10 @@ class Schema {
|
|
|
17209
17296
|
name: this.name,
|
|
17210
17297
|
options: this.options,
|
|
17211
17298
|
attributes: this._exportAttributes(this.attributes),
|
|
17212
|
-
map: this.map
|
|
17299
|
+
map: this.map,
|
|
17300
|
+
pluginMap: this.pluginMap || {},
|
|
17301
|
+
_pluginAttributeMetadata: this._pluginAttributeMetadata || {},
|
|
17302
|
+
_pluginAttributes: this._pluginAttributes || {}
|
|
17213
17303
|
};
|
|
17214
17304
|
return data;
|
|
17215
17305
|
}
|
|
@@ -17262,7 +17352,7 @@ class Schema {
|
|
|
17262
17352
|
const flattenedObj = flatten(obj, { safe: true });
|
|
17263
17353
|
const rest = { "_v": this.version + "" };
|
|
17264
17354
|
for (const [key, value] of Object.entries(flattenedObj)) {
|
|
17265
|
-
const mappedKey = this.map[key] || key;
|
|
17355
|
+
const mappedKey = this.pluginMap[key] || this.map[key] || key;
|
|
17266
17356
|
const attrDef = this.getAttributeDefinition(key);
|
|
17267
17357
|
if (typeof value === "number" && typeof attrDef === "string" && attrDef.includes("number")) {
|
|
17268
17358
|
rest[mappedKey] = encode(value);
|
|
@@ -17283,14 +17373,18 @@ class Schema {
|
|
|
17283
17373
|
await this.applyHooksActions(rest, "afterMap");
|
|
17284
17374
|
return rest;
|
|
17285
17375
|
}
|
|
17286
|
-
async unmapper(mappedResourceItem, mapOverride) {
|
|
17376
|
+
async unmapper(mappedResourceItem, mapOverride, pluginMapOverride) {
|
|
17287
17377
|
let obj = cloneDeep(mappedResourceItem);
|
|
17288
17378
|
delete obj._v;
|
|
17289
17379
|
obj = await this.applyHooksActions(obj, "beforeUnmap");
|
|
17290
17380
|
const reversedMap = mapOverride ? invert(mapOverride) : this.reversedMap;
|
|
17381
|
+
const reversedPluginMap = pluginMapOverride ? invert(pluginMapOverride) : this.reversedPluginMap;
|
|
17291
17382
|
const rest = {};
|
|
17292
17383
|
for (const [key, value] of Object.entries(obj)) {
|
|
17293
|
-
|
|
17384
|
+
let originalKey = reversedPluginMap[key] || reversedMap[key] || key;
|
|
17385
|
+
if (!originalKey) {
|
|
17386
|
+
originalKey = key;
|
|
17387
|
+
}
|
|
17294
17388
|
let parsedValue = value;
|
|
17295
17389
|
const attrDef = this.getAttributeDefinition(originalKey);
|
|
17296
17390
|
const hasAfterUnmapHook = this.options.hooks?.afterUnmap?.[originalKey];
|
|
@@ -17357,6 +17451,37 @@ class Schema {
|
|
|
17357
17451
|
}
|
|
17358
17452
|
return def;
|
|
17359
17453
|
}
|
|
17454
|
+
/**
|
|
17455
|
+
* Regenerate plugin attribute mapping
|
|
17456
|
+
* Called when plugin attributes are added or removed
|
|
17457
|
+
* @returns {void}
|
|
17458
|
+
*/
|
|
17459
|
+
regeneratePluginMapping() {
|
|
17460
|
+
const flatAttrs = flatten(this.attributes, { safe: true });
|
|
17461
|
+
const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
|
|
17462
|
+
const objectKeys = this.extractObjectKeys(this.attributes);
|
|
17463
|
+
const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
|
|
17464
|
+
const pluginAttributes = [];
|
|
17465
|
+
for (const key of allKeys) {
|
|
17466
|
+
const attrDef = this.getAttributeDefinition(key);
|
|
17467
|
+
if (typeof attrDef === "object" && attrDef !== null && attrDef.__plugin__) {
|
|
17468
|
+
pluginAttributes.push({ key, pluginName: attrDef.__plugin__ });
|
|
17469
|
+
} else if (typeof attrDef === "string" && this._pluginAttributeMetadata && this._pluginAttributeMetadata[key]) {
|
|
17470
|
+
const pluginName = this._pluginAttributeMetadata[key].__plugin__;
|
|
17471
|
+
pluginAttributes.push({ key, pluginName });
|
|
17472
|
+
}
|
|
17473
|
+
}
|
|
17474
|
+
const { mapping, reversedMapping } = generatePluginMapping(pluginAttributes);
|
|
17475
|
+
this.pluginMap = mapping;
|
|
17476
|
+
this.reversedPluginMap = reversedMapping;
|
|
17477
|
+
this._pluginAttributes = {};
|
|
17478
|
+
for (const { key, pluginName } of pluginAttributes) {
|
|
17479
|
+
if (!this._pluginAttributes[pluginName]) {
|
|
17480
|
+
this._pluginAttributes[pluginName] = [];
|
|
17481
|
+
}
|
|
17482
|
+
this._pluginAttributes[pluginName].push(key);
|
|
17483
|
+
}
|
|
17484
|
+
}
|
|
17360
17485
|
/**
|
|
17361
17486
|
* Preprocess attributes to convert nested objects into validator-compatible format
|
|
17362
17487
|
* @param {Object} attributes - Original attributes
|
|
@@ -17426,37 +17551,38 @@ class Schema {
|
|
|
17426
17551
|
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
17427
17552
|
const hasValidatorType = value.type !== void 0 && key !== "$$type";
|
|
17428
17553
|
if (hasValidatorType) {
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
17554
|
+
const { __plugin__, __pluginCreated__, ...cleanValue } = value;
|
|
17555
|
+
if (cleanValue.type === "ip4") {
|
|
17556
|
+
processed[key] = { ...cleanValue, type: "string" };
|
|
17557
|
+
} else if (cleanValue.type === "ip6") {
|
|
17558
|
+
processed[key] = { ...cleanValue, type: "string" };
|
|
17559
|
+
} else if (cleanValue.type === "money" || cleanValue.type === "crypto") {
|
|
17560
|
+
processed[key] = { ...cleanValue, type: "number", min: cleanValue.min !== void 0 ? cleanValue.min : 0 };
|
|
17561
|
+
} else if (cleanValue.type === "decimal") {
|
|
17562
|
+
processed[key] = { ...cleanValue, type: "number" };
|
|
17563
|
+
} else if (cleanValue.type === "geo:lat" || cleanValue.type === "geo-lat") {
|
|
17438
17564
|
processed[key] = {
|
|
17439
|
-
...
|
|
17565
|
+
...cleanValue,
|
|
17440
17566
|
type: "number",
|
|
17441
|
-
min:
|
|
17442
|
-
max:
|
|
17567
|
+
min: cleanValue.min !== void 0 ? cleanValue.min : -90,
|
|
17568
|
+
max: cleanValue.max !== void 0 ? cleanValue.max : 90
|
|
17443
17569
|
};
|
|
17444
|
-
} else if (
|
|
17570
|
+
} else if (cleanValue.type === "geo:lon" || cleanValue.type === "geo-lon") {
|
|
17445
17571
|
processed[key] = {
|
|
17446
|
-
...
|
|
17572
|
+
...cleanValue,
|
|
17447
17573
|
type: "number",
|
|
17448
|
-
min:
|
|
17449
|
-
max:
|
|
17574
|
+
min: cleanValue.min !== void 0 ? cleanValue.min : -180,
|
|
17575
|
+
max: cleanValue.max !== void 0 ? cleanValue.max : 180
|
|
17450
17576
|
};
|
|
17451
|
-
} else if (
|
|
17452
|
-
processed[key] = { ...
|
|
17453
|
-
} else if (
|
|
17577
|
+
} else if (cleanValue.type === "geo:point" || cleanValue.type === "geo-point") {
|
|
17578
|
+
processed[key] = { ...cleanValue, type: "any" };
|
|
17579
|
+
} else if (cleanValue.type === "object" && cleanValue.properties) {
|
|
17454
17580
|
processed[key] = {
|
|
17455
|
-
...
|
|
17456
|
-
properties: this.preprocessAttributesForValidation(
|
|
17581
|
+
...cleanValue,
|
|
17582
|
+
properties: this.preprocessAttributesForValidation(cleanValue.properties)
|
|
17457
17583
|
};
|
|
17458
17584
|
} else {
|
|
17459
|
-
processed[key] =
|
|
17585
|
+
processed[key] = cleanValue;
|
|
17460
17586
|
}
|
|
17461
17587
|
} else {
|
|
17462
17588
|
const isExplicitRequired = value.$$type && value.$$type.includes("required");
|
|
@@ -17774,7 +17900,11 @@ async function handleInsert$3({ resource, data, mappedData, originalData }) {
|
|
|
17774
17900
|
excess: totalSize - 2047,
|
|
17775
17901
|
data: originalData || data
|
|
17776
17902
|
});
|
|
17777
|
-
|
|
17903
|
+
const metadataOnly = { _v: mappedData._v };
|
|
17904
|
+
if (resource.schema?.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
17905
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
17906
|
+
}
|
|
17907
|
+
return { mappedData: metadataOnly, body: JSON.stringify(mappedData) };
|
|
17778
17908
|
}
|
|
17779
17909
|
return { mappedData, body: "" };
|
|
17780
17910
|
}
|
|
@@ -17977,6 +18107,12 @@ async function handleInsert$1({ resource, data, mappedData, originalData }) {
|
|
|
17977
18107
|
metadataFields._v = mappedData._v;
|
|
17978
18108
|
currentSize += attributeSizes._v;
|
|
17979
18109
|
}
|
|
18110
|
+
if (resource.schema?.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
18111
|
+
const pluginMapStr = JSON.stringify(resource.schema.pluginMap);
|
|
18112
|
+
const pluginMapSize = calculateUTF8Bytes("_pluginMap") + calculateUTF8Bytes(pluginMapStr);
|
|
18113
|
+
metadataFields._pluginMap = pluginMapStr;
|
|
18114
|
+
currentSize += pluginMapSize;
|
|
18115
|
+
}
|
|
17980
18116
|
let reservedLimit = effectiveLimit;
|
|
17981
18117
|
for (const [fieldName, size] of sortedFields) {
|
|
17982
18118
|
if (fieldName === "_v") continue;
|
|
@@ -18036,6 +18172,9 @@ async function handleInsert({ resource, data, mappedData }) {
|
|
|
18036
18172
|
"_v": mappedData._v || String(resource.version)
|
|
18037
18173
|
};
|
|
18038
18174
|
metadataOnly._map = JSON.stringify(resource.schema.map);
|
|
18175
|
+
if (resource.schema.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
18176
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
18177
|
+
}
|
|
18039
18178
|
const body = JSON.stringify(mappedData);
|
|
18040
18179
|
return { mappedData: metadataOnly, body };
|
|
18041
18180
|
}
|
|
@@ -18044,6 +18183,9 @@ async function handleUpdate({ resource, id, data, mappedData }) {
|
|
|
18044
18183
|
"_v": mappedData._v || String(resource.version)
|
|
18045
18184
|
};
|
|
18046
18185
|
metadataOnly._map = JSON.stringify(resource.schema.map);
|
|
18186
|
+
if (resource.schema.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
18187
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
18188
|
+
}
|
|
18047
18189
|
const body = JSON.stringify(mappedData);
|
|
18048
18190
|
return { mappedData: metadataOnly, body };
|
|
18049
18191
|
}
|
|
@@ -18424,6 +18566,118 @@ ${errorDetails}`,
|
|
|
18424
18566
|
this.applyConfiguration();
|
|
18425
18567
|
return { oldAttributes, newAttributes };
|
|
18426
18568
|
}
|
|
18569
|
+
/**
|
|
18570
|
+
* Add a plugin-created attribute to the resource schema
|
|
18571
|
+
* This ensures plugin attributes don't interfere with user-defined attributes
|
|
18572
|
+
* by using a separate mapping namespace (p0, p1, p2, ...)
|
|
18573
|
+
*
|
|
18574
|
+
* @param {string} name - Attribute name (e.g., '_hasEmbedding', 'clusterId')
|
|
18575
|
+
* @param {Object|string} definition - Attribute definition
|
|
18576
|
+
* @param {string} pluginName - Name of plugin adding the attribute
|
|
18577
|
+
* @returns {void}
|
|
18578
|
+
*
|
|
18579
|
+
* @example
|
|
18580
|
+
* // VectorPlugin adding tracking field
|
|
18581
|
+
* resource.addPluginAttribute('_hasEmbedding', {
|
|
18582
|
+
* type: 'boolean',
|
|
18583
|
+
* optional: true,
|
|
18584
|
+
* default: false
|
|
18585
|
+
* }, 'VectorPlugin');
|
|
18586
|
+
*
|
|
18587
|
+
* // Shorthand notation
|
|
18588
|
+
* resource.addPluginAttribute('clusterId', 'string|optional', 'VectorPlugin');
|
|
18589
|
+
*/
|
|
18590
|
+
addPluginAttribute(name, definition, pluginName) {
|
|
18591
|
+
if (!pluginName) {
|
|
18592
|
+
throw new ResourceError(
|
|
18593
|
+
"Plugin name is required when adding plugin attributes",
|
|
18594
|
+
{ resource: this.name, attribute: name }
|
|
18595
|
+
);
|
|
18596
|
+
}
|
|
18597
|
+
const existingDef = this.schema.getAttributeDefinition(name);
|
|
18598
|
+
if (existingDef && (!existingDef.__plugin__ || existingDef.__plugin__ !== pluginName)) {
|
|
18599
|
+
throw new ResourceError(
|
|
18600
|
+
`Attribute '${name}' already exists and is not from plugin '${pluginName}'`,
|
|
18601
|
+
{ resource: this.name, attribute: name, plugin: pluginName }
|
|
18602
|
+
);
|
|
18603
|
+
}
|
|
18604
|
+
let defObject = definition;
|
|
18605
|
+
if (typeof definition === "object" && definition !== null) {
|
|
18606
|
+
defObject = { ...definition };
|
|
18607
|
+
}
|
|
18608
|
+
if (typeof defObject === "object" && defObject !== null) {
|
|
18609
|
+
defObject.__plugin__ = pluginName;
|
|
18610
|
+
defObject.__pluginCreated__ = Date.now();
|
|
18611
|
+
}
|
|
18612
|
+
this.schema.attributes[name] = defObject;
|
|
18613
|
+
this.attributes[name] = defObject;
|
|
18614
|
+
if (typeof defObject === "string") {
|
|
18615
|
+
if (!this.schema._pluginAttributeMetadata) {
|
|
18616
|
+
this.schema._pluginAttributeMetadata = {};
|
|
18617
|
+
}
|
|
18618
|
+
this.schema._pluginAttributeMetadata[name] = {
|
|
18619
|
+
__plugin__: pluginName,
|
|
18620
|
+
__pluginCreated__: Date.now()
|
|
18621
|
+
};
|
|
18622
|
+
}
|
|
18623
|
+
this.schema.regeneratePluginMapping();
|
|
18624
|
+
if (this.schema.options.generateAutoHooks) {
|
|
18625
|
+
this.schema.generateAutoHooks();
|
|
18626
|
+
}
|
|
18627
|
+
const processedAttributes = this.schema.preprocessAttributesForValidation(this.schema.attributes);
|
|
18628
|
+
this.schema.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
|
|
18629
|
+
{ $$async: true, $$strict: false },
|
|
18630
|
+
processedAttributes
|
|
18631
|
+
));
|
|
18632
|
+
if (this.database) {
|
|
18633
|
+
this.database.emit("plugin-attribute-added", {
|
|
18634
|
+
resource: this.name,
|
|
18635
|
+
attribute: name,
|
|
18636
|
+
plugin: pluginName,
|
|
18637
|
+
definition: defObject
|
|
18638
|
+
});
|
|
18639
|
+
}
|
|
18640
|
+
}
|
|
18641
|
+
/**
|
|
18642
|
+
* Remove a plugin-created attribute from the resource schema
|
|
18643
|
+
* Called when a plugin is uninstalled or no longer needs the attribute
|
|
18644
|
+
*
|
|
18645
|
+
* @param {string} name - Attribute name to remove
|
|
18646
|
+
* @param {string} [pluginName] - Optional plugin name for safety check
|
|
18647
|
+
* @returns {boolean} True if attribute was removed, false if not found
|
|
18648
|
+
*
|
|
18649
|
+
* @example
|
|
18650
|
+
* resource.removePluginAttribute('_hasEmbedding', 'VectorPlugin');
|
|
18651
|
+
*/
|
|
18652
|
+
removePluginAttribute(name, pluginName = null) {
|
|
18653
|
+
const attrDef = this.schema.getAttributeDefinition(name);
|
|
18654
|
+
const metadata = this.schema._pluginAttributeMetadata?.[name];
|
|
18655
|
+
const isPluginAttr = typeof attrDef === "object" && attrDef?.__plugin__ || metadata;
|
|
18656
|
+
if (!attrDef || !isPluginAttr) {
|
|
18657
|
+
return false;
|
|
18658
|
+
}
|
|
18659
|
+
const actualPlugin = attrDef?.__plugin__ || metadata?.__plugin__;
|
|
18660
|
+
if (pluginName && actualPlugin !== pluginName) {
|
|
18661
|
+
throw new ResourceError(
|
|
18662
|
+
`Attribute '${name}' belongs to plugin '${actualPlugin}', not '${pluginName}'`,
|
|
18663
|
+
{ resource: this.name, attribute: name, actualPlugin, requestedPlugin: pluginName }
|
|
18664
|
+
);
|
|
18665
|
+
}
|
|
18666
|
+
delete this.schema.attributes[name];
|
|
18667
|
+
delete this.attributes[name];
|
|
18668
|
+
if (this.schema._pluginAttributeMetadata?.[name]) {
|
|
18669
|
+
delete this.schema._pluginAttributeMetadata[name];
|
|
18670
|
+
}
|
|
18671
|
+
this.schema.regeneratePluginMapping();
|
|
18672
|
+
if (this.database) {
|
|
18673
|
+
this.database.emit("plugin-attribute-removed", {
|
|
18674
|
+
resource: this.name,
|
|
18675
|
+
attribute: name,
|
|
18676
|
+
plugin: actualPlugin
|
|
18677
|
+
});
|
|
18678
|
+
}
|
|
18679
|
+
return true;
|
|
18680
|
+
}
|
|
18427
18681
|
/**
|
|
18428
18682
|
* Add a hook function for a specific event
|
|
18429
18683
|
* @param {string} event - Hook event (beforeInsert, afterInsert, etc.)
|
|
@@ -20725,8 +20979,9 @@ ${errorDetails}`,
|
|
|
20725
20979
|
const filterInternalFields = (obj) => {
|
|
20726
20980
|
if (!obj || typeof obj !== "object") return obj;
|
|
20727
20981
|
const filtered2 = {};
|
|
20982
|
+
const pluginAttrNames = this.schema._pluginAttributes ? Object.values(this.schema._pluginAttributes).flat() : [];
|
|
20728
20983
|
for (const [key, value] of Object.entries(obj)) {
|
|
20729
|
-
if (!key.startsWith("_") || key === "_geohash" || key.startsWith("_geohash_zoom")) {
|
|
20984
|
+
if (!key.startsWith("_") || key === "_geohash" || key.startsWith("_geohash_zoom") || pluginAttrNames.includes(key)) {
|
|
20730
20985
|
filtered2[key] = value;
|
|
20731
20986
|
}
|
|
20732
20987
|
}
|
|
@@ -20752,7 +21007,16 @@ ${errorDetails}`,
|
|
|
20752
21007
|
if (hasOverflow && body) {
|
|
20753
21008
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(JSON.parse(body)));
|
|
20754
21009
|
if (okBody) {
|
|
20755
|
-
|
|
21010
|
+
let pluginMapFromMeta = null;
|
|
21011
|
+
if (metadata && metadata._pluginmap) {
|
|
21012
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(
|
|
21013
|
+
() => Promise.resolve(typeof metadata._pluginmap === "string" ? JSON.parse(metadata._pluginmap) : metadata._pluginmap)
|
|
21014
|
+
);
|
|
21015
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
21016
|
+
}
|
|
21017
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(
|
|
21018
|
+
() => this.schema.unmapper(parsedBody, void 0, pluginMapFromMeta)
|
|
21019
|
+
);
|
|
20756
21020
|
bodyData = okUnmap ? unmappedBody : {};
|
|
20757
21021
|
}
|
|
20758
21022
|
}
|
|
@@ -20769,11 +21033,16 @@ ${errorDetails}`,
|
|
|
20769
21033
|
if (behavior === "body-only") {
|
|
20770
21034
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(body ? JSON.parse(body) : {}));
|
|
20771
21035
|
let mapFromMeta = this.schema.map;
|
|
21036
|
+
let pluginMapFromMeta = null;
|
|
20772
21037
|
if (metadata && metadata._map) {
|
|
20773
21038
|
const [okMap, errMap, parsedMap] = await tryFn(() => Promise.resolve(typeof metadata._map === "string" ? JSON.parse(metadata._map) : metadata._map));
|
|
20774
21039
|
mapFromMeta = okMap ? parsedMap : this.schema.map;
|
|
20775
21040
|
}
|
|
20776
|
-
|
|
21041
|
+
if (metadata && metadata._pluginmap) {
|
|
21042
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(() => Promise.resolve(typeof metadata._pluginmap === "string" ? JSON.parse(metadata._pluginmap) : metadata._pluginmap));
|
|
21043
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
21044
|
+
}
|
|
21045
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody, mapFromMeta, pluginMapFromMeta));
|
|
20777
21046
|
const result2 = okUnmap ? { ...unmappedBody, id } : { id };
|
|
20778
21047
|
Object.keys(result2).forEach((k) => {
|
|
20779
21048
|
result2[k] = fixValue(result2[k]);
|
|
@@ -20783,7 +21052,16 @@ ${errorDetails}`,
|
|
|
20783
21052
|
if (behavior === "user-managed" && body && body.trim() !== "") {
|
|
20784
21053
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(JSON.parse(body)));
|
|
20785
21054
|
if (okBody) {
|
|
20786
|
-
|
|
21055
|
+
let pluginMapFromMeta = null;
|
|
21056
|
+
if (metadata && metadata._pluginmap) {
|
|
21057
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(
|
|
21058
|
+
() => Promise.resolve(typeof metadata._pluginmap === "string" ? JSON.parse(metadata._pluginmap) : metadata._pluginmap)
|
|
21059
|
+
);
|
|
21060
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
21061
|
+
}
|
|
21062
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(
|
|
21063
|
+
() => this.schema.unmapper(parsedBody, void 0, pluginMapFromMeta)
|
|
21064
|
+
);
|
|
20787
21065
|
const bodyData = okUnmap ? unmappedBody : {};
|
|
20788
21066
|
const merged = { ...bodyData, ...unmappedMetadata, id };
|
|
20789
21067
|
Object.keys(merged).forEach((k) => {
|
|
@@ -21031,7 +21309,7 @@ class Database extends EventEmitter {
|
|
|
21031
21309
|
this.id = idGenerator(7);
|
|
21032
21310
|
this.version = "1";
|
|
21033
21311
|
this.s3dbVersion = (() => {
|
|
21034
|
-
const [ok, err, version] = tryFn(() => true ? "12.
|
|
21312
|
+
const [ok, err, version] = tryFn(() => true ? "12.3.0" : "latest");
|
|
21035
21313
|
return ok ? version : "latest";
|
|
21036
21314
|
})();
|
|
21037
21315
|
this._resourcesMap = {};
|
|
@@ -23021,7 +23299,11 @@ class TursoReplicator extends BaseReplicator {
|
|
|
23021
23299
|
}
|
|
23022
23300
|
continue;
|
|
23023
23301
|
}
|
|
23024
|
-
const
|
|
23302
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
23303
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
23304
|
+
const attributes = Object.fromEntries(
|
|
23305
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
23306
|
+
);
|
|
23025
23307
|
for (const tableConfig of tableConfigs) {
|
|
23026
23308
|
const tableName = tableConfig.table;
|
|
23027
23309
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -36856,6 +37138,7 @@ class VectorPlugin extends Plugin {
|
|
|
36856
37138
|
*
|
|
36857
37139
|
* Detects large vector fields and warns if proper behavior is not set.
|
|
36858
37140
|
* Can optionally auto-fix by setting body-overflow behavior.
|
|
37141
|
+
* Auto-creates partitions for optional embedding fields to enable O(1) filtering.
|
|
36859
37142
|
*/
|
|
36860
37143
|
validateVectorStorage() {
|
|
36861
37144
|
for (const resource of Object.values(this.database.resources)) {
|
|
@@ -36892,8 +37175,217 @@ class VectorPlugin extends Plugin {
|
|
|
36892
37175
|
}
|
|
36893
37176
|
}
|
|
36894
37177
|
}
|
|
37178
|
+
this.setupEmbeddingPartitions(resource, vectorFields);
|
|
36895
37179
|
}
|
|
36896
37180
|
}
|
|
37181
|
+
/**
|
|
37182
|
+
* Setup automatic partitions for optional embedding fields
|
|
37183
|
+
*
|
|
37184
|
+
* Creates a partition that separates records with embeddings from those without.
|
|
37185
|
+
* This enables O(1) filtering instead of O(n) full scans when searching/clustering.
|
|
37186
|
+
*
|
|
37187
|
+
* @param {Resource} resource - Resource instance
|
|
37188
|
+
* @param {Array} vectorFields - Detected vector fields with metadata
|
|
37189
|
+
*/
|
|
37190
|
+
setupEmbeddingPartitions(resource, vectorFields) {
|
|
37191
|
+
if (!resource.config) return;
|
|
37192
|
+
for (const vectorField of vectorFields) {
|
|
37193
|
+
const isOptional = this.isFieldOptional(resource.schema.attributes, vectorField.name);
|
|
37194
|
+
if (!isOptional) continue;
|
|
37195
|
+
const partitionName = `byHas${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
37196
|
+
const trackingFieldName = `_has${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
37197
|
+
if (resource.config.partitions && resource.config.partitions[partitionName]) {
|
|
37198
|
+
this.emit("vector:partition-exists", {
|
|
37199
|
+
resource: resource.name,
|
|
37200
|
+
vectorField: vectorField.name,
|
|
37201
|
+
partition: partitionName,
|
|
37202
|
+
timestamp: Date.now()
|
|
37203
|
+
});
|
|
37204
|
+
continue;
|
|
37205
|
+
}
|
|
37206
|
+
if (!resource.config.partitions) {
|
|
37207
|
+
resource.config.partitions = {};
|
|
37208
|
+
}
|
|
37209
|
+
resource.config.partitions[partitionName] = {
|
|
37210
|
+
fields: {
|
|
37211
|
+
[trackingFieldName]: "boolean"
|
|
37212
|
+
}
|
|
37213
|
+
};
|
|
37214
|
+
if (!resource.schema.attributes[trackingFieldName]) {
|
|
37215
|
+
resource.addPluginAttribute(trackingFieldName, {
|
|
37216
|
+
type: "boolean",
|
|
37217
|
+
optional: true,
|
|
37218
|
+
default: false
|
|
37219
|
+
}, "VectorPlugin");
|
|
37220
|
+
}
|
|
37221
|
+
this.emit("vector:partition-created", {
|
|
37222
|
+
resource: resource.name,
|
|
37223
|
+
vectorField: vectorField.name,
|
|
37224
|
+
partition: partitionName,
|
|
37225
|
+
trackingField: trackingFieldName,
|
|
37226
|
+
timestamp: Date.now()
|
|
37227
|
+
});
|
|
37228
|
+
console.log(`\u2705 VectorPlugin: Created partition '${partitionName}' for optional embedding field '${vectorField.name}' in resource '${resource.name}'`);
|
|
37229
|
+
this.installEmbeddingHooks(resource, vectorField.name, trackingFieldName);
|
|
37230
|
+
}
|
|
37231
|
+
}
|
|
37232
|
+
/**
|
|
37233
|
+
* Check if a field is optional in the schema
|
|
37234
|
+
*
|
|
37235
|
+
* @param {Object} attributes - Resource attributes
|
|
37236
|
+
* @param {string} fieldPath - Field path (supports dot notation)
|
|
37237
|
+
* @returns {boolean} True if field is optional
|
|
37238
|
+
*/
|
|
37239
|
+
isFieldOptional(attributes, fieldPath) {
|
|
37240
|
+
const parts = fieldPath.split(".");
|
|
37241
|
+
let current = attributes;
|
|
37242
|
+
for (let i = 0; i < parts.length; i++) {
|
|
37243
|
+
const part = parts[i];
|
|
37244
|
+
const attr = current[part];
|
|
37245
|
+
if (!attr) return true;
|
|
37246
|
+
if (typeof attr === "string") {
|
|
37247
|
+
const flags = attr.split("|");
|
|
37248
|
+
if (flags.includes("required")) return false;
|
|
37249
|
+
if (flags.includes("optional") || flags.some((f) => f.startsWith("optional:"))) return true;
|
|
37250
|
+
return !flags.includes("required");
|
|
37251
|
+
}
|
|
37252
|
+
if (typeof attr === "object") {
|
|
37253
|
+
if (i === parts.length - 1) {
|
|
37254
|
+
if (attr.optional === true) return true;
|
|
37255
|
+
if (attr.optional === false) return false;
|
|
37256
|
+
return attr.optional !== false;
|
|
37257
|
+
}
|
|
37258
|
+
if (attr.type === "object" && attr.props) {
|
|
37259
|
+
current = attr.props;
|
|
37260
|
+
} else {
|
|
37261
|
+
return true;
|
|
37262
|
+
}
|
|
37263
|
+
}
|
|
37264
|
+
}
|
|
37265
|
+
return true;
|
|
37266
|
+
}
|
|
37267
|
+
/**
|
|
37268
|
+
* Capitalize first letter of string
|
|
37269
|
+
*
|
|
37270
|
+
* @param {string} str - Input string
|
|
37271
|
+
* @returns {string} Capitalized string
|
|
37272
|
+
*/
|
|
37273
|
+
capitalize(str) {
|
|
37274
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
37275
|
+
}
|
|
37276
|
+
/**
|
|
37277
|
+
* Install hooks to maintain embedding partition tracking field
|
|
37278
|
+
*
|
|
37279
|
+
* @param {Resource} resource - Resource instance
|
|
37280
|
+
* @param {string} vectorField - Vector field name
|
|
37281
|
+
* @param {string} trackingField - Tracking field name
|
|
37282
|
+
*/
|
|
37283
|
+
installEmbeddingHooks(resource, vectorField, trackingField) {
|
|
37284
|
+
resource.registerHook("beforeInsert", async (data) => {
|
|
37285
|
+
const hasVector = this.hasVectorValue(data, vectorField);
|
|
37286
|
+
this.setNestedValue(data, trackingField, hasVector);
|
|
37287
|
+
return data;
|
|
37288
|
+
});
|
|
37289
|
+
resource.registerHook("beforeUpdate", async (id, updates) => {
|
|
37290
|
+
if (vectorField in updates || this.hasNestedKey(updates, vectorField)) {
|
|
37291
|
+
const hasVector = this.hasVectorValue(updates, vectorField);
|
|
37292
|
+
this.setNestedValue(updates, trackingField, hasVector);
|
|
37293
|
+
}
|
|
37294
|
+
return updates;
|
|
37295
|
+
});
|
|
37296
|
+
this.emit("vector:hooks-installed", {
|
|
37297
|
+
resource: resource.name,
|
|
37298
|
+
vectorField,
|
|
37299
|
+
trackingField,
|
|
37300
|
+
hooks: ["beforeInsert", "beforeUpdate"],
|
|
37301
|
+
timestamp: Date.now()
|
|
37302
|
+
});
|
|
37303
|
+
}
|
|
37304
|
+
/**
|
|
37305
|
+
* Check if data has a valid vector value for the given field
|
|
37306
|
+
*
|
|
37307
|
+
* @param {Object} data - Data object
|
|
37308
|
+
* @param {string} fieldPath - Field path (supports dot notation)
|
|
37309
|
+
* @returns {boolean} True if vector exists and is valid
|
|
37310
|
+
*/
|
|
37311
|
+
hasVectorValue(data, fieldPath) {
|
|
37312
|
+
const value = this.getNestedValue(data, fieldPath);
|
|
37313
|
+
return value != null && Array.isArray(value) && value.length > 0;
|
|
37314
|
+
}
|
|
37315
|
+
/**
|
|
37316
|
+
* Check if object has a nested key
|
|
37317
|
+
*
|
|
37318
|
+
* @param {Object} obj - Object to check
|
|
37319
|
+
* @param {string} path - Dot-notation path
|
|
37320
|
+
* @returns {boolean} True if key exists
|
|
37321
|
+
*/
|
|
37322
|
+
hasNestedKey(obj, path) {
|
|
37323
|
+
const parts = path.split(".");
|
|
37324
|
+
let current = obj;
|
|
37325
|
+
for (const part of parts) {
|
|
37326
|
+
if (current == null || typeof current !== "object") return false;
|
|
37327
|
+
if (!(part in current)) return false;
|
|
37328
|
+
current = current[part];
|
|
37329
|
+
}
|
|
37330
|
+
return true;
|
|
37331
|
+
}
|
|
37332
|
+
/**
|
|
37333
|
+
* Get nested value from object using dot notation
|
|
37334
|
+
*
|
|
37335
|
+
* @param {Object} obj - Object to traverse
|
|
37336
|
+
* @param {string} path - Dot-notation path
|
|
37337
|
+
* @returns {*} Value at path or undefined
|
|
37338
|
+
*/
|
|
37339
|
+
getNestedValue(obj, path) {
|
|
37340
|
+
const parts = path.split(".");
|
|
37341
|
+
let current = obj;
|
|
37342
|
+
for (const part of parts) {
|
|
37343
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
37344
|
+
current = current[part];
|
|
37345
|
+
}
|
|
37346
|
+
return current;
|
|
37347
|
+
}
|
|
37348
|
+
/**
|
|
37349
|
+
* Set nested value in object using dot notation
|
|
37350
|
+
*
|
|
37351
|
+
* @param {Object} obj - Object to modify
|
|
37352
|
+
* @param {string} path - Dot-notation path
|
|
37353
|
+
* @param {*} value - Value to set
|
|
37354
|
+
*/
|
|
37355
|
+
setNestedValue(obj, path, value) {
|
|
37356
|
+
const parts = path.split(".");
|
|
37357
|
+
let current = obj;
|
|
37358
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
37359
|
+
const part = parts[i];
|
|
37360
|
+
if (!(part in current) || typeof current[part] !== "object") {
|
|
37361
|
+
current[part] = {};
|
|
37362
|
+
}
|
|
37363
|
+
current = current[part];
|
|
37364
|
+
}
|
|
37365
|
+
current[parts[parts.length - 1]] = value;
|
|
37366
|
+
}
|
|
37367
|
+
/**
|
|
37368
|
+
* Get auto-created embedding partition for a vector field
|
|
37369
|
+
*
|
|
37370
|
+
* Returns partition configuration if an auto-partition exists for the given vector field.
|
|
37371
|
+
* Auto-partitions enable O(1) filtering to only records with embeddings.
|
|
37372
|
+
*
|
|
37373
|
+
* @param {Resource} resource - Resource instance
|
|
37374
|
+
* @param {string} vectorField - Vector field name
|
|
37375
|
+
* @returns {Object|null} Partition config or null
|
|
37376
|
+
*/
|
|
37377
|
+
getAutoEmbeddingPartition(resource, vectorField) {
|
|
37378
|
+
if (!resource.config) return null;
|
|
37379
|
+
const partitionName = `byHas${this.capitalize(vectorField.replace(/\./g, "_"))}`;
|
|
37380
|
+
const trackingFieldName = `_has${this.capitalize(vectorField.replace(/\./g, "_"))}`;
|
|
37381
|
+
if (resource.config.partitions && resource.config.partitions[partitionName]) {
|
|
37382
|
+
return {
|
|
37383
|
+
partitionName,
|
|
37384
|
+
partitionValues: { [trackingFieldName]: true }
|
|
37385
|
+
};
|
|
37386
|
+
}
|
|
37387
|
+
return null;
|
|
37388
|
+
}
|
|
36897
37389
|
/**
|
|
36898
37390
|
* Auto-detect vector field from resource schema
|
|
36899
37391
|
*
|
|
@@ -37031,11 +37523,12 @@ class VectorPlugin extends Plugin {
|
|
|
37031
37523
|
} else if (!vectorField) {
|
|
37032
37524
|
vectorField = "vector";
|
|
37033
37525
|
}
|
|
37034
|
-
|
|
37526
|
+
let {
|
|
37035
37527
|
limit = 10,
|
|
37036
37528
|
distanceMetric = this.config.distanceMetric,
|
|
37037
37529
|
threshold = null,
|
|
37038
|
-
partition = null
|
|
37530
|
+
partition = null,
|
|
37531
|
+
partitionValues = null
|
|
37039
37532
|
} = options;
|
|
37040
37533
|
const distanceFn = this.distanceFunctions[distanceMetric];
|
|
37041
37534
|
if (!distanceFn) {
|
|
@@ -37051,31 +37544,61 @@ class VectorPlugin extends Plugin {
|
|
|
37051
37544
|
});
|
|
37052
37545
|
throw error;
|
|
37053
37546
|
}
|
|
37547
|
+
if (!partition) {
|
|
37548
|
+
const autoPartition = this.getAutoEmbeddingPartition(resource, vectorField);
|
|
37549
|
+
if (autoPartition) {
|
|
37550
|
+
partition = autoPartition.partitionName;
|
|
37551
|
+
partitionValues = autoPartition.partitionValues;
|
|
37552
|
+
this._emitEvent("vector:auto-partition-used", {
|
|
37553
|
+
resource: resource.name,
|
|
37554
|
+
vectorField,
|
|
37555
|
+
partition,
|
|
37556
|
+
partitionValues,
|
|
37557
|
+
timestamp: Date.now()
|
|
37558
|
+
});
|
|
37559
|
+
}
|
|
37560
|
+
}
|
|
37054
37561
|
this._emitEvent("vector:search-start", {
|
|
37055
37562
|
resource: resource.name,
|
|
37056
37563
|
vectorField,
|
|
37057
37564
|
limit,
|
|
37058
37565
|
distanceMetric,
|
|
37059
37566
|
partition,
|
|
37567
|
+
partitionValues,
|
|
37060
37568
|
threshold,
|
|
37061
37569
|
queryDimensions: queryVector.length,
|
|
37062
37570
|
timestamp: startTime
|
|
37063
37571
|
});
|
|
37064
37572
|
try {
|
|
37065
37573
|
let allRecords;
|
|
37066
|
-
if (partition) {
|
|
37574
|
+
if (partition && partitionValues) {
|
|
37067
37575
|
this._emitEvent("vector:partition-filter", {
|
|
37068
37576
|
resource: resource.name,
|
|
37069
37577
|
partition,
|
|
37578
|
+
partitionValues,
|
|
37070
37579
|
timestamp: Date.now()
|
|
37071
37580
|
});
|
|
37072
|
-
allRecords = await resource.list({ partition, partitionValues
|
|
37581
|
+
allRecords = await resource.list({ partition, partitionValues });
|
|
37073
37582
|
} else {
|
|
37074
|
-
allRecords = await resource.getAll();
|
|
37583
|
+
allRecords = resource.getAll ? await resource.getAll() : await resource.list();
|
|
37075
37584
|
}
|
|
37076
37585
|
const totalRecords = allRecords.length;
|
|
37077
37586
|
let processedRecords = 0;
|
|
37078
37587
|
let dimensionMismatches = 0;
|
|
37588
|
+
if (!partition && totalRecords > 1e3) {
|
|
37589
|
+
const warning = {
|
|
37590
|
+
resource: resource.name,
|
|
37591
|
+
operation: "vectorSearch",
|
|
37592
|
+
totalRecords,
|
|
37593
|
+
vectorField,
|
|
37594
|
+
recommendation: "Use partitions to filter data before vector search for better performance"
|
|
37595
|
+
};
|
|
37596
|
+
this._emitEvent("vector:performance-warning", warning);
|
|
37597
|
+
console.warn(`\u26A0\uFE0F VectorPlugin: Performing vectorSearch on ${totalRecords} records without partition filter`);
|
|
37598
|
+
console.warn(` Resource: '${resource.name}'`);
|
|
37599
|
+
console.warn(` Recommendation: Use partition parameter to reduce search space`);
|
|
37600
|
+
console.warn(` Example: resource.vectorSearch(vector, { partition: 'byCategory', partitionValues: { category: 'books' } })`);
|
|
37601
|
+
}
|
|
37079
37602
|
const results = allRecords.filter((record) => record[vectorField] && Array.isArray(record[vectorField])).map((record, index) => {
|
|
37080
37603
|
try {
|
|
37081
37604
|
const distance = distanceFn(queryVector, record[vectorField]);
|
|
@@ -37159,10 +37682,11 @@ class VectorPlugin extends Plugin {
|
|
|
37159
37682
|
} else if (!vectorField) {
|
|
37160
37683
|
vectorField = "vector";
|
|
37161
37684
|
}
|
|
37162
|
-
|
|
37685
|
+
let {
|
|
37163
37686
|
k = 5,
|
|
37164
37687
|
distanceMetric = this.config.distanceMetric,
|
|
37165
37688
|
partition = null,
|
|
37689
|
+
partitionValues = null,
|
|
37166
37690
|
...kmeansOptions
|
|
37167
37691
|
} = options;
|
|
37168
37692
|
const distanceFn = this.distanceFunctions[distanceMetric];
|
|
@@ -37179,30 +37703,62 @@ class VectorPlugin extends Plugin {
|
|
|
37179
37703
|
});
|
|
37180
37704
|
throw error;
|
|
37181
37705
|
}
|
|
37706
|
+
if (!partition) {
|
|
37707
|
+
const autoPartition = this.getAutoEmbeddingPartition(resource, vectorField);
|
|
37708
|
+
if (autoPartition) {
|
|
37709
|
+
partition = autoPartition.partitionName;
|
|
37710
|
+
partitionValues = autoPartition.partitionValues;
|
|
37711
|
+
this._emitEvent("vector:auto-partition-used", {
|
|
37712
|
+
resource: resource.name,
|
|
37713
|
+
vectorField,
|
|
37714
|
+
partition,
|
|
37715
|
+
partitionValues,
|
|
37716
|
+
timestamp: Date.now()
|
|
37717
|
+
});
|
|
37718
|
+
}
|
|
37719
|
+
}
|
|
37182
37720
|
this._emitEvent("vector:cluster-start", {
|
|
37183
37721
|
resource: resource.name,
|
|
37184
37722
|
vectorField,
|
|
37185
37723
|
k,
|
|
37186
37724
|
distanceMetric,
|
|
37187
37725
|
partition,
|
|
37726
|
+
partitionValues,
|
|
37188
37727
|
maxIterations: kmeansOptions.maxIterations || 100,
|
|
37189
37728
|
timestamp: startTime
|
|
37190
37729
|
});
|
|
37191
37730
|
try {
|
|
37192
37731
|
let allRecords;
|
|
37193
|
-
if (partition) {
|
|
37732
|
+
if (partition && partitionValues) {
|
|
37194
37733
|
this._emitEvent("vector:partition-filter", {
|
|
37195
37734
|
resource: resource.name,
|
|
37196
37735
|
partition,
|
|
37736
|
+
partitionValues,
|
|
37197
37737
|
timestamp: Date.now()
|
|
37198
37738
|
});
|
|
37199
|
-
allRecords = await resource.list({ partition, partitionValues
|
|
37739
|
+
allRecords = await resource.list({ partition, partitionValues });
|
|
37200
37740
|
} else {
|
|
37201
|
-
allRecords = await resource.getAll();
|
|
37741
|
+
allRecords = resource.getAll ? await resource.getAll() : await resource.list();
|
|
37202
37742
|
}
|
|
37203
37743
|
const recordsWithVectors = allRecords.filter(
|
|
37204
37744
|
(record) => record[vectorField] && Array.isArray(record[vectorField])
|
|
37205
37745
|
);
|
|
37746
|
+
if (!partition && allRecords.length > 1e3) {
|
|
37747
|
+
const warning = {
|
|
37748
|
+
resource: resource.name,
|
|
37749
|
+
operation: "cluster",
|
|
37750
|
+
totalRecords: allRecords.length,
|
|
37751
|
+
recordsWithVectors: recordsWithVectors.length,
|
|
37752
|
+
vectorField,
|
|
37753
|
+
recommendation: "Use partitions to filter data before clustering for better performance"
|
|
37754
|
+
};
|
|
37755
|
+
this._emitEvent("vector:performance-warning", warning);
|
|
37756
|
+
console.warn(`\u26A0\uFE0F VectorPlugin: Performing clustering on ${allRecords.length} records without partition filter`);
|
|
37757
|
+
console.warn(` Resource: '${resource.name}'`);
|
|
37758
|
+
console.warn(` Records with vectors: ${recordsWithVectors.length}`);
|
|
37759
|
+
console.warn(` Recommendation: Use partition parameter to reduce clustering space`);
|
|
37760
|
+
console.warn(` Example: resource.cluster({ k: 5, partition: 'byCategory', partitionValues: { category: 'books' } })`);
|
|
37761
|
+
}
|
|
37206
37762
|
if (recordsWithVectors.length === 0) {
|
|
37207
37763
|
const error = new VectorError("No vectors found in resource", {
|
|
37208
37764
|
operation: "cluster",
|
|
@@ -37446,9 +38002,13 @@ async function generateTypes(database, options = {}) {
|
|
|
37446
38002
|
}
|
|
37447
38003
|
const resourceInterfaces = [];
|
|
37448
38004
|
for (const [name, resource] of Object.entries(database.resources)) {
|
|
37449
|
-
const
|
|
38005
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
37450
38006
|
const timestamps = resource.config?.timestamps || false;
|
|
37451
|
-
const
|
|
38007
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
38008
|
+
const userAttributes = Object.fromEntries(
|
|
38009
|
+
Object.entries(allAttributes).filter(([name2]) => !pluginAttrNames.includes(name2))
|
|
38010
|
+
);
|
|
38011
|
+
const interfaceDef = generateResourceInterface(name, userAttributes, timestamps);
|
|
37452
38012
|
lines.push(interfaceDef);
|
|
37453
38013
|
resourceInterfaces.push({
|
|
37454
38014
|
name,
|