s3db.js 4.1.14 → 5.0.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
@@ -247,7 +247,7 @@ var substr = 'ab'.substr(-1) === 'b' ?
247
247
 
248
248
  const idGenerator = nanoid.customAlphabet(nanoid.urlAlphabet, 22);
249
249
  const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
250
- nanoid.customAlphabet(passwordAlphabet, 12);
250
+ const passwordGenerator = nanoid.customAlphabet(passwordAlphabet, 12);
251
251
 
252
252
  var domain;
253
253
 
@@ -3450,10 +3450,31 @@ const ValidatorManager = new Proxy(Validator, {
3450
3450
  }
3451
3451
  });
3452
3452
 
3453
+ function toBase36(num) {
3454
+ return num.toString(36);
3455
+ }
3456
+ function generateBase36Mapping(keys) {
3457
+ const mapping = {};
3458
+ const reversedMapping = {};
3459
+ keys.forEach((key, index) => {
3460
+ const base36Key = toBase36(index);
3461
+ mapping[key] = base36Key;
3462
+ reversedMapping[base36Key] = key;
3463
+ });
3464
+ return { mapping, reversedMapping };
3465
+ }
3453
3466
  const SchemaActions = {
3454
3467
  trim: (value) => value.trim(),
3455
3468
  encrypt: (value, { passphrase }) => encrypt(value, passphrase),
3456
- decrypt: (value, { passphrase }) => decrypt(value, passphrase),
3469
+ decrypt: async (value, { passphrase }) => {
3470
+ try {
3471
+ const raw = await decrypt(value, passphrase);
3472
+ return raw;
3473
+ } catch (error) {
3474
+ console.warn(`Schema decrypt error: ${error}`, error);
3475
+ return value;
3476
+ }
3477
+ },
3457
3478
  toString: (value) => String(value),
3458
3479
  fromArray: (value, { separator }) => {
3459
3480
  if (value === null || value === void 0 || !Array.isArray(value)) {
@@ -3544,8 +3565,9 @@ class Schema {
3544
3565
  const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
3545
3566
  const objectKeys = this.extractObjectKeys(this.attributes);
3546
3567
  const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
3547
- this.reversedMap = { ...allKeys };
3548
- this.map = lodashEs.invert(this.reversedMap);
3568
+ const { mapping, reversedMapping } = generateBase36Mapping(allKeys);
3569
+ this.map = mapping;
3570
+ this.reversedMap = reversedMapping;
3549
3571
  }
3550
3572
  }
3551
3573
  defaultOptions() {
@@ -8618,7 +8640,6 @@ function calculateUTF8Bytes(str) {
8618
8640
  function calculateAttributeNamesSize(mappedObject) {
8619
8641
  let totalSize = 0;
8620
8642
  for (const key of Object.keys(mappedObject)) {
8621
- if (key === "_v") continue;
8622
8643
  totalSize += calculateUTF8Bytes(key);
8623
8644
  }
8624
8645
  return totalSize;
@@ -8662,6 +8683,30 @@ function calculateTotalSize(mappedObject) {
8662
8683
  const namesSize = calculateAttributeNamesSize(mappedObject);
8663
8684
  return valueTotal + namesSize;
8664
8685
  }
8686
+ function getSizeBreakdown(mappedObject) {
8687
+ const valueSizes = calculateAttributeSizes(mappedObject);
8688
+ const namesSize = calculateAttributeNamesSize(mappedObject);
8689
+ const valueTotal = Object.values(valueSizes).reduce((sum, size) => sum + size, 0);
8690
+ const total = valueTotal + namesSize;
8691
+ const sortedAttributes = Object.entries(valueSizes).sort(([, a], [, b]) => b - a).map(([key, size]) => ({
8692
+ attribute: key,
8693
+ size,
8694
+ percentage: (size / total * 100).toFixed(2) + "%"
8695
+ }));
8696
+ return {
8697
+ total,
8698
+ valueSizes,
8699
+ namesSize,
8700
+ valueTotal,
8701
+ breakdown: sortedAttributes,
8702
+ // Add detailed breakdown including names
8703
+ detailedBreakdown: {
8704
+ values: valueTotal,
8705
+ names: namesSize,
8706
+ total
8707
+ }
8708
+ };
8709
+ }
8665
8710
 
8666
8711
  const S3_METADATA_LIMIT_BYTES = 2048;
8667
8712
  async function handleInsert$3({ resource, data, mappedData }) {
@@ -8882,6 +8927,7 @@ function getBehavior(behaviorName) {
8882
8927
  }
8883
8928
  return behavior;
8884
8929
  }
8930
+ const AVAILABLE_BEHAVIORS = Object.keys(behaviors);
8885
8931
  const DEFAULT_BEHAVIOR = "user-management";
8886
8932
 
8887
8933
  class Resource extends EventEmitter {
@@ -8951,17 +8997,8 @@ ${validation.errors.join("\n")}`);
8951
8997
  partitions = {},
8952
8998
  paranoid = true,
8953
8999
  allNestedObjectsOptional = true,
8954
- hooks = {},
8955
- options = {}
9000
+ hooks = {}
8956
9001
  } = config;
8957
- const mergedOptions = {
8958
- cache: typeof options.cache === "boolean" ? options.cache : cache,
8959
- autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
8960
- timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
8961
- paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
8962
- allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
8963
- partitions: options.partitions || partitions || {}
8964
- };
8965
9002
  this.name = name;
8966
9003
  this.client = client;
8967
9004
  this.version = version;
@@ -8970,13 +9007,13 @@ ${validation.errors.join("\n")}`);
8970
9007
  this.parallelism = parallelism;
8971
9008
  this.passphrase = passphrase ?? "secret";
8972
9009
  this.config = {
8973
- cache: mergedOptions.cache,
9010
+ cache,
8974
9011
  hooks,
8975
- paranoid: mergedOptions.paranoid,
8976
- timestamps: mergedOptions.timestamps,
8977
- partitions: mergedOptions.partitions,
8978
- autoDecrypt: mergedOptions.autoDecrypt,
8979
- allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
9012
+ paranoid,
9013
+ timestamps,
9014
+ partitions,
9015
+ autoDecrypt,
9016
+ allNestedObjectsOptional
8980
9017
  };
8981
9018
  this.hooks = {
8982
9019
  preInsert: [],
@@ -8987,39 +9024,7 @@ ${validation.errors.join("\n")}`);
8987
9024
  afterDelete: []
8988
9025
  };
8989
9026
  this.attributes = attributes || {};
8990
- if (this.config.timestamps) {
8991
- this.attributes.createdAt = "string|optional";
8992
- this.attributes.updatedAt = "string|optional";
8993
- if (!this.config.partitions) {
8994
- this.config.partitions = {};
8995
- }
8996
- if (!this.config.partitions.byCreatedDate) {
8997
- this.config.partitions.byCreatedDate = {
8998
- fields: {
8999
- createdAt: "date|maxlength:10"
9000
- }
9001
- };
9002
- }
9003
- if (!this.config.partitions.byUpdatedDate) {
9004
- this.config.partitions.byUpdatedDate = {
9005
- fields: {
9006
- updatedAt: "date|maxlength:10"
9007
- }
9008
- };
9009
- }
9010
- }
9011
- this.schema = new Schema({
9012
- name,
9013
- attributes: this.attributes,
9014
- passphrase,
9015
- version: this.version,
9016
- options: {
9017
- autoDecrypt: this.config.autoDecrypt,
9018
- allNestedObjectsOptional: this.config.allNestedObjectsOptional
9019
- }
9020
- });
9021
- this.setupPartitionHooks();
9022
- this.validatePartitions();
9027
+ this.applyConfiguration();
9023
9028
  if (hooks) {
9024
9029
  for (const [event, hooksArr] of Object.entries(hooks)) {
9025
9030
  if (Array.isArray(hooksArr) && this.hooks[event]) {
@@ -9058,15 +9063,17 @@ ${validation.errors.join("\n")}`);
9058
9063
  return exported;
9059
9064
  }
9060
9065
  /**
9061
- * Update resource attributes and rebuild schema
9062
- * @param {Object} newAttributes - New attributes definition
9066
+ * Apply configuration settings (timestamps, partitions, hooks)
9067
+ * This method ensures that all configuration-dependent features are properly set up
9063
9068
  */
9064
- updateAttributes(newAttributes) {
9065
- const oldAttributes = this.attributes;
9066
- this.attributes = newAttributes;
9069
+ applyConfiguration() {
9067
9070
  if (this.config.timestamps) {
9068
- newAttributes.createdAt = "string|optional";
9069
- newAttributes.updatedAt = "string|optional";
9071
+ if (!this.attributes.createdAt) {
9072
+ this.attributes.createdAt = "string|optional";
9073
+ }
9074
+ if (!this.attributes.updatedAt) {
9075
+ this.attributes.updatedAt = "string|optional";
9076
+ }
9070
9077
  if (!this.config.partitions) {
9071
9078
  this.config.partitions = {};
9072
9079
  }
@@ -9085,9 +9092,10 @@ ${validation.errors.join("\n")}`);
9085
9092
  };
9086
9093
  }
9087
9094
  }
9095
+ this.setupPartitionHooks();
9088
9096
  this.schema = new Schema({
9089
9097
  name: this.name,
9090
- attributes: newAttributes,
9098
+ attributes: this.attributes,
9091
9099
  passphrase: this.passphrase,
9092
9100
  version: this.version,
9093
9101
  options: {
@@ -9095,8 +9103,16 @@ ${validation.errors.join("\n")}`);
9095
9103
  allNestedObjectsOptional: this.config.allNestedObjectsOptional
9096
9104
  }
9097
9105
  });
9098
- this.setupPartitionHooks();
9099
9106
  this.validatePartitions();
9107
+ }
9108
+ /**
9109
+ * Update resource attributes and rebuild schema
9110
+ * @param {Object} newAttributes - New attributes definition
9111
+ */
9112
+ updateAttributes(newAttributes) {
9113
+ const oldAttributes = this.attributes;
9114
+ this.attributes = newAttributes;
9115
+ this.applyConfiguration();
9100
9116
  return { oldAttributes, newAttributes };
9101
9117
  }
9102
9118
  /**
@@ -9184,7 +9200,7 @@ ${validation.errors.join("\n")}`);
9184
9200
  for (const fieldName of Object.keys(partitionDef.fields)) {
9185
9201
  if (!this.fieldExistsInAttributes(fieldName)) {
9186
9202
  throw new Error(
9187
- `Partition '${partitionName}' uses field '${fieldName}' which does not exist in resource version '${this.version}'. Available fields: ${currentAttributes.join(", ")}. This version of resource does not have support for this partition.`
9203
+ `Partition '${partitionName}' uses field '${fieldName}' which does not exist in resource attributes. Available fields: ${currentAttributes.join(", ")}.`
9188
9204
  );
9189
9205
  }
9190
9206
  }
@@ -9297,7 +9313,11 @@ ${validation.errors.join("\n")}`);
9297
9313
  if (partitionSegments.length === 0) {
9298
9314
  return null;
9299
9315
  }
9300
- return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${id}`);
9316
+ const finalId = id || data?.id;
9317
+ if (!finalId) {
9318
+ return null;
9319
+ }
9320
+ return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${finalId}`);
9301
9321
  }
9302
9322
  /**
9303
9323
  * Get nested field value from data object using dot notation
@@ -9526,12 +9546,12 @@ ${validation.errors.join("\n")}`);
9526
9546
  * console.log(updatedUser.updatedAt); // ISO timestamp
9527
9547
  */
9528
9548
  async update(id, attributes) {
9529
- const live = await this.get(id);
9549
+ const originalData = await this.get(id);
9530
9550
  if (this.config.timestamps) {
9531
9551
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9532
9552
  }
9533
9553
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
9534
- const attrs = lodashEs.merge(live, preProcessedData);
9554
+ const attrs = lodashEs.merge(originalData, preProcessedData);
9535
9555
  delete attrs.id;
9536
9556
  const { isValid, errors, data: validated } = await this.validate(attrs);
9537
9557
  if (!isValid) {
@@ -9542,6 +9562,9 @@ ${validation.errors.join("\n")}`);
9542
9562
  validation: errors
9543
9563
  });
9544
9564
  }
9565
+ const oldData = { ...originalData, id };
9566
+ const newData = { ...validated, id };
9567
+ await this.handlePartitionReferenceUpdates(oldData, newData);
9545
9568
  const mappedData = await this.schema.mapper(validated);
9546
9569
  const behaviorImpl = getBehavior(this.behavior);
9547
9570
  const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
@@ -9577,7 +9600,6 @@ ${validation.errors.join("\n")}`);
9577
9600
  });
9578
9601
  validated.id = id;
9579
9602
  await this.executeHooks("afterUpdate", validated);
9580
- await this.updatePartitionReferences(validated);
9581
9603
  this.emit("update", preProcessedData, validated);
9582
9604
  return validated;
9583
9605
  }
