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.iife.js CHANGED
@@ -237,7 +237,7 @@ var S3DB = (function (exports, nanoid, lodashEs, promisePool, clientS3, crypto,
237
237
 
238
238
  const idGenerator = nanoid.customAlphabet(nanoid.urlAlphabet, 22);
239
239
  const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
240
- nanoid.customAlphabet(passwordAlphabet, 12);
240
+ const passwordGenerator = nanoid.customAlphabet(passwordAlphabet, 12);
241
241
 
242
242
  var domain;
243
243
 
@@ -3440,10 +3440,31 @@ ${JSON.stringify(validation, null, 2)}`, {
3440
3440
  }
3441
3441
  });
3442
3442
 
3443
+ function toBase36(num) {
3444
+ return num.toString(36);
3445
+ }
3446
+ function generateBase36Mapping(keys) {
3447
+ const mapping = {};
3448
+ const reversedMapping = {};
3449
+ keys.forEach((key, index) => {
3450
+ const base36Key = toBase36(index);
3451
+ mapping[key] = base36Key;
3452
+ reversedMapping[base36Key] = key;
3453
+ });
3454
+ return { mapping, reversedMapping };
3455
+ }
3443
3456
  const SchemaActions = {
3444
3457
  trim: (value) => value.trim(),
3445
3458
  encrypt: (value, { passphrase }) => encrypt(value, passphrase),
3446
- decrypt: (value, { passphrase }) => decrypt(value, passphrase),
3459
+ decrypt: async (value, { passphrase }) => {
3460
+ try {
3461
+ const raw = await decrypt(value, passphrase);
3462
+ return raw;
3463
+ } catch (error) {
3464
+ console.warn(`Schema decrypt error: ${error}`, error);
3465
+ return value;
3466
+ }
3467
+ },
3447
3468
  toString: (value) => String(value),
3448
3469
  fromArray: (value, { separator }) => {
3449
3470
  if (value === null || value === void 0 || !Array.isArray(value)) {
@@ -3534,8 +3555,9 @@ ${JSON.stringify(validation, null, 2)}`, {
3534
3555
  const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
3535
3556
  const objectKeys = this.extractObjectKeys(this.attributes);
3536
3557
  const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
3537
- this.reversedMap = { ...allKeys };
3538
- this.map = lodashEs.invert(this.reversedMap);
3558
+ const { mapping, reversedMapping } = generateBase36Mapping(allKeys);
3559
+ this.map = mapping;
3560
+ this.reversedMap = reversedMapping;
3539
3561
  }
3540
3562
  }
3541
3563
  defaultOptions() {
@@ -8608,7 +8630,6 @@ ${JSON.stringify(validation, null, 2)}`, {
8608
8630
  function calculateAttributeNamesSize(mappedObject) {
8609
8631
  let totalSize = 0;
8610
8632
  for (const key of Object.keys(mappedObject)) {
8611
- if (key === "_v") continue;
8612
8633
  totalSize += calculateUTF8Bytes(key);
8613
8634
  }
8614
8635
  return totalSize;
@@ -8652,6 +8673,30 @@ ${JSON.stringify(validation, null, 2)}`, {
8652
8673
  const namesSize = calculateAttributeNamesSize(mappedObject);
8653
8674
  return valueTotal + namesSize;
8654
8675
  }
8676
+ function getSizeBreakdown(mappedObject) {
8677
+ const valueSizes = calculateAttributeSizes(mappedObject);
8678
+ const namesSize = calculateAttributeNamesSize(mappedObject);
8679
+ const valueTotal = Object.values(valueSizes).reduce((sum, size) => sum + size, 0);
8680
+ const total = valueTotal + namesSize;
8681
+ const sortedAttributes = Object.entries(valueSizes).sort(([, a], [, b]) => b - a).map(([key, size]) => ({
8682
+ attribute: key,
8683
+ size,
8684
+ percentage: (size / total * 100).toFixed(2) + "%"
8685
+ }));
8686
+ return {
8687
+ total,
8688
+ valueSizes,
8689
+ namesSize,
8690
+ valueTotal,
8691
+ breakdown: sortedAttributes,
8692
+ // Add detailed breakdown including names
8693
+ detailedBreakdown: {
8694
+ values: valueTotal,
8695
+ names: namesSize,
8696
+ total
8697
+ }
8698
+ };
8699
+ }
8655
8700
 
8656
8701
  const S3_METADATA_LIMIT_BYTES = 2048;
8657
8702
  async function handleInsert$3({ resource, data, mappedData }) {
@@ -8872,6 +8917,7 @@ ${JSON.stringify(validation, null, 2)}`, {
8872
8917
  }
8873
8918
  return behavior;
8874
8919
  }
8920
+ const AVAILABLE_BEHAVIORS = Object.keys(behaviors);
8875
8921
  const DEFAULT_BEHAVIOR = "user-management";
8876
8922
 
8877
8923
  class Resource extends EventEmitter {
@@ -8941,17 +8987,8 @@ ${validation.errors.join("\n")}`);
8941
8987
  partitions = {},
8942
8988
  paranoid = true,
8943
8989
  allNestedObjectsOptional = true,
8944
- hooks = {},
8945
- options = {}
8990
+ hooks = {}
8946
8991
  } = config;
8947
- const mergedOptions = {
8948
- cache: typeof options.cache === "boolean" ? options.cache : cache,
8949
- autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
8950
- timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
8951
- paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
8952
- allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
8953
- partitions: options.partitions || partitions || {}
8954
- };
8955
8992
  this.name = name;
8956
8993
  this.client = client;
8957
8994
  this.version = version;
@@ -8960,13 +8997,13 @@ ${validation.errors.join("\n")}`);
8960
8997
  this.parallelism = parallelism;
8961
8998
  this.passphrase = passphrase ?? "secret";
8962
8999
  this.config = {
8963
- cache: mergedOptions.cache,
9000
+ cache,
8964
9001
  hooks,
8965
- paranoid: mergedOptions.paranoid,
8966
- timestamps: mergedOptions.timestamps,
8967
- partitions: mergedOptions.partitions,
8968
- autoDecrypt: mergedOptions.autoDecrypt,
8969
- allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
9002
+ paranoid,
9003
+ timestamps,
9004
+ partitions,
9005
+ autoDecrypt,
9006
+ allNestedObjectsOptional
8970
9007
  };
8971
9008
  this.hooks = {
8972
9009
  preInsert: [],
@@ -8977,39 +9014,7 @@ ${validation.errors.join("\n")}`);
8977
9014
  afterDelete: []
