s3db.js 4.1.13 → 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.es.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* istanbul ignore file */
2
2
  import { customAlphabet, urlAlphabet } from 'nanoid';
3
- import { chunk, merge, isEmpty, invert, uniq, cloneDeep, isString as isString$1, get as get$1, set, isFunction as isFunction$1 } from 'lodash-es';
3
+ import { chunk, merge, isString as isString$1, isEmpty, invert, uniq, cloneDeep, get as get$1, set, isFunction as isFunction$1 } from 'lodash-es';
4
4
  import { PromisePool } from '@supercharge/promise-pool';
5
5
  import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand, CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
6
6
  import { createHash } from 'crypto';
@@ -243,7 +243,7 @@ var substr = 'ab'.substr(-1) === 'b' ?
243
243
 
244
244
  const idGenerator = customAlphabet(urlAlphabet, 22);
245
245
  const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
246
- customAlphabet(passwordAlphabet, 12);
246
+ const passwordGenerator = customAlphabet(passwordAlphabet, 12);
247
247
 
248
248
  var domain;
249
249
 
@@ -3446,10 +3446,31 @@ const ValidatorManager = new Proxy(Validator, {
3446
3446
  }
3447
3447
  });
3448
3448
 
3449
+ function toBase36(num) {
3450
+ return num.toString(36);
3451
+ }
3452
+ function generateBase36Mapping(keys) {
3453
+ const mapping = {};
3454
+ const reversedMapping = {};
3455
+ keys.forEach((key, index) => {
3456
+ const base36Key = toBase36(index);
3457
+ mapping[key] = base36Key;
3458
+ reversedMapping[base36Key] = key;
3459
+ });
3460
+ return { mapping, reversedMapping };
3461
+ }
3449
3462
  const SchemaActions = {
3450
3463
  trim: (value) => value.trim(),
3451
3464
  encrypt: (value, { passphrase }) => encrypt(value, passphrase),
3452
- decrypt: (value, { passphrase }) => decrypt(value, passphrase),
3465
+ decrypt: async (value, { passphrase }) => {
3466
+ try {
3467
+ const raw = await decrypt(value, passphrase);
3468
+ return raw;
3469
+ } catch (error) {
3470
+ console.warn(`Schema decrypt error: ${error}`, error);
3471
+ return value;
3472
+ }
3473
+ },
3453
3474
  toString: (value) => String(value),
3454
3475
  fromArray: (value, { separator }) => {
3455
3476
  if (value === null || value === void 0 || !Array.isArray(value)) {
@@ -3540,8 +3561,9 @@ class Schema {
3540
3561
  const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
3541
3562
  const objectKeys = this.extractObjectKeys(this.attributes);
3542
3563
  const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
3543
- this.reversedMap = { ...allKeys };
3544
- this.map = invert(this.reversedMap);
3564
+ const { mapping, reversedMapping } = generateBase36Mapping(allKeys);
3565
+ this.map = mapping;
3566
+ this.reversedMap = reversedMapping;
3545
3567
  }
3546
3568
  }
3547
3569
  defaultOptions() {
@@ -8614,7 +8636,6 @@ function calculateUTF8Bytes(str) {
8614
8636
  function calculateAttributeNamesSize(mappedObject) {
8615
8637
  let totalSize = 0;
8616
8638
  for (const key of Object.keys(mappedObject)) {
8617
- if (key === "_v") continue;
8618
8639
  totalSize += calculateUTF8Bytes(key);
8619
8640
  }
8620
8641
  return totalSize;
@@ -8658,6 +8679,30 @@ function calculateTotalSize(mappedObject) {
8658
8679
  const namesSize = calculateAttributeNamesSize(mappedObject);
8659
8680
  return valueTotal + namesSize;
8660
8681
  }
8682
+ function getSizeBreakdown(mappedObject) {
8683
+ const valueSizes = calculateAttributeSizes(mappedObject);
8684
+ const namesSize = calculateAttributeNamesSize(mappedObject);
8685
+ const valueTotal = Object.values(valueSizes).reduce((sum, size) => sum + size, 0);
8686
+ const total = valueTotal + namesSize;
8687
+ const sortedAttributes = Object.entries(valueSizes).sort(([, a], [, b]) => b - a).map(([key, size]) => ({
8688
+ attribute: key,
8689
+ size,
8690
+ percentage: (size / total * 100).toFixed(2) + "%"
8691
+ }));
8692
+ return {
8693
+ total,
8694
+ valueSizes,
8695
+ namesSize,
8696
+ valueTotal,
8697
+ breakdown: sortedAttributes,
8698
+ // Add detailed breakdown including names
8699
+ detailedBreakdown: {
8700
+ values: valueTotal,
8701
+ names: namesSize,
8702
+ total
8703
+ }
8704
+ };
8705
+ }
8661
8706
 
8662
8707
  const S3_METADATA_LIMIT_BYTES = 2048;
8663
8708
  async function handleInsert$3({ resource, data, mappedData }) {
@@ -8878,6 +8923,7 @@ function getBehavior(behaviorName) {
8878
8923
  }
8879
8924
  return behavior;
8880
8925
  }
8926
+ const AVAILABLE_BEHAVIORS = Object.keys(behaviors);
8881
8927
  const DEFAULT_BEHAVIOR = "user-management";
8882
8928
 
8883
8929
  class Resource extends EventEmitter {
@@ -8947,17 +8993,8 @@ ${validation.errors.join("\n")}`);
8947
8993
  partitions = {},
8948
8994
  paranoid = true,
8949
8995
  allNestedObjectsOptional = true,
8950
- hooks = {},
8951
- options = {}
8996
+ hooks = {}
8952
8997
  } = config;
8953
- const mergedOptions = {
8954
- cache: typeof options.cache === "boolean" ? options.cache : cache,
8955
- autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
8956
- timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
8957
- paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
8958
- allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
8959
- partitions: options.partitions || partitions || {}
8960
- };
8961
8998
  this.name = name;
8962
8999
  this.client = client;
8963
9000
  this.version = version;
@@ -8966,13 +9003,13 @@ ${validation.errors.join("\n")}`);
8966
9003
  this.parallelism = parallelism;
8967
9004
  this.passphrase = passphrase ?? "secret";
8968
9005
  this.config = {
8969
- cache: mergedOptions.cache,
9006
+ cache,
8970
9007
  hooks,
8971
- paranoid: mergedOptions.paranoid,
8972
- timestamps: mergedOptions.timestamps,
8973
- partitions: mergedOptions.partitions,
8974
- autoDecrypt: mergedOptions.autoDecrypt,
8975
- allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
9008
+ paranoid,
9009
+ timestamps,
9010
+ partitions,
9011
+ autoDecrypt,
9012
+ allNestedObjectsOptional
8976
9013
  };
8977
9014
  this.hooks = {
8978
9015
  preInsert: [],
@@ -8983,39 +9020,7 @@ ${validation.errors.join("\n")}`);
8983
9020
  afterDelete: []
8984
9021
  };
8985
9022
  this.attributes = attributes || {};
8986
- if (this.config.timestamps) {
8987
- this.attributes.createdAt = "string|optional";
8988
- this.attributes.updatedAt = "string|optional";
8989
- if (!this.config.partitions) {
8990
- this.config.partitions = {};
8991
- }
8992
- if (!this.config.partitions.byCreatedDate) {
8993
- this.config.partitions.byCreatedDate = {
8994
- fields: {
8995
- createdAt: "date|maxlength:10"
8996
- }
8997
- };
8998
- }
8999
- if (!this.config.partitions.byUpdatedDate) {
9000
- this.config.partitions.byUpdatedDate = {
9001
- fields: {
9002
- updatedAt: "date|maxlength:10"
9003
- }
9004
- };
9005
- }
9006
- }
9007
- this.schema = new Schema({
9008
- name,
9009
- attributes: this.attributes,
9010
- passphrase,
9011
- version: this.version,
9012
- options: {
9013
- autoDecrypt: this.config.autoDecrypt,
9014
- allNestedObjectsOptional: this.config.allNestedObjectsOptional
9015
- }
9016
- });
9017
- this.setupPartitionHooks();
9018
- this.validatePartitions();
9023
+ this.applyConfiguration();
9019
9024
  if (hooks) {
9020
9025
  for (const [event, hooksArr] of Object.entries(hooks)) {
9021
9026
  if (Array.isArray(hooksArr) && this.hooks[event]) {
@@ -9054,15 +9059,17 @@ ${validation.errors.join("\n")}`);
9054
9059
  return exported;
9055
9060
  }
9056
9061
  /**
9057
- * Update resource attributes and rebuild schema
9058
- * @param {Object} newAttributes - New attributes definition
9062
+ * Apply configuration settings (timestamps, partitions, hooks)
9063
+ * This method ensures that all configuration-dependent features are properly set up
9059
9064
  */
9060
- updateAttributes(newAttributes) {
9061
- const oldAttributes = this.attributes;
9062
- this.attributes = newAttributes;
9065
+ applyConfiguration() {
9063
9066
  if (this.config.timestamps) {
9064
- newAttributes.createdAt = "string|optional";
9065
- newAttributes.updatedAt = "string|optional";
9067
+ if (!this.attributes.createdAt) {
9068
+ this.attributes.createdAt = "string|optional";
9069
+ }
9070
+ if (!this.attributes.updatedAt) {
9071
+ this.attributes.updatedAt = "string|optional";
9072
+ }
9066
9073
  if (!this.config.partitions) {
9067
9074
  this.config.partitions = {};
9068
9075
  }
@@ -9081,9 +9088,10 @@ ${validation.errors.join("\n")}`);
9081
9088
  };
9082
9089
  }
9083
9090
  }
9091
+ this.setupPartitionHooks();
9084
9092
  this.schema = new Schema({
9085
9093
  name: this.name,
9086
- attributes: newAttributes,
9094
+ attributes: this.attributes,
9087
9095
  passphrase: this.passphrase,
9088
9096
  version: this.version,
9089
9097
  options: {
@@ -9091,8 +9099,16 @@ ${validation.errors.join("\n")}`);
9091
9099
  allNestedObjectsOptional: this.config.allNestedObjectsOptional
9092
9100
  }
9093
9101
  });
9094
- this.setupPartitionHooks();
9095
9102
  this.validatePartitions();
9103
+ }
9104
+ /**
9105
+ * Update resource attributes and rebuild schema
9106
+ * @param {Object} newAttributes - New attributes definition
9107
+ */
9108
+ updateAttributes(newAttributes) {
9109
+ const oldAttributes = this.attributes;
9110
+ this.attributes = newAttributes;
9111
+ this.applyConfiguration();
9096
9112
  return { oldAttributes, newAttributes };
9097
9113
  }
9098
9114
  /**
@@ -9180,7 +9196,7 @@ ${validation.errors.join("\n")}`);
9180
9196
  for (const fieldName of Object.keys(partitionDef.fields)) {
9181
9197
  if (!this.fieldExistsInAttributes(fieldName)) {
9182
9198
  throw new Error(
9183
- `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.`
9199
+ `Partition '${partitionName}' uses field '${fieldName}' which does not exist in resource attributes. Available fields: ${currentAttributes.join(", ")}.`
9184
9200
  );
9185
9201
  }
9186
9202
  }
@@ -9293,7 +9309,11 @@ ${validation.errors.join("\n")}`);
9293
9309
  if (partitionSegments.length === 0) {
9294
9310
  return null;
9295
9311
  }
9296
- return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${id}`);
9312
+ const finalId = id || data?.id;
9313
+ if (!finalId) {
9314
+ return null;
9315
+ }
9316
+ return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${finalId}`);
9297
9317
  }
9298
9318
  /**
9299
9319
  * Get nested field value from data object using dot notation
@@ -9522,12 +9542,12 @@ ${validation.errors.join("\n")}`);
9522
9542
  * console.log(updatedUser.updatedAt); // ISO timestamp
9523
9543
  */
9524
9544
  async update(id, attributes) {
9525
- const live = await this.get(id);
9545
+ const originalData = await this.get(id);
9526
9546
  if (this.config.timestamps) {
9527
9547
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9528
9548
  }
9529
9549
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
9530
- const attrs = merge(live, preProcessedData);
9550
+ const attrs = merge(originalData, preProcessedData);
9531
9551
  delete attrs.id;
9532
9552
  const { isValid, errors, data: validated } = await this.validate(attrs);
9533
9553
  if (!isValid) {
@@ -9538,6 +9558,9 @@ ${validation.errors.join("\n")}`);
9538
9558
  validation: errors
9539
9559
  });
9540
9560
  }
9561
+ const oldData = { ...originalData, id };
9562
+ const newData = { ...validated, id };
9563
+ await this.handlePartitionReferenceUpdates(oldData, newData);
9541
9564
  const mappedData = await this.schema.mapper(validated);
9542
9565
  const behaviorImpl = getBehavior(this.behavior);
9543
9566
  const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
@@ -9573,7 +9596,6 @@ ${validation.errors.join("\n")}`);
9573
9596
  });
9574
9597
  validated.id = id;
9575
9598
  await this.executeHooks("afterUpdate", validated);
9576
- await this.updatePartitionReferences(validated);
9577
9599
  this.emit("update", preProcessedData, validated);
9578
9600
  return validated;
9579
9601
  }