@@ -9864,23 +9886,104 @@ ${validation.errors.join("\n")}`);
9864
9886
  * });
9865
9887
  */
9866
9888
  async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9867
- if (!partition) {
9868
- const ids2 = await this.listIds({ partition, partitionValues });
9869
- let filteredIds2 = ids2.slice(offset);
9889
+ try {
9890
+ if (!partition) {
9891
+ let ids2 = [];
9892
+ try {
9893
+ ids2 = await this.listIds({ partition, partitionValues });
9894
+ } catch (listIdsError) {
9895
+ console.warn(`Failed to get list IDs:`, listIdsError.message);
9896
+ return [];
9897
+ }
9898
+ let filteredIds2 = ids2.slice(offset);
9899
+ if (limit) {
9900
+ filteredIds2 = filteredIds2.slice(0, limit);
9901
+ }
9902
+ const { results: results2, errors: errors2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9903
+ console.warn(`Failed to get resource ${id}:`, error.message);
9904
+ return null;
9905
+ }).process(async (id) => {
9906
+ try {
9907
+ return await this.get(id);
9908
+ } catch (error) {
9909
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9910
+ console.warn(`Decryption failed for ${id}, returning basic info`);
9911
+ return {
9912
+ id,
9913
+ _decryptionFailed: true,
9914
+ _error: error.message
9915
+ };
9916
+ }
9917
+ throw error;
9918
+ }
9919
+ });
9920
+ const validResults2 = results2.filter((item) => item !== null);
9921
+ this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9922
+ return validResults2;
9923
+ }
9924
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9925
+ console.warn(`Partition '${partition}' not found in resource '${this.name}'`);
9926
+ this.emit("list", { partition, partitionValues, count: 0, errors: 0 });
9927
+ return [];
9928
+ }
9929
+ const partitionDef = this.config.partitions[partition];
9930
+ const partitionSegments = [];
9931
+ const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9932
+ for (const [fieldName, rule] of sortedFields) {
9933
+ const value = partitionValues[fieldName];
9934
+ if (value !== void 0 && value !== null) {
9935
+ const transformedValue = this.applyPartitionRule(value, rule);
9936
+ partitionSegments.push(`${fieldName}=${transformedValue}`);
9937
+ }
9938
+ }
9939
+ let prefix;
9940
+ if (partitionSegments.length > 0) {
9941
+ prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9942
+ } else {
9943
+ prefix = `resource=${this.name}/partition=${partition}`;
9944
+ }
9945
+ let keys = [];
9946
+ try {
9947
+ keys = await this.client.getAllKeys({ prefix });
9948
+ } catch (getKeysError) {
9949
+ console.warn(`Failed to get partition keys:`, getKeysError.message);
9950
+ return [];
9951
+ }
9952
+ const ids = keys.map((key) => {
9953
+ const parts = key.split("/");
9954
+ const idPart = parts.find((part) => part.startsWith("id="));
9955
+ return idPart ? idPart.replace("id=", "") : null;
9956
+ }).filter(Boolean);
9957
+ let filteredIds = ids.slice(offset);
9870
9958
  if (limit) {
9871
- filteredIds2 = filteredIds2.slice(0, limit);
9959
+ filteredIds = filteredIds.slice(0, limit);
9872
9960
  }
9873
- const { results: results2, errors: errors2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9874
- console.warn(`Failed to get resource ${id}:`, error.message);
9961
+ const { results, errors } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9962
+ console.warn(`Failed to get partition resource ${id}:`, error.message);
9875
9963
  return null;
9876
9964
  }).process(async (id) => {
9877
9965
  try {
9878
- return await this.get(id);
9966
+ const keyForId = keys.find((key) => key.includes(`id=${id}`));
9967
+ if (!keyForId) {
9968
+ throw new Error(`Partition key not found for ID ${id}`);
9969
+ }
9970
+ const keyParts = keyForId.split("/");
9971
+ const actualPartitionValues = {};
9972
+ for (const [fieldName, rule] of sortedFields) {
9973
+ const fieldPart = keyParts.find((part) => part.startsWith(`${fieldName}=`));
9974
+ if (fieldPart) {
9975
+ const value = fieldPart.replace(`${fieldName}=`, "");
9976
+ actualPartitionValues[fieldName] = value;
9977
+ }
9978
+ }
9979
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues: actualPartitionValues });
9879
9980
  } catch (error) {
9880
9981
  if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9881
- console.warn(`Decryption failed for ${id}, returning basic info`);
9982
+ console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9882
9983
  return {
9883
9984
  id,
9985
+ _partition: partition,
9986
+ _partitionValues: partitionValues,
9884
9987
  _decryptionFailed: true,
9885
9988
  _error: error.message
9886
9989
  };
@@ -9888,62 +9991,19 @@ ${validation.errors.join("\n")}`);
9888
9991
  throw error;