8978
9015
  };
8979
9016
  this.attributes = attributes || {};
8980
- if (this.config.timestamps) {
8981
- this.attributes.createdAt = "string|optional";
8982
- this.attributes.updatedAt = "string|optional";
8983
- if (!this.config.partitions) {
8984
- this.config.partitions = {};
8985
- }
8986
- if (!this.config.partitions.byCreatedDate) {
8987
- this.config.partitions.byCreatedDate = {
8988
- fields: {
8989
- createdAt: "date|maxlength:10"
8990
- }
8991
- };
8992
- }
8993
- if (!this.config.partitions.byUpdatedDate) {
8994
- this.config.partitions.byUpdatedDate = {
8995
- fields: {
8996
- updatedAt: "date|maxlength:10"
8997
- }
8998
- };
8999
- }
9000
- }
9001
- this.schema = new Schema({
9002
- name,
9003
- attributes: this.attributes,
9004
- passphrase,
9005
- version: this.version,
9006
- options: {
9007
- autoDecrypt: this.config.autoDecrypt,
9008
- allNestedObjectsOptional: this.config.allNestedObjectsOptional
9009
- }
9010
- });
9011
- this.setupPartitionHooks();
9012
- this.validatePartitions();
9017
+ this.applyConfiguration();
9013
9018
  if (hooks) {
9014
9019
  for (const [event, hooksArr] of Object.entries(hooks)) {
9015
9020
  if (Array.isArray(hooksArr) && this.hooks[event]) {
@@ -9048,15 +9053,17 @@ ${validation.errors.join("\n")}`);
9048
9053
  return exported;
9049
9054
  }
9050
9055
  /**
9051
- * Update resource attributes and rebuild schema
9052
- * @param {Object} newAttributes - New attributes definition
9056
+ * Apply configuration settings (timestamps, partitions, hooks)
9057
+ * This method ensures that all configuration-dependent features are properly set up
9053
9058
  */
9054
- updateAttributes(newAttributes) {
9055
- const oldAttributes = this.attributes;
9056
- this.attributes = newAttributes;
9059
+ applyConfiguration() {
9057
9060
  if (this.config.timestamps) {
9058
- newAttributes.createdAt = "string|optional";
9059
- newAttributes.updatedAt = "string|optional";
9061
+ if (!this.attributes.createdAt) {
9062
+ this.attributes.createdAt = "string|optional";
9063
+ }
9064
+ if (!this.attributes.updatedAt) {
9065
+ this.attributes.updatedAt = "string|optional";
9066
+ }
9060
9067
  if (!this.config.partitions) {
9061
9068
  this.config.partitions = {};
9062
9069
  }
@@ -9075,9 +9082,10 @@ ${validation.errors.join("\n")}`);
9075
9082
  };
9076
9083
  }
9077
9084
  }
