s3db.js 4.1.10 → 4.1.13

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) {
8988
9127
  return;
8989
9128
  }
8990
- this.addHook("afterInsert", async (data) => {
9129
+ const partitions = this.config.partitions;
9130
+ if (Object.keys(partitions).length === 0) {
9131
+ return;
9132
+ }
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) {
@@ -9278,7 +9436,7 @@ class Resource extends EventEmitter {
9278
9436
  passphrase: this.passphrase,
9279
9437
  version: objectVersion,
9280
9438
  options: {
9281
- ...this.options,
9439
+ ...this.config,
9282
9440
  autoDecrypt: false,
9283
9441
  // Disable decryption
9284
9442
  autoEncrypt: false
@@ -9365,7 +9523,7 @@ class Resource extends EventEmitter {
9365
9523
  */
9366
9524
  async update(id, attributes) {
9367
9525
  const live = await this.get(id);
9368
- if (this.options.timestamps) {
9526
+ if (this.config.timestamps) {
9369
9527
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9370
9528
  }
9371
9529
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
@@ -9487,7 +9645,7 @@ class Resource extends EventEmitter {
9487
9645
  async count({ partition = null, partitionValues = {} } = {}) {
9488
9646
  let prefix;
9489
9647
  if (partition && Object.keys(partitionValues).length > 0) {
9490
- const partitionDef = this.options.partitions[partition];
9648
+ const partitionDef = this.config.partitions[partition];
9491
9649
  if (!partitionDef) {
9492
9650
  throw new Error(`Partition '${partition}' not found`);
9493
9651
  }
@@ -9572,9 +9730,9 @@ class Resource extends EventEmitter {
9572
9730
  return results;
9573
9731
  }
9574
9732
  async deleteAll() {
9575
- if (this.options.paranoid !== false) {
9733
+ if (this.config.paranoid !== false) {
9576
9734
  throw new Error(
9577
- `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}`
9578
9736
  );
9579
9737
  }
9580
9738
  const prefix = `resource=${this.name}/v=${this.version}`;
@@ -9591,9 +9749,9 @@ class Resource extends EventEmitter {
9591
9749
  * @returns {Promise<Object>} Deletion report
9592
9750
  */
9593
9751
  async deleteAllData() {
9594
- if (this.options.paranoid !== false) {
9752
+ if (this.config.paranoid !== false) {
9595
9753
  throw new Error(
9596
- `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}`
9597
9755
  );
9598
9756
  }
9599
9757
  const prefix = `resource=${this.name}`;
@@ -9636,10 +9794,10 @@ class Resource extends EventEmitter {
9636
9794
  async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9637
9795
  let prefix;
9638
9796
  if (partition && Object.keys(partitionValues).length > 0) {
9639
- const partitionDef = this.options.partitions[partition];
9640
- if (!partitionDef) {
9797
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9641
9798
  throw new Error(`Partition '${partition}' not found`);
9642
9799
  }
9800
+ const partitionDef = this.config.partitions[partition];
9643
9801
  const partitionSegments = [];
9644
9802
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9645
9803
  for (const [fieldName, rule] of sortedFields) {
@@ -9730,10 +9888,10 @@ class Resource extends EventEmitter {
9730
9888
  this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9731
9889
  return validResults2;
9732
9890
  }
9733
- const partitionDef = this.options.partitions[partition];
9734
- if (!partitionDef) {
9891
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9735
9892
  throw new Error(`Partition '${partition}' not found`);
9736
9893
  }
9894
+ const partitionDef = this.config.partitions[partition];
9737
9895
  const partitionSegments = [];
9738
9896
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9739
9897
  for (const [fieldName, rule] of sortedFields) {
@@ -10038,16 +10196,14 @@ class Resource extends EventEmitter {
10038
10196
  }
10039
10197
  /**
10040
10198
  * Generate definition hash for this resource
10041
- * @returns {string} SHA256 hash of the schema definition
10199
+ * @returns {string} SHA256 hash of the resource definition (name + attributes)
10042
10200
  */
10043
10201
  getDefinitionHash() {
10044
- const attributes = this.schema.export().attributes;
10045
- const stableAttributes = { ...attributes };
10046
- if (this.options.timestamps) {
10047
- delete stableAttributes.createdAt;
10048
- delete stableAttributes.updatedAt;
10049
- }
10050
- const stableString = jsonStableStringify(stableAttributes);
10202
+ const definition = {
10203
+ attributes: this.attributes,
10204
+ behavior: this.behavior
10205
+ };
10206
+ const stableString = jsonStableStringify(definition);
10051
10207
  return `sha256:${createHash("sha256").update(stableString).digest("hex")}`;
10052
10208
  }
10053
10209
  /**
@@ -10076,7 +10232,7 @@ class Resource extends EventEmitter {
10076
10232
  passphrase: this.passphrase,
10077
10233
  version,
10078
10234
  options: {
10079
- ...this.options,
10235
+ ...this.config,
10080
10236
  // For older versions, be more lenient with decryption
10081
10237
  autoDecrypt: true,
10082
10238
  autoEncrypt: true
@@ -10093,7 +10249,7 @@ class Resource extends EventEmitter {
10093
10249
  * @param {Object} data - Inserted object data
10094
10250
  */
10095
10251
  async createPartitionReferences(data) {
10096
- const partitions = this.options.partitions;
10252
+ const partitions = this.config.partitions;
10097
10253
  if (!partitions || Object.keys(partitions).length === 0) {
10098
10254
  return;
10099
10255
  }
@@ -10124,7 +10280,7 @@ class Resource extends EventEmitter {
10124
10280
  * @param {Object} data - Deleted object data
10125
10281
  */
10126
10282
  async deletePartitionReferences(data) {
10127
- const partitions = this.options.partitions;
10283
+ const partitions = this.config.partitions;
10128
10284
  if (!partitions || Object.keys(partitions).length === 0) {
10129
10285
  return;
10130
10286
  }
@@ -10216,7 +10372,7 @@ class Resource extends EventEmitter {
10216
10372
  * @param {Object} data - Updated object data
10217
10373
  */
10218
10374
  async updatePartitionReferences(data) {
10219
- const partitions = this.options.partitions;
10375
+ const partitions = this.config.partitions;
10220
10376
  if (!partitions || Object.keys(partitions).length === 0) {
10221
10377
  return;
10222
10378
  }
@@ -10272,10 +10428,10 @@ class Resource extends EventEmitter {
10272
10428
  * });
10273
10429
  */
10274
10430
  async getFromPartition({ id, partitionName, partitionValues = {} }) {
10275
- const partition = this.options.partitions[partitionName];
10276
- if (!partition) {
10431
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
10277
10432
  throw new Error(`Partition '${partitionName}' not found`);
10278
10433
  }
10434
+ const partition = this.config.partitions[partitionName];
10279
10435
  const partitionSegments = [];
10280
10436
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
10281
10437
  for (const [fieldName, rule] of sortedFields) {
@@ -10323,6 +10479,98 @@ class Resource extends EventEmitter {
10323
10479
  return data;
10324
10480
  }
10325
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
+ }
10326
10574
 
10327
10575
  class Database extends EventEmitter {
10328
10576
  constructor(options) {
@@ -10330,7 +10578,7 @@ class Database extends EventEmitter {
10330
10578
  this.version = "1";
10331
10579
  this.s3dbVersion = (() => {
10332
10580
  try {
10333
- return true ? "4.1.9" : "latest";
10581
+ return true ? "4.1.12" : "latest";
10334
10582
  } catch (e) {
10335
10583
  return "latest";
10336
10584
  }
@@ -10343,10 +10591,21 @@ class Database extends EventEmitter {
10343
10591
  this.plugins = options.plugins || [];
10344
10592
  this.cache = options.cache;
10345
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
+ }
10346
10605
  this.client = options.client || new Client({
10347
10606
  verbose: this.verbose,
10348
10607
  parallelism: this.parallelism,
10349
- connectionString: options.connectionString
10608
+ connectionString
10350
10609
  });
10351
10610
  this.bucket = this.client.bucket;
10352
10611
  this.keyPrefix = this.client.keyPrefix;
@@ -10371,15 +10630,18 @@ class Database extends EventEmitter {
10371
10630
  name,
10372
10631
  client: this.client,
10373
10632
  version: currentVersion,
10374
- options: {
10375
- ...versionData.options,
10376
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {}
10377
- },
10378
10633
  attributes: versionData.attributes,
10379
10634
  behavior: versionData.behavior || "user-management",
10380
10635
  parallelism: this.parallelism,
10381
10636
  passphrase: this.passphrase,
10382
- 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: versionData.options?.hooks || {}
10383
10645
  });
10384
10646
  }
10385
10647
  }
@@ -10448,7 +10710,7 @@ class Database extends EventEmitter {
10448
10710
  generateDefinitionHash(definition, behavior = void 0) {
10449
10711
  const attributes = definition.attributes;
10450
10712
  const stableAttributes = { ...attributes };
10451
- if (definition.options?.timestamps) {
10713
+ if (definition.timestamps) {
10452
10714
  delete stableAttributes.createdAt;
10453
10715
  delete stableAttributes.updatedAt;
10454
10716
  }
@@ -10510,14 +10772,22 @@ class Database extends EventEmitter {
10510
10772
  }
10511
10773
  metadata.resources[name] = {
10512
10774
  currentVersion: version,
10513
- partitions: resourceDef.options?.partitions || {},
10775
+ partitions: resource.config.partitions || {},
10514
10776
  versions: {
10515
10777
  ...existingResource?.versions,
10516
10778
  // Preserve previous versions
10517
10779
  [version]: {
10518
10780
  hash: definitionHash,
10519
10781
  attributes: resourceDef.attributes,
10520
- 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
+ hooks: resourceDef.hooks || {}
10790
+ },
10521
10791
  behavior: resourceDef.behavior || "user-management",
10522
10792
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10523
10793
  }
@@ -10570,10 +10840,9 @@ class Database extends EventEmitter {
10570
10840
  observers: [],
10571
10841
  client: this.client,
10572
10842
  version: "temp",
10573
- options: {
10574
- cache: this.cache,
10575
- ...options
10576
- }
10843
+ passphrase: this.passphrase,
10844
+ cache: this.cache,
10845
+ ...options
10577
10846
  });
10578
10847
  const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10579
10848
  const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
@@ -10613,7 +10882,7 @@ class Database extends EventEmitter {
10613
10882
  async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
10614
10883
  if (this.resources[name]) {
10615
10884
  const existingResource = this.resources[name];
10616
- Object.assign(existingResource.options, {
10885
+ Object.assign(existingResource.config, {
10617
10886
  cache: this.cache,
10618
10887
  ...options
10619
10888
  });
@@ -10640,10 +10909,9 @@ class Database extends EventEmitter {
10640
10909
  observers: [this],
10641
10910
  client: this.client,
10642
10911
  version,
10643
- options: {
10644
- cache: this.cache,
10645
- ...options
10646
- }
10912
+ passphrase: this.passphrase,
10913
+ cache: this.cache,
10914
+ ...options
10647
10915
  });
10648
10916
  this.resources[name] = resource;
10649
10917
  await this.uploadMetadataFile();
@@ -17345,4 +17613,4 @@ class CachePlugin extends Plugin {
17345
17613
  }
17346
17614
  }
17347
17615
 
17348
- 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 };
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 };