s3db.js 12.2.2 → 12.2.4

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
@@ -6976,18 +6976,13 @@ class CachePlugin extends Plugin {
6976
6976
  }
6977
6977
  }
6978
6978
 
6979
- const CostsPlugin = {
6980
- async setup(db, options = {}) {
6981
- if (!db || !db.client) {
6982
- return;
6983
- }
6984
- this.client = db.client;
6985
- this.options = {
6986
- considerFreeTier: false,
6987
- // Flag to consider AWS free tier in calculations
6988
- region: "us-east-1",
6989
- // AWS region for pricing (future use)
6990
- ...options
6979
+ class CostsPlugin extends Plugin {
6980
+ constructor(config = {}) {
6981
+ super(config);
6982
+ this.config = {
6983
+ considerFreeTier: config.considerFreeTier !== void 0 ? config.considerFreeTier : false,
6984
+ region: config.region || "us-east-1",
6985
+ ...config
6991
6986
  };
6992
6987
  this.map = {
6993
6988
  PutObjectCommand: "put",
@@ -7081,14 +7076,20 @@ const CostsPlugin = {
7081
7076
  // Data transfer out cost
7082
7077
  }
7083
7078
  };
7079
+ }
7080
+ async onInstall() {
7081
+ if (!this.database || !this.database.client) {
7082
+ return;
7083
+ }
7084
+ this.client = this.database.client;
7084
7085
  this.client.costs = JSON.parse(JSON.stringify(this.costs));
7085
- },
7086
- async start() {
7086
+ }
7087
+ async onStart() {
7087
7088
  if (this.client) {
7088
7089
  this.client.on("command.response", (name, response, input) => this.addRequest(name, this.map[name], response, input));
7089
7090
  this.client.on("command.error", (name, response, input) => this.addRequest(name, this.map[name], response, input));
7090
7091
  }
7091
- },
7092
+ }
7092
7093
  addRequest(name, method, response = {}, input = {}) {
7093
7094
  if (!method) return;
7094
7095
  this.costs.requests.totalEvents++;
@@ -7128,7 +7129,7 @@ const CostsPlugin = {
7128
7129
  this.client.costs.requests.subtotal += requestCost;
7129
7130
  }
7130
7131
  this.updateTotal();
7131
- },
7132
+ }
7132
7133
  trackStorage(bytes) {
7133
7134
  this.costs.storage.totalBytes += bytes;
7134
7135
  this.costs.storage.totalGB = this.costs.storage.totalBytes / (1024 * 1024 * 1024);
@@ -7139,7 +7140,7 @@ const CostsPlugin = {
7139
7140
  this.client.costs.storage.subtotal = this.calculateStorageCost(this.client.costs.storage);
7140
7141
  }
7141
7142
  this.updateTotal();
7142
- },
7143
+ }
7143
7144
  trackDataTransferIn(bytes) {
7144
7145
  this.costs.dataTransfer.inBytes += bytes;
7145
7146
  this.costs.dataTransfer.inGB = this.costs.dataTransfer.inBytes / (1024 * 1024 * 1024);
@@ -7148,7 +7149,7 @@ const CostsPlugin = {
7148
7149
  this.client.costs.dataTransfer.inGB = this.client.costs.dataTransfer.inBytes / (1024 * 1024 * 1024);
7149
7150
  }
7150
7151
  this.updateTotal();
7151
- },
7152
+ }
7152
7153
  trackDataTransferOut(bytes) {
7153
7154
  this.costs.dataTransfer.outBytes += bytes;
7154
7155
  this.costs.dataTransfer.outGB = this.costs.dataTransfer.outBytes / (1024 * 1024 * 1024);
@@ -7159,7 +7160,7 @@ const CostsPlugin = {
7159
7160
  this.client.costs.dataTransfer.subtotal = this.calculateDataTransferCost(this.client.costs.dataTransfer);
7160
7161
  }
7161
7162
  this.updateTotal();
7162
- },
7163
+ }
7163
7164
  calculateStorageCost(storage) {
7164
7165
  const totalGB = storage.totalGB;
7165
7166
  let cost = 0;
@@ -7178,11 +7179,11 @@ const CostsPlugin = {
7178
7179
  }
7179
7180
  }
7180
7181
  return cost;