9085
+ this.setupPartitionHooks();
9078
9086
  this.schema = new Schema({
9079
9087
  name: this.name,
9080
- attributes: newAttributes,
9088
+ attributes: this.attributes,
9081
9089
  passphrase: this.passphrase,
9082
9090
  version: this.version,
9083
9091
  options: {
@@ -9085,8 +9093,16 @@ ${validation.errors.join("\n")}`);
9085
9093
  allNestedObjectsOptional: this.config.allNestedObjectsOptional
9086
9094
  }
9087
9095
  });
9088
- this.setupPartitionHooks();
9089
9096
  this.validatePartitions();
9097
+ }
9098
+ /**
9099
+ * Update resource attributes and rebuild schema
9100
+ * @param {Object} newAttributes - New attributes definition
9101
+ */
9102
+ updateAttributes(newAttributes) {
9103
+ const oldAttributes = this.attributes;
9104
+ this.attributes = newAttributes;
9105
+ this.applyConfiguration();
9090
9106
  return { oldAttributes, newAttributes };
9091
9107
  }
9092
9108
  /**
@@ -9174,7 +9190,7 @@ ${validation.errors.join("\n")}`);
9174
9190
  for (const fieldName of Object.keys(partitionDef.fields)) {
9175
9191
  if (!this.fieldExistsInAttributes(fieldName)) {
9176
9192
  throw new Error(
9177
- `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.`
9193
+ `Partition '${partitionName}' uses field '${fieldName}' which does not exist in resource attributes. Available fields: ${currentAttributes.join(", ")}.`
9178
9194
  );
9179
9195
  }
9180
9196
  }
@@ -9287,7 +9303,11 @@ ${validation.errors.join("\n")}`);
9287
9303
  if (partitionSegments.length === 0) {
9288
9304
  return null;
9289
9305
  }
9290
- return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${id}`);
9306
+ const finalId = id || data?.id;
9307
+ if (!finalId) {
9308
+ return null;
9309
+ }
9310
+ return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${finalId}`);
9291
9311
  }
9292
9312
  /**
9293
9313
  * Get nested field value from data object using dot notation
@@ -9516,12 +9536,12 @@ ${validation.errors.join("\n")}`);
9516
9536
  * console.log(updatedUser.updatedAt); // ISO timestamp
9517
9537
  */
9518
9538
  async update(id, attributes) {
9519
- const live = await this.get(id);
9539
+ const originalData = await this.get(id);
9520
9540
  if (this.config.timestamps) {
9521
9541
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9522
9542
  }
9523
9543
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
9524
- const attrs = lodashEs.merge(live, preProcessedData);
9544
+ const attrs = lodashEs.merge(originalData, preProcessedData);
9525
9545
  delete attrs.id;
9526
9546
  const { isValid, errors, data: validated } = await this.validate(attrs);
9527
9547
  if (!isValid) {
@@ -9532,6 +9552,9 @@ ${validation.errors.join("\n")}`);
9532
9552
  validation: errors
9533
9553
  });
9534
9554
  }
9555
+ const oldData = { ...originalData, id };
9556
+ const newData = { ...validated, id };
9557
+ await this.handlePartitionReferenceUpdates(oldData, newData);
9535
9558
  const mappedData = await this.schema.mapper(validated);
9536
9559
  const behaviorImpl = getBehavior(this.behavior);
9537
9560
  const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
@@ -9567,7 +9590,6 @@ ${validation.errors.join("\n")}`);
9567
9590
  });
9568
9591
  validated.id = id;
9569
9592
  await this.executeHooks("afterUpdate", validated);
9570
- await this.updatePartitionReferences(validated);
9571
9593
  this.emit("update", preProcessedData, validated);
9572
9594
  return validated;
9573
9595
  }
@@ -9854,23 +9876,104 @@ ${validation.errors.join("\n")}`);
9854
9876
  * });
9855
9877
  */
9856
9878
  async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9857
- if (!partition) {
9858
- const ids2 = await this.listIds({ partition, partitionValues });
9859
- let filteredIds2 = ids2.slice(offset);
9879
+ try {
9880
+ if (!partition) {
9881
+ let ids2 = [];
9882
+ try {
9883
+ ids2 = await this.listIds({ partition, partitionValues });
9884
+ } catch (listIdsError) {
9885
+ console.warn(`Failed to get list IDs:`, listIdsError.message);
9886
+ return [];
9887
+ }
9888
+ let filteredIds2 = ids2.slice(offset);
9889
+ if (limit) {
9890
+ filteredIds2 = filteredIds2.slice(0, limit);
9891
+ }
9892
+ const { results: results2, errors: errors2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9893
+ console.warn(`Failed to get resource ${id}:`, error.message);
9894
+ return null;
9895
+ }).process(async (id) => {
9896
+ try {
9897
+ return await this.get(id);
9898
+ } catch (error) {
9899
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9900
+ console.warn(`Decryption failed for ${id}, returning basic info`);
9901
+ return {
9902
+ id,
9903
+ _decryptionFailed: true,
9904
+ _error: error.message
9905
+ };
9906
+ }
9907
+ throw error;
9908
+ }
9909
+ });
9910
+ const validResults2 = results2.filter((item) => item !== null);
9911
+ this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9912
+ return validResults2;
9913
+ }
9914
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9915
+ console.warn(`Partition '${partition}' not found in resource '${this.name}'`);
9916
+ this.emit("list", { partition, partitionValues, count: 0, errors: 0 });
9917
+ return [];
9918
+ }
9919
+ const partitionDef = this.config.partitions[partition];
9920
+ const partitionSegments = [];
9921
+ const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9922
+ for (const [fieldName, rule] of sortedFields) {
9923
+ const value = partitionValues[fieldName];
9924
+ if (value !== void 0 && value !== null) {
9925
+ const transformedValue = this.applyPartitionRule(value, rule);
9926
+ partitionSegments.push(`${fieldName}=${transformedValue}`);
9927
+ }
9928
+ }
9929
+ let prefix;
9930
+ if (partitionSegments.length > 0) {
9931
+ prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9932
+ } else {
9933
+ prefix = `resource=${this.name}/partition=${partition}`;
9934
+ }
9935
+ let keys = [];
9936
+ try {
9937
+ keys = await this.client.getAllKeys({ prefix });
9938
+ } catch (getKeysError) {
9939
+ console.warn(`Failed to get partition keys:`, getKeysError.message);
9940
+ return [];
9941
+ }
9942
+ const ids = keys.map((key) => {
9943
+ const parts = key.split("/");
9944
+ const idPart = parts.find((part) => part.startsWith("id="));
9945
+ return idPart ? idPart.replace("id=", "") : null;
9946
+ }).filter(Boolean);
9947
+ let filteredIds = ids.slice(offset);
9860
9948
  if (limit) {
9861
- filteredIds2 = filteredIds2.slice(0, limit);
9949
+ filteredIds = filteredIds.slice(0, limit);
9862
9950
  }
9863
- const { results: results2, errors: errors2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9864
- console.warn(`Failed to get resource ${id}:`, error.message);
9951
+ const { results, errors } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9952
+ console.warn(`Failed to get partition resource ${id}:`, error.message);
9865
9953
  return null;
9866
9954
  }).process(async (id) => {
9867
9955
  try {
9868
- return await this.get(id);
9956
+ const keyForId = keys.find((key) => key.includes(`id=${id}`));
9957
+ if (!keyForId) {
9958
+ throw new Error(`Partition key not found for ID ${id}`);
9959
+ }
9960
+ const keyParts = keyForId.split("/");
9961
+ const actualPartitionValues = {};
9962
+ for (const [fieldName, rule] of sortedFields) {
9963
+ const fieldPart = keyParts.find((part) => part.startsWith(`${fieldName}=`));
9964
+ if (fieldPart) {
9965
+ const value = fieldPart.replace(`${fieldName}=`, "");
9966
+ actualPartitionValues[fieldName] = value;
9967
+ }
9968
+ }
9969
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues: actualPartitionValues });
9869
9970
  } catch (error) {
9870
9971
  if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9871
- console.warn(`Decryption failed for ${id}, returning basic info`);
9972
+ console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9872
9973
  return {
9873
9974
  id,
9975
+ _partition: partition,
9976
+ _partitionValues: partitionValues,
9874
9977
  _decryptionFailed: true,
9875
9978
  _error: error.message
9876
9979
  };
@@ -9878,62 +9981,19 @@ ${validation.errors.join("\n")}`);
9878
9981
  throw error;
