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/dist/s3db.es.js CHANGED
@@ -13670,7 +13670,11 @@ class BigqueryReplicator extends BaseReplicator {
13670
13670
  }
13671
13671
  continue;
13672
13672
  }
13673
- const attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 { mapping, reversedMapping } = generateBase62Mapping(allKeys);
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
- const originalKey = reversedMap && reversedMap[key] ? reversedMap[key] : key;
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
- if (value.type === "ip4") {
17430
- processed[key] = { ...value, type: "string" };
17431
- } else if (value.type === "ip6") {
17432
- processed[key] = { ...value, type: "string" };
17433
- } else if (value.type === "money" || value.type === "crypto") {
17434
- processed[key] = { ...value, type: "number", min: value.min !== void 0 ? value.min : 0 };
17435
- } else if (value.type === "decimal") {
17436
- processed[key] = { ...value, type: "number" };
17437
- } else if (value.type === "geo:lat" || value.type === "geo-lat") {
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
- ...value,
17565
+ ...cleanValue,
17440
17566
  type: "number",
17441
- min: value.min !== void 0 ? value.min : -90,
17442
- max: value.max !== void 0 ? value.max : 90
17567
+ min: cleanValue.min !== void 0 ? cleanValue.min : -90,
17568
+ max: cleanValue.max !== void 0 ? cleanValue.max : 90
17443
17569
  };
17444
- } else if (value.type === "geo:lon" || value.type === "geo-lon") {
17570
+ } else if (cleanValue.type === "geo:lon" || cleanValue.type === "geo-lon") {
17445
17571
  processed[key] = {
17446
- ...value,
17572
+ ...cleanValue,
17447
17573
  type: "number",
17448
- min: value.min !== void 0 ? value.min : -180,
17449
- max: value.max !== void 0 ? value.max : 180
17574
+ min: cleanValue.min !== void 0 ? cleanValue.min : -180,
17575
+ max: cleanValue.max !== void 0 ? cleanValue.max : 180
17450
17576
  };
17451
- } else if (value.type === "geo:point" || value.type === "geo-point") {
17452
- processed[key] = { ...value, type: "any" };
17453
- } else if (value.type === "object" && value.properties) {
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
- ...value,
17456
- properties: this.preprocessAttributesForValidation(value.properties)
17581
+ ...cleanValue,
17582
+ properties: this.preprocessAttributesForValidation(cleanValue.properties)
17457
17583
  };
17458
17584
  } else {
17459
- processed[key] = value;
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
- return { mappedData: { _v: mappedData._v }, body: JSON.stringify(mappedData) };
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
- const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody));
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
- const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody, mapFromMeta));
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
- const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody));
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.2.3" : "latest");
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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
- const {
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: partition });
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
- const {
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: partition });
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 attributes = resource.config?.attributes || resource.attributes || {};
38005
+ const allAttributes = resource.config?.attributes || resource.attributes || {};
37450
38006
  const timestamps = resource.config?.timestamps || false;
37451
- const interfaceDef = generateResourceInterface(name, attributes, timestamps);
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,