@@ -9860,23 +9882,104 @@ ${validation.errors.join("\n")}`);
9860
9882
  * });
9861
9883
  */
9862
9884
  async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9863
- if (!partition) {
9864
- const ids2 = await this.listIds({ partition, partitionValues });
9865
- let filteredIds2 = ids2.slice(offset);
9885
+ try {
9886
+ if (!partition) {
9887
+ let ids2 = [];
9888
+ try {
9889
+ ids2 = await this.listIds({ partition, partitionValues });
9890
+ } catch (listIdsError) {
9891
+ console.warn(`Failed to get list IDs:`, listIdsError.message);
9892
+ return [];
9893
+ }
9894
+ let filteredIds2 = ids2.slice(offset);
9895
+ if (limit) {
9896
+ filteredIds2 = filteredIds2.slice(0, limit);
9897
+ }
9898
+ const { results: results2, errors: errors2 } = await PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9899
+ console.warn(`Failed to get resource ${id}:`, error.message);
9900
+ return null;
9901
+ }).process(async (id) => {
9902
+ try {
9903
+ return await this.get(id);
9904
+ } catch (error) {
9905
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9906
+ console.warn(`Decryption failed for ${id}, returning basic info`);
9907
+ return {
9908
+ id,
9909
+ _decryptionFailed: true,
9910
+ _error: error.message
9911
+ };
9912
+ }
9913
+ throw error;
9914
+ }
9915
+ });
9916
+ const validResults2 = results2.filter((item) => item !== null);
9917
+ this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9918
+ return validResults2;
9919
+ }
9920
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9921
+ console.warn(`Partition '${partition}' not found in resource '${this.name}'`);
9922
+ this.emit("list", { partition, partitionValues, count: 0, errors: 0 });
9923
+ return [];
9924
+ }
9925
+ const partitionDef = this.config.partitions[partition];
9926
+ const partitionSegments = [];
9927
+ const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9928
+ for (const [fieldName, rule] of sortedFields) {
9929
+ const value = partitionValues[fieldName];
9930
+ if (value !== void 0 && value !== null) {
9931
+ const transformedValue = this.applyPartitionRule(value, rule);
9932
+ partitionSegments.push(`${fieldName}=${transformedValue}`);
9933
+ }
9934
+ }
9935
+ let prefix;
9936
+ if (partitionSegments.length > 0) {
9937
+ prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9938
+ } else {
9939
+ prefix = `resource=${this.name}/partition=${partition}`;
9940
+ }
9941
+ let keys = [];
9942
+ try {
9943
+ keys = await this.client.getAllKeys({ prefix });
9944
+ } catch (getKeysError) {
9945
+ console.warn(`Failed to get partition keys:`, getKeysError.message);
9946
+ return [];
9947
+ }
9948
+ const ids = keys.map((key) => {
9949
+ const parts = key.split("/");
9950
+ const idPart = parts.find((part) => part.startsWith("id="));
9951
+ return idPart ? idPart.replace("id=", "") : null;
9952
+ }).filter(Boolean);
9953
+ let filteredIds = ids.slice(offset);
9866
9954
  if (limit) {
9867
- filteredIds2 = filteredIds2.slice(0, limit);
9955
+ filteredIds = filteredIds.slice(0, limit);
9868
9956
  }
9869
- const { results: results2, errors: errors2 } = await PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9870
- console.warn(`Failed to get resource ${id}:`, error.message);
9957
+ const { results, errors } = await PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9958
+ console.warn(`Failed to get partition resource ${id}:`, error.message);
9871
9959
  return null;
9872
9960
  }).process(async (id) => {
9873
9961
  try {
9874
- return await this.get(id);
9962
+ const keyForId = keys.find((key) => key.includes(`id=${id}`));
9963
+ if (!keyForId) {
9964
+ throw new Error(`Partition key not found for ID ${id}`);
9965
+ }
9966
+ const keyParts = keyForId.split("/");
9967
+ const actualPartitionValues = {};
9968
+ for (const [fieldName, rule] of sortedFields) {
9969
+ const fieldPart = keyParts.find((part) => part.startsWith(`${fieldName}=`));
9970
+ if (fieldPart) {
9971
+ const value = fieldPart.replace(`${fieldName}=`, "");
9972
+ actualPartitionValues[fieldName] = value;
9973
+ }
9974
+ }
9975
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues: actualPartitionValues });
9875
9976
  } catch (error) {
9876
9977
  if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9877
- console.warn(`Decryption failed for ${id}, returning basic info`);
9978
+ console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9878
9979
  return {
9879
9980
  id,
9981
+ _partition: partition,
9982
+ _partitionValues: partitionValues,
9880
9983
  _decryptionFailed: true,
9881
9984
  _error: error.message
9882
9985
  };
@@ -9884,62 +9987,19 @@ ${validation.errors.join("\n")}`);
9884
9987
  throw error;