9879
9982
  }
9880
9983
  });
9881
- const validResults2 = results2.filter((item) => item !== null);
9882
- this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9883
- return validResults2;
9884
- }
9885
- if (!this.config.partitions || !this.config.partitions[partition]) {
9886
- throw new Error(`Partition '${partition}' not found`);
9887
- }
9888
- const partitionDef = this.config.partitions[partition];
9889
- const partitionSegments = [];
9890
- const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9891
- for (const [fieldName, rule] of sortedFields) {
9892
- const value = partitionValues[fieldName];
9893
- if (value !== void 0 && value !== null) {
9894
- const transformedValue = this.applyPartitionRule(value, rule);
9895
- partitionSegments.push(`${fieldName}=${transformedValue}`);
9984
+ const validResults = results.filter((item) => item !== null);
9985
+ this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9986
+ return validResults;
9987
+ } catch (error) {
9988
+ if (error.message.includes("Partition '") && error.message.includes("' not found")) {
9989
+ console.warn(`Partition error in list method:`, error.message);
9990
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
9991
+ return [];
9896
9992
  }
9993
+ console.error(`Critical error in list method:`, error.message);
9994
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
9995
+ return [];
9897
9996
  }
9898
- let prefix;
9899
- if (partitionSegments.length > 0) {
9900
- prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9901
- } else {
9902
- prefix = `resource=${this.name}/partition=${partition}`;
9903
- }
9904
- const keys = await this.client.getAllKeys({ prefix });
9905
- const ids = keys.map((key) => {
9906
- const parts = key.split("/");
9907
- const idPart = parts.find((part) => part.startsWith("id="));
9908
- return idPart ? idPart.replace("id=", "") : null;
9909
- }).filter(Boolean);
9910
- let filteredIds = ids.slice(offset);
9911
- if (limit) {
9912
- filteredIds = filteredIds.slice(0, limit);
9913
- }
9914
- const { results, errors } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9915
- console.warn(`Failed to get partition resource ${id}:`, error.message);
9916
- return null;
9917
- }).process(async (id) => {
9918
- try {
9919
- return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9920
- } catch (error) {
9921
- if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9922
- console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9923
- return {
9924
- id,
9925
- _partition: partition,
9926
- _partitionValues: partitionValues,
9927
- _decryptionFailed: true,
9928
- _error: error.message
9929
- };
9930
- }
9931
- throw error;
9932
- }
9933
- });
9934
- const validResults = results.filter((item) => item !== null);
9935
- this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9936
- return validResults;
9937
9997
  }
9938
9998
  /**
9939
9999
  * Get multiple resources by their IDs
@@ -10040,36 +10100,67 @@ ${validation.errors.join("\n")}`);
10040
10100
  * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
10041
10101
  */
