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.cjs.js
CHANGED
|
@@ -13693,7 +13693,11 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
13693
13693
|
}
|
|
13694
13694
|
continue;
|
|
13695
13695
|
}
|
|
13696
|
-
const
|
|
13696
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
13697
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
13698
|
+
const attributes = Object.fromEntries(
|
|
13699
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
13700
|
+
);
|
|
13697
13701
|
for (const tableConfig of tableConfigs) {
|
|
13698
13702
|
const tableName = tableConfig.table;
|
|
13699
13703
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -14711,7 +14715,11 @@ class MySQLReplicator extends BaseReplicator {
|
|
|
14711
14715
|
}
|
|
14712
14716
|
continue;
|
|
14713
14717
|
}
|
|
14714
|
-
const
|
|
14718
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
14719
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
14720
|
+
const attributes = Object.fromEntries(
|
|
14721
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
14722
|
+
);
|
|
14715
14723
|
for (const tableConfig of tableConfigs) {
|
|
14716
14724
|
const tableName = tableConfig.table;
|
|
14717
14725
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -15090,7 +15098,11 @@ class PlanetScaleReplicator extends BaseReplicator {
|
|
|
15090
15098
|
}
|
|
15091
15099
|
continue;
|
|
15092
15100
|
}
|
|
15093
|
-
const
|
|
15101
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
15102
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
15103
|
+
const attributes = Object.fromEntries(
|
|
15104
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
15105
|
+
);
|
|
15094
15106
|
for (const tableConfig of tableConfigs) {
|
|
15095
15107
|
const tableName = tableConfig.table;
|
|
15096
15108
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -15411,7 +15423,11 @@ class PostgresReplicator extends BaseReplicator {
|
|
|
15411
15423
|
}
|
|
15412
15424
|
continue;
|
|
15413
15425
|
}
|
|
15414
|
-
const
|
|
15426
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
15427
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
15428
|
+
const attributes = Object.fromEntries(
|
|
15429
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
15430
|
+
);
|
|
15415
15431
|
for (const tableConfig of tableConfigs) {
|
|
15416
15432
|
const tableName = tableConfig.table;
|
|
15417
15433
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -16533,6 +16549,32 @@ function generateBase62Mapping(keys) {
|
|
|
16533
16549
|
});
|
|
16534
16550
|
return { mapping, reversedMapping };
|
|
16535
16551
|
}
|
|
16552
|
+
function generatePluginAttributeHash(pluginName, attributeName) {
|
|
16553
|
+
const input = `${pluginName}:${attributeName}`;
|
|
16554
|
+
const hash = crypto$1.createHash("sha256").update(input).digest();
|
|
16555
|
+
const num = hash.readUInt32BE(0);
|
|
16556
|
+
const base62Hash = encode(num);
|
|
16557
|
+
const paddedHash = base62Hash.padStart(3, "0").substring(0, 3);
|
|
16558
|
+
return "p" + paddedHash.toLowerCase();
|
|
16559
|
+
}
|
|
16560
|
+
function generatePluginMapping(attributes) {
|
|
16561
|
+
const mapping = {};
|
|
16562
|
+
const reversedMapping = {};
|
|
16563
|
+
const usedHashes = /* @__PURE__ */ new Set();
|
|
16564
|
+
for (const { key, pluginName } of attributes) {
|
|
16565
|
+
let hash = generatePluginAttributeHash(pluginName, key);
|
|
16566
|
+
let counter = 1;
|
|
16567
|
+
let finalHash = hash;
|
|
16568
|
+
while (usedHashes.has(finalHash)) {
|
|
16569
|
+
finalHash = `${hash}${counter}`;
|
|
16570
|
+
counter++;
|
|
16571
|
+
}
|
|
16572
|
+
usedHashes.add(finalHash);
|
|
16573
|
+
mapping[key] = finalHash;
|
|
16574
|
+
reversedMapping[finalHash] = key;
|
|
16575
|
+
}
|
|
16576
|
+
return { mapping, reversedMapping };
|
|
16577
|
+
}
|
|
16536
16578
|
const SchemaActions = {
|
|
16537
16579
|
trim: (value) => value == null ? value : value.trim(),
|
|
16538
16580
|
encrypt: async (value, { passphrase }) => {
|
|
@@ -16923,11 +16965,14 @@ class Schema {
|
|
|
16923
16965
|
constructor(args) {
|
|
16924
16966
|
const {
|
|
16925
16967
|
map,
|
|
16968
|
+
pluginMap,
|
|
16926
16969
|
name,
|
|
16927
16970
|
attributes,
|
|
16928
16971
|
passphrase,
|
|
16929
16972
|
version = 1,
|
|
16930
|
-
options = {}
|
|
16973
|
+
options = {},
|
|
16974
|
+
_pluginAttributeMetadata,
|
|
16975
|
+
_pluginAttributes
|
|
16931
16976
|
} = args;
|
|
16932
16977
|
this.name = name;
|
|
16933
16978
|
this.version = version;
|
|
@@ -16935,6 +16980,8 @@ class Schema {
|
|
|
16935
16980
|
this.passphrase = passphrase ?? "secret";
|
|
16936
16981
|
this.options = lodashEs.merge({}, this.defaultOptions(), options);
|
|
16937
16982
|
this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
|
|
16983
|
+
this._pluginAttributeMetadata = _pluginAttributeMetadata || {};
|
|
16984
|
+
this._pluginAttributes = _pluginAttributes || {};
|
|
16938
16985
|
const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
|
|
16939
16986
|
this.validator = new ValidatorManager({ autoEncrypt: false }).compile(lodashEs.merge(
|
|
16940
16987
|
{ $$async: true, $$strict: false },
|
|
@@ -16949,9 +16996,43 @@ class Schema {
|
|
|
16949
16996
|
const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
|
|
16950
16997
|
const objectKeys = this.extractObjectKeys(this.attributes);
|
|
16951
16998
|
const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
|
|
16952
|
-
const
|
|
16999
|
+
const userKeys = [];
|
|
17000
|
+
const pluginAttributes = [];
|
|
17001
|
+
for (const key of allKeys) {
|
|
17002
|
+
const attrDef = this.getAttributeDefinition(key);
|
|
17003
|
+
if (typeof attrDef === "object" && attrDef !== null && attrDef.__plugin__) {
|
|
17004
|
+
pluginAttributes.push({ key, pluginName: attrDef.__plugin__ });
|
|
17005
|
+
} else if (typeof attrDef === "string" && this._pluginAttributeMetadata && this._pluginAttributeMetadata[key]) {
|
|
17006
|
+
const pluginName = this._pluginAttributeMetadata[key].__plugin__;
|
|
17007
|
+
pluginAttributes.push({ key, pluginName });
|
|
17008
|
+
} else {
|
|
17009
|
+
userKeys.push(key);
|
|
17010
|
+
}
|
|
17011
|
+
}
|
|
17012
|
+
const { mapping, reversedMapping } = generateBase62Mapping(userKeys);
|
|
16953
17013
|
this.map = mapping;
|
|
16954
17014
|
this.reversedMap = reversedMapping;
|
|
17015
|
+
const { mapping: pMapping, reversedMapping: pReversedMapping } = generatePluginMapping(pluginAttributes);
|
|
17016
|
+
this.pluginMap = pMapping;
|
|
17017
|
+
this.reversedPluginMap = pReversedMapping;
|
|
17018
|
+
this._pluginAttributes = {};
|
|
17019
|
+
for (const { key, pluginName } of pluginAttributes) {
|
|
17020
|
+
if (!this._pluginAttributes[pluginName]) {
|
|
17021
|
+
this._pluginAttributes[pluginName] = [];
|
|
17022
|
+
}
|
|
17023
|
+
this._pluginAttributes[pluginName].push(key);
|
|
17024
|
+
}
|
|
17025
|
+
}
|
|
17026
|
+
if (!lodashEs.isEmpty(pluginMap)) {
|
|
17027
|
+
this.pluginMap = pluginMap;
|
|
17028
|
+
this.reversedPluginMap = lodashEs.invert(pluginMap);
|
|
17029
|
+
}
|
|
17030
|
+
if (!this.pluginMap) {
|
|
17031
|
+
this.pluginMap = {};
|
|
17032
|
+
this.reversedPluginMap = {};
|
|
17033
|
+
}
|
|
17034
|
+
if (!this._pluginAttributes) {
|
|
17035
|
+
this._pluginAttributes = {};
|
|
16955
17036
|
}
|
|
16956
17037
|
}
|
|
16957
17038
|
defaultOptions() {
|
|
@@ -17180,6 +17261,8 @@ class Schema {
|
|
|
17180
17261
|
static import(data) {
|
|
17181
17262
|
let {
|
|
17182
17263
|
map,
|
|
17264
|
+
pluginMap,
|
|
17265
|
+
_pluginAttributeMetadata,
|
|
17183
17266
|
name,
|
|
17184
17267
|
options,
|
|
17185
17268
|
version,
|
|
@@ -17190,11 +17273,15 @@ class Schema {
|
|
|
17190
17273
|
attributes = attrs;
|
|
17191
17274
|
const schema = new Schema({
|
|
17192
17275
|
map,
|
|
17276
|
+
pluginMap: pluginMap || {},
|
|
17193
17277
|
name,
|
|
17194
17278
|
options,
|
|
17195
17279
|
version,
|
|
17196
17280
|
attributes
|
|
17197
17281
|
});
|
|
17282
|
+
if (_pluginAttributeMetadata) {
|
|
17283
|
+
schema._pluginAttributeMetadata = _pluginAttributeMetadata;
|
|
17284
|
+
}
|
|
17198
17285
|
return schema;
|
|
17199
17286
|
}
|
|
17200
17287
|
/**
|
|
@@ -17232,7 +17319,10 @@ class Schema {
|
|
|
17232
17319
|
name: this.name,
|
|
17233
17320
|
options: this.options,
|
|
17234
17321
|
attributes: this._exportAttributes(this.attributes),
|
|
17235
|
-
map: this.map
|
|
17322
|
+
map: this.map,
|
|
17323
|
+
pluginMap: this.pluginMap || {},
|
|
17324
|
+
_pluginAttributeMetadata: this._pluginAttributeMetadata || {},
|
|
17325
|
+
_pluginAttributes: this._pluginAttributes || {}
|
|
17236
17326
|
};
|
|
17237
17327
|
return data;
|
|
17238
17328
|
}
|
|
@@ -17285,7 +17375,7 @@ class Schema {
|
|
|
17285
17375
|
const flattenedObj = flat.flatten(obj, { safe: true });
|
|
17286
17376
|
const rest = { "_v": this.version + "" };
|
|
17287
17377
|
for (const [key, value] of Object.entries(flattenedObj)) {
|
|
17288
|
-
const mappedKey = this.map[key] || key;
|
|
17378
|
+
const mappedKey = this.pluginMap[key] || this.map[key] || key;
|
|
17289
17379
|
const attrDef = this.getAttributeDefinition(key);
|
|
17290
17380
|
if (typeof value === "number" && typeof attrDef === "string" && attrDef.includes("number")) {
|
|
17291
17381
|
rest[mappedKey] = encode(value);
|
|
@@ -17306,14 +17396,18 @@ class Schema {
|
|
|
17306
17396
|
await this.applyHooksActions(rest, "afterMap");
|
|
17307
17397
|
return rest;
|
|
17308
17398
|
}
|
|
17309
|
-
async unmapper(mappedResourceItem, mapOverride) {
|
|
17399
|
+
async unmapper(mappedResourceItem, mapOverride, pluginMapOverride) {
|
|
17310
17400
|
let obj = lodashEs.cloneDeep(mappedResourceItem);
|
|
17311
17401
|
delete obj._v;
|
|
17312
17402
|
obj = await this.applyHooksActions(obj, "beforeUnmap");
|
|
17313
17403
|
const reversedMap = mapOverride ? lodashEs.invert(mapOverride) : this.reversedMap;
|
|
17404
|
+
const reversedPluginMap = pluginMapOverride ? lodashEs.invert(pluginMapOverride) : this.reversedPluginMap;
|
|
17314
17405
|
const rest = {};
|
|
17315
17406
|
for (const [key, value] of Object.entries(obj)) {
|
|
17316
|
-
|
|
17407
|
+
let originalKey = reversedPluginMap[key] || reversedMap[key] || key;
|
|
17408
|
+
if (!originalKey) {
|
|
17409
|
+
originalKey = key;
|
|
17410
|
+
}
|
|
17317
17411
|
let parsedValue = value;
|
|
17318
17412
|
const attrDef = this.getAttributeDefinition(originalKey);
|
|
17319
17413
|
const hasAfterUnmapHook = this.options.hooks?.afterUnmap?.[originalKey];
|
|
@@ -17380,6 +17474,37 @@ class Schema {
|
|
|
17380
17474
|
}
|
|
17381
17475
|
return def;
|
|
17382
17476
|
}
|
|
17477
|
+
/**
|
|
17478
|
+
* Regenerate plugin attribute mapping
|
|
17479
|
+
* Called when plugin attributes are added or removed
|
|
17480
|
+
* @returns {void}
|
|
17481
|
+
*/
|
|
17482
|
+
regeneratePluginMapping() {
|
|
17483
|
+
const flatAttrs = flat.flatten(this.attributes, { safe: true });
|
|
17484
|
+
const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
|
|
17485
|
+
const objectKeys = this.extractObjectKeys(this.attributes);
|
|
17486
|
+
const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
|
|
17487
|
+
const pluginAttributes = [];
|
|
17488
|
+
for (const key of allKeys) {
|
|
17489
|
+
const attrDef = this.getAttributeDefinition(key);
|
|
17490
|
+
if (typeof attrDef === "object" && attrDef !== null && attrDef.__plugin__) {
|
|
17491
|
+
pluginAttributes.push({ key, pluginName: attrDef.__plugin__ });
|
|
17492
|
+
} else if (typeof attrDef === "string" && this._pluginAttributeMetadata && this._pluginAttributeMetadata[key]) {
|
|
17493
|
+
const pluginName = this._pluginAttributeMetadata[key].__plugin__;
|
|
17494
|
+
pluginAttributes.push({ key, pluginName });
|
|
17495
|
+
}
|
|
17496
|
+
}
|
|
17497
|
+
const { mapping, reversedMapping } = generatePluginMapping(pluginAttributes);
|
|
17498
|
+
this.pluginMap = mapping;
|
|
17499
|
+
this.reversedPluginMap = reversedMapping;
|
|
17500
|
+
this._pluginAttributes = {};
|
|
17501
|
+
for (const { key, pluginName } of pluginAttributes) {
|
|
17502
|
+
if (!this._pluginAttributes[pluginName]) {
|
|
17503
|
+
this._pluginAttributes[pluginName] = [];
|
|
17504
|
+
}
|
|
17505
|
+
this._pluginAttributes[pluginName].push(key);
|
|
17506
|
+
}
|
|
17507
|
+
}
|
|
17383
17508
|
/**
|
|
17384
17509
|
* Preprocess attributes to convert nested objects into validator-compatible format
|
|
17385
17510
|
* @param {Object} attributes - Original attributes
|
|
@@ -17449,37 +17574,38 @@ class Schema {
|
|
|
17449
17574
|
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
17450
17575
|
const hasValidatorType = value.type !== void 0 && key !== "$$type";
|
|
17451
17576
|
if (hasValidatorType) {
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17577
|
+
const { __plugin__, __pluginCreated__, ...cleanValue } = value;
|
|
17578
|
+
if (cleanValue.type === "ip4") {
|
|
17579
|
+
processed[key] = { ...cleanValue, type: "string" };
|
|
17580
|
+
} else if (cleanValue.type === "ip6") {
|
|
17581
|
+
processed[key] = { ...cleanValue, type: "string" };
|
|
17582
|
+
} else if (cleanValue.type === "money" || cleanValue.type === "crypto") {
|
|
17583
|
+
processed[key] = { ...cleanValue, type: "number", min: cleanValue.min !== void 0 ? cleanValue.min : 0 };
|
|
17584
|
+
} else if (cleanValue.type === "decimal") {
|
|
17585
|
+
processed[key] = { ...cleanValue, type: "number" };
|
|
17586
|
+
} else if (cleanValue.type === "geo:lat" || cleanValue.type === "geo-lat") {
|
|
17461
17587
|
processed[key] = {
|
|
17462
|
-
...
|
|
17588
|
+
...cleanValue,
|
|
17463
17589
|
type: "number",
|
|
17464
|
-
min:
|
|
17465
|
-
max:
|
|
17590
|
+
min: cleanValue.min !== void 0 ? cleanValue.min : -90,
|
|
17591
|
+
max: cleanValue.max !== void 0 ? cleanValue.max : 90
|
|
17466
17592
|
};
|
|
17467
|
-
} else if (
|
|
17593
|
+
} else if (cleanValue.type === "geo:lon" || cleanValue.type === "geo-lon") {
|
|
17468
17594
|
processed[key] = {
|
|
17469
|
-
...
|
|
17595
|
+
...cleanValue,
|
|
17470
17596
|
type: "number",
|
|
17471
|
-
min:
|
|
17472
|
-
max:
|
|
17597
|
+
min: cleanValue.min !== void 0 ? cleanValue.min : -180,
|
|
17598
|
+
max: cleanValue.max !== void 0 ? cleanValue.max : 180
|
|
17473
17599
|
};
|
|
17474
|
-
} else if (
|
|
17475
|
-
processed[key] = { ...
|
|
17476
|
-
} else if (
|
|
17600
|
+
} else if (cleanValue.type === "geo:point" || cleanValue.type === "geo-point") {
|
|
17601
|
+
processed[key] = { ...cleanValue, type: "any" };
|
|
17602
|
+
} else if (cleanValue.type === "object" && cleanValue.properties) {
|
|
17477
17603
|
processed[key] = {
|
|
17478
|
-
...
|
|
17479
|
-
properties: this.preprocessAttributesForValidation(
|
|
17604
|
+
...cleanValue,
|
|
17605
|
+
properties: this.preprocessAttributesForValidation(cleanValue.properties)
|
|
17480
17606
|
};
|
|
17481
17607
|
} else {
|
|
17482
|
-
processed[key] =
|
|
17608
|
+
processed[key] = cleanValue;
|
|
17483
17609
|
}
|
|
17484
17610
|
} else {
|
|
17485
17611
|
const isExplicitRequired = value.$$type && value.$$type.includes("required");
|
|
@@ -17797,7 +17923,11 @@ async function handleInsert$3({ resource, data, mappedData, originalData }) {
|
|
|
17797
17923
|
excess: totalSize - 2047,
|
|
17798
17924
|
data: originalData || data
|
|
17799
17925
|
});
|
|
17800
|
-
|
|
17926
|
+
const metadataOnly = { _v: mappedData._v };
|
|
17927
|
+
if (resource.schema?.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
17928
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
17929
|
+
}
|
|
17930
|
+
return { mappedData: metadataOnly, body: JSON.stringify(mappedData) };
|
|
17801
17931
|
}
|
|
17802
17932
|
return { mappedData, body: "" };
|
|
17803
17933
|
}
|
|
@@ -18000,6 +18130,12 @@ async function handleInsert$1({ resource, data, mappedData, originalData }) {
|
|
|
18000
18130
|
metadataFields._v = mappedData._v;
|
|
18001
18131
|
currentSize += attributeSizes._v;
|
|
18002
18132
|
}
|
|
18133
|
+
if (resource.schema?.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
18134
|
+
const pluginMapStr = JSON.stringify(resource.schema.pluginMap);
|
|
18135
|
+
const pluginMapSize = calculateUTF8Bytes("_pluginMap") + calculateUTF8Bytes(pluginMapStr);
|
|
18136
|
+
metadataFields._pluginMap = pluginMapStr;
|
|
18137
|
+
currentSize += pluginMapSize;
|
|
18138
|
+
}
|
|
18003
18139
|
let reservedLimit = effectiveLimit;
|
|
18004
18140
|
for (const [fieldName, size] of sortedFields) {
|
|
18005
18141
|
if (fieldName === "_v") continue;
|
|
@@ -18059,6 +18195,9 @@ async function handleInsert({ resource, data, mappedData }) {
|
|
|
18059
18195
|
"_v": mappedData._v || String(resource.version)
|
|
18060
18196
|
};
|
|
18061
18197
|
metadataOnly._map = JSON.stringify(resource.schema.map);
|
|
18198
|
+
if (resource.schema.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
18199
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
18200
|
+
}
|
|
18062
18201
|
const body = JSON.stringify(mappedData);
|
|
18063
18202
|
return { mappedData: metadataOnly, body };
|
|
18064
18203
|
}
|
|
@@ -18067,6 +18206,9 @@ async function handleUpdate({ resource, id, data, mappedData }) {
|
|
|
18067
18206
|
"_v": mappedData._v || String(resource.version)
|
|
18068
18207
|
};
|
|
18069
18208
|
metadataOnly._map = JSON.stringify(resource.schema.map);
|
|
18209
|
+
if (resource.schema.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
18210
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
18211
|
+
}
|
|
18070
18212
|
const body = JSON.stringify(mappedData);
|
|
18071
18213
|
return { mappedData: metadataOnly, body };
|
|
18072
18214
|
}
|
|
@@ -18447,6 +18589,118 @@ ${errorDetails}`,
|
|
|
18447
18589
|
this.applyConfiguration();
|
|
18448
18590
|
return { oldAttributes, newAttributes };
|
|
18449
18591
|
}
|
|
18592
|
+
/**
|
|
18593
|
+
* Add a plugin-created attribute to the resource schema
|
|
18594
|
+
* This ensures plugin attributes don't interfere with user-defined attributes
|
|
18595
|
+
* by using a separate mapping namespace (p0, p1, p2, ...)
|
|
18596
|
+
*
|
|
18597
|
+
* @param {string} name - Attribute name (e.g., '_hasEmbedding', 'clusterId')
|
|
18598
|
+
* @param {Object|string} definition - Attribute definition
|
|
18599
|
+
* @param {string} pluginName - Name of plugin adding the attribute
|
|
18600
|
+
* @returns {void}
|
|
18601
|
+
*
|
|
18602
|
+
* @example
|
|
18603
|
+
* // VectorPlugin adding tracking field
|
|
18604
|
+
* resource.addPluginAttribute('_hasEmbedding', {
|
|
18605
|
+
* type: 'boolean',
|
|
18606
|
+
* optional: true,
|
|
18607
|
+
* default: false
|
|
18608
|
+
* }, 'VectorPlugin');
|
|
18609
|
+
*
|
|
18610
|
+
* // Shorthand notation
|
|
18611
|
+
* resource.addPluginAttribute('clusterId', 'string|optional', 'VectorPlugin');
|
|
18612
|
+
*/
|
|
18613
|
+
addPluginAttribute(name, definition, pluginName) {
|
|
18614
|
+
if (!pluginName) {
|
|
18615
|
+
throw new ResourceError(
|
|
18616
|
+
"Plugin name is required when adding plugin attributes",
|
|
18617
|
+
{ resource: this.name, attribute: name }
|
|
18618
|
+
);
|
|
18619
|
+
}
|
|
18620
|
+
const existingDef = this.schema.getAttributeDefinition(name);
|
|
18621
|
+
if (existingDef && (!existingDef.__plugin__ || existingDef.__plugin__ !== pluginName)) {
|
|
18622
|
+
throw new ResourceError(
|
|
18623
|
+
`Attribute '${name}' already exists and is not from plugin '${pluginName}'`,
|
|
18624
|
+
{ resource: this.name, attribute: name, plugin: pluginName }
|
|
18625
|
+
);
|
|
18626
|
+
}
|
|
18627
|
+
let defObject = definition;
|
|
18628
|
+
if (typeof definition === "object" && definition !== null) {
|
|
18629
|
+
defObject = { ...definition };
|
|
18630
|
+
}
|
|
18631
|
+
if (typeof defObject === "object" && defObject !== null) {
|
|
18632
|
+
defObject.__plugin__ = pluginName;
|
|
18633
|
+
defObject.__pluginCreated__ = Date.now();
|
|
18634
|
+
}
|
|
18635
|
+
this.schema.attributes[name] = defObject;
|
|
18636
|
+
this.attributes[name] = defObject;
|
|
18637
|
+
if (typeof defObject === "string") {
|
|
18638
|
+
if (!this.schema._pluginAttributeMetadata) {
|
|
18639
|
+
this.schema._pluginAttributeMetadata = {};
|
|
18640
|
+
}
|
|
18641
|
+
this.schema._pluginAttributeMetadata[name] = {
|
|
18642
|
+
__plugin__: pluginName,
|
|
18643
|
+
__pluginCreated__: Date.now()
|
|
18644
|
+
};
|
|
18645
|
+
}
|
|
18646
|
+
this.schema.regeneratePluginMapping();
|
|
18647
|
+
if (this.schema.options.generateAutoHooks) {
|
|
18648
|
+
this.schema.generateAutoHooks();
|
|
18649
|
+
}
|
|
18650
|
+
const processedAttributes = this.schema.preprocessAttributesForValidation(this.schema.attributes);
|
|
18651
|
+
this.schema.validator = new ValidatorManager({ autoEncrypt: false }).compile(lodashEs.merge(
|
|
18652
|
+
{ $$async: true, $$strict: false },
|
|
18653
|
+
processedAttributes
|
|
18654
|
+
));
|
|
18655
|
+
if (this.database) {
|
|
18656
|
+
this.database.emit("plugin-attribute-added", {
|
|
18657
|
+
resource: this.name,
|
|
18658
|
+
attribute: name,
|
|
18659
|
+
plugin: pluginName,
|
|
18660
|
+
definition: defObject
|
|
18661
|
+
});
|
|
18662
|
+
}
|
|
18663
|
+
}
|
|
18664
|
+
/**
|
|
18665
|
+
* Remove a plugin-created attribute from the resource schema
|
|
18666
|
+
* Called when a plugin is uninstalled or no longer needs the attribute
|
|
18667
|
+
*
|
|
18668
|
+
* @param {string} name - Attribute name to remove
|
|
18669
|
+
* @param {string} [pluginName] - Optional plugin name for safety check
|
|
18670
|
+
* @returns {boolean} True if attribute was removed, false if not found
|
|
18671
|
+
*
|
|
18672
|
+
* @example
|
|
18673
|
+
* resource.removePluginAttribute('_hasEmbedding', 'VectorPlugin');
|
|
18674
|
+
*/
|
|
18675
|
+
removePluginAttribute(name, pluginName = null) {
|
|
18676
|
+
const attrDef = this.schema.getAttributeDefinition(name);
|
|
18677
|
+
const metadata = this.schema._pluginAttributeMetadata?.[name];
|
|
18678
|
+
const isPluginAttr = typeof attrDef === "object" && attrDef?.__plugin__ || metadata;
|
|
18679
|
+
if (!attrDef || !isPluginAttr) {
|
|
18680
|
+
return false;
|
|
18681
|
+
}
|
|
18682
|
+
const actualPlugin = attrDef?.__plugin__ || metadata?.__plugin__;
|
|
18683
|
+
if (pluginName && actualPlugin !== pluginName) {
|
|
18684
|
+
throw new ResourceError(
|
|
18685
|
+
`Attribute '${name}' belongs to plugin '${actualPlugin}', not '${pluginName}'`,
|
|
18686
|
+
{ resource: this.name, attribute: name, actualPlugin, requestedPlugin: pluginName }
|
|
18687
|
+
);
|
|
18688
|
+
}
|
|
18689
|
+
delete this.schema.attributes[name];
|
|
18690
|
+
delete this.attributes[name];
|
|
18691
|
+
if (this.schema._pluginAttributeMetadata?.[name]) {
|
|
18692
|
+
delete this.schema._pluginAttributeMetadata[name];
|
|
18693
|
+
}
|
|
18694
|
+
this.schema.regeneratePluginMapping();
|
|
18695
|
+
if (this.database) {
|
|
18696
|
+
this.database.emit("plugin-attribute-removed", {
|
|
18697
|
+
resource: this.name,
|
|
18698
|
+
attribute: name,
|
|
18699
|
+
plugin: actualPlugin
|
|
18700
|
+
});
|
|
18701
|
+
}
|
|
18702
|
+
return true;
|
|
18703
|
+
}
|
|
18450
18704
|
/**
|
|
18451
18705
|
* Add a hook function for a specific event
|
|
18452
18706
|
* @param {string} event - Hook event (beforeInsert, afterInsert, etc.)
|
|
@@ -20748,8 +21002,9 @@ ${errorDetails}`,
|
|
|
20748
21002
|
const filterInternalFields = (obj) => {
|
|
20749
21003
|
if (!obj || typeof obj !== "object") return obj;
|
|
20750
21004
|
const filtered2 = {};
|
|
21005
|
+
const pluginAttrNames = this.schema._pluginAttributes ? Object.values(this.schema._pluginAttributes).flat() : [];
|
|
20751
21006
|
for (const [key, value] of Object.entries(obj)) {
|
|
20752
|
-
if (!key.startsWith("_") || key === "_geohash" || key.startsWith("_geohash_zoom")) {
|
|
21007
|
+
if (!key.startsWith("_") || key === "_geohash" || key.startsWith("_geohash_zoom") || pluginAttrNames.includes(key)) {
|
|
20753
21008
|
filtered2[key] = value;
|
|
20754
21009
|
}
|
|
20755
21010
|
}
|
|
@@ -20775,7 +21030,16 @@ ${errorDetails}`,
|
|
|
20775
21030
|
if (hasOverflow && body) {
|
|
20776
21031
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(JSON.parse(body)));
|
|
20777
21032
|
if (okBody) {
|
|
20778
|
-
|
|
21033
|
+
let pluginMapFromMeta = null;
|
|
21034
|
+
if (metadata && metadata._pluginmap) {
|
|
21035
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(
|
|
21036
|
+
() => Promise.resolve(typeof metadata._pluginmap === "string" ? JSON.parse(metadata._pluginmap) : metadata._pluginmap)
|
|
21037
|
+
);
|
|
21038
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
21039
|
+
}
|
|
21040
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(
|
|
21041
|
+
() => this.schema.unmapper(parsedBody, void 0, pluginMapFromMeta)
|
|
21042
|
+
);
|
|
20779
21043
|
bodyData = okUnmap ? unmappedBody : {};
|
|
20780
21044
|
}
|
|
20781
21045
|
}
|
|
@@ -20792,11 +21056,16 @@ ${errorDetails}`,
|
|
|
20792
21056
|
if (behavior === "body-only") {
|
|
20793
21057
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(body ? JSON.parse(body) : {}));
|
|
20794
21058
|
let mapFromMeta = this.schema.map;
|
|
21059
|
+
let pluginMapFromMeta = null;
|
|
20795
21060
|
if (metadata && metadata._map) {
|
|
20796
21061
|
const [okMap, errMap, parsedMap] = await tryFn(() => Promise.resolve(typeof metadata._map === "string" ? JSON.parse(metadata._map) : metadata._map));
|
|
20797
21062
|
mapFromMeta = okMap ? parsedMap : this.schema.map;
|
|
20798
21063
|
}
|
|
20799
|
-
|
|
21064
|
+
if (metadata && metadata._pluginmap) {
|
|
21065
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(() => Promise.resolve(typeof metadata._pluginmap === "string" ? JSON.parse(metadata._pluginmap) : metadata._pluginmap));
|
|
21066
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
21067
|
+
}
|
|
21068
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody, mapFromMeta, pluginMapFromMeta));
|
|
20800
21069
|
const result2 = okUnmap ? { ...unmappedBody, id } : { id };
|
|
20801
21070
|
Object.keys(result2).forEach((k) => {
|
|
20802
21071
|
result2[k] = fixValue(result2[k]);
|
|
@@ -20806,7 +21075,16 @@ ${errorDetails}`,
|
|
|
20806
21075
|
if (behavior === "user-managed" && body && body.trim() !== "") {
|
|
20807
21076
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(JSON.parse(body)));
|
|
20808
21077
|
if (okBody) {
|
|
20809
|
-
|
|
21078
|
+
let pluginMapFromMeta = null;
|
|
21079
|
+
if (metadata && metadata._pluginmap) {
|
|
21080
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(
|
|
21081
|
+
() => Promise.resolve(typeof metadata._pluginmap === "string" ? JSON.parse(metadata._pluginmap) : metadata._pluginmap)
|
|
21082
|
+
);
|
|
21083
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
21084
|
+
}
|
|
21085
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(
|
|
21086
|
+
() => this.schema.unmapper(parsedBody, void 0, pluginMapFromMeta)
|
|
21087
|
+
);
|
|
20810
21088
|
const bodyData = okUnmap ? unmappedBody : {};
|
|
20811
21089
|
const merged = { ...bodyData, ...unmappedMetadata, id };
|
|
20812
21090
|
Object.keys(merged).forEach((k) => {
|
|
@@ -21054,7 +21332,7 @@ class Database extends EventEmitter {
|
|
|
21054
21332
|
this.id = idGenerator(7);
|
|
21055
21333
|
this.version = "1";
|
|
21056
21334
|
this.s3dbVersion = (() => {
|
|
21057
|
-
const [ok, err, version] = tryFn(() => true ? "12.
|
|
21335
|
+
const [ok, err, version] = tryFn(() => true ? "12.3.0" : "latest");
|
|
21058
21336
|
return ok ? version : "latest";
|
|
21059
21337
|
})();
|
|
21060
21338
|
this._resourcesMap = {};
|
|
@@ -23044,7 +23322,11 @@ class TursoReplicator extends BaseReplicator {
|
|
|
23044
23322
|
}
|
|
23045
23323
|
continue;
|
|
23046
23324
|
}
|
|
23047
|
-
const
|
|
23325
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
23326
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
23327
|
+
const attributes = Object.fromEntries(
|
|
23328
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
23329
|
+
);
|
|
23048
23330
|
for (const tableConfig of tableConfigs) {
|
|
23049
23331
|
const tableName = tableConfig.table;
|
|
23050
23332
|
const [okSync, errSync] = await tryFn(async () => {
|
|
@@ -36879,6 +37161,7 @@ class VectorPlugin extends Plugin {
|
|
|
36879
37161
|
*
|
|
36880
37162
|
* Detects large vector fields and warns if proper behavior is not set.
|
|
36881
37163
|
* Can optionally auto-fix by setting body-overflow behavior.
|
|
37164
|
+
* Auto-creates partitions for optional embedding fields to enable O(1) filtering.
|
|
36882
37165
|
*/
|
|
36883
37166
|
validateVectorStorage() {
|
|
36884
37167
|
for (const resource of Object.values(this.database.resources)) {
|
|
@@ -36915,8 +37198,217 @@ class VectorPlugin extends Plugin {
|
|
|
36915
37198
|
}
|
|
36916
37199
|
}
|
|
36917
37200
|
}
|
|
37201
|
+
this.setupEmbeddingPartitions(resource, vectorFields);
|
|
36918
37202
|
}
|
|
36919
37203
|
}
|
|
37204
|
+
/**
|
|
37205
|
+
* Setup automatic partitions for optional embedding fields
|
|
37206
|
+
*
|
|
37207
|
+
* Creates a partition that separates records with embeddings from those without.
|
|
37208
|
+
* This enables O(1) filtering instead of O(n) full scans when searching/clustering.
|
|
37209
|
+
*
|
|
37210
|
+
* @param {Resource} resource - Resource instance
|
|
37211
|
+
* @param {Array} vectorFields - Detected vector fields with metadata
|
|
37212
|
+
*/
|
|
37213
|
+
setupEmbeddingPartitions(resource, vectorFields) {
|
|
37214
|
+
if (!resource.config) return;
|
|
37215
|
+
for (const vectorField of vectorFields) {
|
|
37216
|
+
const isOptional = this.isFieldOptional(resource.schema.attributes, vectorField.name);
|
|
37217
|
+
if (!isOptional) continue;
|
|
37218
|
+
const partitionName = `byHas${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
37219
|
+
const trackingFieldName = `_has${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
37220
|
+
if (resource.config.partitions && resource.config.partitions[partitionName]) {
|
|
37221
|
+
this.emit("vector:partition-exists", {
|
|
37222
|
+
resource: resource.name,
|
|
37223
|
+
vectorField: vectorField.name,
|
|
37224
|
+
partition: partitionName,
|
|
37225
|
+
timestamp: Date.now()
|
|
37226
|
+
});
|
|
37227
|
+
continue;
|
|
37228
|
+
}
|
|
37229
|
+
if (!resource.config.partitions) {
|
|
37230
|
+
resource.config.partitions = {};
|
|
37231
|
+
}
|
|
37232
|
+
resource.config.partitions[partitionName] = {
|
|
37233
|
+
fields: {
|
|
37234
|
+
[trackingFieldName]: "boolean"
|
|
37235
|
+
}
|
|
37236
|
+
};
|
|
37237
|
+
if (!resource.schema.attributes[trackingFieldName]) {
|
|
37238
|
+
resource.addPluginAttribute(trackingFieldName, {
|
|
37239
|
+
type: "boolean",
|
|
37240
|
+
optional: true,
|
|
37241
|
+
default: false
|
|
37242
|
+
}, "VectorPlugin");
|
|
37243
|
+
}
|
|
37244
|
+
this.emit("vector:partition-created", {
|
|
37245
|
+
resource: resource.name,
|
|
37246
|
+
vectorField: vectorField.name,
|
|
37247
|
+
partition: partitionName,
|
|
37248
|
+
trackingField: trackingFieldName,
|
|
37249
|
+
timestamp: Date.now()
|
|
37250
|
+
});
|
|
37251
|
+
console.log(`\u2705 VectorPlugin: Created partition '${partitionName}' for optional embedding field '${vectorField.name}' in resource '${resource.name}'`);
|
|
37252
|
+
this.installEmbeddingHooks(resource, vectorField.name, trackingFieldName);
|
|
37253
|
+
}
|
|
37254
|
+
}
|
|
37255
|
+
/**
|
|
37256
|
+
* Check if a field is optional in the schema
|
|
37257
|
+
*
|
|
37258
|
+
* @param {Object} attributes - Resource attributes
|
|
37259
|
+
* @param {string} fieldPath - Field path (supports dot notation)
|
|
37260
|
+
* @returns {boolean} True if field is optional
|
|
37261
|
+
*/
|
|
37262
|
+
isFieldOptional(attributes, fieldPath) {
|
|
37263
|
+
const parts = fieldPath.split(".");
|
|
37264
|
+
let current = attributes;
|
|
37265
|
+
for (let i = 0; i < parts.length; i++) {
|
|
37266
|
+
const part = parts[i];
|
|
37267
|
+
const attr = current[part];
|
|
37268
|
+
if (!attr) return true;
|
|
37269
|
+
if (typeof attr === "string") {
|
|
37270
|
+
const flags = attr.split("|");
|
|
37271
|
+
if (flags.includes("required")) return false;
|
|
37272
|
+
if (flags.includes("optional") || flags.some((f) => f.startsWith("optional:"))) return true;
|
|
37273
|
+
return !flags.includes("required");
|
|
37274
|
+
}
|
|
37275
|
+
if (typeof attr === "object") {
|
|
37276
|
+
if (i === parts.length - 1) {
|
|
37277
|
+
if (attr.optional === true) return true;
|
|
37278
|
+
if (attr.optional === false) return false;
|
|
37279
|
+
return attr.optional !== false;
|
|
37280
|
+
}
|
|
37281
|
+
if (attr.type === "object" && attr.props) {
|
|
37282
|
+
current = attr.props;
|
|
37283
|
+
} else {
|
|
37284
|
+
return true;
|
|
37285
|
+
}
|
|
37286
|
+
}
|
|
37287
|
+
}
|
|
37288
|
+
return true;
|
|
37289
|
+
}
|
|
37290
|
+
/**
|
|
37291
|
+
* Capitalize first letter of string
|
|
37292
|
+
*
|
|
37293
|
+
* @param {string} str - Input string
|
|
37294
|
+
* @returns {string} Capitalized string
|
|
37295
|
+
*/
|
|
37296
|
+
capitalize(str) {
|
|
37297
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
37298
|
+
}
|
|
37299
|
+
/**
|
|
37300
|
+
* Install hooks to maintain embedding partition tracking field
|
|
37301
|
+
*
|
|
37302
|
+
* @param {Resource} resource - Resource instance
|
|
37303
|
+
* @param {string} vectorField - Vector field name
|
|
37304
|
+
* @param {string} trackingField - Tracking field name
|
|
37305
|
+
*/
|
|
37306
|
+
installEmbeddingHooks(resource, vectorField, trackingField) {
|
|
37307
|
+
resource.registerHook("beforeInsert", async (data) => {
|
|
37308
|
+
const hasVector = this.hasVectorValue(data, vectorField);
|
|
37309
|
+
this.setNestedValue(data, trackingField, hasVector);
|
|
37310
|
+
return data;
|
|
37311
|
+
});
|
|
37312
|
+
resource.registerHook("beforeUpdate", async (id, updates) => {
|
|
37313
|
+
if (vectorField in updates || this.hasNestedKey(updates, vectorField)) {
|
|
37314
|
+
const hasVector = this.hasVectorValue(updates, vectorField);
|
|
37315
|
+
this.setNestedValue(updates, trackingField, hasVector);
|
|
37316
|
+
}
|
|
37317
|
+
return updates;
|
|
37318
|
+
});
|
|
37319
|
+
this.emit("vector:hooks-installed", {
|
|
37320
|
+
resource: resource.name,
|
|
37321
|
+
vectorField,
|
|
37322
|
+
trackingField,
|
|
37323
|
+
hooks: ["beforeInsert", "beforeUpdate"],
|
|
37324
|
+
timestamp: Date.now()
|
|
37325
|
+
});
|
|
37326
|
+
}
|
|
37327
|
+
/**
|
|
37328
|
+
* Check if data has a valid vector value for the given field
|
|
37329
|
+
*
|
|
37330
|
+
* @param {Object} data - Data object
|
|
37331
|
+
* @param {string} fieldPath - Field path (supports dot notation)
|
|
37332
|
+
* @returns {boolean} True if vector exists and is valid
|
|
37333
|
+
*/
|
|
37334
|
+
hasVectorValue(data, fieldPath) {
|
|
37335
|
+
const value = this.getNestedValue(data, fieldPath);
|
|
37336
|
+
return value != null && Array.isArray(value) && value.length > 0;
|
|
37337
|
+
}
|
|
37338
|
+
/**
|
|
37339
|
+
* Check if object has a nested key
|
|
37340
|
+
*
|
|
37341
|
+
* @param {Object} obj - Object to check
|
|
37342
|
+
* @param {string} path - Dot-notation path
|
|
37343
|
+
* @returns {boolean} True if key exists
|
|
37344
|
+
*/
|
|
37345
|
+
hasNestedKey(obj, path) {
|
|
37346
|
+
const parts = path.split(".");
|
|
37347
|
+
let current = obj;
|
|
37348
|
+
for (const part of parts) {
|
|
37349
|
+
if (current == null || typeof current !== "object") return false;
|
|
37350
|
+
if (!(part in current)) return false;
|
|
37351
|
+
current = current[part];
|
|
37352
|
+
}
|
|
37353
|
+
return true;
|
|
37354
|
+
}
|
|
37355
|
+
/**
|
|
37356
|
+
* Get nested value from object using dot notation
|
|
37357
|
+
*
|
|
37358
|
+
* @param {Object} obj - Object to traverse
|
|
37359
|
+
* @param {string} path - Dot-notation path
|
|
37360
|
+
* @returns {*} Value at path or undefined
|
|
37361
|
+
*/
|
|
37362
|
+
getNestedValue(obj, path) {
|
|
37363
|
+
const parts = path.split(".");
|
|
37364
|
+
let current = obj;
|
|
37365
|
+
for (const part of parts) {
|
|
37366
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
37367
|
+
current = current[part];
|
|
37368
|
+
}
|
|
37369
|
+
return current;
|
|
37370
|
+
}
|
|
37371
|
+
/**
|
|
37372
|
+
* Set nested value in object using dot notation
|
|
37373
|
+
*
|
|
37374
|
+
* @param {Object} obj - Object to modify
|
|
37375
|
+
* @param {string} path - Dot-notation path
|
|
37376
|
+
* @param {*} value - Value to set
|
|
37377
|
+
*/
|
|
37378
|
+
setNestedValue(obj, path, value) {
|
|
37379
|
+
const parts = path.split(".");
|
|
37380
|
+
let current = obj;
|
|
37381
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
37382
|
+
const part = parts[i];
|
|
37383
|
+
if (!(part in current) || typeof current[part] !== "object") {
|
|
37384
|
+
current[part] = {};
|
|
37385
|
+
}
|
|
37386
|
+
current = current[part];
|
|
37387
|
+
}
|
|
37388
|
+
current[parts[parts.length - 1]] = value;
|
|
37389
|
+
}
|
|
37390
|
+
/**
|
|
37391
|
+
* Get auto-created embedding partition for a vector field
|
|
37392
|
+
*
|
|
37393
|
+
* Returns partition configuration if an auto-partition exists for the given vector field.
|
|
37394
|
+
* Auto-partitions enable O(1) filtering to only records with embeddings.
|
|
37395
|
+
*
|
|
37396
|
+
* @param {Resource} resource - Resource instance
|
|
37397
|
+
* @param {string} vectorField - Vector field name
|
|
37398
|
+
* @returns {Object|null} Partition config or null
|
|
37399
|
+
*/
|
|
37400
|
+
getAutoEmbeddingPartition(resource, vectorField) {
|
|
37401
|
+
if (!resource.config) return null;
|
|
37402
|
+
const partitionName = `byHas${this.capitalize(vectorField.replace(/\./g, "_"))}`;
|
|
37403
|
+
const trackingFieldName = `_has${this.capitalize(vectorField.replace(/\./g, "_"))}`;
|
|
37404
|
+
if (resource.config.partitions && resource.config.partitions[partitionName]) {
|
|
37405
|
+
return {
|
|
37406
|
+
partitionName,
|
|
37407
|
+
partitionValues: { [trackingFieldName]: true }
|
|
37408
|
+
};
|
|
37409
|
+
}
|
|
37410
|
+
return null;
|
|
37411
|
+
}
|
|
36920
37412
|
/**
|
|
36921
37413
|
* Auto-detect vector field from resource schema
|
|
36922
37414
|
*
|
|
@@ -37054,11 +37546,12 @@ class VectorPlugin extends Plugin {
|
|
|
37054
37546
|
} else if (!vectorField) {
|
|
37055
37547
|
vectorField = "vector";
|
|
37056
37548
|
}
|
|
37057
|
-
|
|
37549
|
+
let {
|
|
37058
37550
|
limit = 10,
|
|
37059
37551
|
distanceMetric = this.config.distanceMetric,
|
|
37060
37552
|
threshold = null,
|
|
37061
|
-
partition = null
|
|
37553
|
+
partition = null,
|
|
37554
|
+
partitionValues = null
|
|
37062
37555
|
} = options;
|
|
37063
37556
|
const distanceFn = this.distanceFunctions[distanceMetric];
|
|
37064
37557
|
if (!distanceFn) {
|
|
@@ -37074,31 +37567,61 @@ class VectorPlugin extends Plugin {
|
|
|
37074
37567
|
});
|
|
37075
37568
|
throw error;
|
|
37076
37569
|
}
|
|
37570
|
+
if (!partition) {
|
|
37571
|
+
const autoPartition = this.getAutoEmbeddingPartition(resource, vectorField);
|
|
37572
|
+
if (autoPartition) {
|
|
37573
|
+
partition = autoPartition.partitionName;
|
|
37574
|
+
partitionValues = autoPartition.partitionValues;
|
|
37575
|
+
this._emitEvent("vector:auto-partition-used", {
|
|
37576
|
+
resource: resource.name,
|
|
37577
|
+
vectorField,
|
|
37578
|
+
partition,
|
|
37579
|
+
partitionValues,
|
|
37580
|
+
timestamp: Date.now()
|
|
37581
|
+
});
|
|
37582
|
+
}
|
|
37583
|
+
}
|
|
37077
37584
|
this._emitEvent("vector:search-start", {
|
|
37078
37585
|
resource: resource.name,
|
|
37079
37586
|
vectorField,
|
|
37080
37587
|
limit,
|
|
37081
37588
|
distanceMetric,
|
|
37082
37589
|
partition,
|
|
37590
|
+
partitionValues,
|
|
37083
37591
|
threshold,
|
|
37084
37592
|
queryDimensions: queryVector.length,
|
|
37085
37593
|
timestamp: startTime
|
|
37086
37594
|
});
|
|
37087
37595
|
try {
|
|
37088
37596
|
let allRecords;
|
|
37089
|
-
if (partition) {
|
|
37597
|
+
if (partition && partitionValues) {
|
|
37090
37598
|
this._emitEvent("vector:partition-filter", {
|
|
37091
37599
|
resource: resource.name,
|
|
37092
37600
|
partition,
|
|
37601
|
+
partitionValues,
|
|
37093
37602
|
timestamp: Date.now()
|
|
37094
37603
|
});
|
|
37095
|
-
allRecords = await resource.list({ partition, partitionValues
|
|
37604
|
+
allRecords = await resource.list({ partition, partitionValues });
|
|
37096
37605
|
} else {
|
|
37097
|
-
allRecords = await resource.getAll();
|
|
37606
|
+
allRecords = resource.getAll ? await resource.getAll() : await resource.list();
|
|
37098
37607
|
}
|
|
37099
37608
|
const totalRecords = allRecords.length;
|
|
37100
37609
|
let processedRecords = 0;
|
|
37101
37610
|
let dimensionMismatches = 0;
|
|
37611
|
+
if (!partition && totalRecords > 1e3) {
|
|
37612
|
+
const warning = {
|
|
37613
|
+
resource: resource.name,
|
|
37614
|
+
operation: "vectorSearch",
|
|
37615
|
+
totalRecords,
|
|
37616
|
+
vectorField,
|
|
37617
|
+
recommendation: "Use partitions to filter data before vector search for better performance"
|
|
37618
|
+
};
|
|
37619
|
+
this._emitEvent("vector:performance-warning", warning);
|
|
37620
|
+
console.warn(`\u26A0\uFE0F VectorPlugin: Performing vectorSearch on ${totalRecords} records without partition filter`);
|
|
37621
|
+
console.warn(` Resource: '${resource.name}'`);
|
|
37622
|
+
console.warn(` Recommendation: Use partition parameter to reduce search space`);
|
|
37623
|
+
console.warn(` Example: resource.vectorSearch(vector, { partition: 'byCategory', partitionValues: { category: 'books' } })`);
|
|
37624
|
+
}
|
|
37102
37625
|
const results = allRecords.filter((record) => record[vectorField] && Array.isArray(record[vectorField])).map((record, index) => {
|
|
37103
37626
|
try {
|
|
37104
37627
|
const distance = distanceFn(queryVector, record[vectorField]);
|
|
@@ -37182,10 +37705,11 @@ class VectorPlugin extends Plugin {
|
|
|
37182
37705
|
} else if (!vectorField) {
|
|
37183
37706
|
vectorField = "vector";
|
|
37184
37707
|
}
|
|
37185
|
-
|
|
37708
|
+
let {
|
|
37186
37709
|
k = 5,
|
|
37187
37710
|
distanceMetric = this.config.distanceMetric,
|
|
37188
37711
|
partition = null,
|
|
37712
|
+
partitionValues = null,
|
|
37189
37713
|
...kmeansOptions
|
|
37190
37714
|
} = options;
|
|
37191
37715
|
const distanceFn = this.distanceFunctions[distanceMetric];
|
|
@@ -37202,30 +37726,62 @@ class VectorPlugin extends Plugin {
|
|
|
37202
37726
|
});
|
|
37203
37727
|
throw error;
|
|
37204
37728
|
}
|
|
37729
|
+
if (!partition) {
|
|
37730
|
+
const autoPartition = this.getAutoEmbeddingPartition(resource, vectorField);
|
|
37731
|
+
if (autoPartition) {
|
|
37732
|
+
partition = autoPartition.partitionName;
|
|
37733
|
+
partitionValues = autoPartition.partitionValues;
|
|
37734
|
+
this._emitEvent("vector:auto-partition-used", {
|
|
37735
|
+
resource: resource.name,
|
|
37736
|
+
vectorField,
|
|
37737
|
+
partition,
|
|
37738
|
+
partitionValues,
|
|
37739
|
+
timestamp: Date.now()
|
|
37740
|
+
});
|
|
37741
|
+
}
|
|
37742
|
+
}
|
|
37205
37743
|
this._emitEvent("vector:cluster-start", {
|
|
37206
37744
|
resource: resource.name,
|
|
37207
37745
|
vectorField,
|
|
37208
37746
|
k,
|
|
37209
37747
|
distanceMetric,
|
|
37210
37748
|
partition,
|
|
37749
|
+
partitionValues,
|
|
37211
37750
|
maxIterations: kmeansOptions.maxIterations || 100,
|
|
37212
37751
|
timestamp: startTime
|
|
37213
37752
|
});
|
|
37214
37753
|
try {
|
|
37215
37754
|
let allRecords;
|
|
37216
|
-
if (partition) {
|
|
37755
|
+
if (partition && partitionValues) {
|
|
37217
37756
|
this._emitEvent("vector:partition-filter", {
|
|
37218
37757
|
resource: resource.name,
|
|
37219
37758
|
partition,
|
|
37759
|
+
partitionValues,
|
|
37220
37760
|
timestamp: Date.now()
|
|
37221
37761
|
});
|
|
37222
|
-
allRecords = await resource.list({ partition, partitionValues
|
|
37762
|
+
allRecords = await resource.list({ partition, partitionValues });
|
|
37223
37763
|
} else {
|
|
37224
|
-
allRecords = await resource.getAll();
|
|
37764
|
+
allRecords = resource.getAll ? await resource.getAll() : await resource.list();
|
|
37225
37765
|
}
|
|
37226
37766
|
const recordsWithVectors = allRecords.filter(
|
|
37227
37767
|
(record) => record[vectorField] && Array.isArray(record[vectorField])
|
|
37228
37768
|
);
|
|
37769
|
+
if (!partition && allRecords.length > 1e3) {
|
|
37770
|
+
const warning = {
|
|
37771
|
+
resource: resource.name,
|
|
37772
|
+
operation: "cluster",
|
|
37773
|
+
totalRecords: allRecords.length,
|
|
37774
|
+
recordsWithVectors: recordsWithVectors.length,
|
|
37775
|
+
vectorField,
|
|
37776
|
+
recommendation: "Use partitions to filter data before clustering for better performance"
|
|
37777
|
+
};
|
|
37778
|
+
this._emitEvent("vector:performance-warning", warning);
|
|
37779
|
+
console.warn(`\u26A0\uFE0F VectorPlugin: Performing clustering on ${allRecords.length} records without partition filter`);
|
|
37780
|
+
console.warn(` Resource: '${resource.name}'`);
|
|
37781
|
+
console.warn(` Records with vectors: ${recordsWithVectors.length}`);
|
|
37782
|
+
console.warn(` Recommendation: Use partition parameter to reduce clustering space`);
|
|
37783
|
+
console.warn(` Example: resource.cluster({ k: 5, partition: 'byCategory', partitionValues: { category: 'books' } })`);
|
|
37784
|
+
}
|
|
37229
37785
|
if (recordsWithVectors.length === 0) {
|
|
37230
37786
|
const error = new VectorError("No vectors found in resource", {
|
|
37231
37787
|
operation: "cluster",
|
|
@@ -37469,9 +38025,13 @@ async function generateTypes(database, options = {}) {
|
|
|
37469
38025
|
}
|
|
37470
38026
|
const resourceInterfaces = [];
|
|
37471
38027
|
for (const [name, resource] of Object.entries(database.resources)) {
|
|
37472
|
-
const
|
|
38028
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
37473
38029
|
const timestamps = resource.config?.timestamps || false;
|
|
37474
|
-
const
|
|
38030
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
38031
|
+
const userAttributes = Object.fromEntries(
|
|
38032
|
+
Object.entries(allAttributes).filter(([name2]) => !pluginAttrNames.includes(name2))
|
|
38033
|
+
);
|
|
38034
|
+
const interfaceDef = generateResourceInterface(name, userAttributes, timestamps);
|
|
37475
38035
|
lines.push(interfaceDef);
|
|
37476
38036
|
resourceInterfaces.push({
|
|
37477
38037
|
name,
|