9885
9988
  }
9886
9989
  });
9887
- const validResults2 = results2.filter((item) => item !== null);
9888
- this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9889
- return validResults2;
9890
- }
9891
- if (!this.config.partitions || !this.config.partitions[partition]) {
9892
- throw new Error(`Partition '${partition}' not found`);
9893
- }
9894
- const partitionDef = this.config.partitions[partition];
9895
- const partitionSegments = [];
9896
- const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9897
- for (const [fieldName, rule] of sortedFields) {
9898
- const value = partitionValues[fieldName];
9899
- if (value !== void 0 && value !== null) {
9900
- const transformedValue = this.applyPartitionRule(value, rule);
9901
- partitionSegments.push(`${fieldName}=${transformedValue}`);
9990
+ const validResults = results.filter((item) => item !== null);
9991
+ this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9992
+ return validResults;
9993
+ } catch (error) {
9994
+ if (error.message.includes("Partition '") && error.message.includes("' not found")) {
9995
+ console.warn(`Partition error in list method:`, error.message);
9996
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
9997
+ return [];
9902
9998
  }
9999
+ console.error(`Critical error in list method:`, error.message);
10000
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10001
+ return [];
9903
10002
  }
9904
- let prefix;
9905
- if (partitionSegments.length > 0) {
9906
- prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9907
- } else {
9908
- prefix = `resource=${this.name}/partition=${partition}`;
9909
- }
9910
- const keys = await this.client.getAllKeys({ prefix });
9911
- const ids = keys.map((key) => {
9912
- const parts = key.split("/");
9913
- const idPart = parts.find((part) => part.startsWith("id="));
9914
- return idPart ? idPart.replace("id=", "") : null;
9915
- }).filter(Boolean);
9916
- let filteredIds = ids.slice(offset);
9917
- if (limit) {
9918
- filteredIds = filteredIds.slice(0, limit);
9919
- }
9920
- const { results, errors } = await PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9921
- console.warn(`Failed to get partition resource ${id}:`, error.message);
9922
- return null;
9923
- }).process(async (id) => {
9924
- try {
9925
- return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9926
- } catch (error) {
9927
- if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9928
- console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9929
- return {
9930
- id,
9931
- _partition: partition,
9932
- _partitionValues: partitionValues,
9933
- _decryptionFailed: true,
9934
- _error: error.message
9935
- };
9936
- }
9937
- throw error;
9938
- }
9939
- });
9940
- const validResults = results.filter((item) => item !== null);
9941
- this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9942
- return validResults;
9943
10003
  }