10042
10102
  async page({ offset = 0, size = 100, partition = null, partitionValues = {}, skipCount = false } = {}) {
10043
- let totalItems = null;
10044
- let totalPages = null;
10045
- if (!skipCount) {
10046
- totalItems = await this.count({ partition, partitionValues });
10047
- totalPages = Math.ceil(totalItems / size);
10048
- }
10049
- const page = Math.floor(offset / size);
10050
- const items = await this.list({
10051
- partition,
10052
- partitionValues,
10053
- limit: size,
10054
- offset
10055
- });
10056
- const result = {
10057
- items,
10058
- totalItems,
10059
- page,
10060
- pageSize: size,
10061
- totalPages,
10062
- // Add additional metadata for debugging
10063
- _debug: {
10064
- requestedSize: size,
10065
- requestedOffset: offset,
10066
- actualItemsReturned: items.length,
10067
- skipCount,
10068
- hasTotalItems: totalItems !== null
10103
+ try {
10104
+ let totalItems = null;
10105
+ let totalPages = null;
10106
+ if (!skipCount) {
10107
+ try {
10108
+ totalItems = await this.count({ partition, partitionValues });
10109
+ totalPages = Math.ceil(totalItems / size);
10110
+ } catch (countError) {
10111
+ console.warn(`Failed to get count for page:`, countError.message);
10112
+ totalItems = null;
10113
+ totalPages = null;
10114
+ }
10069
10115
  }
10070
- };
10071
- this.emit("page", result);
10072
- return result;
10116
+ const page = Math.floor(offset / size);
10117
+ let items = [];
10118
+ try {
10119
+ items = await this.list({
10120
+ partition,
10121
+ partitionValues,
10122
+ limit: size,
10123
+ offset
10124
+ });
10125
+ } catch (listError) {
10126
+ console.warn(`Failed to get items for page:`, listError.message);
10127
+ items = [];
10128
+ }
10129
+ const result = {
10130
+ items,
10131
+ totalItems,
10132
+ page,
10133
+ pageSize: size,
10134
+ totalPages,
10135
+ // Add additional metadata for debugging
10136
+ _debug: {
10137
+ requestedSize: size,
10138
+ requestedOffset: offset,
10139
+ actualItemsReturned: items.length,
10140
+ skipCount,
10141
+ hasTotalItems: totalItems !== null
10142
+ }
10143
+ };
10144
+ this.emit("page", result);
10145
+ return result;
10146
+ } catch (error) {
10147
+ console.error(`Critical error in page method:`, error.message);
10148
+ return {
10149
+ items: [],
10150
+ totalItems: null,
10151
+ page: Math.floor(offset / size),
10152
+ pageSize: size,
10153
+ totalPages: null,
10154
+ _debug: {
10155
+ requestedSize: size,
10156
+ requestedOffset: offset,
10157
+ actualItemsReturned: 0,
10158
+ skipCount,
10159
+ hasTotalItems: false,
10160
+ error: error.message
10161
+ }
10162
+ };
10163
+ }
10073
10164
  }
10074
10165
  readable() {
10075
10166
  const stream = new ResourceReader({ resource: this });
@@ -10362,7 +10453,118 @@ ${validation.errors.join("\n")}`);
10362
10453
  return results.slice(0, limit);
10363
10454
  }
10364
10455
  /**
10365
- * Update partition objects to keep them in sync
10456
+ * Handle partition reference updates with change detection
10457
+ * @param {Object} oldData - Original object data before update
10458
+ * @param {Object} newData - Updated object data
10459
+ */
10460
+ async handlePartitionReferenceUpdates(oldData, newData) {
10461
+ const partitions = this.config.partitions;
10462
+ if (!partitions || Object.keys(partitions).length === 0) {
10463
+ return;
10464
+ }
10465
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10466
+ try {
10467
+ await this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData);
10468
+ } catch (error) {
10469
+ console.warn(`Failed to update partition references for ${partitionName}:`, error.message);
10470
+ }
10471
+ }
10472
+ const id = newData.id || oldData.id;
10473
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10474
+ const prefix = `resource=${this.name}/partition=${partitionName}`;
10475
+ let allKeys = [];
10476
+ try {
10477
+ allKeys = await this.client.getAllKeys({ prefix });
10478
+ } catch (error) {
10479
+ console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, error.message);
10480
+ continue;
10481
+ }
10482
+ const validKey = this.getPartitionKey({ partitionName, id, data: newData });
10483
+ for (const key of allKeys) {
10484
+ if (key.endsWith(`/id=${id}`) && key !== validKey) {
10485
+ try {
10486
+ await this.client.deleteObject(key);
10487
+ } catch (error) {
10488
+ console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, error.message);
10489
+ }
10490
+ }
10491
+ }
10492
+ }
10493
+ }
10494
+ /**
10495
+ * Handle partition reference update for a specific partition
10496
+ * @param {string} partitionName - Name of the partition
10497
+ * @param {Object} partition - Partition definition
10498
+ * @param {Object} oldData - Original object data before update
10499
+ * @param {Object} newData - Updated object data
10500
+ */
10501
+ async handlePartitionReferenceUpdate(partitionName, partition, oldData, newData) {
10502
+ const id = newData.id || oldData.id;
10503
+ const oldPartitionKey = this.getPartitionKey({ partitionName, id, data: oldData });
10504
+ const newPartitionKey = this.getPartitionKey({ partitionName, id, data: newData });
10505
+ if (oldPartitionKey !== newPartitionKey) {
10506
+ if (oldPartitionKey) {
10507
+ try {
10508
+ await this.client.deleteObject(oldPartitionKey);
10509
+ } catch (error) {
10510
+ console.warn(`Old partition object could not be deleted for ${partitionName}:`, error.message);
10511
+ }
10512
+ }
10513
+ if (newPartitionKey) {
10514
+ try {
10515
+ const mappedData = await this.schema.mapper(newData);
10516
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10517
+ const behaviorImpl = getBehavior(this.behavior);
10518
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10519
+ resource: this,
10520
+ id,
10521
+ data: newData,
10522
+ mappedData
10523
+ });
10524
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10525
+ const partitionMetadata = {
10526
+ ...processedMetadata,
10527
+ _version: this.version
10528
+ };
10529
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10530
+ await this.client.putObject({
10531
+ key: newPartitionKey,
10532
+ metadata: partitionMetadata,
10533
+ body
10534
+ });
10535
+ } catch (error) {
10536
+ console.warn(`New partition object could not be created for ${partitionName}:`, error.message);
10537
+ }
10538
+ }
10539
+ } else if (newPartitionKey) {
10540
+ try {
10541
+ const mappedData = await this.schema.mapper(newData);
10542
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10543
+ const behaviorImpl = getBehavior(this.behavior);
10544
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10545
+ resource: this,
10546
+ id,
10547
+ data: newData,
10548
+ mappedData
10549
+ });
10550
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10551
+ const partitionMetadata = {
10552
+ ...processedMetadata,
10553
+ _version: this.version
10554
+ };
10555
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10556
+ await this.client.putObject({
10557
+ key: newPartitionKey,
10558
+ metadata: partitionMetadata,
10559
+ body
10560
+ });
10561
+ } catch (error) {
10562
+ console.warn(`Partition object could not be updated for ${partitionName}:`, error.message);
10563
+ }
10564
+ }
10565
+ }
10566
+ /**
10567
+ * Update partition objects to keep them in sync (legacy method for backward compatibility)
10366
10568
  * @param {Object} data - Updated object data
10367
10569
  */
10368
10570
  async updatePartitionReferences(data) {
@@ -10371,6 +10573,10 @@ ${validation.errors.join("\n")}`);
10371
10573
  return;
10372
10574
  }