9889
9992
  }
9890
9993
  });
9891
- const validResults2 = results2.filter((item) => item !== null);
9892
- this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9893
- return validResults2;
9894
- }
9895
- if (!this.config.partitions || !this.config.partitions[partition]) {
9896
- throw new Error(`Partition '${partition}' not found`);
9897
- }
9898
- const partitionDef = this.config.partitions[partition];
9899
- const partitionSegments = [];
9900
- const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9901
- for (const [fieldName, rule] of sortedFields) {
9902
- const value = partitionValues[fieldName];
9903
- if (value !== void 0 && value !== null) {
9904
- const transformedValue = this.applyPartitionRule(value, rule);
9905
- partitionSegments.push(`${fieldName}=${transformedValue}`);
9994
+ const validResults = results.filter((item) => item !== null);
9995
+ this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9996
+ return validResults;
9997
+ } catch (error) {
9998
+ if (error.message.includes("Partition '") && error.message.includes("' not found")) {
9999
+ console.warn(`Partition error in list method:`, error.message);
10000
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10001
+ return [];
9906
10002
  }
10003
+ console.error(`Critical error in list method:`, error.message);
10004
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10005
+ return [];
9907
10006
  }
9908
- let prefix;
9909
- if (partitionSegments.length > 0) {
9910
- prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9911
- } else {
9912
- prefix = `resource=${this.name}/partition=${partition}`;
9913
- }
9914
- const keys = await this.client.getAllKeys({ prefix });
9915
- const ids = keys.map((key) => {
9916
- const parts = key.split("/");
9917
- const idPart = parts.find((part) => part.startsWith("id="));
9918
- return idPart ? idPart.replace("id=", "") : null;
9919
- }).filter(Boolean);
9920
- let filteredIds = ids.slice(offset);
9921
- if (limit) {
9922
- filteredIds = filteredIds.slice(0, limit);
9923
- }
9924
- const { results, errors } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9925
- console.warn(`Failed to get partition resource ${id}:`, error.message);
9926
- return null;
9927
- }).process(async (id) => {
9928
- try {
9929
- return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9930
- } catch (error) {
9931
- if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9932
- console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9933
- return {
9934
- id,
9935
- _partition: partition,
9936
- _partitionValues: partitionValues,
9937
- _decryptionFailed: true,
9938
- _error: error.message
9939
- };
9940
- }
9941
- throw error;
9942
- }
9943
- });
9944
- const validResults = results.filter((item) => item !== null);
9945
- this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9946
- return validResults;
9947
10007
  }