7181
- },
7182
+ }
7182
7183
  calculateDataTransferCost(dataTransfer) {
7183
7184
  let totalGB = dataTransfer.outGB;
7184
7185
  let cost = 0;
7185
- if (this.options && this.options.considerFreeTier) {
7186
+ if (this.config && this.config.considerFreeTier) {
7186
7187
  const freeTierRemaining = dataTransfer.freeTierGB - dataTransfer.freeTierUsed;
7187
7188
  if (freeTierRemaining > 0 && totalGB > 0) {
7188
7189
  const gbToDeduct = Math.min(totalGB, freeTierRemaining);
@@ -7205,14 +7206,14 @@ const CostsPlugin = {
7205
7206
  }
7206
7207
  }
7207
7208
  return cost;
7208
- },
7209
+ }
7209
7210
  updateTotal() {
7210
7211
  this.costs.total = this.costs.requests.subtotal + this.costs.storage.subtotal + this.costs.dataTransfer.subtotal;
7211
7212
  if (this.client && this.client.costs) {
7212
7213
  this.client.costs.total = this.client.costs.requests.subtotal + this.client.costs.storage.subtotal + this.client.costs.dataTransfer.subtotal;
7213
7214
  }
7214
7215
  }
7215
- };
7216
+ }
7216
7217
 