10373
10575
  for (const [partitionName, partition] of Object.entries(partitions)) {
10576
+ if (!partition || !partition.fields || typeof partition.fields !== "object") {
10577
+ console.warn(`Skipping invalid partition '${partitionName}' in resource '${this.name}'`);
10578
+ continue;
10579
+ }
10374
10580
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
10375
10581
  if (partitionKey) {
10376
10582
  const mappedData = await this.schema.mapper(data);
@@ -10469,6 +10675,7 @@ ${validation.errors.join("\n")}`);
10469
10675
  if (request.VersionId) data._versionId = request.VersionId;
10470
10676
  if (request.Expiration) data._expiresAt = request.Expiration;
10471
10677
  data._definitionHash = this.getDefinitionHash();
10678
+ if (data.undefined !== void 0) delete data.undefined;
10472
10679
  this.emit("getFromPartition", data);
10473
10680
  return data;
10474
10681
  }
@@ -10572,7 +10779,7 @@ ${validation.errors.join("\n")}`);
10572
10779
  this.version = "1";
10573
10780
  this.s3dbVersion = (() => {
10574
10781
  try {
10575
- return true ? "4.1.10" : "latest";
10782
+ return true ? "5.0.0" : "latest";
10576
10783
  } catch (e) {
10577
10784
  return "latest";
10578
10785
  }
@@ -10587,14 +10794,24 @@ ${validation.errors.join("\n")}`);
10587
10794
  this.passphrase = options.passphrase || "secret";
10588
10795
  let connectionString = options.connectionString;
10589
10796
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
10590
- connectionString = ConnectionString.buildFromParams({
10591
- bucket: options.bucket,
10592
- region: options.region,
10593
- accessKeyId: options.accessKeyId,
10594
- secretAccessKey: options.secretAccessKey,
10595
- endpoint: options.endpoint,
10596
- forcePathStyle: options.forcePathStyle
10597
- });
10797
+ const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
10798
+ if (endpoint) {
10799
+ const url = new URL(endpoint);
10800
+ if (accessKeyId) url.username = encodeURIComponent(accessKeyId);
10801
+ if (secretAccessKey) url.password = encodeURIComponent(secretAccessKey);
10802
+ url.pathname = `/${bucket || "s3db"}`;
10803
+ if (forcePathStyle) {
10804
+ url.searchParams.set("forcePathStyle", "true");
10805
+ }
10806
+ connectionString = url.toString();
10807
+ } else if (accessKeyId && secretAccessKey) {
10808
+ const params = new URLSearchParams();
10809
+ params.set("region", region || "us-east-1");
10810
+ if (forcePathStyle) {
10811
+ params.set("forcePathStyle", "true");
10812
+ }
10813
+ connectionString = `s3://${encodeURIComponent(accessKeyId)}:${encodeURIComponent(secretAccessKey)}@${bucket || "s3db"}?${params.toString()}`;
10814
+ }
10598
10815
  }
10599
10816
  this.client = options.client || new Client({
10600
10817
  verbose: this.verbose,
@@ -10630,12 +10847,12 @@ ${validation.errors.join("\n")}`);
10630
10847
  passphrase: this.passphrase,
10631
10848
  observers: [this],
10632
10849
  cache: this.cache,
10633
- timestamps: versionData.options?.timestamps || false,
10634
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
10635
- paranoid: versionData.options?.paranoid !== false,
10636
- allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
10637
- autoDecrypt: versionData.options?.autoDecrypt !== false,
10638
- hooks: {}
10850
+ timestamps: versionData.timestamps !== void 0 ? versionData.timestamps : false,
10851
+ partitions: resourceMetadata.partitions || versionData.partitions || {},
10852
+ paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
10853
+ allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
10854
+ autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
10855
+ hooks: versionData.hooks || {}
10639
10856
  });
10640
10857
  }
10641
10858
  }
@@ -10773,15 +10990,14 @@ ${validation.errors.join("\n")}`);
10773
10990
  [version]: {
10774
10991
  hash: definitionHash,
10775
10992
  attributes: resourceDef.attributes,
10776
- options: {
10777
- timestamps: resource.config.timestamps,
10778
- partitions: resource.config.partitions,
10779
- paranoid: resource.config.paranoid,
10780
- allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
10781
- autoDecrypt: resource.config.autoDecrypt,
10782
- cache: resource.config.cache
10783
- },
10784
10993
  behavior: resourceDef.behavior || "user-management",
10994
+ timestamps: resource.config.timestamps,
10995
+ partitions: resource.config.partitions,
10996
+ paranoid: resource.config.paranoid,
10997
+ allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
10998
+ autoDecrypt: resource.config.autoDecrypt,
10999
+ cache: resource.config.cache,
11000
+ hooks: resource.config.hooks,
10785
11001
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10786
11002
  }
10787
11003
  }
@@ -10816,73 +11032,58 @@ ${validation.errors.join("\n")}`);
10816
11032
  }
10817
11033
  /**
10818
11034
  * Check if a resource exists with the same definition hash
10819
- * @param {string} name - Resource name
10820
- * @param {Object} attributes - Resource attributes
10821
- * @param {Object} options - Resource options
10822
- * @param {string} behavior - Resource behavior
10823
- * @returns {Object} Object with exists flag and hash information
11035
+ * @param {Object} config - Resource configuration
11036
+ * @param {string} config.name - Resource name
11037
+ * @param {Object} config.attributes - Resource attributes
11038
+ * @param {string} [config.behavior] - Resource behavior
11039
+ * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
11040
+ * @returns {Object} Result with exists and hash information
10824
11041
  */
10825
- resourceExistsWithSameHash({ name, attributes, options = {}, behavior = "user-management" }) {
11042
+ resourceExistsWithSameHash({ name, attributes, behavior = "user-management", options = {} }) {
10826
11043
  if (!this.resources[name]) {
10827
11044
  return { exists: false, sameHash: false, hash: null };
10828
11045
  }
10829
- const tempResource = new Resource({
11046
+ const existingResource = this.resources[name];
11047
+ const existingHash = this.generateDefinitionHash(existingResource.export());
11048
+ const mockResource = new Resource({
10830
11049
  name,
10831
11050
  attributes,
10832
11051
  behavior,
10833
- observers: [],
10834
11052
  client: this.client,
10835
- version: "temp",
11053
+ version: existingResource.version,
10836
11054
  passphrase: this.passphrase,
10837
- cache: this.cache,
10838
11055
  ...options
10839
11056
  });
10840
- const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10841
- const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
11057
+ const newHash = this.generateDefinitionHash(mockResource.export());
10842
11058
  return {
10843
11059
  exists: true,
10844
- sameHash: newHash === existingHash,
11060
+ sameHash: existingHash === newHash,
10845
11061
  hash: newHash,
10846
11062
  existingHash
10847
11063
  };
10848
11064
  }
10849
- /**
10850
- * Create a resource only if it doesn't exist with the same definition hash
10851
- * @param {Object} params - Resource parameters
10852
- * @param {string} params.name - Resource name
10853
- * @param {Object} params.attributes - Resource attributes
10854
- * @param {Object} params.options - Resource options
10855
- * @param {string} params.behavior - Resource behavior
10856
- * @returns {Object} Object with resource and created flag
10857
- */
10858
- async createResourceIfNotExists({ name, attributes, options = {}, behavior = "user-management" }) {
10859
- const alreadyExists = !!this.resources[name];
10860
- const hashCheck = this.resourceExistsWithSameHash({ name, attributes, options, behavior });
10861
- if (hashCheck.exists && hashCheck.sameHash) {
10862
- return {
10863
- resource: this.resources[name],
10864
- created: false,
10865
- reason: "Resource already exists with same definition hash"
10866
- };
10867
- }
10868
- const resource = await this.createResource({ name, attributes, options, behavior });
10869
- return {
10870
- resource,
10871
- created: !alreadyExists,
10872
- reason: alreadyExists ? "Resource updated with new definition" : "New resource created"
10873
- };
10874
- }
10875
- async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
11065
+ async createResource({ name, attributes, behavior = "user-management", hooks, ...config }) {
10876
11066
  if (this.resources[name]) {
10877
11067
  const existingResource = this.resources[name];
10878
11068
  Object.assign(existingResource.config, {
10879
11069
  cache: this.cache,
10880
- ...options
11070
+ ...config
10881
11071
  });
10882
11072
  if (behavior) {
10883
11073
  existingResource.behavior = behavior;
10884
11074
  }
10885
11075
  existingResource.updateAttributes(attributes);
11076
+ if (hooks) {
11077
+ for (const [event, hooksArr] of Object.entries(hooks)) {
11078
+ if (Array.isArray(hooksArr) && existingResource.hooks[event]) {
11079
+ for (const fn of hooksArr) {
11080
+ if (typeof fn === "function") {
11081
+ existingResource.hooks[event].push(fn.bind(existingResource));
11082
+ }
11083
+ }
11084
+ }
11085
+ }
11086
+ }
10886
11087
  const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
10887
11088
  const existingMetadata2 = this.savedMetadata?.resources?.[name];
10888
11089
  const currentVersion = existingMetadata2?.currentVersion || "v0";
@@ -10904,7 +11105,8 @@ ${validation.errors.join("\n")}`);
10904
11105
  version,