9948
10008
  /**
9949
10009
  * Get multiple resources by their IDs
@@ -10050,36 +10110,67 @@ ${validation.errors.join("\n")}`);
10050
10110
  * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
10051
10111
  */
10052
10112
  async page({ offset = 0, size = 100, partition = null, partitionValues = {}, skipCount = false } = {}) {
10053
- let totalItems = null;
10054
- let totalPages = null;
10055
- if (!skipCount) {
10056
- totalItems = await this.count({ partition, partitionValues });
10057
- totalPages = Math.ceil(totalItems / size);
10058
- }
10059
- const page = Math.floor(offset / size);
10060
- const items = await this.list({
10061
- partition,
10062
- partitionValues,
10063
- limit: size,
10064
- offset
10065
- });
10066
- const result = {
10067
- items,
10068
- totalItems,
10069
- page,
10070
- pageSize: size,
10071
- totalPages,
10072
- // Add additional metadata for debugging
10073
- _debug: {
10074
- requestedSize: size,
10075
- requestedOffset: offset,
10076
- actualItemsReturned: items.length,
10077
- skipCount,
10078
- hasTotalItems: totalItems !== null
10113
+ try {
10114
+ let totalItems = null;
10115
+ let totalPages = null;
10116
+ if (!skipCount) {
10117
+ try {
10118
+ totalItems = await this.count({ partition, partitionValues });
10119
+ totalPages = Math.ceil(totalItems / size);
10120
+ } catch (countError) {
10121
+ console.warn(`Failed to get count for page:`, countError.message);
10122
+ totalItems = null;
10123
+ totalPages = null;
10124
+ }
10079
10125
  }
10080
- };
10081
- this.emit("page", result);
10082
- return result;
10126
+ const page = Math.floor(offset / size);
10127
+ let items = [];
10128
+ try {
10129
+ items = await this.list({
10130
+ partition,
10131
+ partitionValues,
10132
+ limit: size,
10133
+ offset
10134
+ });
10135
+ } catch (listError) {
10136
+ console.warn(`Failed to get items for page:`, listError.message);
10137
+ items = [];
10138
+ }
10139
+ const result = {
10140
+ items,
10141
+ totalItems,
10142
+ page,
10143
+ pageSize: size,
10144
+ totalPages,
10145
+ // Add additional metadata for debugging
10146
+ _debug: {
10147
+ requestedSize: size,
10148
+ requestedOffset: offset,
10149
+ actualItemsReturned: items.length,
10150
+ skipCount,
10151
+ hasTotalItems: totalItems !== null
10152
+ }
10153
+ };
10154
+ this.emit("page", result);
10155
+ return result;
10156
+ } catch (error) {
10157
+ console.error(`Critical error in page method:`, error.message);
10158
+ return {
10159
+ items: [],
10160
+ totalItems: null,
10161
+ page: Math.floor(offset / size),
10162
+ pageSize: size,
10163
+ totalPages: null,
10164
+ _debug: {
10165
+ requestedSize: size,
10166
+ requestedOffset: offset,
10167
+ actualItemsReturned: 0,
10168
+ skipCount,
10169
+ hasTotalItems: false,
10170
+ error: error.message
10171
+ }
10172
+ };
10173
+ }
10083
10174
  }
10084
10175
  readable() {
10085
10176
  const stream = new ResourceReader({ resource: this });
@@ -10372,7 +10463,118 @@ ${validation.errors.join("\n")}`);
10372
10463
  return results.slice(0, limit);
10373
10464
  }
10374
10465
  /**
10375
- * Update partition objects to keep them in sync
10466
+ * Handle partition reference updates with change detection
10467
+ * @param {Object} oldData - Original object data before update
10468
+ * @param {Object} newData - Updated object data
10469
+ */
10470
+ async handlePartitionReferenceUpdates(oldData, newData) {
10471
+ const partitions = this.config.partitions;
10472
+ if (!partitions || Object.keys(partitions).length === 0) {
10473
+ return;
10474
+ }
10475
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10476
+ try {
10477
+ await this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData);
10478
+ } catch (error) {
10479
+ console.warn(`Failed to update partition references for ${partitionName}:`, error.message);
10480
+ }
10481
+ }
10482
+ const id = newData.id || oldData.id;
10483
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10484
+ const prefix = `resource=${this.name}/partition=${partitionName}`;
10485
+ let allKeys = [];
10486
+ try {
10487
+ allKeys = await this.client.getAllKeys({ prefix });
10488
+ } catch (error) {
10489
+ console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, error.message);
10490
+ continue;
10491
+ }
10492
+ const validKey = this.getPartitionKey({ partitionName, id, data: newData });
10493
+ for (const key of allKeys) {
10494
+ if (key.endsWith(`/id=${id}`) && key !== validKey) {
10495
+ try {
10496
+ await this.client.deleteObject(key);
10497
+ } catch (error) {
10498
+ console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, error.message);
10499
+ }
10500
+ }
10501
+ }
10502
+ }
10503
+ }
10504
+ /**
10505
+ * Handle partition reference update for a specific partition
10506
+ * @param {string} partitionName - Name of the partition
10507
+ * @param {Object} partition - Partition definition
10508
+ * @param {Object} oldData - Original object data before update
10509
+ * @param {Object} newData - Updated object data
10510
+ */
10511
+ async handlePartitionReferenceUpdate(partitionName, partition, oldData, newData) {
10512
+ const id = newData.id || oldData.id;
10513
+ const oldPartitionKey = this.getPartitionKey({ partitionName, id, data: oldData });
10514
+ const newPartitionKey = this.getPartitionKey({ partitionName, id, data: newData });
10515
+ if (oldPartitionKey !== newPartitionKey) {
10516
+ if (oldPartitionKey) {
10517
+ try {
10518
+ await this.client.deleteObject(oldPartitionKey);
10519
+ } catch (error) {
10520
+ console.warn(`Old partition object could not be deleted for ${partitionName}:`, error.message);
10521
+ }
10522
+ }
10523
+ if (newPartitionKey) {
10524
+ try {
10525
+ const mappedData = await this.schema.mapper(newData);
10526
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10527
+ const behaviorImpl = getBehavior(this.behavior);
10528
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10529
+ resource: this,
10530
+ id,
10531
+ data: newData,
10532
+ mappedData
10533
+ });
10534
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10535
+ const partitionMetadata = {
10536
+ ...processedMetadata,
10537
+ _version: this.version
10538
+ };
10539
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10540
+ await this.client.putObject({
10541
+ key: newPartitionKey,
10542
+ metadata: partitionMetadata,
10543
+ body
10544
+ });
10545
+ } catch (error) {
10546
+ console.warn(`New partition object could not be created for ${partitionName}:`, error.message);
10547
+ }
10548
+ }
10549
+ } else if (newPartitionKey) {
10550
+ try {
10551
+ const mappedData = await this.schema.mapper(newData);
10552
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10553
+ const behaviorImpl = getBehavior(this.behavior);
10554
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10555
+ resource: this,
10556
+ id,
10557
+ data: newData,
10558
+ mappedData
10559
+ });
10560
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10561
+ const partitionMetadata = {
10562
+ ...processedMetadata,
10563
+ _version: this.version
10564
+ };
10565
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10566
+ await this.client.putObject({
10567
+ key: newPartitionKey,
10568
+ metadata: partitionMetadata,
10569
+ body
10570
+ });
10571
+ } catch (error) {
10572
+ console.warn(`Partition object could not be updated for ${partitionName}:`, error.message);
10573
+ }
10574
+ }
10575
+ }
10576
+ /**
10577
+ * Update partition objects to keep them in sync (legacy method for backward compatibility)
10376
10578
  * @param {Object} data - Updated object data
10377
10579
  */
10378
10580
  async updatePartitionReferences(data) {
@@ -10381,6 +10583,10 @@ ${validation.errors.join("\n")}`);
10381
10583
  return;
10382
10584
  }