9944
10004
  /**
9945
10005
  * Get multiple resources by their IDs
@@ -10046,36 +10106,67 @@ ${validation.errors.join("\n")}`);
10046
10106
  * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
10047
10107
  */
10048
10108
  async page({ offset = 0, size = 100, partition = null, partitionValues = {}, skipCount = false } = {}) {
10049
- let totalItems = null;
10050
- let totalPages = null;
10051
- if (!skipCount) {
10052
- totalItems = await this.count({ partition, partitionValues });
10053
- totalPages = Math.ceil(totalItems / size);
10054
- }
10055
- const page = Math.floor(offset / size);
10056
- const items = await this.list({
10057
- partition,
10058
- partitionValues,
10059
- limit: size,
10060
- offset
10061
- });
10062
- const result = {
10063
- items,
10064
- totalItems,
10065
- page,
10066
- pageSize: size,
10067
- totalPages,
10068
- // Add additional metadata for debugging
10069
- _debug: {
10070
- requestedSize: size,
10071
- requestedOffset: offset,
10072
- actualItemsReturned: items.length,
10073
- skipCount,
10074
- hasTotalItems: totalItems !== null
10109
+ try {
10110
+ let totalItems = null;
10111
+ let totalPages = null;
10112
+ if (!skipCount) {
10113
+ try {
10114
+ totalItems = await this.count({ partition, partitionValues });
10115
+ totalPages = Math.ceil(totalItems / size);
10116
+ } catch (countError) {
10117
+ console.warn(`Failed to get count for page:`, countError.message);
10118
+ totalItems = null;
10119
+ totalPages = null;
10120
+ }
10075
10121
  }
10076
- };
10077
- this.emit("page", result);
10078
- return result;
10122
+ const page = Math.floor(offset / size);
10123
+ let items = [];
10124
+ try {
10125
+ items = await this.list({
10126
+ partition,
10127
+ partitionValues,
10128
+ limit: size,
10129
+ offset
10130
+ });
10131
+ } catch (listError) {
10132
+ console.warn(`Failed to get items for page:`, listError.message);
10133
+ items = [];
10134
+ }
10135
+ const result = {
10136
+ items,
10137
+ totalItems,
10138
+ page,
10139
+ pageSize: size,
10140
+ totalPages,
10141
+ // Add additional metadata for debugging
10142
+ _debug: {
10143
+ requestedSize: size,
10144
+ requestedOffset: offset,
10145
+ actualItemsReturned: items.length,
10146
+ skipCount,
10147
+ hasTotalItems: totalItems !== null
10148
+ }
10149
+ };
10150
+ this.emit("page", result);
10151
+ return result;
10152
+ } catch (error) {
10153
+ console.error(`Critical error in page method:`, error.message);
10154
+ return {
10155
+ items: [],
10156
+ totalItems: null,
10157
+ page: Math.floor(offset / size),
10158
+ pageSize: size,
10159
+ totalPages: null,
10160
+ _debug: {
10161
+ requestedSize: size,
10162
+ requestedOffset: offset,
10163
+ actualItemsReturned: 0,
10164
+ skipCount,
10165
+ hasTotalItems: false,
10166
+ error: error.message
10167
+ }
10168
+ };
10169
+ }
10079
10170
  }
10080
10171
  readable() {
10081
10172
  const stream = new ResourceReader({ resource: this });
@@ -10368,7 +10459,118 @@ ${validation.errors.join("\n")}`);
10368
10459
  return results.slice(0, limit);
10369
10460
  }
10370
10461
  /**
10371
- * Update partition objects to keep them in sync
10462
+ * Handle partition reference updates with change detection
10463
+ * @param {Object} oldData - Original object data before update
10464
+ * @param {Object} newData - Updated object data
10465
+ */
10466
+ async handlePartitionReferenceUpdates(oldData, newData) {
10467
+ const partitions = this.config.partitions;
10468
+ if (!partitions || Object.keys(partitions).length === 0) {
10469
+ return;
10470
+ }
10471
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10472
+ try {
10473
+ await this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData);
10474
+ } catch (error) {
10475
+ console.warn(`Failed to update partition references for ${partitionName}:`, error.message);
10476
+ }
10477
+ }
10478
+ const id = newData.id || oldData.id;
10479
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10480
+ const prefix = `resource=${this.name}/partition=${partitionName}`;
10481
+ let allKeys = [];
10482
+ try {
10483
+ allKeys = await this.client.getAllKeys({ prefix });
10484
+ } catch (error) {
10485
+ console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, error.message);
10486
+ continue;
10487
+ }
10488
+ const validKey = this.getPartitionKey({ partitionName, id, data: newData });
10489
+ for (const key of allKeys) {
10490
+ if (key.endsWith(`/id=${id}`) && key !== validKey) {
10491
+ try {
10492
+ await this.client.deleteObject(key);
10493
+ } catch (error) {
10494
+ console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, error.message);
10495
+ }
10496
+ }
10497
+ }
10498
+ }
10499
+ }
10500
+ /**
10501
+ * Handle partition reference update for a specific partition
10502
+ * @param {string} partitionName - Name of the partition
10503
+ * @param {Object} partition - Partition definition
10504
+ * @param {Object} oldData - Original object data before update
10505
+ * @param {Object} newData - Updated object data
10506
+ */
10507
+ async handlePartitionReferenceUpdate(partitionName, partition, oldData, newData) {
10508
+ const id = newData.id || oldData.id;
10509
+ const oldPartitionKey = this.getPartitionKey({ partitionName, id, data: oldData });
10510
+ const newPartitionKey = this.getPartitionKey({ partitionName, id, data: newData });
10511
+ if (oldPartitionKey !== newPartitionKey) {
10512
+ if (oldPartitionKey) {
10513
+ try {
10514
+ await this.client.deleteObject(oldPartitionKey);
10515
+ } catch (error) {
10516
+ console.warn(`Old partition object could not be deleted for ${partitionName}:`, error.message);
10517
+ }
10518
+ }
10519
+ if (newPartitionKey) {
10520
+ try {
10521
+ const mappedData = await this.schema.mapper(newData);
10522
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10523
+ const behaviorImpl = getBehavior(this.behavior);
10524
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10525
+ resource: this,
10526
+ id,
10527
+ data: newData,
10528
+ mappedData
10529
+ });
10530
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10531
+ const partitionMetadata = {
10532
+ ...processedMetadata,
10533
+ _version: this.version
10534
+ };
10535
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10536
+ await this.client.putObject({
10537
+ key: newPartitionKey,
10538
+ metadata: partitionMetadata,
10539
+ body
10540
+ });
10541
+ } catch (error) {
10542
+ console.warn(`New partition object could not be created for ${partitionName}:`, error.message);
10543
+ }
10544
+ }
10545
+ } else if (newPartitionKey) {
10546
+ try {
10547
+ const mappedData = await this.schema.mapper(newData);
10548
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10549
+ const behaviorImpl = getBehavior(this.behavior);
10550
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10551
+ resource: this,
10552
+ id,
10553
+ data: newData,
10554
+ mappedData
10555
+ });
10556
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10557
+ const partitionMetadata = {
10558
+ ...processedMetadata,
10559
+ _version: this.version
10560
+ };
10561
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10562
+ await this.client.putObject({
10563
+ key: newPartitionKey,
10564
+ metadata: partitionMetadata,
10565
+ body
10566
+ });
10567
+ } catch (error) {
10568
+ console.warn(`Partition object could not be updated for ${partitionName}:`, error.message);
10569
+ }
10570
+ }
10571
+ }
10572
+ /**
10573
+ * Update partition objects to keep them in sync (legacy method for backward compatibility)
10372
10574
  * @param {Object} data - Updated object data
10373
10575
  */
10374
10576
  async updatePartitionReferences(data) {
@@ -10377,6 +10579,10 @@ ${validation.errors.join("\n")}`);
10377
10579
  return;
10378
10580
  }