7217
7218
  function createConfig(options, detectedTimezone) {
7218
7219
  const consolidation = options.consolidation || {};
@@ -21030,7 +21031,7 @@ class Database extends EventEmitter {
21030
21031
  this.id = idGenerator(7);
21031
21032
  this.version = "1";
21032
21033
  this.s3dbVersion = (() => {
21033
- const [ok, err, version] = tryFn(() => true ? "12.2.2" : "latest");
21034
+ const [ok, err, version] = tryFn(() => true ? "12.2.4" : "latest");
21034
21035
  return ok ? version : "latest";
21035
21036
  })();
21036
21037
  this._resourcesMap = {};
@@ -36855,6 +36856,7 @@ class VectorPlugin extends Plugin {
36855
36856
  *
36856
36857
  * Detects large vector fields and warns if proper behavior is not set.
36857
36858
  * Can optionally auto-fix by setting body-overflow behavior.
36859
+ * Auto-creates partitions for optional embedding fields to enable O(1) filtering.
36858
36860
  */
36859
36861
  validateVectorStorage() {
36860
36862
  for (const resource of Object.values(this.database.resources)) {
@@ -36891,7 +36893,216 @@ class VectorPlugin extends Plugin {
36891
36893
  }
36892
36894
  }
36893
36895
  }
36896
+ this.setupEmbeddingPartitions(resource, vectorFields);
36897
+ }
36898
+ }
36899
+ /**
36900
+ * Setup automatic partitions for optional embedding fields
36901
+ *
36902
+ * Creates a partition that separates records with embeddings from those without.
36903
+ * This enables O(1) filtering instead of O(n) full scans when searching/clustering.
36904
+ *
36905
+ * @param {Resource} resource - Resource instance
36906
+ * @param {Array} vectorFields - Detected vector fields with metadata
36907
+ */
36908
+ setupEmbeddingPartitions(resource, vectorFields) {
36909
+ if (!resource.config) return;
36910
+ for (const vectorField of vectorFields) {
36911
+ const isOptional = this.isFieldOptional(resource.schema.attributes, vectorField.name);
36912
+ if (!isOptional) continue;
36913
+ const partitionName = `byHas${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
36914
+ const trackingFieldName = `_has${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
36915
+ if (resource.config.partitions && resource.config.partitions[partitionName]) {
36916
+ this.emit("vector:partition-exists", {
36917
+ resource: resource.name,
36918
+ vectorField: vectorField.name,
36919
+ partition: partitionName,
36920
+ timestamp: Date.now()
36921
+ });
36922
+ continue;
36923
+ }
36924
+ if (!resource.config.partitions) {
36925
+ resource.config.partitions = {};
36926
+ }
36927
+ resource.config.partitions[partitionName] = {
36928
+ fields: {
36929
+ [trackingFieldName]: "boolean"
36930
+ }
36931
+ };
36932
+ if (!resource.schema.attributes[trackingFieldName]) {
36933
+ resource.schema.attributes[trackingFieldName] = {
36934
+ type: "boolean",
36935
+ optional: true,
36936
+ default: false
36937
+ };
36938
+ }
36939
+ this.emit("vector:partition-created", {
36940
+ resource: resource.name,
36941
+ vectorField: vectorField.name,
36942
+ partition: partitionName,
36943
+ trackingField: trackingFieldName,
36944
+ timestamp: Date.now()
36945
+ });
36946
+ console.log(`\u2705 VectorPlugin: Created partition '${partitionName}' for optional embedding field '${vectorField.name}' in resource '${resource.name}'`);
36947
+ this.installEmbeddingHooks(resource, vectorField.name, trackingFieldName);
36948
+ }
36949
+ }
36950
+ /**
36951
+ * Check if a field is optional in the schema
36952
+ *
36953
+ * @param {Object} attributes - Resource attributes
36954
+ * @param {string} fieldPath - Field path (supports dot notation)
36955
+ * @returns {boolean} True if field is optional
36956
+ */
36957
+ isFieldOptional(attributes, fieldPath) {
36958
+ const parts = fieldPath.split(".");
36959
+ let current = attributes;
36960
+ for (let i = 0; i < parts.length; i++) {
36961
+ const part = parts[i];
36962
+ const attr = current[part];
36963
+ if (!attr) return true;
36964
+ if (typeof attr === "string") {
36965
+ const flags = attr.split("|");
36966
+ if (flags.includes("required")) return false;
36967
+ if (flags.includes("optional") || flags.some((f) => f.startsWith("optional:"))) return true;
36968
+ return !flags.includes("required");
36969
+ }
36970
+ if (typeof attr === "object") {
36971
+ if (i === parts.length - 1) {
36972
+ if (attr.optional === true) return true;
36973
+ if (attr.optional === false) return false;
36974
+ return attr.optional !== false;
36975
+ }
36976
+ if (attr.type === "object" && attr.props) {
36977
+ current = attr.props;
36978
+ } else {
36979
+ return true;
36980
+ }
36981
+ }
36982
+ }
36983
+ return true;
36984
+ }
36985
+ /**
36986
+ * Capitalize first letter of string
36987
+ *
36988
+ * @param {string} str - Input string
36989
+ * @returns {string} Capitalized string
36990
+ */
36991
+ capitalize(str) {
36992
+ return str.charAt(0).toUpperCase() + str.slice(1);
36993
+ }
36994
+ /**
36995
+ * Install hooks to maintain embedding partition tracking field
36996
+ *
36997
+ * @param {Resource} resource - Resource instance
36998
+ * @param {string} vectorField - Vector field name
36999
+ * @param {string} trackingField - Tracking field name
37000
+ */
37001
+ installEmbeddingHooks(resource, vectorField, trackingField) {
37002
+ resource.registerHook("beforeInsert", async (data) => {
37003
+ const hasVector = this.hasVectorValue(data, vectorField);
37004
+ this.setNestedValue(data, trackingField, hasVector);
37005
+ return data;
37006
+ });
37007
+ resource.registerHook("beforeUpdate", async (id, updates) => {
37008
+ if (vectorField in updates || this.hasNestedKey(updates, vectorField)) {
37009
+ const hasVector = this.hasVectorValue(updates, vectorField);
37010
+ this.setNestedValue(updates, trackingField, hasVector);
37011
+ }
37012
+ return updates;
37013
+ });
37014
+ this.emit("vector:hooks-installed", {
37015
+ resource: resource.name,
37016
+ vectorField,
37017
+ trackingField,
37018
+ hooks: ["beforeInsert", "beforeUpdate"],
37019
+ timestamp: Date.now()
37020
+ });
37021
+ }
37022
+ /**
37023
+ * Check if data has a valid vector value for the given field
37024
+ *
37025
+ * @param {Object} data - Data object
37026
+ * @param {string} fieldPath - Field path (supports dot notation)
37027
+ * @returns {boolean} True if vector exists and is valid
37028
+ */
37029
+ hasVectorValue(data, fieldPath) {
37030
+ const value = this.getNestedValue(data, fieldPath);
37031
+ return value != null && Array.isArray(value) && value.length > 0;
37032
+ }
37033
+ /**
37034
+ * Check if object has a nested key
37035
+ *
37036
+ * @param {Object} obj - Object to check
37037
+ * @param {string} path - Dot-notation path
37038
+ * @returns {boolean} True if key exists
37039
+ */
37040
+ hasNestedKey(obj, path) {
37041
+ const parts = path.split(".");
37042
+ let current = obj;
37043
+ for (const part of parts) {
37044
+ if (current == null || typeof current !== "object") return false;
37045
+ if (!(part in current)) return false;
37046
+ current = current[part];
36894
37047
  }
37048
+ return true;
37049
+ }
37050
+ /**
37051
+ * Get nested value from object using dot notation
37052
+ *
37053
+ * @param {Object} obj - Object to traverse
37054
+ * @param {string} path - Dot-notation path
37055
+ * @returns {*} Value at path or undefined
37056
+ */
37057
+ getNestedValue(obj, path) {
37058
+ const parts = path.split(".");
37059
+ let current = obj;
37060
+ for (const part of parts) {
37061
+ if (current == null || typeof current !== "object") return void 0;
37062
+ current = current[part];
37063
+ }
37064
+ return current;
37065
+ }
37066
+ /**
37067
+ * Set nested value in object using dot notation
37068
+ *
37069
+ * @param {Object} obj - Object to modify
37070
+ * @param {string} path - Dot-notation path
37071
+ * @param {*} value - Value to set
37072
+ */
37073
+ setNestedValue(obj, path, value) {
37074
+ const parts = path.split(".");
37075
+ let current = obj;
37076
+ for (let i = 0; i < parts.length - 1; i++) {
37077
+ const part = parts[i];
37078
+ if (!(part in current) || typeof current[part] !== "object") {
37079
+ current[part] = {};
37080
+ }
37081
+ current = current[part];
37082
+ }
37083
+ current[parts[parts.length - 1]] = value;
37084
+ }
37085
+ /**
37086
+ * Get auto-created embedding partition for a vector field
37087
+ *
37088
+ * Returns partition configuration if an auto-partition exists for the given vector field.
37089
+ * Auto-partitions enable O(1) filtering to only records with embeddings.
37090
+ *
37091
+ * @param {Resource} resource - Resource instance
37092
+ * @param {string} vectorField - Vector field name
37093
+ * @returns {Object|null} Partition config or null
37094
+ */
37095
+ getAutoEmbeddingPartition(resource, vectorField) {
37096
+ if (!resource.config) return null;
37097
+ const partitionName = `byHas${this.capitalize(vectorField.replace(/\./g, "_"))}`;
37098
+ const trackingFieldName = `_has${this.capitalize(vectorField.replace(/\./g, "_"))}`;
37099
+ if (resource.config.partitions && resource.config.partitions[partitionName]) {
37100
+ return {
37101
+ partitionName,
37102
+ partitionValues: { [trackingFieldName]: true }
37103
+ };
37104
+ }
37105
+ return null;
36895
37106
  }
36896
37107
  /**
36897
37108
  * Auto-detect vector field from resource schema
@@ -37030,11 +37241,12 @@ class VectorPlugin extends Plugin {
37030
37241
  } else if (!vectorField) {
37031
37242
  vectorField = "vector";
37032
37243
  }
37033
- const {
37244
+ let {
37034
37245
  limit = 10,
37035
37246
  distanceMetric = this.config.distanceMetric,
37036
37247
  threshold = null,
37037
- partition = null
37248
+ partition = null,
37249
+ partitionValues = null
37038
37250
  } = options;
37039
37251
  const distanceFn = this.distanceFunctions[distanceMetric];
37040
37252
  if (!distanceFn) {
@@ -37050,31 +37262,61 @@ class VectorPlugin extends Plugin {
37050
37262
  });
37051
37263
  throw error;
37052
37264
  }
37265
+ if (!partition) {
37266
+ const autoPartition = this.getAutoEmbeddingPartition(resource, vectorField);
37267
+ if (autoPartition) {
37268
+ partition = autoPartition.partitionName;
37269
+ partitionValues = autoPartition.partitionValues;
37270
+ this._emitEvent("vector:auto-partition-used", {
37271
+ resource: resource.name,
37272
+ vectorField,
37273
+ partition,
37274
+ partitionValues,
37275
+ timestamp: Date.now()
37276
+ });
37277
+ }
37278
+ }
37053
37279
  this._emitEvent("vector:search-start", {
37054
37280
  resource: resource.name,
37055
37281
  vectorField,
37056
37282
  limit,
37057
37283
  distanceMetric,
37058
37284
  partition,
37285
+ partitionValues,
37059
37286
  threshold,
37060
37287
  queryDimensions: queryVector.length,
37061
37288
  timestamp: startTime
37062
37289
  });
37063
37290
  try {
37064
37291
  let allRecords;
37065
- if (partition) {
37292
+ if (partition && partitionValues) {
37066
37293
  this._emitEvent("vector:partition-filter", {
37067
37294
  resource: resource.name,
37068
37295
  partition,
37296
+ partitionValues,
37069
37297
  timestamp: Date.now()
37070
37298
  });
37071
- allRecords = await resource.list({ partition, partitionValues: partition });
37299
+ allRecords = await resource.list({ partition, partitionValues });
37072
37300
  } else {
37073
- allRecords = await resource.getAll();
37301
+ allRecords = resource.getAll ? await resource.getAll() : await resource.list();
37074
37302
  }
37075
37303
  const totalRecords = allRecords.length;
37076
37304
  let processedRecords = 0;
37077
37305
  let dimensionMismatches = 0;
37306
+ if (!partition && totalRecords > 1e3) {
37307
+ const warning = {
37308
+ resource: resource.name,
37309
+ operation: "vectorSearch",
37310
+ totalRecords,
37311
+ vectorField,
37312
+ recommendation: "Use partitions to filter data before vector search for better performance"
37313
+ };
37314
+ this._emitEvent("vector:performance-warning", warning);
37315
+ console.warn(`\u26A0\uFE0F VectorPlugin: Performing vectorSearch on ${totalRecords} records without partition filter`);
37316
+ console.warn(` Resource: '${resource.name}'`);
37317
+ console.warn(` Recommendation: Use partition parameter to reduce search space`);
37318
+ console.warn(` Example: resource.vectorSearch(vector, { partition: 'byCategory', partitionValues: { category: 'books' } })`);
37319
+ }
37078
37320
  const results = allRecords.filter((record) => record[vectorField] && Array.isArray(record[vectorField])).map((record, index) => {
37079
37321
  try {
37080
37322
  const distance = distanceFn(queryVector, record[vectorField]);
@@ -37158,10 +37400,11 @@ class VectorPlugin extends Plugin {
37158
37400
  } else if (!vectorField) {
37159
37401
  vectorField = "vector";
37160
37402
  }
37161
- const {
37403
+ let {
37162
37404
  k = 5,
37163
37405
  distanceMetric = this.config.distanceMetric,
37164
37406
  partition = null,
37407
+ partitionValues = null,
37165
37408
  ...kmeansOptions
37166
37409
  } = options;
37167
37410
  const distanceFn = this.distanceFunctions[distanceMetric];
@@ -37178,30 +37421,62 @@ class VectorPlugin extends Plugin {
37178
37421
  });
37179
37422
  throw error;
37180
37423
  }
37424
+ if (!partition) {
37425
+ const autoPartition = this.getAutoEmbeddingPartition(resource, vectorField);
37426
+ if (autoPartition) {
37427
+ partition = autoPartition.partitionName;
37428
+ partitionValues = autoPartition.partitionValues;
37429
+ this._emitEvent("vector:auto-partition-used", {
37430
+ resource: resource.name,
37431
+ vectorField,
37432
+ partition,
37433
+ partitionValues,
37434
+ timestamp: Date.now()
37435
+ });
37436
+ }
37437
+ }
37181
37438
  this._emitEvent("vector:cluster-start", {
37182
37439
  resource: resource.name,
37183
37440
  vectorField,
37184
37441
  k,
37185
37442
  distanceMetric,
37186
37443
  partition,
37444
+ partitionValues,
37187
37445
  maxIterations: kmeansOptions.maxIterations || 100,
37188
37446
  timestamp: startTime
37189
37447
  });
37190
37448
  try {
37191
37449
  let allRecords;
37192
- if (partition) {
37450
+ if (partition && partitionValues) {
37193
37451
  this._emitEvent("vector:partition-filter", {
37194
37452
  resource: resource.name,
37195
37453
  partition,
37454
+ partitionValues,
37196
37455
  timestamp: Date.now()
37197
37456
  });
37198
- allRecords = await resource.list({ partition, partitionValues: partition });
37457
+ allRecords = await resource.list({ partition, partitionValues });
37199
37458
  } else {
37200
- allRecords = await resource.getAll();
37459
+ allRecords = resource.getAll ? await resource.getAll() : await resource.list();
37201
37460
  }
37202
37461
  const recordsWithVectors = allRecords.filter(
37203
37462
  (record) => record[vectorField] && Array.isArray(record[vectorField])
37204
37463
  );
37464
+ if (!partition && allRecords.length > 1e3) {
37465
+ const warning = {
37466
+ resource: resource.name,
37467
+ operation: "cluster",
37468
+ totalRecords: allRecords.length,
37469
+ recordsWithVectors: recordsWithVectors.length,
37470
+ vectorField,
37471
+ recommendation: "Use partitions to filter data before clustering for better performance"
37472
+ };
37473
+ this._emitEvent("vector:performance-warning", warning);
37474
+ console.warn(`\u26A0\uFE0F VectorPlugin: Performing clustering on ${allRecords.length} records without partition filter`);
37475
+ console.warn(` Resource: '${resource.name}'`);
37476
+ console.warn(` Records with vectors: ${recordsWithVectors.length}`);
37477
+ console.warn(` Recommendation: Use partition parameter to reduce clustering space`);
37478
+ console.warn(` Example: resource.cluster({ k: 5, partition: 'byCategory', partitionValues: { category: 'books' } })`);
37479
+ }
37205
37480
  if (recordsWithVectors.length === 0) {
37206
37481
  const error = new VectorError("No vectors found in resource", {
37207
37482
  operation: "cluster",