10383
10585
  for (const [partitionName, partition] of Object.entries(partitions)) {
10586
+ if (!partition || !partition.fields || typeof partition.fields !== "object") {
10587
+ console.warn(`Skipping invalid partition '${partitionName}' in resource '${this.name}'`);
10588
+ continue;
10589
+ }
10384
10590
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
10385
10591
  if (partitionKey) {
10386
10592
  const mappedData = await this.schema.mapper(data);
@@ -10479,6 +10685,7 @@ ${validation.errors.join("\n")}`);
10479
10685
  if (request.VersionId) data._versionId = request.VersionId;
10480
10686
  if (request.Expiration) data._expiresAt = request.Expiration;
10481
10687
  data._definitionHash = this.getDefinitionHash();
10688
+ if (data.undefined !== void 0) delete data.undefined;
10482
10689
  this.emit("getFromPartition", data);
10483
10690
  return data;
10484
10691
  }
@@ -10582,7 +10789,7 @@ class Database extends EventEmitter {
10582
10789
  this.version = "1";
10583
10790
  this.s3dbVersion = (() => {
10584
10791
  try {
10585
- return true ? "4.1.10" : "latest";
10792
+ return true ? "5.0.0" : "latest";
10586
10793
  } catch (e) {
10587
10794
  return "latest";
10588
10795
  }
@@ -10597,14 +10804,24 @@ class Database extends EventEmitter {
10597
10804
  this.passphrase = options.passphrase || "secret";
10598
10805
  let connectionString = options.connectionString;
10599
10806
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
10600
- connectionString = ConnectionString.buildFromParams({
10601
- bucket: options.bucket,
10602
- region: options.region,
10603
- accessKeyId: options.accessKeyId,
10604
- secretAccessKey: options.secretAccessKey,
10605
- endpoint: options.endpoint,
10606
- forcePathStyle: options.forcePathStyle
10607
- });
10807
+ const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
10808
+ if (endpoint) {
10809
+ const url = new URL(endpoint);
10810
+ if (accessKeyId) url.username = encodeURIComponent(accessKeyId);
10811
+ if (secretAccessKey) url.password = encodeURIComponent(secretAccessKey);
10812
+ url.pathname = `/${bucket || "s3db"}`;
10813
+ if (forcePathStyle) {
10814
+ url.searchParams.set("forcePathStyle", "true");
10815
+ }
10816
+ connectionString = url.toString();
10817
+ } else if (accessKeyId && secretAccessKey) {
10818
+ const params = new URLSearchParams();
10819
+ params.set("region", region || "us-east-1");
10820
+ if (forcePathStyle) {
10821
+ params.set("forcePathStyle", "true");
10822
+ }
10823
+ connectionString = `s3://${encodeURIComponent(accessKeyId)}:${encodeURIComponent(secretAccessKey)}@${bucket || "s3db"}?${params.toString()}`;
10824
+ }
10608
10825
  }