10379
10581
  for (const [partitionName, partition] of Object.entries(partitions)) {
10582
+ if (!partition || !partition.fields || typeof partition.fields !== "object") {
10583
+ console.warn(`Skipping invalid partition '${partitionName}' in resource '${this.name}'`);
10584
+ continue;
10585
+ }
10380
10586
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
10381
10587
  if (partitionKey) {
10382
10588
  const mappedData = await this.schema.mapper(data);
@@ -10475,6 +10681,7 @@ ${validation.errors.join("\n")}`);
10475
10681
  if (request.VersionId) data._versionId = request.VersionId;
10476
10682
  if (request.Expiration) data._expiresAt = request.Expiration;
10477
10683
  data._definitionHash = this.getDefinitionHash();
10684
+ if (data.undefined !== void 0) delete data.undefined;
10478
10685
  this.emit("getFromPartition", data);
10479
10686
  return data;
10480
10687
  }
@@ -10578,7 +10785,7 @@ class Database extends EventEmitter {
10578
10785
  this.version = "1";
10579
10786
  this.s3dbVersion = (() => {
10580
10787
  try {
10581
- return true ? "4.1.12" : "latest";
10788
+ return true ? "5.0.0" : "latest";
10582
10789
  } catch (e) {
10583
10790
  return "latest";
10584
10791
  }
@@ -10593,14 +10800,24 @@ class Database extends EventEmitter {
10593
10800
  this.passphrase = options.passphrase || "secret";
10594
10801
  let connectionString = options.connectionString;
10595
10802
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
10596
- connectionString = ConnectionString.buildFromParams({
10597
- bucket: options.bucket,
10598
- region: options.region,
10599
- accessKeyId: options.accessKeyId,
10600
- secretAccessKey: options.secretAccessKey,
10601
- endpoint: options.endpoint,
10602
- forcePathStyle: options.forcePathStyle
10603
- });
10803
+ const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
10804
+ if (endpoint) {
10805
+ const url = new URL(endpoint);
10806
+ if (accessKeyId) url.username = encodeURIComponent(accessKeyId);
10807
+ if (secretAccessKey) url.password = encodeURIComponent(secretAccessKey);
10808
+ url.pathname = `/${bucket || "s3db"}`;
10809
+ if (forcePathStyle) {
10810
+ url.searchParams.set("forcePathStyle", "true");
10811
+ }
10812
+ connectionString = url.toString();
10813
+ } else if (accessKeyId && secretAccessKey) {
10814
+ const params = new URLSearchParams();
10815
+ params.set("region", region || "us-east-1");
10816
+ if (forcePathStyle) {
10817
+ params.set("forcePathStyle", "true");
10818
+ }
10819
+ connectionString = `s3://${encodeURIComponent(accessKeyId)}:${encodeURIComponent(secretAccessKey)}@${bucket || "s3db"}?${params.toString()}`;
10820
+ }
10604
10821
  }
