s3db.js 4.1.8 → 4.1.11

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
@@ -242,6 +242,8 @@ var substr = 'ab'.substr(-1) === 'b' ?
242
242
  ;
243
243
 
244
244
  const idGenerator = customAlphabet(urlAlphabet, 22);
245
+ const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
246
+ customAlphabet(passwordAlphabet, 12);
245
247
 
246
248
  var domain;
247
249
 
@@ -742,42 +744,85 @@ ${JSON.stringify(rest, null, 2)}`;
742
744
  return `${this.name} | ${this.message}`;
743
745
  }
744
746
  }
745
- class NoSuchBucket extends BaseError {
747
+ class S3DBError extends BaseError {
748
+ constructor(message, details = {}) {
749
+ super({ message, ...details });
750
+ }
751
+ }
752
+ class DatabaseError extends S3DBError {
753
+ constructor(message, details = {}) {
754
+ super(message, details);
755
+ Object.assign(this, details);
756
+ }
757
+ }
758
+ class ValidationError extends S3DBError {
759
+ constructor(message, details = {}) {
760
+ super(message, details);
761
+ Object.assign(this, details);
762
+ }
763
+ }
764
+ class AuthenticationError extends S3DBError {
765
+ constructor(message, details = {}) {
766
+ super(message, details);
767
+ Object.assign(this, details);
768
+ }
769
+ }
770
+ class PermissionError extends S3DBError {
771
+ constructor(message, details = {}) {
772
+ super(message, details);
773
+ Object.assign(this, details);
774
+ }
775
+ }
776
+ class EncryptionError extends S3DBError {
777
+ constructor(message, details = {}) {
778
+ super(message, details);
779
+ Object.assign(this, details);
780
+ }
781
+ }
782
+ class ResourceNotFound extends S3DBError {
783
+ constructor({ bucket, resourceName, id, ...rest }) {
784
+ super(`Resource not found: ${resourceName}/${id} [bucket:${bucket}]`, {
785
+ bucket,
786
+ resourceName,
787
+ id,
788
+ ...rest
789
+ });
790
+ }
791
+ }
792
+ class NoSuchBucket extends S3DBError {
746
793
  constructor({ bucket, ...rest }) {
747
- super({ ...rest, bucket, message: `Bucket does not exists [bucket:${bucket}]` });
794
+ super(`Bucket does not exists [bucket:${bucket}]`, { bucket, ...rest });
748
795
  }
749
796
  }
750
- class NoSuchKey extends BaseError {
797
+ class NoSuchKey extends S3DBError {
751
798
  constructor({ bucket, key, ...rest }) {
752
- super({ ...rest, bucket, message: `Key [${key}] does not exists [bucket:${bucket}/${key}]` });
753
- this.key = key;
799
+ super(`Key [${key}] does not exists [bucket:${bucket}/${key}]`, { bucket, key, ...rest });
754
800
  }
755
801
  }
756
802
  class NotFound extends NoSuchKey {
757
803
  }
758
- class MissingMetadata extends BaseError {
804
+ class MissingMetadata extends S3DBError {
759
805
  constructor({ bucket, ...rest }) {
760
- super({ ...rest, bucket, message: `Missing metadata for bucket [bucket:${bucket}]` });
806
+ super(`Missing metadata for bucket [bucket:${bucket}]`, { bucket, ...rest });
761
807
  }
762
808
  }
763
- class InvalidResourceItem extends BaseError {
809
+ class InvalidResourceItem extends S3DBError {
764
810
  constructor({
765
811
  bucket,
766
812
  resourceName,
767
813
  attributes,
768
814
  validation
769
815
  }) {
770
- super({
816
+ super(`This item is not valid. Resource=${resourceName} [bucket:${bucket}].
817
+ ${JSON.stringify(validation, null, 2)}`, {
771
818
  bucket,
772
- message: `This item is not valid. Resource=${resourceName} [bucket:${bucket}].
773
- ${JSON.stringify(validation, null, 2)}`
819
+ resourceName,
820
+ attributes,
821
+ validation
774
822
  });
775
- this.resourceName = resourceName;
776
- this.attributes = attributes;
777
- this.validation = validation;
778
823
  }
779
824
  }
780
- class UnknownError extends BaseError {
825
+ class UnknownError extends S3DBError {
781
826
  }
782
827
  const ErrorMap = {
783
828
  "NotFound": NotFound,
@@ -806,9 +851,9 @@ class ConnectionString {
806
851
  }
807
852
  }
808
853
  defineS3(uri) {
809
- this.bucket = uri.hostname;
810
- this.accessKeyId = uri.username;
811
- this.secretAccessKey = uri.password;
854
+ this.bucket = decodeURIComponent(uri.hostname);
855
+ this.accessKeyId = decodeURIComponent(uri.username);
856
+ this.secretAccessKey = decodeURIComponent(uri.password);
812
857
  this.endpoint = S3_DEFAULT_ENDPOINT;
813
858
  if (["/", "", null].includes(uri.pathname)) {
814
859
  this.keyPrefix = "";
@@ -820,14 +865,14 @@ class ConnectionString {
820
865
  defineMinio(uri) {
821
866
  this.forcePathStyle = true;
822
867
  this.endpoint = uri.origin;
823
- this.accessKeyId = uri.username;
824
- this.secretAccessKey = uri.password;
868
+ this.accessKeyId = decodeURIComponent(uri.username);
869
+ this.secretAccessKey = decodeURIComponent(uri.password);
825
870
  if (["/", "", null].includes(uri.pathname)) {
826
871
  this.bucket = "s3db";
827
872
  this.keyPrefix = "";
828
873
  } else {
829
874
  let [, bucket, ...subpath] = uri.pathname.split("/");
830
- this.bucket = bucket;
875
+ this.bucket = decodeURIComponent(bucket);
831
876
  this.keyPrefix = [...subpath || []].join("/");
832
877
  }
833
878
  }
@@ -8836,18 +8881,83 @@ function getBehavior(behaviorName) {
8836
8881
  const DEFAULT_BEHAVIOR = "user-management";
8837
8882
 
8838
8883
  class Resource extends EventEmitter {
8839
- constructor({
8840
- name,
8841
- client,
8842
- version = "1",
8843
- options = {},
8844
- attributes = {},
8845
- parallelism = 10,
8846
- passphrase = "secret",
8847
- observers = [],
8848
- behavior = DEFAULT_BEHAVIOR
8849
- }) {
8884
+ /**
8885
+ * Create a new Resource instance
8886
+ * @param {Object} config - Resource configuration
8887
+ * @param {string} config.name - Resource name
8888
+ * @param {Object} config.client - S3 client instance
8889
+ * @param {string} [config.version='v0'] - Resource version
8890
+ * @param {Object} [config.attributes={}] - Resource attributes schema
8891
+ * @param {string} [config.behavior='user-management'] - Resource behavior strategy
8892
+ * @param {string} [config.passphrase='secret'] - Encryption passphrase
8893
+ * @param {number} [config.parallelism=10] - Parallelism for bulk operations
8894
+ * @param {Array} [config.observers=[]] - Observer instances
8895
+ * @param {boolean} [config.cache=false] - Enable caching
8896
+ * @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
8897
+ * @param {boolean} [config.timestamps=false] - Enable automatic timestamps
8898
+ * @param {Object} [config.partitions={}] - Partition definitions
8899
+ * @param {boolean} [config.paranoid=true] - Security flag for dangerous operations
8900
+ * @param {boolean} [config.allNestedObjectsOptional=false] - Make nested objects optional
8901
+ * @param {Object} [config.hooks={}] - Custom hooks
8902
+ * @param {Object} [config.options={}] - Additional options
8903
+ * @example
8904
+ * const users = new Resource({
8905
+ * name: 'users',
8906
+ * client: s3Client,
8907
+ * attributes: {
8908
+ * name: 'string|required',
8909
+ * email: 'string|required',
8910
+ * password: 'secret|required'
8911
+ * },
8912
+ * behavior: 'user-management',
8913
+ * passphrase: 'my-secret-key',
8914
+ * timestamps: true,
8915
+ * partitions: {
8916
+ * byRegion: {
8917
+ * fields: { region: 'string' }
8918
+ * }
8919
+ * },
8920
+ * hooks: {
8921
+ * preInsert: [async (data) => {
8922
+ * console.log('Pre-insert hook:', data);
8923
+ * return data;
8924
+ * }]
8925
+ * }
8926
+ * });
8927
+ */
8928
+ constructor(config) {
8850
8929
  super();
8930
+ const validation = validateResourceConfig(config);
8931
+ if (!validation.isValid) {
8932
+ throw new Error(`Invalid Resource configuration:
8933
+ ${validation.errors.join("\n")}`);
8934
+ }
8935
+ const {
8936
+ name,
8937
+ client,
8938
+ version = "1",
8939
+ attributes = {},
8940
+ behavior = DEFAULT_BEHAVIOR,
8941
+ passphrase = "secret",
8942
+ parallelism = 10,
8943
+ observers = [],
8944
+ cache = false,
8945
+ autoDecrypt = true,
8946
+ timestamps = false,
8947
+ partitions = {},
8948
+ paranoid = true,
8949
+ allNestedObjectsOptional = true,
8950
+ hooks = {},
8951
+ options = {}
8952
+ } = 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
+ };
8851
8961
  this.name = name;
8852
8962
  this.client = client;
8853
8963
  this.version = version;
@@ -8855,15 +8965,14 @@ class Resource extends EventEmitter {
8855
8965
  this.observers = observers;
8856
8966
  this.parallelism = parallelism;
8857
8967
  this.passphrase = passphrase ?? "secret";
8858
- this.options = {
8859
- cache: false,
8860
- autoDecrypt: true,
8861
- timestamps: false,
8862
- partitions: {},
8863
- paranoid: true,
8864
- // Security flag for dangerous operations
8865
- allNestedObjectsOptional: options.allNestedObjectsOptional ?? false,
8866
- ...options
8968
+ this.config = {
8969
+ cache: mergedOptions.cache,
8970
+ hooks,
8971
+ paranoid: mergedOptions.paranoid,
8972
+ timestamps: mergedOptions.timestamps,
8973
+ partitions: mergedOptions.partitions,
8974
+ autoDecrypt: mergedOptions.autoDecrypt,
8975
+ allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
8867
8976
  };
8868
8977
  this.hooks = {
8869
8978
  preInsert: [],
@@ -8874,18 +8983,21 @@ class Resource extends EventEmitter {
8874
8983
  afterDelete: []
8875
8984
  };
8876
8985
  this.attributes = attributes || {};
8877
- if (options.timestamps) {
8986
+ if (this.config.timestamps) {
8878
8987
  this.attributes.createdAt = "string|optional";
8879
8988
  this.attributes.updatedAt = "string|optional";
8880
- if (!this.options.partitions.byCreatedDate) {
8881
- this.options.partitions.byCreatedDate = {
8989
+ if (!this.config.partitions) {
8990
+ this.config.partitions = {};
8991
+ }
8992
+ if (!this.config.partitions.byCreatedDate) {
8993
+ this.config.partitions.byCreatedDate = {
8882
8994
  fields: {
8883
8995
  createdAt: "date|maxlength:10"
8884
8996
  }
8885
8997
  };
8886
8998
  }
8887
- if (!this.options.partitions.byUpdatedDate) {
8888
- this.options.partitions.byUpdatedDate = {
8999
+ if (!this.config.partitions.byUpdatedDate) {
9000
+ this.config.partitions.byUpdatedDate = {
8889
9001
  fields: {
8890
9002
  updatedAt: "date|maxlength:10"
8891
9003
  }
@@ -8898,25 +9010,47 @@ class Resource extends EventEmitter {
8898
9010
  passphrase,
8899
9011
  version: this.version,
8900
9012
  options: {
8901
- ...this.options,
8902
- allNestedObjectsOptional: this.options.allNestedObjectsOptional ?? false
9013
+ autoDecrypt: this.config.autoDecrypt,
9014
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
8903
9015
  }
8904
9016
  });
8905
- this.validatePartitions();
8906
9017
  this.setupPartitionHooks();
8907
- if (options.hooks) {
8908
- for (const [event, hooksArr] of Object.entries(options.hooks)) {
9018
+ this.validatePartitions();
9019
+ if (hooks) {
9020
+ for (const [event, hooksArr] of Object.entries(hooks)) {
8909
9021
  if (Array.isArray(hooksArr) && this.hooks[event]) {
8910
9022
  for (const fn of hooksArr) {
8911
- this.hooks[event].push(fn.bind(this));
9023
+ if (typeof fn === "function") {
9024
+ this.hooks[event].push(fn.bind(this));
9025
+ }
8912
9026
  }
8913
9027
  }
8914
9028
  }
8915
9029
  }
8916
9030
  }
9031
+ /**
9032
+ * Get resource options (for backward compatibility with tests)
9033
+ */
9034
+ get options() {
9035
+ return {
9036
+ timestamps: this.config.timestamps,
9037
+ partitions: this.config.partitions || {},
9038
+ cache: this.config.cache,
9039
+ autoDecrypt: this.config.autoDecrypt,
9040
+ paranoid: this.config.paranoid,
9041
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
9042
+ };
9043
+ }
8917
9044
  export() {
8918
9045
  const exported = this.schema.export();
8919
9046
  exported.behavior = this.behavior;
9047
+ exported.timestamps = this.config.timestamps;
9048
+ exported.partitions = this.config.partitions || {};
9049
+ exported.paranoid = this.config.paranoid;
9050
+ exported.allNestedObjectsOptional = this.config.allNestedObjectsOptional;
9051
+ exported.autoDecrypt = this.config.autoDecrypt;
9052
+ exported.cache = this.config.cache;
9053
+ exported.hooks = this.hooks;
8920
9054
  return exported;
8921
9055
  }
8922
9056
  /**
@@ -8926,18 +9060,21 @@ class Resource extends EventEmitter {
8926
9060
  updateAttributes(newAttributes) {
8927
9061
  const oldAttributes = this.attributes;
8928
9062
  this.attributes = newAttributes;
8929
- if (this.options.timestamps) {
9063
+ if (this.config.timestamps) {
8930
9064
  newAttributes.createdAt = "string|optional";
8931
9065
  newAttributes.updatedAt = "string|optional";
8932
- if (!this.options.partitions.byCreatedDate) {
8933
- this.options.partitions.byCreatedDate = {
9066
+ if (!this.config.partitions) {
9067
+ this.config.partitions = {};
9068
+ }
9069
+ if (!this.config.partitions.byCreatedDate) {
9070
+ this.config.partitions.byCreatedDate = {
8934
9071
  fields: {
8935
9072
  createdAt: "date|maxlength:10"
8936
9073
  }
8937
9074
  };
8938
9075
  }
8939
- if (!this.options.partitions.byUpdatedDate) {
8940
- this.options.partitions.byUpdatedDate = {
9076
+ if (!this.config.partitions.byUpdatedDate) {
9077
+ this.config.partitions.byUpdatedDate = {
8941
9078
  fields: {
8942
9079
  updatedAt: "date|maxlength:10"
8943
9080
  }
@@ -8949,10 +9086,13 @@ class Resource extends EventEmitter {
8949
9086
  attributes: newAttributes,
8950
9087
  passphrase: this.passphrase,
8951
9088
  version: this.version,
8952
- options: this.options
9089
+ options: {
9090
+ autoDecrypt: this.config.autoDecrypt,
9091
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
9092
+ }
8953
9093
  });
8954
- this.validatePartitions();
8955
9094
  this.setupPartitionHooks();
9095
+ this.validatePartitions();
8956
9096
  return { oldAttributes, newAttributes };
8957
9097
  }
8958
9098
  /**
@@ -8983,15 +9123,24 @@ class Resource extends EventEmitter {
8983
9123
  * Setup automatic partition hooks
8984
9124
  */
8985
9125
  setupPartitionHooks() {
8986
- const partitions = this.options.partitions;
8987
- if (!partitions || Object.keys(partitions).length === 0) {
9126
+ if (!this.config.partitions) {
9127
+ return;
9128
+ }
9129
+ const partitions = this.config.partitions;
9130
+ if (Object.keys(partitions).length === 0) {
8988
9131
  return;
8989
9132
  }
8990
- this.addHook("afterInsert", async (data) => {
9133
+ if (!this.hooks.afterInsert) {
9134
+ this.hooks.afterInsert = [];
9135
+ }
9136
+ this.hooks.afterInsert.push(async (data) => {
8991
9137
  await this.createPartitionReferences(data);
8992
9138
  return data;
8993
9139
  });
8994
- this.addHook("afterDelete", async (data) => {
9140
+ if (!this.hooks.afterDelete) {
9141
+ this.hooks.afterDelete = [];
9142
+ }
9143
+ this.hooks.afterDelete.push(async (data) => {
8995
9144
  await this.deletePartitionReferences(data);
8996
9145
  return data;
8997
9146
  });
@@ -9016,8 +9165,11 @@ class Resource extends EventEmitter {
9016
9165
  * @throws {Error} If partition fields don't exist in current schema
9017
9166
  */
9018
9167
  validatePartitions() {
9019
- const partitions = this.options.partitions;
9020
- if (!partitions || Object.keys(partitions).length === 0) {
9168
+ if (!this.config.partitions) {
9169
+ return;
9170
+ }
9171
+ const partitions = this.config.partitions;
9172
+ if (Object.keys(partitions).length === 0) {
9021
9173
  return;
9022
9174
  }
9023
9175
  const currentAttributes = Object.keys(this.attributes || {});
@@ -9124,10 +9276,10 @@ class Resource extends EventEmitter {
9124
9276
  * // Returns: null
9125
9277
  */
9126
9278
  getPartitionKey({ partitionName, id, data }) {
9127
- const partition = this.options.partitions[partitionName];
9128
- if (!partition) {
9279
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
9129
9280
  throw new Error(`Partition '${partitionName}' not found`);
9130
9281
  }
9282
+ const partition = this.config.partitions[partitionName];
9131
9283
  const partitionSegments = [];
9132
9284
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
9133
9285
  for (const [fieldName, rule] of sortedFields) {
@@ -9183,6 +9335,12 @@ class Resource extends EventEmitter {
9183
9335
  * name: 'Jane Smith',
9184
9336
  * email: 'jane@example.com'
9185
9337
  * });
9338
+ *
9339
+ * // Insert with auto-generated password for secret field
9340
+ * const user = await resource.insert({
9341
+ * name: 'John Doe',
9342
+ * email: 'john@example.com',
9343
+ * });
9186
9344
  */
9187
9345
  async insert({ id, ...attributes }) {
9188
9346
  if (this.options.timestamps) {
@@ -9267,6 +9425,58 @@ class Resource extends EventEmitter {
9267
9425
  this.emit("get", data);
9268
9426
  return data;
9269
9427
  } catch (error) {
9428
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError") || error.originalError?.message?.includes("Cipher job failed")) {
9429
+ try {
9430
+ console.warn(`Decryption failed for resource ${id}, attempting to get raw metadata`);
9431
+ const request = await this.client.headObject(key);
9432
+ const objectVersion = this.extractVersionFromKey(key) || this.version;
9433
+ const tempSchema = new Schema({
9434
+ name: this.name,
9435
+ attributes: this.attributes,
9436
+ passphrase: this.passphrase,
9437
+ version: objectVersion,
9438
+ options: {
9439
+ ...this.config,
9440
+ autoDecrypt: false,
9441
+ // Disable decryption
9442
+ autoEncrypt: false
9443
+ // Disable encryption
9444
+ }
9445
+ });
9446
+ let metadata = await tempSchema.unmapper(request.Metadata);
9447
+ const behaviorImpl = getBehavior(this.behavior);
9448
+ let body = "";
9449
+ if (request.ContentLength > 0) {
9450
+ try {
9451
+ const fullObject = await this.client.getObject(key);
9452
+ body = await streamToString(fullObject.Body);
9453
+ } catch (bodyError) {
9454
+ console.warn(`Failed to read body for resource ${id}:`, bodyError.message);
9455
+ body = "";
9456
+ }
9457
+ }
9458
+ const { metadata: processedMetadata } = await behaviorImpl.handleGet({
9459
+ resource: this,
9460
+ metadata,
9461
+ body
9462
+ });
9463
+ let data = processedMetadata;
9464
+ data.id = id;
9465
+ data._contentLength = request.ContentLength;
9466
+ data._lastModified = request.LastModified;
9467
+ data._hasContent = request.ContentLength > 0;
9468
+ data._mimeType = request.ContentType || null;
9469
+ data._version = objectVersion;
9470
+ data._decryptionFailed = true;
9471
+ if (request.VersionId) data._versionId = request.VersionId;
9472
+ if (request.Expiration) data._expiresAt = request.Expiration;
9473
+ data._definitionHash = this.getDefinitionHash();
9474
+ this.emit("get", data);
9475
+ return data;
9476
+ } catch (fallbackError) {
9477
+ console.error(`Fallback attempt also failed for resource ${id}:`, fallbackError.message);
9478
+ }
9479
+ }
9270
9480
  const enhancedError = new Error(`Failed to get resource with id '${id}': ${error.message}`);
9271
9481
  enhancedError.originalError = error;
9272
9482
  enhancedError.resourceId = id;
@@ -9313,7 +9523,7 @@ class Resource extends EventEmitter {
9313
9523
  */
9314
9524
  async update(id, attributes) {
9315
9525
  const live = await this.get(id);
9316
- if (this.options.timestamps) {
9526
+ if (this.config.timestamps) {
9317
9527
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9318
9528
  }
9319
9529
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
@@ -9435,7 +9645,7 @@ class Resource extends EventEmitter {
9435
9645
  async count({ partition = null, partitionValues = {} } = {}) {
9436
9646
  let prefix;
9437
9647
  if (partition && Object.keys(partitionValues).length > 0) {
9438
- const partitionDef = this.options.partitions[partition];
9648
+ const partitionDef = this.config.partitions[partition];
9439
9649
  if (!partitionDef) {
9440
9650
  throw new Error(`Partition '${partition}' not found`);
9441
9651
  }
@@ -9520,9 +9730,9 @@ class Resource extends EventEmitter {
9520
9730
  return results;
9521
9731
  }
9522
9732
  async deleteAll() {
9523
- if (this.options.paranoid !== false) {
9733
+ if (this.config.paranoid !== false) {
9524
9734
  throw new Error(
9525
- `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.options.paranoid}`
9735
+ `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9526
9736
  );