10609
10826
  this.client = options.client || new Client({
10610
10827
  verbose: this.verbose,
@@ -10640,12 +10857,12 @@ class Database extends EventEmitter {
10640
10857
  passphrase: this.passphrase,
10641
10858
  observers: [this],
10642
10859
  cache: this.cache,
10643
- timestamps: versionData.options?.timestamps || false,
10644
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
10645
- paranoid: versionData.options?.paranoid !== false,
10646
- allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
10647
- autoDecrypt: versionData.options?.autoDecrypt !== false,
10648
- hooks: {}
10860
+ timestamps: versionData.timestamps !== void 0 ? versionData.timestamps : false,
10861
+ partitions: resourceMetadata.partitions || versionData.partitions || {},
10862
+ paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
10863
+ allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
10864
+ autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
10865
+ hooks: versionData.hooks || {}
10649
10866
  });
10650
10867
  }
10651
10868
  }
@@ -10783,15 +11000,14 @@ class Database extends EventEmitter {
10783
11000
  [version]: {
10784
11001
  hash: definitionHash,
10785
11002
  attributes: resourceDef.attributes,
10786
- options: {
10787
- timestamps: resource.config.timestamps,
10788
- partitions: resource.config.partitions,
10789
- paranoid: resource.config.paranoid,
10790
- allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
10791
- autoDecrypt: resource.config.autoDecrypt,
10792
- cache: resource.config.cache
10793
- },
10794
11003
  behavior: resourceDef.behavior || "user-management",
11004
+ timestamps: resource.config.timestamps,
11005
+ partitions: resource.config.partitions,
11006
+ paranoid: resource.config.paranoid,
11007
+ allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
11008
+ autoDecrypt: resource.config.autoDecrypt,
11009
+ cache: resource.config.cache,
11010
+ hooks: resource.config.hooks,
10795
11011
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10796
11012
  }
10797
11013
  }