10605
10822
  this.client = options.client || new Client({
10606
10823
  verbose: this.verbose,
@@ -10636,12 +10853,12 @@ class Database extends EventEmitter {
10636
10853
  passphrase: this.passphrase,
10637
10854
  observers: [this],
10638
10855
  cache: this.cache,
10639
- timestamps: versionData.options?.timestamps || false,
10640
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
10641
- paranoid: versionData.options?.paranoid !== false,
10642
- allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
10643
- autoDecrypt: versionData.options?.autoDecrypt !== false,
10644
- hooks: versionData.options?.hooks || {}
10856
+ timestamps: versionData.timestamps !== void 0 ? versionData.timestamps : false,
10857
+ partitions: resourceMetadata.partitions || versionData.partitions || {},
10858
+ paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
10859
+ allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
10860
+ autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
10861
+ hooks: versionData.hooks || {}
10645
10862
  });
10646
10863
  }
10647
10864
  }
@@ -10779,16 +10996,14 @@ class Database extends EventEmitter {
10779
10996
  [version]: {
10780
10997
  hash: definitionHash,
10781
10998
  attributes: resourceDef.attributes,
10782
- options: {
10783
- timestamps: resource.config.timestamps,
10784
- partitions: resource.config.partitions,
10785
- paranoid: resource.config.paranoid,
10786
- allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
10787
- autoDecrypt: resource.config.autoDecrypt,
10788
- cache: resource.config.cache,
10789
- hooks: resourceDef.hooks || {}
10790
- },
10791
10999
  behavior: resourceDef.behavior || "user-management",
11000
+ timestamps: resource.config.timestamps,
11001
+ partitions: resource.config.partitions,
11002
+ paranoid: resource.config.paranoid,
11003
+ allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
11004
+ autoDecrypt: resource.config.autoDecrypt,
11005
+ cache: resource.config.cache,
11006
+ hooks: resource.config.hooks,
10792
11007
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10793
11008
  }
10794
11009
  }
