s3db.js 4.1.14 → 5.1.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
 
@@ -3388,7 +3388,7 @@ function requireJsonStableStringify () {
3388
3388
  var jsonStableStringifyExports = requireJsonStableStringify();
3389
3389
  var jsonStableStringify = /*@__PURE__*/getDefaultExportFromCjs(jsonStableStringifyExports);
3390
3390
 
3391
- async function custom(actual, errors, schema) {
3391
+ async function secretHandler(actual, errors, schema) {
3392
3392
  if (!this.passphrase) {
3393
3393
  errors.push({ actual, type: "encryptionKeyMissing" });
3394
3394
  return actual;
@@ -3401,6 +3401,10 @@ async function custom(actual, errors, schema) {
3401
3401
  }
3402
3402
  return actual;
3403
3403
  }
3404
+ async function jsonHandler(actual, errors, schema) {
3405
+ if (isString$1(actual)) return actual;
3406
+ return JSON.stringify(actual);
3407
+ }
3404
3408
  class Validator extends FastestValidator {
3405
3409
  constructor({ options, passphrase, autoEncrypt = true } = {}) {
3406
3410
  super(merge({}, {
@@ -3422,7 +3426,7 @@ class Validator extends FastestValidator {
3422
3426
  this.autoEncrypt = autoEncrypt;
3423
3427
  this.alias("secret", {
3424
3428
  type: "string",
3425
- custom: this.autoEncrypt ? custom : void 0,
3429
+ custom: this.autoEncrypt ? secretHandler : void 0,
3426
3430
  messages: {
3427
3431
  string: "The '{field}' field must be a string.",
3428
3432
  stringMin: "This secret '{field}' field length must be at least {expected} long."
@@ -3430,11 +3434,15 @@ class Validator extends FastestValidator {
3430
3434
  });
3431
3435
  this.alias("secretAny", {
3432
3436
  type: "any",
3433
- custom: this.autoEncrypt ? custom : void 0
3437
+ custom: this.autoEncrypt ? secretHandler : void 0
3434
3438
  });
3435
3439
  this.alias("secretNumber", {
3436
3440
  type: "number",
3437
- custom: this.autoEncrypt ? custom : void 0
3441
+ custom: this.autoEncrypt ? secretHandler : void 0
3442
+ });
3443
+ this.alias("json", {
3444
+ type: "any",
3445
+ custom: this.autoEncrypt ? jsonHandler : void 0
3438
3446
  });
3439
3447
  }
3440
3448
  }
@@ -3446,10 +3454,31 @@ const ValidatorManager = new Proxy(Validator, {
3446
3454
  }
3447
3455
  });
3448
3456
 
3457
+ function toBase36(num) {
3458
+ return num.toString(36);
3459
+ }
3460
+ function generateBase36Mapping(keys) {
3461
+ const mapping = {};
3462
+ const reversedMapping = {};
3463
+ keys.forEach((key, index) => {
3464
+ const base36Key = toBase36(index);
3465
+ mapping[key] = base36Key;
3466
+ reversedMapping[base36Key] = key;
3467
+ });
3468
+ return { mapping, reversedMapping };
3469
+ }
3449
3470
  const SchemaActions = {
3450
3471
  trim: (value) => value.trim(),
3451
3472
  encrypt: (value, { passphrase }) => encrypt(value, passphrase),
3452
- decrypt: (value, { passphrase }) => decrypt(value, passphrase),
3473
+ decrypt: async (value, { passphrase }) => {
3474
+ try {
3475
+ const raw = await decrypt(value, passphrase);
3476
+ return raw;
3477
+ } catch (error) {
3478
+ console.warn(`Schema decrypt error: ${error}`, error);
3479
+ return value;
3480
+ }
3481
+ },
3453
3482
  toString: (value) => String(value),
3454
3483
  fromArray: (value, { separator }) => {
3455
3484
  if (value === null || value === void 0 || !Array.isArray(value)) {
@@ -3540,8 +3569,9 @@ class Schema {
3540
3569
  const leafKeys = Object.keys(flatAttrs).filter((k) => !k.includes("$$"));
3541
3570
  const objectKeys = this.extractObjectKeys(this.attributes);
3542
3571
  const allKeys = [.../* @__PURE__ */ new Set([...leafKeys, ...objectKeys])];
3543
- this.reversedMap = { ...allKeys };
3544
- this.map = invert(this.reversedMap);
3572
+ const { mapping, reversedMapping } = generateBase36Mapping(allKeys);
3573
+ this.map = mapping;
3574
+ this.reversedMap = reversedMapping;
3545
3575
  }
3546
3576
  }
3547
3577
  defaultOptions() {
@@ -3582,24 +3612,27 @@ class Schema {
3582
3612
  if (definition.includes("array")) {
3583
3613
  this.addHook("beforeMap", name, "fromArray");
3584
3614
  this.addHook("afterUnmap", name, "toArray");
3585
- } else {
3586
- if (definition.includes("secret")) {
3587
- if (this.options.autoEncrypt) {
3588
- this.addHook("beforeMap", name, "encrypt");
3589
- }
3590
- if (this.options.autoDecrypt) {
3591
- this.addHook("afterUnmap", name, "decrypt");
3592
- }
3593
- }
3594
- if (definition.includes("number")) {
3595
- this.addHook("beforeMap", name, "toString");
3596
- this.addHook("afterUnmap", name, "toNumber");
3615
+ }
3616
+ if (definition.includes("secret")) {
3617
+ if (this.options.autoEncrypt) {
3618
+ this.addHook("beforeMap", name, "encrypt");
3597
3619
  }
3598
- if (definition.includes("boolean")) {
3599
- this.addHook("beforeMap", name, "fromBool");
3600
- this.addHook("afterUnmap", name, "toBool");
3620
+ if (this.options.autoDecrypt) {
3621
+ this.addHook("afterUnmap", name, "decrypt");
3601
3622
  }
3602
3623
  }
3624
+ if (definition.includes("number")) {
3625
+ this.addHook("beforeMap", name, "toString");
3626
+ this.addHook("afterUnmap", name, "toNumber");
3627
+ }
3628
+ if (definition.includes("boolean")) {
3629
+ this.addHook("beforeMap", name, "fromBool");
3630
+ this.addHook("afterUnmap", name, "toBool");
3631
+ }
3632
+ if (definition.includes("json")) {
3633
+ this.addHook("beforeMap", name, "toJSON");
3634
+ this.addHook("afterUnmap", name, "fromJSON");
3635
+ }
3603
3636
  }
3604
3637
  }
3605
3638
  static import(data) {
@@ -8614,7 +8647,6 @@ function calculateUTF8Bytes(str) {
8614
8647
  function calculateAttributeNamesSize(mappedObject) {
8615
8648
  let totalSize = 0;
8616
8649
  for (const key of Object.keys(mappedObject)) {
8617
- if (key === "_v") continue;
8618
8650
  totalSize += calculateUTF8Bytes(key);
8619
8651
  }
8620
8652
  return totalSize;
@@ -8658,6 +8690,30 @@ function calculateTotalSize(mappedObject) {
8658
8690
  const namesSize = calculateAttributeNamesSize(mappedObject);
8659
8691
  return valueTotal + namesSize;
8660
8692
  }
8693
+ function getSizeBreakdown(mappedObject) {
8694
+ const valueSizes = calculateAttributeSizes(mappedObject);
8695
+ const namesSize = calculateAttributeNamesSize(mappedObject);
8696
+ const valueTotal = Object.values(valueSizes).reduce((sum, size) => sum + size, 0);
8697
+ const total = valueTotal + namesSize;
8698
+ const sortedAttributes = Object.entries(valueSizes).sort(([, a], [, b]) => b - a).map(([key, size]) => ({
8699
+ attribute: key,
8700
+ size,
8701
+ percentage: (size / total * 100).toFixed(2) + "%"
8702
+ }));
8703
+ return {
8704
+ total,
8705
+ valueSizes,
8706
+ namesSize,
8707
+ valueTotal,
8708
+ breakdown: sortedAttributes,
8709
+ // Add detailed breakdown including names
8710
+ detailedBreakdown: {
8711
+ values: valueTotal,
8712
+ names: namesSize,
8713
+ total
8714
+ }
8715
+ };
8716
+ }
8661
8717
 
8662
8718
  const S3_METADATA_LIMIT_BYTES = 2048;
8663
8719
  async function handleInsert$3({ resource, data, mappedData }) {
@@ -8878,6 +8934,7 @@ function getBehavior(behaviorName) {
8878
8934
  }
8879
8935
  return behavior;
8880
8936
  }
8937
+ const AVAILABLE_BEHAVIORS = Object.keys(behaviors);
8881
8938
  const DEFAULT_BEHAVIOR = "user-management";
8882
8939
 
8883
8940
  class Resource extends EventEmitter {
@@ -8947,17 +9004,8 @@ ${validation.errors.join("\n")}`);
8947
9004
  partitions = {},
8948
9005
  paranoid = true,
8949
9006
  allNestedObjectsOptional = true,
8950
- hooks = {},
8951
- options = {}
9007
+ hooks = {}
8952
9008
  } = 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
9009
  this.name = name;
8962
9010
  this.client = client;
8963
9011
  this.version = version;
@@ -8966,13 +9014,13 @@ ${validation.errors.join("\n")}`);
8966
9014
  this.parallelism = parallelism;
8967
9015
  this.passphrase = passphrase ?? "secret";
8968
9016
  this.config = {
8969
- cache: mergedOptions.cache,
9017
+ cache,
8970
9018
  hooks,
8971
- paranoid: mergedOptions.paranoid,
8972
- timestamps: mergedOptions.timestamps,
8973
- partitions: mergedOptions.partitions,
8974
- autoDecrypt: mergedOptions.autoDecrypt,
8975
- allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
9019
+ paranoid,
9020
+ timestamps,
9021
+ partitions,
9022
+ autoDecrypt,
9023
+ allNestedObjectsOptional
8976
9024
  };
8977
9025
  this.hooks = {
8978
9026
  preInsert: [],
@@ -8983,39 +9031,7 @@ ${validation.errors.join("\n")}`);
8983
9031
  afterDelete: []
8984
9032
  };
8985
9033
  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();
9034
+ this.applyConfiguration();
9019
9035
  if (hooks) {
9020
9036
  for (const [event, hooksArr] of Object.entries(hooks)) {
9021
9037
  if (Array.isArray(hooksArr) && this.hooks[event]) {
@@ -9054,15 +9070,17 @@ ${validation.errors.join("\n")}`);
9054
9070
  return exported;
9055
9071
  }
9056
9072
  /**
9057
- * Update resource attributes and rebuild schema
9058
- * @param {Object} newAttributes - New attributes definition
9073
+ * Apply configuration settings (timestamps, partitions, hooks)
9074
+ * This method ensures that all configuration-dependent features are properly set up
9059
9075
  */
9060
- updateAttributes(newAttributes) {
9061
- const oldAttributes = this.attributes;
9062
- this.attributes = newAttributes;
9076
+ applyConfiguration() {
9063
9077
  if (this.config.timestamps) {
9064
- newAttributes.createdAt = "string|optional";
9065
- newAttributes.updatedAt = "string|optional";
9078
+ if (!this.attributes.createdAt) {
9079
+ this.attributes.createdAt = "string|optional";
9080
+ }
9081
+ if (!this.attributes.updatedAt) {
9082
+ this.attributes.updatedAt = "string|optional";
9083
+ }
9066
9084
  if (!this.config.partitions) {
9067
9085
  this.config.partitions = {};
9068
9086
  }
@@ -9081,9 +9099,10 @@ ${validation.errors.join("\n")}`);
9081
9099
  };
9082
9100
  }
9083
9101
  }
9102
+ this.setupPartitionHooks();
9084
9103
  this.schema = new Schema({
9085
9104
  name: this.name,
9086
- attributes: newAttributes,
9105
+ attributes: this.attributes,
9087
9106
  passphrase: this.passphrase,
9088
9107
  version: this.version,
9089
9108
  options: {
@@ -9091,8 +9110,16 @@ ${validation.errors.join("\n")}`);
9091
9110
  allNestedObjectsOptional: this.config.allNestedObjectsOptional
9092
9111
  }
9093
9112
  });
9094
- this.setupPartitionHooks();
9095
9113
  this.validatePartitions();
9114
+ }
9115
+ /**
9116
+ * Update resource attributes and rebuild schema
9117
+ * @param {Object} newAttributes - New attributes definition
9118
+ */
9119
+ updateAttributes(newAttributes) {
9120
+ const oldAttributes = this.attributes;
9121
+ this.attributes = newAttributes;
9122
+ this.applyConfiguration();
9096
9123
  return { oldAttributes, newAttributes };
9097
9124
  }
9098
9125
  /**
@@ -9180,7 +9207,7 @@ ${validation.errors.join("\n")}`);
9180
9207
  for (const fieldName of Object.keys(partitionDef.fields)) {
9181
9208
  if (!this.fieldExistsInAttributes(fieldName)) {
9182
9209
  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.`
9210
+ `Partition '${partitionName}' uses field '${fieldName}' which does not exist in resource attributes. Available fields: ${currentAttributes.join(", ")}.`
9184
9211
  );
9185
9212
  }
9186
9213
  }
@@ -9293,7 +9320,11 @@ ${validation.errors.join("\n")}`);
9293
9320
  if (partitionSegments.length === 0) {
9294
9321
  return null;
9295
9322
  }
9296
- return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${id}`);
9323
+ const finalId = id || data?.id;
9324
+ if (!finalId) {
9325
+ return null;
9326
+ }
9327
+ return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${finalId}`);
9297
9328
  }
9298
9329
  /**
9299
9330
  * Get nested field value from data object using dot notation
@@ -9522,12 +9553,12 @@ ${validation.errors.join("\n")}`);
9522
9553
  * console.log(updatedUser.updatedAt); // ISO timestamp
9523
9554
  */
9524
9555
  async update(id, attributes) {
9525
- const live = await this.get(id);
9556
+ const originalData = await this.get(id);
9526
9557
  if (this.config.timestamps) {
9527
9558
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9528
9559
  }
9529
9560
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
9530
- const attrs = merge(live, preProcessedData);
9561
+ const attrs = merge(originalData, preProcessedData);
9531
9562
  delete attrs.id;
9532
9563
  const { isValid, errors, data: validated } = await this.validate(attrs);
9533
9564
  if (!isValid) {
@@ -9538,6 +9569,9 @@ ${validation.errors.join("\n")}`);
9538
9569
  validation: errors
9539
9570
  });
9540
9571
  }
9572
+ const oldData = { ...originalData, id };
9573
+ const newData = { ...validated, id };
9574
+ await this.handlePartitionReferenceUpdates(oldData, newData);
9541
9575
  const mappedData = await this.schema.mapper(validated);
9542
9576
  const behaviorImpl = getBehavior(this.behavior);
9543
9577
  const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
@@ -9573,7 +9607,6 @@ ${validation.errors.join("\n")}`);
9573
9607
  });
9574
9608
  validated.id = id;
9575
9609
  await this.executeHooks("afterUpdate", validated);
9576
- await this.updatePartitionReferences(validated);
9577
9610
  this.emit("update", preProcessedData, validated);
9578
9611
  return validated;
9579
9612
  }
@@ -9860,23 +9893,104 @@ ${validation.errors.join("\n")}`);
9860
9893
  * });
9861
9894
  */
9862
9895
  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);
9896
+ try {
9897
+ if (!partition) {
9898
+ let ids2 = [];
9899
+ try {
9900
+ ids2 = await this.listIds({ partition, partitionValues });
9901
+ } catch (listIdsError) {
9902
+ console.warn(`Failed to get list IDs:`, listIdsError.message);
9903
+ return [];
9904
+ }
9905
+ let filteredIds2 = ids2.slice(offset);
9906
+ if (limit) {
9907
+ filteredIds2 = filteredIds2.slice(0, limit);
9908
+ }
9909
+ const { results: results2, errors: errors2 } = await PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9910
+ console.warn(`Failed to get resource ${id}:`, error.message);
9911
+ return null;
9912
+ }).process(async (id) => {
9913
+ try {
9914
+ return await this.get(id);
9915
+ } catch (error) {
9916
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9917
+ console.warn(`Decryption failed for ${id}, returning basic info`);
9918
+ return {
9919
+ id,
9920
+ _decryptionFailed: true,
9921
+ _error: error.message
9922
+ };
9923
+ }
9924
+ throw error;
9925
+ }
9926
+ });
9927
+ const validResults2 = results2.filter((item) => item !== null);
9928
+ this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9929
+ return validResults2;
9930
+ }
9931
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9932
+ console.warn(`Partition '${partition}' not found in resource '${this.name}'`);
9933
+ this.emit("list", { partition, partitionValues, count: 0, errors: 0 });
9934
+ return [];
9935
+ }
9936
+ const partitionDef = this.config.partitions[partition];
9937
+ const partitionSegments = [];
9938
+ const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9939
+ for (const [fieldName, rule] of sortedFields) {
9940
+ const value = partitionValues[fieldName];
9941
+ if (value !== void 0 && value !== null) {
9942
+ const transformedValue = this.applyPartitionRule(value, rule);
9943
+ partitionSegments.push(`${fieldName}=${transformedValue}`);
9944
+ }
9945
+ }
9946
+ let prefix;
9947
+ if (partitionSegments.length > 0) {
9948
+ prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9949
+ } else {
9950
+ prefix = `resource=${this.name}/partition=${partition}`;
9951
+ }
9952
+ let keys = [];
9953
+ try {
9954
+ keys = await this.client.getAllKeys({ prefix });
9955
+ } catch (getKeysError) {
9956
+ console.warn(`Failed to get partition keys:`, getKeysError.message);
9957
+ return [];
9958
+ }
9959
+ const ids = keys.map((key) => {
9960
+ const parts = key.split("/");
9961
+ const idPart = parts.find((part) => part.startsWith("id="));
9962
+ return idPart ? idPart.replace("id=", "") : null;
9963
+ }).filter(Boolean);
9964
+ let filteredIds = ids.slice(offset);
9866
9965
  if (limit) {
9867
- filteredIds2 = filteredIds2.slice(0, limit);
9966
+ filteredIds = filteredIds.slice(0, limit);
9868
9967
  }
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);
9968
+ const { results, errors } = await PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9969
+ console.warn(`Failed to get partition resource ${id}:`, error.message);
9871
9970
  return null;
9872
9971
  }).process(async (id) => {
9873
9972
  try {
9874
- return await this.get(id);
9973
+ const keyForId = keys.find((key) => key.includes(`id=${id}`));
9974
+ if (!keyForId) {
9975
+ throw new Error(`Partition key not found for ID ${id}`);
9976
+ }
9977
+ const keyParts = keyForId.split("/");
9978
+ const actualPartitionValues = {};
9979
+ for (const [fieldName, rule] of sortedFields) {
9980
+ const fieldPart = keyParts.find((part) => part.startsWith(`${fieldName}=`));
9981
+ if (fieldPart) {
9982
+ const value = fieldPart.replace(`${fieldName}=`, "");
9983
+ actualPartitionValues[fieldName] = value;
9984
+ }
9985
+ }
9986
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues: actualPartitionValues });
9875
9987
  } catch (error) {
9876
9988
  if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9877
- console.warn(`Decryption failed for ${id}, returning basic info`);
9989
+ console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9878
9990
  return {
9879
9991
  id,
9992
+ _partition: partition,
9993
+ _partitionValues: partitionValues,
9880
9994
  _decryptionFailed: true,
9881
9995
  _error: error.message
9882
9996
  };
@@ -9884,62 +9998,19 @@ ${validation.errors.join("\n")}`);
9884
9998
  throw error;
9885
9999
  }
9886
10000
  });
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}`);
10001
+ const validResults = results.filter((item) => item !== null);
10002
+ this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
10003
+ return validResults;
10004
+ } catch (error) {
10005
+ if (error.message.includes("Partition '") && error.message.includes("' not found")) {
10006
+ console.warn(`Partition error in list method:`, error.message);
10007
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10008
+ return [];
9902
10009
  }
10010
+ console.error(`Critical error in list method:`, error.message);
10011
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10012
+ return [];
9903
10013
  }
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
10014
  }
9944
10015
  /**
9945
10016
  * Get multiple resources by their IDs
@@ -10046,36 +10117,67 @@ ${validation.errors.join("\n")}`);
10046
10117
  * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
10047
10118
  */
10048
10119
  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
10120
+ try {
10121
+ let totalItems = null;
10122
+ let totalPages = null;
10123
+ if (!skipCount) {
10124
+ try {
10125
+ totalItems = await this.count({ partition, partitionValues });
10126
+ totalPages = Math.ceil(totalItems / size);
10127
+ } catch (countError) {
10128
+ console.warn(`Failed to get count for page:`, countError.message);
10129
+ totalItems = null;
10130
+ totalPages = null;
10131
+ }
10075
10132
  }
10076
- };
10077
- this.emit("page", result);
10078
- return result;
10133
+ const page = Math.floor(offset / size);
10134
+ let items = [];
10135
+ try {
10136
+ items = await this.list({
10137
+ partition,
10138
+ partitionValues,
10139
+ limit: size,
10140
+ offset
10141
+ });
10142
+ } catch (listError) {
10143
+ console.warn(`Failed to get items for page:`, listError.message);
10144
+ items = [];
10145
+ }
10146
+ const result = {
10147
+ items,
10148
+ totalItems,
10149
+ page,
10150
+ pageSize: size,
10151
+ totalPages,
10152
+ // Add additional metadata for debugging
10153
+ _debug: {
10154
+ requestedSize: size,
10155
+ requestedOffset: offset,
10156
+ actualItemsReturned: items.length,
10157
+ skipCount,
10158
+ hasTotalItems: totalItems !== null
10159
+ }
10160
+ };
10161
+ this.emit("page", result);
10162
+ return result;
10163
+ } catch (error) {
10164
+ console.error(`Critical error in page method:`, error.message);
10165
+ return {
10166
+ items: [],
10167
+ totalItems: null,
10168
+ page: Math.floor(offset / size),
10169
+ pageSize: size,
10170
+ totalPages: null,
10171
+ _debug: {
10172
+ requestedSize: size,
10173
+ requestedOffset: offset,
10174
+ actualItemsReturned: 0,
10175
+ skipCount,
10176
+ hasTotalItems: false,
10177
+ error: error.message
10178
+ }
10179
+ };
10180
+ }
10079
10181
  }
10080
10182
  readable() {
10081
10183
  const stream = new ResourceReader({ resource: this });
@@ -10368,7 +10470,118 @@ ${validation.errors.join("\n")}`);
10368
10470
  return results.slice(0, limit);
10369
10471
  }
10370
10472
  /**
10371
- * Update partition objects to keep them in sync
10473
+ * Handle partition reference updates with change detection
10474
+ * @param {Object} oldData - Original object data before update
10475
+ * @param {Object} newData - Updated object data
10476
+ */
10477
+ async handlePartitionReferenceUpdates(oldData, newData) {
10478
+ const partitions = this.config.partitions;
10479
+ if (!partitions || Object.keys(partitions).length === 0) {
10480
+ return;
10481
+ }
10482
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10483
+ try {
10484
+ await this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData);
10485
+ } catch (error) {
10486
+ console.warn(`Failed to update partition references for ${partitionName}:`, error.message);
10487
+ }
10488
+ }
10489
+ const id = newData.id || oldData.id;
10490
+ for (const [partitionName, partition] of Object.entries(partitions)) {
10491
+ const prefix = `resource=${this.name}/partition=${partitionName}`;
10492
+ let allKeys = [];
10493
+ try {
10494
+ allKeys = await this.client.getAllKeys({ prefix });
10495
+ } catch (error) {
10496
+ console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, error.message);
10497
+ continue;
10498
+ }
10499
+ const validKey = this.getPartitionKey({ partitionName, id, data: newData });
10500
+ for (const key of allKeys) {
10501
+ if (key.endsWith(`/id=${id}`) && key !== validKey) {
10502
+ try {
10503
+ await this.client.deleteObject(key);
10504
+ } catch (error) {
10505
+ console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, error.message);
10506
+ }
10507
+ }
10508
+ }
10509
+ }
10510
+ }
10511
+ /**
10512
+ * Handle partition reference update for a specific partition
10513
+ * @param {string} partitionName - Name of the partition
10514
+ * @param {Object} partition - Partition definition
10515
+ * @param {Object} oldData - Original object data before update
10516
+ * @param {Object} newData - Updated object data
10517
+ */
10518
+ async handlePartitionReferenceUpdate(partitionName, partition, oldData, newData) {
10519
+ const id = newData.id || oldData.id;
10520
+ const oldPartitionKey = this.getPartitionKey({ partitionName, id, data: oldData });
10521
+ const newPartitionKey = this.getPartitionKey({ partitionName, id, data: newData });
10522
+ if (oldPartitionKey !== newPartitionKey) {
10523
+ if (oldPartitionKey) {
10524
+ try {
10525
+ await this.client.deleteObject(oldPartitionKey);
10526
+ } catch (error) {
10527
+ console.warn(`Old partition object could not be deleted for ${partitionName}:`, error.message);
10528
+ }
10529
+ }
10530
+ if (newPartitionKey) {
10531
+ try {
10532
+ const mappedData = await this.schema.mapper(newData);
10533
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10534
+ const behaviorImpl = getBehavior(this.behavior);
10535
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10536
+ resource: this,
10537
+ id,
10538
+ data: newData,
10539
+ mappedData
10540
+ });
10541
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10542
+ const partitionMetadata = {
10543
+ ...processedMetadata,
10544
+ _version: this.version
10545
+ };
10546
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10547
+ await this.client.putObject({
10548
+ key: newPartitionKey,
10549
+ metadata: partitionMetadata,
10550
+ body
10551
+ });
10552
+ } catch (error) {
10553
+ console.warn(`New partition object could not be created for ${partitionName}:`, error.message);
10554
+ }
10555
+ }
10556
+ } else if (newPartitionKey) {
10557
+ try {
10558
+ const mappedData = await this.schema.mapper(newData);
10559
+ if (mappedData.undefined !== void 0) delete mappedData.undefined;
10560
+ const behaviorImpl = getBehavior(this.behavior);
10561
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10562
+ resource: this,
10563
+ id,
10564
+ data: newData,
10565
+ mappedData
10566
+ });
10567
+ if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10568
+ const partitionMetadata = {
10569
+ ...processedMetadata,
10570
+ _version: this.version
10571
+ };
10572
+ if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10573
+ await this.client.putObject({
10574
+ key: newPartitionKey,
10575
+ metadata: partitionMetadata,
10576
+ body
10577
+ });
10578
+ } catch (error) {
10579
+ console.warn(`Partition object could not be updated for ${partitionName}:`, error.message);
10580
+ }
10581
+ }
10582
+ }
10583
+ /**
10584
+ * Update partition objects to keep them in sync (legacy method for backward compatibility)
10372
10585
  * @param {Object} data - Updated object data
10373
10586
  */
10374
10587
  async updatePartitionReferences(data) {
@@ -10377,6 +10590,10 @@ ${validation.errors.join("\n")}`);
10377
10590
  return;
10378
10591
  }
10379
10592
  for (const [partitionName, partition] of Object.entries(partitions)) {
10593
+ if (!partition || !partition.fields || typeof partition.fields !== "object") {
10594
+ console.warn(`Skipping invalid partition '${partitionName}' in resource '${this.name}'`);
10595
+ continue;
10596
+ }
10380
10597
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
10381
10598
  if (partitionKey) {
10382
10599
  const mappedData = await this.schema.mapper(data);
@@ -10475,6 +10692,7 @@ ${validation.errors.join("\n")}`);
10475
10692
  if (request.VersionId) data._versionId = request.VersionId;
10476
10693
  if (request.Expiration) data._expiresAt = request.Expiration;
10477
10694
  data._definitionHash = this.getDefinitionHash();
10695
+ if (data.undefined !== void 0) delete data.undefined;
10478
10696
  this.emit("getFromPartition", data);
10479
10697
  return data;
10480
10698
  }
@@ -10578,7 +10796,7 @@ class Database extends EventEmitter {
10578
10796
  this.version = "1";
10579
10797
  this.s3dbVersion = (() => {
10580
10798
  try {
10581
- return true ? "4.1.10" : "latest";
10799
+ return true ? "5.0.0" : "latest";
10582
10800
  } catch (e) {
10583
10801
  return "latest";
10584
10802
  }
@@ -10593,14 +10811,24 @@ class Database extends EventEmitter {
10593
10811
  this.passphrase = options.passphrase || "secret";
10594
10812
  let connectionString = options.connectionString;
10595
10813
  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
- });
10814
+ const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
10815
+ if (endpoint) {
10816
+ const url = new URL(endpoint);
10817
+ if (accessKeyId) url.username = encodeURIComponent(accessKeyId);
10818
+ if (secretAccessKey) url.password = encodeURIComponent(secretAccessKey);
10819
+ url.pathname = `/${bucket || "s3db"}`;
10820
+ if (forcePathStyle) {
10821
+ url.searchParams.set("forcePathStyle", "true");
10822
+ }
10823
+ connectionString = url.toString();
10824
+ } else if (accessKeyId && secretAccessKey) {
10825
+ const params = new URLSearchParams();
10826
+ params.set("region", region || "us-east-1");
10827
+ if (forcePathStyle) {
10828
+ params.set("forcePathStyle", "true");
10829
+ }
10830
+ connectionString = `s3://${encodeURIComponent(accessKeyId)}:${encodeURIComponent(secretAccessKey)}@${bucket || "s3db"}?${params.toString()}`;
10831
+ }
10604
10832
  }
10605
10833
  this.client = options.client || new Client({
10606
10834
  verbose: this.verbose,
@@ -10636,12 +10864,12 @@ class Database extends EventEmitter {
10636
10864
  passphrase: this.passphrase,
10637
10865
  observers: [this],
10638
10866
  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: {}
10867
+ timestamps: versionData.timestamps !== void 0 ? versionData.timestamps : false,
10868
+ partitions: resourceMetadata.partitions || versionData.partitions || {},
10869
+ paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
10870
+ allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
10871
+ autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
10872
+ hooks: versionData.hooks || {}
10645
10873
  });
10646
10874
  }
10647
10875
  }