9527
9737
  }
9528
9738
  const prefix = `resource=${this.name}/v=${this.version}`;
@@ -9539,9 +9749,9 @@ class Resource extends EventEmitter {
9539
9749
  * @returns {Promise<Object>} Deletion report
9540
9750
  */
9541
9751
  async deleteAllData() {
9542
- if (this.options.paranoid !== false) {
9752
+ if (this.config.paranoid !== false) {
9543
9753
  throw new Error(
9544
- `deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.options.paranoid}`
9754
+ `deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9545
9755
  );
9546
9756
  }
9547
9757
  const prefix = `resource=${this.name}`;
@@ -9584,10 +9794,10 @@ class Resource extends EventEmitter {
9584
9794
  async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9585
9795
  let prefix;
9586
9796
  if (partition && Object.keys(partitionValues).length > 0) {
9587
- const partitionDef = this.options.partitions[partition];
9588
- if (!partitionDef) {
9797
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9589
9798
  throw new Error(`Partition '${partition}' not found`);
9590
9799
  }
9800
+ const partitionDef = this.config.partitions[partition];
9591
9801
  const partitionSegments = [];
9592
9802
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9593
9803
  for (const [fieldName, rule] of sortedFields) {
@@ -9656,16 +9866,32 @@ class Resource extends EventEmitter {
9656
9866
  if (limit) {
9657
9867
  filteredIds2 = filteredIds2.slice(0, limit);
9658
9868
  }
9659
- const { results: results2 } = await PromisePool.for(filteredIds2).withConcurrency(this.parallelism).process(async (id) => {
9660
- return await this.get(id);
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);
9871
+ return null;
9872
+ }).process(async (id) => {
9873
+ try {
9874
+ return await this.get(id);
9875
+ } catch (error) {
9876
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9877
+ console.warn(`Decryption failed for ${id}, returning basic info`);
9878
+ return {
9879
+ id,
9880
+ _decryptionFailed: true,
9881
+ _error: error.message
9882
+ };
9883
+ }
9884
+ throw error;
9885
+ }
9661
9886
  });
9662
- this.emit("list", { partition, partitionValues, count: results2.length });
9663
- return results2;
9887
+ const validResults2 = results2.filter((item) => item !== null);
9888
+ this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9889
+ return validResults2;
9664
9890
  }
9665
- const partitionDef = this.options.partitions[partition];
9666
- if (!partitionDef) {
9891
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9667
9892
  throw new Error(`Partition '${partition}' not found`);
9668
9893
  }
9894
+ const partitionDef = this.config.partitions[partition];
9669
9895
  const partitionSegments = [];
9670
9896
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9671
9897
  for (const [fieldName, rule] of sortedFields) {
@@ -9691,11 +9917,29 @@ class Resource extends EventEmitter {
9691
9917
  if (limit) {
9692
9918
  filteredIds = filteredIds.slice(0, limit);
9693
9919
  }
9694
- const { results } = await PromisePool.for(filteredIds).withConcurrency(this.parallelism).process(async (id) => {
9695
- return await this.getFromPartition({ id, partitionName: partition, partitionValues });
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
+ }
9696
9939
  });
9697
- this.emit("list", { partition, partitionValues, count: results.length });
9698
- return results;
9940
+ const validResults = results.filter((item) => item !== null);
9941
+ this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
9942
+ return validResults;
9699
9943
  }
9700
9944
  /**
9701
9945
  * Get multiple resources by their IDs
@@ -9706,11 +9950,30 @@ class Resource extends EventEmitter {
9706
9950
  * users.forEach(user => console.log(user.name));
9707
9951
  */
9708
9952
  async getMany(ids) {
9709
- const { results } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
9953
+ const { results, errors } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
9954
+ console.warn(`Failed to get resource ${id}:`, error.message);
9955
+ return {
9956
+ id,
9957
+ _error: error.message,
9958
+ _decryptionFailed: error.message.includes("Cipher job failed") || error.message.includes("OperationError")
9959
+ };
9960
+ }).process(async (id) => {
9710
9961
  this.emit("id", id);
9711
- const data = await this.get(id);
9712
- this.emit("data", data);
9713
- return data;
9962
+ try {
9963
+ const data = await this.get(id);
9964
+ this.emit("data", data);
9965
+ return data;
9966
+ } catch (error) {
9967
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9968
+ console.warn(`Decryption failed for ${id}, returning basic info`);
9969
+ return {
9970
+ id,
9971
+ _decryptionFailed: true,
9972
+ _error: error.message
9973
+ };
9974
+ }
9975
+ throw error;
9976
+ }
9714
9977
  });
9715
9978
  this.emit("getMany", ids.length);
9716
9979
  return results;
@@ -9725,9 +9988,28 @@ class Resource extends EventEmitter {
9725
9988
  async getAll() {
9726
9989
  let ids = await this.listIds();
9727
9990
  if (ids.length === 0) return [];
9728
- const { results } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
9729
- const data = await this.get(id);
9730
- return data;
9991
+ const { results, errors } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
9992
+ console.warn(`Failed to get resource ${id}:`, error.message);
9993
+ return {
9994
+ id,
9995
+ _error: error.message,
9996
+ _decryptionFailed: error.message.includes("Cipher job failed") || error.message.includes("OperationError")
9997
+ };
9998
+ }).process(async (id) => {
9999
+ try {
10000
+ const data = await this.get(id);
10001
+ return data;
10002
+ } catch (error) {
10003
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
10004
+ console.warn(`Decryption failed for ${id}, returning basic info`);
10005
+ return {
10006
+ id,
10007
+ _decryptionFailed: true,
10008
+ _error: error.message
10009
+ };
10010
+ }
10011
+ throw error;
10012
+ }
9731
10013
  });
9732
10014
  this.emit("getAll", results.length);
9733
10015
  return results;
@@ -9914,16 +10196,14 @@ class Resource extends EventEmitter {
9914
10196
  }
9915
10197
  /**
9916
10198
  * Generate definition hash for this resource
9917
- * @returns {string} SHA256 hash of the schema definition
10199
+ * @returns {string} SHA256 hash of the resource definition (name + attributes)
9918
10200
  */
9919
10201
  getDefinitionHash() {
9920
- const attributes = this.schema.export().attributes;
9921
- const stableAttributes = { ...attributes };
9922
- if (this.options.timestamps) {
9923
- delete stableAttributes.createdAt;
9924
- delete stableAttributes.updatedAt;
9925
- }
9926
- const stableString = jsonStableStringify(stableAttributes);
10202
+ const definition = {
10203
+ attributes: this.attributes,
10204
+ behavior: this.behavior
10205
+ };
10206
+ const stableString = jsonStableStringify(definition);
9927
10207
  return `sha256:${createHash("sha256").update(stableString).digest("hex")}`;
9928
10208
  }
9929
10209
  /**
@@ -9942,14 +10222,34 @@ class Resource extends EventEmitter {
9942
10222
  * @returns {Object} Schema object for the version
9943
10223
  */
9944
10224
  async getSchemaForVersion(version) {
9945
- return this.schema;
10225
+ if (version === this.version) {
10226
+ return this.schema;
10227
+ }
10228
+ try {
10229
+ const compatibleSchema = new Schema({
10230
+ name: this.name,
10231
+ attributes: this.attributes,
10232
+ passphrase: this.passphrase,
10233
+ version,
10234
+ options: {
10235
+ ...this.config,
10236
+ // For older versions, be more lenient with decryption
10237
+ autoDecrypt: true,
10238
+ autoEncrypt: true
10239
+ }
10240
+ });
10241
+ return compatibleSchema;
10242
+ } catch (error) {
10243
+ console.warn(`Failed to create compatible schema for version ${version}, using current schema:`, error.message);
10244
+ return this.schema;
10245
+ }
9946
10246
  }
9947
10247
  /**
9948
10248
  * Create partition references after insert
9949
10249
  * @param {Object} data - Inserted object data
9950
10250
  */
9951
10251
  async createPartitionReferences(data) {
9952
- const partitions = this.options.partitions;
10252
+ const partitions = this.config.partitions;
9953
10253
  if (!partitions || Object.keys(partitions).length === 0) {
9954
10254
  return;
9955
10255
  }
@@ -9980,7 +10280,7 @@ class Resource extends EventEmitter {
9980
10280
  * @param {Object} data - Deleted object data
9981
10281
  */
9982
10282
  async deletePartitionReferences(data) {
9983
- const partitions = this.options.partitions;
10283
+ const partitions = this.config.partitions;
9984
10284
  if (!partitions || Object.keys(partitions).length === 0) {
9985
10285
  return;
9986
10286
  }
@@ -10072,7 +10372,7 @@ class Resource extends EventEmitter {
10072
10372
  * @param {Object} data - Updated object data
10073
10373
  */
10074
10374
  async updatePartitionReferences(data) {
10075
- const partitions = this.options.partitions;
10375
+ const partitions = this.config.partitions;
10076
10376
  if (!partitions || Object.keys(partitions).length === 0) {
10077
10377
  return;
10078
10378
  }
@@ -10128,10 +10428,10 @@ class Resource extends EventEmitter {
10128
10428
  * });
10129
10429
  */
10130
10430
  async getFromPartition({ id, partitionName, partitionValues = {} }) {
10131
- const partition = this.options.partitions[partitionName];
10132
- if (!partition) {
10431
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
10133
10432
  throw new Error(`Partition '${partitionName}' not found`);
10134
10433
  }
10434
+ const partition = this.config.partitions[partitionName];
10135
10435
  const partitionSegments = [];
10136
10436
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
10137
10437
  for (const [fieldName, rule] of sortedFields) {
@@ -10179,6 +10479,98 @@ class Resource extends EventEmitter {
10179
10479
  return data;
10180
10480
  }
10181
10481
  }
10482
+ function validateResourceConfig(config) {
10483
+ const errors = [];
10484
+ if (!config.name) {
10485
+ errors.push("Resource 'name' is required");
10486
+ } else if (typeof config.name !== "string") {
10487
+ errors.push("Resource 'name' must be a string");
10488
+ } else if (config.name.trim() === "") {
10489
+ errors.push("Resource 'name' cannot be empty");
10490
+ }
10491
+ if (!config.client) {
10492
+ errors.push("S3 'client' is required");
10493
+ }
10494
+ if (!config.attributes) {
10495
+ errors.push("Resource 'attributes' are required");
10496
+ } else if (typeof config.attributes !== "object" || Array.isArray(config.attributes)) {
10497
+ errors.push("Resource 'attributes' must be an object");
10498
+ } else if (Object.keys(config.attributes).length === 0) {
10499
+ errors.push("Resource 'attributes' cannot be empty");
10500
+ }
10501
+ if (config.version !== void 0 && typeof config.version !== "string") {
10502
+ errors.push("Resource 'version' must be a string");
10503
+ }
10504
+ if (config.behavior !== void 0 && typeof config.behavior !== "string") {
10505
+ errors.push("Resource 'behavior' must be a string");
10506
+ }
10507
+ if (config.passphrase !== void 0 && typeof config.passphrase !== "string") {
10508
+ errors.push("Resource 'passphrase' must be a string");
10509
+ }
10510
+ if (config.parallelism !== void 0) {
10511
+ if (typeof config.parallelism !== "number" || !Number.isInteger(config.parallelism)) {
10512
+ errors.push("Resource 'parallelism' must be an integer");
10513
+ } else if (config.parallelism < 1) {
10514
+ errors.push("Resource 'parallelism' must be greater than 0");
10515
+ }
10516
+ }
10517
+ if (config.observers !== void 0 && !Array.isArray(config.observers)) {
10518
+ errors.push("Resource 'observers' must be an array");
10519
+ }
10520
+ const booleanFields = ["cache", "autoDecrypt", "timestamps", "paranoid", "allNestedObjectsOptional"];
10521
+ for (const field of booleanFields) {
10522
+ if (config[field] !== void 0 && typeof config[field] !== "boolean") {
10523
+ errors.push(`Resource '${field}' must be a boolean`);
10524
+ }
10525
+ }
10526
+ if (config.partitions !== void 0) {
10527
+ if (typeof config.partitions !== "object" || Array.isArray(config.partitions)) {
10528
+ errors.push("Resource 'partitions' must be an object");
10529
+ } else {
10530
+ for (const [partitionName, partitionDef] of Object.entries(config.partitions)) {
10531
+ if (typeof partitionDef !== "object" || Array.isArray(partitionDef)) {
10532
+ errors.push(`Partition '${partitionName}' must be an object`);
10533
+ } else if (!partitionDef.fields) {
10534
+ errors.push(`Partition '${partitionName}' must have a 'fields' property`);
10535
+ } else if (typeof partitionDef.fields !== "object" || Array.isArray(partitionDef.fields)) {
10536
+ errors.push(`Partition '${partitionName}.fields' must be an object`);
10537
+ } else {
10538
+ for (const [fieldName, fieldType] of Object.entries(partitionDef.fields)) {
10539
+ if (typeof fieldType !== "string") {
10540
+ errors.push(`Partition '${partitionName}.fields.${fieldName}' must be a string`);
10541
+ }
10542
+ }
10543
+ }
10544
+ }
10545
+ }
10546
+ }
10547
+ if (config.hooks !== void 0) {
10548
+ if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
10549
+ errors.push("Resource 'hooks' must be an object");
10550
+ } else {
10551
+ const validHookEvents = ["preInsert", "afterInsert", "preUpdate", "afterUpdate", "preDelete", "afterDelete"];
10552
+ for (const [event, hooksArr] of Object.entries(config.hooks)) {
10553
+ if (!validHookEvents.includes(event)) {
10554
+ errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
10555
+ } else if (!Array.isArray(hooksArr)) {
10556
+ errors.push(`Resource 'hooks.${event}' must be an array`);
10557
+ } else {
10558
+ for (let i = 0; i < hooksArr.length; i++) {
10559
+ const hook = hooksArr[i];
10560
+ if (typeof hook !== "function") {
10561
+ if (typeof hook === "string") continue;
10562
+ continue;
10563
+ }
10564
+ }
10565
+ }
10566
+ }
10567
+ }
10568
+ }
10569
+ return {
10570
+ isValid: errors.length === 0,
10571
+ errors
10572
+ };
10573
+ }
10182
10574
 
10183
10575
  class Database extends EventEmitter {
10184
10576
  constructor(options) {
@@ -10186,7 +10578,7 @@ class Database extends EventEmitter {
10186
10578
  this.version = "1";
10187
10579
  this.s3dbVersion = (() => {
10188
10580
  try {
10189
- return true ? "4.1.7" : "latest";
10581
+ return true ? "4.1.10" : "latest";
10190
10582
  } catch (e) {
10191
10583
  return "latest";
10192
10584
  }
@@ -10199,10 +10591,21 @@ class Database extends EventEmitter {
10199
10591
  this.plugins = options.plugins || [];
10200
10592
  this.cache = options.cache;
10201
10593
  this.passphrase = options.passphrase || "secret";
10594
+ let connectionString = options.connectionString;
10595
+ 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
+ });
10604
+ }
10202
10605
  this.client = options.client || new Client({
10203
10606
  verbose: this.verbose,
10204
10607
  parallelism: this.parallelism,
10205
- connectionString: options.connectionString
10608
+ connectionString
10206
10609
  });
10207
10610
  this.bucket = this.client.bucket;
10208
10611
  this.keyPrefix = this.client.keyPrefix;
@@ -10227,15 +10630,18 @@ class Database extends EventEmitter {
10227
10630
  name,
10228
10631
  client: this.client,
10229
10632
  version: currentVersion,
10230
- options: {
10231
- ...versionData.options,
10232
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {}
10233
- },
10234
10633
  attributes: versionData.attributes,
10235
10634
  behavior: versionData.behavior || "user-management",
10236
10635
  parallelism: this.parallelism,
10237
10636
  passphrase: this.passphrase,
10238
- observers: [this]
10637
+ observers: [this],
10638
+ 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: {}
10239
10645
  });
10240
10646
  }
10241
10647
  }
@@ -10304,7 +10710,7 @@ class Database extends EventEmitter {
10304
10710
  generateDefinitionHash(definition, behavior = void 0) {
10305
10711
  const attributes = definition.attributes;
10306
10712
  const stableAttributes = { ...attributes };
10307
- if (definition.options?.timestamps) {
10713
+ if (definition.timestamps) {
10308
10714
  delete stableAttributes.createdAt;
10309
10715
  delete stableAttributes.updatedAt;
10310
10716
  }
@@ -10366,14 +10772,21 @@ class Database extends EventEmitter {
10366
10772
  }
10367
10773
  metadata.resources[name] = {
10368
10774
  currentVersion: version,
10369
- partitions: resourceDef.options?.partitions || {},
10775
+ partitions: resource.config.partitions || {},
10370
10776
  versions: {
10371
10777
  ...existingResource?.versions,
10372
10778
  // Preserve previous versions
10373
10779
  [version]: {
10374
10780
  hash: definitionHash,
10375
10781
  attributes: resourceDef.attributes,
10376
- options: resourceDef.options,
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
+ },
10377
10790
  behavior: resourceDef.behavior || "user-management",
10378
10791
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10379
10792
  }
@@ -10426,10 +10839,9 @@ class Database extends EventEmitter {
10426
10839
  observers: [],
10427
10840
  client: this.client,
10428
10841
  version: "temp",
10429
- options: {
10430
- cache: this.cache,
10431
- ...options
10432
- }
10842
+ passphrase: this.passphrase,
10843
+ cache: this.cache,
10844
+ ...options
10433
10845
  });
10434
10846
  const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10435
10847
  const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
@@ -10469,7 +10881,7 @@ class Database extends EventEmitter {
10469
10881
  async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
10470
10882
  if (this.resources[name]) {
10471
10883
  const existingResource = this.resources[name];
10472
- Object.assign(existingResource.options, {
10884
+ Object.assign(existingResource.config, {
10473
10885
  cache: this.cache,
10474
10886
  ...options
10475
10887
  });
@@ -10496,10 +10908,9 @@ class Database extends EventEmitter {
10496
10908
  observers: [this],
10497
10909
  client: this.client,
10498
10910
  version,
10499
- options: {
10500
- cache: this.cache,
10501
- ...options
10502
- }
10911
+ passphrase: this.passphrase,
10912
+ cache: this.cache,
10913
+ ...options
10503
10914
  });
10504
10915
  this.resources[name] = resource;
10505
10916
  await this.uploadMetadataFile();
@@ -17201,4 +17612,4 @@ class CachePlugin extends Plugin {
17201
17612
  }
17202
17613
  }
17203
17614
 
17204
- export { BaseError, Cache, CachePlugin, Client, ConnectionString, CostsPlugin, Database, ErrorMap, InvalidResourceItem, MemoryCache, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, Plugin, PluginObject, ResourceIdsPageReader, ResourceIdsReader, ResourceReader, ResourceWriter, S3Cache, S3_DEFAULT_ENDPOINT, S3_DEFAULT_REGION, S3db, UnknownError, Validator, ValidatorManager, decrypt, encrypt, sha256, streamToString };
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 };