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.cjs.js CHANGED
@@ -13693,7 +13693,11 @@ class BigqueryReplicator extends BaseReplicator {
13693
13693
  }
13694
13694
  continue;
13695
13695
  }
13696
- const attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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 { mapping, reversedMapping } = generateBase62Mapping(allKeys);
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
- const originalKey = reversedMap && reversedMap[key] ? reversedMap[key] : key;
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
- if (value.type === "ip4") {
17453
- processed[key] = { ...value, type: "string" };
17454
- } else if (value.type === "ip6") {
17455
- processed[key] = { ...value, type: "string" };
17456
- } else if (value.type === "money" || value.type === "crypto") {
17457
- processed[key] = { ...value, type: "number", min: value.min !== void 0 ? value.min : 0 };
17458
- } else if (value.type === "decimal") {
17459
- processed[key] = { ...value, type: "number" };
17460
- } else if (value.type === "geo:lat" || value.type === "geo-lat") {
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
- ...value,
17588
+ ...cleanValue,
17463
17589
  type: "number",
17464
- min: value.min !== void 0 ? value.min : -90,
17465
- max: value.max !== void 0 ? value.max : 90
17590
+ min: cleanValue.min !== void 0 ? cleanValue.min : -90,
17591
+ max: cleanValue.max !== void 0 ? cleanValue.max : 90
17466
17592
  };
17467
- } else if (value.type === "geo:lon" || value.type === "geo-lon") {
17593
+ } else if (cleanValue.type === "geo:lon" || cleanValue.type === "geo-lon") {
17468
17594
  processed[key] = {
17469
- ...value,
17595
+ ...cleanValue,
17470
17596
  type: "number",
17471
- min: value.min !== void 0 ? value.min : -180,
17472
- max: value.max !== void 0 ? value.max : 180
17597
+ min: cleanValue.min !== void 0 ? cleanValue.min : -180,
17598
+ max: cleanValue.max !== void 0 ? cleanValue.max : 180
17473
17599
  };
17474
- } else if (value.type === "geo:point" || value.type === "geo-point") {
17475
- processed[key] = { ...value, type: "any" };
17476
- } else if (value.type === "object" && value.properties) {
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
- ...value,
17479
- properties: this.preprocessAttributesForValidation(value.properties)
17604
+ ...cleanValue,
17605
+ properties: this.preprocessAttributesForValidation(cleanValue.properties)
17480
17606
  };
17481
17607
  } else {
17482
- processed[key] = value;
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
- return { mappedData: { _v: mappedData._v }, body: JSON.stringify(mappedData) };
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
- const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody));
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
- const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody, mapFromMeta));
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
- const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody));
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.2.3" : "latest");
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 attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
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
- const {
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: partition });
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
- const {
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: partition });
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 attributes = resource.config?.attributes || resource.attributes || {};
38028
+ const allAttributes = resource.config?.attributes || resource.attributes || {};
37473
38029
  const timestamps = resource.config?.timestamps || false;
37474
- const interfaceDef = generateResourceInterface(name, attributes, timestamps);
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,