@@ -10826,73 +11042,58 @@ class Database extends EventEmitter {
10826
11042
  }
10827
11043
  /**
10828
11044
  * Check if a resource exists with the same definition hash
10829
- * @param {string} name - Resource name
10830
- * @param {Object} attributes - Resource attributes
10831
- * @param {Object} options - Resource options
10832
- * @param {string} behavior - Resource behavior
10833
- * @returns {Object} Object with exists flag and hash information
11045
+ * @param {Object} config - Resource configuration
11046
+ * @param {string} config.name - Resource name
11047
+ * @param {Object} config.attributes - Resource attributes
11048
+ * @param {string} [config.behavior] - Resource behavior
11049
+ * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
11050
+ * @returns {Object} Result with exists and hash information
10834
11051
  */
10835
- resourceExistsWithSameHash({ name, attributes, options = {}, behavior = "user-management" }) {
11052
+ resourceExistsWithSameHash({ name, attributes, behavior = "user-management", options = {} }) {
10836
11053
  if (!this.resources[name]) {
10837
11054
  return { exists: false, sameHash: false, hash: null };
10838
11055
  }
10839
- const tempResource = new Resource({
11056
+ const existingResource = this.resources[name];
11057
+ const existingHash = this.generateDefinitionHash(existingResource.export());
11058
+ const mockResource = new Resource({
10840
11059
  name,
10841
11060
  attributes,
10842
11061
  behavior,
10843
- observers: [],
10844
11062
  client: this.client,
10845
- version: "temp",
11063
+ version: existingResource.version,
10846
11064
  passphrase: this.passphrase,
10847
- cache: this.cache,
10848
11065
  ...options
10849
11066
  });
10850
- const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10851
- const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
11067
+ const newHash = this.generateDefinitionHash(mockResource.export());
10852
11068
  return {
10853
11069
  exists: true,
10854
- sameHash: newHash === existingHash,
11070
+ sameHash: existingHash === newHash,
10855
11071
  hash: newHash,
10856
11072
  existingHash
10857
11073
  };
10858
11074
  }
10859
- /**
10860
- * Create a resource only if it doesn't exist with the same definition hash
10861
- * @param {Object} params - Resource parameters
10862
- * @param {string} params.name - Resource name
10863
- * @param {Object} params.attributes - Resource attributes
10864
- * @param {Object} params.options - Resource options
10865
- * @param {string} params.behavior - Resource behavior
10866
- * @returns {Object} Object with resource and created flag
10867
- */
10868
- async createResourceIfNotExists({ name, attributes, options = {}, behavior = "user-management" }) {
10869
- const alreadyExists = !!this.resources[name];
10870
- const hashCheck = this.resourceExistsWithSameHash({ name, attributes, options, behavior });
10871
- if (hashCheck.exists && hashCheck.sameHash) {
10872
- return {
10873
- resource: this.resources[name],
10874
- created: false,
10875
- reason: "Resource already exists with same definition hash"
10876
- };
10877
- }
10878
- const resource = await this.createResource({ name, attributes, options, behavior });
10879
- return {
10880
- resource,
10881
- created: !alreadyExists,
10882
- reason: alreadyExists ? "Resource updated with new definition" : "New resource created"
10883
- };
10884
- }
10885
- async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
11075
+ async createResource({ name, attributes, behavior = "user-management", hooks, ...config }) {
10886
11076
  if (this.resources[name]) {
10887
11077
  const existingResource = this.resources[name];
10888
11078
  Object.assign(existingResource.config, {
10889
11079
  cache: this.cache,
10890
- ...options
11080
+ ...config
10891
11081
  });
10892
11082
  if (behavior) {
10893
11083
  existingResource.behavior = behavior;
10894
11084
  }
10895
11085
  existingResource.updateAttributes(attributes);
11086
+ if (hooks) {
11087
+ for (const [event, hooksArr] of Object.entries(hooks)) {
11088
+ if (Array.isArray(hooksArr) && existingResource.hooks[event]) {
11089
+ for (const fn of hooksArr) {
11090
+ if (typeof fn === "function") {
11091
+ existingResource.hooks[event].push(fn.bind(existingResource));
11092
+ }
11093
+ }
11094
+ }
11095
+ }
11096
+ }
10896
11097
  const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
10897
11098
  const existingMetadata2 = this.savedMetadata?.resources?.[name];
10898
11099
  const currentVersion = existingMetadata2?.currentVersion || "v0";
@@ -10914,7 +11115,8 @@ class Database extends EventEmitter {
10914
11115
  version,
10915
11116
  passphrase: this.passphrase,
10916
11117
  cache: this.cache,
10917
- ...options
11118
+ hooks,
11119
+ ...config
10918
11120
  });
10919
11121
  this.resources[name] = resource;
10920
11122
  await this.uploadMetadataFile();
@@ -17616,6 +17818,7 @@ class CachePlugin extends Plugin {
17616
17818
  }
17617
17819
  }
17618
17820
 
17821
+ exports.AVAILABLE_BEHAVIORS = AVAILABLE_BEHAVIORS;
17619
17822
  exports.AuthenticationError = AuthenticationError;
17620
17823
  exports.BaseError = BaseError;
17621
17824
  exports.Cache = Cache;
@@ -17623,6 +17826,7 @@ exports.CachePlugin = CachePlugin;
17623
17826
  exports.Client = Client;
17624
17827
  exports.ConnectionString = ConnectionString;
17625
17828
  exports.CostsPlugin = CostsPlugin;
17829
+ exports.DEFAULT_BEHAVIOR = DEFAULT_BEHAVIOR;
17626
17830
  exports.Database = Database;
17627
17831
  exports.DatabaseError = DatabaseError;
17628
17832
  exports.EncryptionError = EncryptionError;
@@ -17636,25 +17840,35 @@ exports.NotFound = NotFound;
17636
17840
  exports.PermissionError = PermissionError;
17637
17841
  exports.Plugin = Plugin;
17638
17842
  exports.PluginObject = PluginObject;
17843
+ exports.Resource = Resource;
17639
17844
  exports.ResourceIdsPageReader = ResourceIdsPageReader;
17640
17845
  exports.ResourceIdsReader = ResourceIdsReader;
17641
17846
  exports.ResourceNotFound = ResourceNotFound;
17642
- exports.ResourceNotFoundError = ResourceNotFound;
17643
17847
  exports.ResourceReader = ResourceReader;
17644
17848
  exports.ResourceWriter = ResourceWriter;
17645
17849
  exports.S3Cache = S3Cache;
17646
- exports.S3DB = S3db;
17647
17850
  exports.S3DBError = S3DBError;
17648
17851
  exports.S3_DEFAULT_ENDPOINT = S3_DEFAULT_ENDPOINT;
17649
17852
  exports.S3_DEFAULT_REGION = S3_DEFAULT_REGION;
17650
17853
  exports.S3db = S3db;
17651
- exports.S3dbError = S3DBError;
17854
+ exports.Schema = Schema;
17855
+ exports.SchemaActions = SchemaActions;
17652
17856
  exports.UnknownError = UnknownError;
17653
17857
  exports.ValidationError = ValidationError;
17654
17858
  exports.Validator = Validator;
17655
17859
  exports.ValidatorManager = ValidatorManager;
17860
+ exports.behaviors = behaviors;
17861
+ exports.calculateAttributeNamesSize = calculateAttributeNamesSize;
17862
+ exports.calculateAttributeSizes = calculateAttributeSizes;
17863
+ exports.calculateTotalSize = calculateTotalSize;
17864
+ exports.calculateUTF8Bytes = calculateUTF8Bytes;
17656
17865
  exports.decrypt = decrypt;
17657
17866
  exports.default = S3db;
17658
17867
  exports.encrypt = encrypt;
17868
+ exports.getBehavior = getBehavior;
17869
+ exports.getSizeBreakdown = getSizeBreakdown;
17870
+ exports.idGenerator = idGenerator;
17871
+ exports.passwordGenerator = passwordGenerator;
17659
17872
  exports.sha256 = sha256;
17660
17873
  exports.streamToString = streamToString;
17874
+ exports.transformValue = transformValue;