@@ -10823,73 +11038,58 @@ class Database extends EventEmitter {
10823
11038
  }
10824
11039
  /**
10825
11040
  * Check if a resource exists with the same definition hash
10826
- * @param {string} name - Resource name
10827
- * @param {Object} attributes - Resource attributes
10828
- * @param {Object} options - Resource options
10829
- * @param {string} behavior - Resource behavior
10830
- * @returns {Object} Object with exists flag and hash information
11041
+ * @param {Object} config - Resource configuration
11042
+ * @param {string} config.name - Resource name
11043
+ * @param {Object} config.attributes - Resource attributes
11044
+ * @param {string} [config.behavior] - Resource behavior
11045
+ * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
11046
+ * @returns {Object} Result with exists and hash information
10831
11047
  */
10832
- resourceExistsWithSameHash({ name, attributes, options = {}, behavior = "user-management" }) {
11048
+ resourceExistsWithSameHash({ name, attributes, behavior = "user-management", options = {} }) {
10833
11049
  if (!this.resources[name]) {
10834
11050
  return { exists: false, sameHash: false, hash: null };
10835
11051
  }
10836
- const tempResource = new Resource({
11052
+ const existingResource = this.resources[name];
11053
+ const existingHash = this.generateDefinitionHash(existingResource.export());
11054
+ const mockResource = new Resource({
10837
11055
  name,
10838
11056
  attributes,
10839
11057
  behavior,
10840
- observers: [],
10841
11058
  client: this.client,
10842
- version: "temp",
11059
+ version: existingResource.version,
10843
11060
  passphrase: this.passphrase,
10844
- cache: this.cache,
10845
11061
  ...options
10846
11062
  });
10847
- const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10848
- const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
11063
+ const newHash = this.generateDefinitionHash(mockResource.export());
10849
11064
  return {
10850
11065
  exists: true,
10851
- sameHash: newHash === existingHash,
11066
+ sameHash: existingHash === newHash,
10852
11067
  hash: newHash,
10853
11068
  existingHash
10854
11069
  };
10855
11070
  }
10856
- /**
10857
- * Create a resource only if it doesn't exist with the same definition hash
10858
- * @param {Object} params - Resource parameters
10859
- * @param {string} params.name - Resource name
10860
- * @param {Object} params.attributes - Resource attributes
10861
- * @param {Object} params.options - Resource options
10862
- * @param {string} params.behavior - Resource behavior
10863
- * @returns {Object} Object with resource and created flag
10864
- */
10865
- async createResourceIfNotExists({ name, attributes, options = {}, behavior = "user-management" }) {
10866
- const alreadyExists = !!this.resources[name];
10867
- const hashCheck = this.resourceExistsWithSameHash({ name, attributes, options, behavior });
10868
- if (hashCheck.exists && hashCheck.sameHash) {
10869
- return {
10870
- resource: this.resources[name],
10871
- created: false,
10872
- reason: "Resource already exists with same definition hash"
10873
- };
10874
- }
10875
- const resource = await this.createResource({ name, attributes, options, behavior });
10876
- return {
10877
- resource,
10878
- created: !alreadyExists,
10879
- reason: alreadyExists ? "Resource updated with new definition" : "New resource created"
10880
- };
10881
- }
10882
- async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
11071
+ async createResource({ name, attributes, behavior = "user-management", hooks, ...config }) {
10883
11072
  if (this.resources[name]) {
10884
11073
  const existingResource = this.resources[name];
10885
11074
  Object.assign(existingResource.config, {
10886
11075
  cache: this.cache,
10887
- ...options
11076
+ ...config
10888
11077
  });
10889
11078
  if (behavior) {
10890
11079
  existingResource.behavior = behavior;
10891
11080
  }
10892
11081
  existingResource.updateAttributes(attributes);
11082
+ if (hooks) {
11083
+ for (const [event, hooksArr] of Object.entries(hooks)) {
11084
+ if (Array.isArray(hooksArr) && existingResource.hooks[event]) {
11085
+ for (const fn of hooksArr) {
11086
+ if (typeof fn === "function") {
11087
+ existingResource.hooks[event].push(fn.bind(existingResource));
11088
+ }
11089
+ }
11090
+ }
11091
+ }
11092
+ }
10893
11093
  const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
10894
11094
  const existingMetadata2 = this.savedMetadata?.resources?.[name];
10895
11095
  const currentVersion = existingMetadata2?.currentVersion || "v0";
@@ -10911,7 +11111,8 @@ class Database extends EventEmitter {
10911
11111
  version,
10912
11112
  passphrase: this.passphrase,
10913
11113
  cache: this.cache,
10914
- ...options
11114
+ hooks,
11115
+ ...config
10915
11116
  });
10916
11117
  this.resources[name] = resource;
10917
11118
  await this.uploadMetadataFile();
@@ -17613,4 +17814,4 @@ class CachePlugin extends Plugin {
17613
17814
  }
17614
17815
  }
17615
17816
 
17616
- export { AuthenticationError, BaseError, Cache, CachePlugin, Client, ConnectionString, CostsPlugin, Database, DatabaseError, EncryptionError, ErrorMap, InvalidResourceItem, MemoryCache, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PermissionError, Plugin, PluginObject, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceNotFound as ResourceNotFoundError, ResourceReader, ResourceWriter, S3Cache, S3db as S3DB, S3DBError, S3_DEFAULT_ENDPOINT, S3_DEFAULT_REGION, S3db, S3DBError as S3dbError, UnknownError, ValidationError, Validator, ValidatorManager, decrypt, S3db as default, encrypt, sha256, streamToString };
17817
+ export { AVAILABLE_BEHAVIORS, AuthenticationError, BaseError, Cache, CachePlugin, Client, ConnectionString, CostsPlugin, DEFAULT_BEHAVIOR, Database, DatabaseError, EncryptionError, ErrorMap, InvalidResourceItem, MemoryCache, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PermissionError, Plugin, PluginObject, Resource, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3Cache, S3DBError, S3_DEFAULT_ENDPOINT, S3_DEFAULT_REGION, S3db, Schema, SchemaActions, UnknownError, ValidationError, Validator, ValidatorManager, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateTotalSize, calculateUTF8Bytes, decrypt, S3db as default, encrypt, getBehavior, getSizeBreakdown, idGenerator, passwordGenerator, sha256, streamToString, transformValue };