10905
11106
  passphrase: this.passphrase,
10906
11107
  cache: this.cache,
10907
- ...options
11108
+ hooks,
11109
+ ...config
10908
11110
  });
10909
11111
  this.resources[name] = resource;
10910
11112
  await this.uploadMetadataFile();
@@ -17606,6 +17808,7 @@ ${validation.errors.join("\n")}`);
17606
17808
  }
17607
17809
  }
17608
17810
 
17811
+ exports.AVAILABLE_BEHAVIORS = AVAILABLE_BEHAVIORS;
17609
17812
  exports.AuthenticationError = AuthenticationError;
17610
17813
  exports.BaseError = BaseError;
17611
17814
  exports.Cache = Cache;
@@ -17613,6 +17816,7 @@ ${validation.errors.join("\n")}`);
17613
17816
  exports.Client = Client;
17614
17817
  exports.ConnectionString = ConnectionString;
17615
17818
  exports.CostsPlugin = CostsPlugin;
17819
+ exports.DEFAULT_BEHAVIOR = DEFAULT_BEHAVIOR;
17616
17820
  exports.Database = Database;
17617
17821
  exports.DatabaseError = DatabaseError;
17618
17822
  exports.EncryptionError = EncryptionError;
@@ -17626,28 +17830,38 @@ ${validation.errors.join("\n")}`);
17626
17830
  exports.PermissionError = PermissionError;
17627
17831
  exports.Plugin = Plugin;
17628
17832
  exports.PluginObject = PluginObject;
17833
+ exports.Resource = Resource;
17629
17834
  exports.ResourceIdsPageReader = ResourceIdsPageReader;
17630
17835
  exports.ResourceIdsReader = ResourceIdsReader;
17631
17836
  exports.ResourceNotFound = ResourceNotFound;
17632
- exports.ResourceNotFoundError = ResourceNotFound;
17633
17837
  exports.ResourceReader = ResourceReader;
17634
17838
  exports.ResourceWriter = ResourceWriter;
17635
17839
  exports.S3Cache = S3Cache;
17636
- exports.S3DB = S3db;
17637
17840
  exports.S3DBError = S3DBError;
17638
17841
  exports.S3_DEFAULT_ENDPOINT = S3_DEFAULT_ENDPOINT;
17639
17842
  exports.S3_DEFAULT_REGION = S3_DEFAULT_REGION;
17640
17843
  exports.S3db = S3db;
17641
- exports.S3dbError = S3DBError;
17844
+ exports.Schema = Schema;
17845
+ exports.SchemaActions = SchemaActions;
17642
17846
  exports.UnknownError = UnknownError;
17643
17847
  exports.ValidationError = ValidationError;
17644
17848
  exports.Validator = Validator;
17645
17849
  exports.ValidatorManager = ValidatorManager;
17850
+ exports.behaviors = behaviors;
17851
+ exports.calculateAttributeNamesSize = calculateAttributeNamesSize;
17852
+ exports.calculateAttributeSizes = calculateAttributeSizes;
17853
+ exports.calculateTotalSize = calculateTotalSize;
17854
+ exports.calculateUTF8Bytes = calculateUTF8Bytes;
17646
17855
  exports.decrypt = decrypt;
17647
17856
  exports.default = S3db;
17648
17857
  exports.encrypt = encrypt;
17858
+ exports.getBehavior = getBehavior;
17859
+ exports.getSizeBreakdown = getSizeBreakdown;
17860
+ exports.idGenerator = idGenerator;
17861
+ exports.passwordGenerator = passwordGenerator;
17649
17862
  exports.sha256 = sha256;
17650
17863
  exports.streamToString = streamToString;
17864
+ exports.transformValue = transformValue;
17651
17865
 
17652
17866
  Object.defineProperty(exports, '__esModule', { value: true });
17653
17867