@@ -10779,15 +11007,14 @@ class Database extends EventEmitter {
10779
11007
  [version]: {
10780
11008
  hash: definitionHash,
10781
11009
  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
- },
10790
11010
  behavior: resourceDef.behavior || "user-management",
11011
+ timestamps: resource.config.timestamps,
11012
+ partitions: resource.config.partitions,
11013
+ paranoid: resource.config.paranoid,
11014
+ allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
11015
+ autoDecrypt: resource.config.autoDecrypt,
11016
+ cache: resource.config.cache,
11017
+ hooks: resource.config.hooks,
10791
11018
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10792
11019
  }
10793
11020
  }
@@ -10822,73 +11049,58 @@ class Database extends EventEmitter {
10822
11049
  }
10823
11050
  /**
10824
11051
  * Check if a resource exists with the same definition hash
10825
- * @param {string} name - Resource name
10826
- * @param {Object} attributes - Resource attributes
10827
- * @param {Object} options - Resource options
10828
- * @param {string} behavior - Resource behavior
10829
- * @returns {Object} Object with exists flag and hash information
11052
+ * @param {Object} config - Resource configuration
11053
+ * @param {string} config.name - Resource name
11054
+ * @param {Object} config.attributes - Resource attributes
11055
+ * @param {string} [config.behavior] - Resource behavior
11056
+ * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
11057
+ * @returns {Object} Result with exists and hash information
10830
11058
  */
10831
- resourceExistsWithSameHash({ name, attributes, options = {}, behavior = "user-management" }) {
11059
+ resourceExistsWithSameHash({ name, attributes, behavior = "user-management", options = {} }) {
10832
11060
  if (!this.resources[name]) {
10833
11061
  return { exists: false, sameHash: false, hash: null };
10834
11062
  }
10835
- const tempResource = new Resource({
11063
+ const existingResource = this.resources[name];
11064
+ const existingHash = this.generateDefinitionHash(existingResource.export());
11065
+ const mockResource = new Resource({
10836
11066
  name,
10837
11067
  attributes,
10838
11068
  behavior,
10839
- observers: [],
10840
11069
  client: this.client,
10841
- version: "temp",
11070
+ version: existingResource.version,
10842
11071
  passphrase: this.passphrase,
10843
- cache: this.cache,
10844
11072
  ...options
10845
11073
  });
10846
- const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10847
- const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
11074
+ const newHash = this.generateDefinitionHash(mockResource.export());
10848
11075
  return {
10849
11076
  exists: true,
10850
- sameHash: newHash === existingHash,
11077
+ sameHash: existingHash === newHash,
10851
11078
  hash: newHash,
10852
11079
  existingHash
10853
11080
  };
10854
11081
  }
10855
- /**
10856
- * Create a resource only if it doesn't exist with the same definition hash
10857
- * @param {Object} params - Resource parameters
10858
- * @param {string} params.name - Resource name
10859
- * @param {Object} params.attributes - Resource attributes
10860
- * @param {Object} params.options - Resource options
10861
- * @param {string} params.behavior - Resource behavior
10862
- * @returns {Object} Object with resource and created flag
10863
- */
10864
- async createResourceIfNotExists({ name, attributes, options = {}, behavior = "user-management" }) {
10865
- const alreadyExists = !!this.resources[name];
10866
- const hashCheck = this.resourceExistsWithSameHash({ name, attributes, options, behavior });
10867
- if (hashCheck.exists && hashCheck.sameHash) {
10868
- return {
10869
- resource: this.resources[name],
10870
- created: false,
10871
- reason: "Resource already exists with same definition hash"
10872
- };
10873
- }
10874
- const resource = await this.createResource({ name, attributes, options, behavior });
10875
- return {
10876
- resource,
10877
- created: !alreadyExists,
10878
- reason: alreadyExists ? "Resource updated with new definition" : "New resource created"
10879
- };
10880
- }
10881
- async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
11082
+ async createResource({ name, attributes, behavior = "user-management", hooks, ...config }) {
10882
11083
  if (this.resources[name]) {
10883
11084
  const existingResource = this.resources[name];
10884
11085
  Object.assign(existingResource.config, {
10885
11086
  cache: this.cache,
10886
- ...options
11087
+ ...config
10887
11088
  });
10888
11089
  if (behavior) {
10889
11090
  existingResource.behavior = behavior;
10890
11091
  }
10891
11092
  existingResource.updateAttributes(attributes);
11093
+ if (hooks) {
11094
+ for (const [event, hooksArr] of Object.entries(hooks)) {
11095
+ if (Array.isArray(hooksArr) && existingResource.hooks[event]) {
11096
+ for (const fn of hooksArr) {
11097
+ if (typeof fn === "function") {
11098
+ existingResource.hooks[event].push(fn.bind(existingResource));
11099
+ }
11100
+ }
11101
+ }
11102
+ }
11103
+ }
10892
11104
  const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
10893
11105
  const existingMetadata2 = this.savedMetadata?.resources?.[name];
10894
11106
  const currentVersion = existingMetadata2?.currentVersion || "v0";
@@ -10910,7 +11122,8 @@ class Database extends EventEmitter {
10910
11122
  version,
10911
11123
  passphrase: this.passphrase,
10912
11124
  cache: this.cache,
10913
- ...options
11125
+ hooks,
11126
+ ...config
10914
11127
  });
10915
11128
  this.resources[name] = resource;
10916
11129
  await this.uploadMetadataFile();
@@ -17612,4 +17825,4 @@ class CachePlugin extends Plugin {
17612
17825
  }
17613
17826
  }
17614
17827
 
17615
- 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 };
17828
+ 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 };