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.cjs.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /* istanbul ignore file */
2
2
  'use strict';
3
3
 
4
+ Object.defineProperty(exports, '__esModule', { value: true });
5
+
4
6
  var nanoid = require('nanoid');
5
7
  var lodashEs = require('lodash-es');
6
8
  var promisePool = require('@supercharge/promise-pool');
@@ -244,6 +246,8 @@ var substr = 'ab'.substr(-1) === 'b' ?
244
246
  ;
245
247
 
246
248
  const idGenerator = nanoid.customAlphabet(nanoid.urlAlphabet, 22);
249
+ const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
250
+ nanoid.customAlphabet(passwordAlphabet, 12);
247
251
 
248
252
  var domain;
249
253
 
@@ -744,42 +748,85 @@ ${JSON.stringify(rest, null, 2)}`;
744
748
  return `${this.name} | ${this.message}`;
745
749
  }
746
750
  }
747
- class NoSuchBucket extends BaseError {
751
+ class S3DBError extends BaseError {
752
+ constructor(message, details = {}) {
753
+ super({ message, ...details });
754
+ }
755
+ }
756
+ class DatabaseError extends S3DBError {
757
+ constructor(message, details = {}) {
758
+ super(message, details);
759
+ Object.assign(this, details);
760
+ }
761
+ }
762
+ class ValidationError extends S3DBError {
763
+ constructor(message, details = {}) {
764
+ super(message, details);
765
+ Object.assign(this, details);
766
+ }
767
+ }
768
+ class AuthenticationError extends S3DBError {
769
+ constructor(message, details = {}) {
770
+ super(message, details);
771
+ Object.assign(this, details);
772
+ }
773
+ }
774
+ class PermissionError extends S3DBError {
775
+ constructor(message, details = {}) {
776
+ super(message, details);
777
+ Object.assign(this, details);
778
+ }
779
+ }
780
+ class EncryptionError extends S3DBError {
781
+ constructor(message, details = {}) {
782
+ super(message, details);
783
+ Object.assign(this, details);
784
+ }
785
+ }
786
+ class ResourceNotFound extends S3DBError {
787
+ constructor({ bucket, resourceName, id, ...rest }) {
788
+ super(`Resource not found: ${resourceName}/${id} [bucket:${bucket}]`, {
789
+ bucket,
790
+ resourceName,
791
+ id,
792
+ ...rest
793
+ });
794
+ }
795
+ }
796
+ class NoSuchBucket extends S3DBError {
748
797
  constructor({ bucket, ...rest }) {
749
- super({ ...rest, bucket, message: `Bucket does not exists [bucket:${bucket}]` });
798
+ super(`Bucket does not exists [bucket:${bucket}]`, { bucket, ...rest });
750
799
  }
751
800
  }
752
- class NoSuchKey extends BaseError {
801
+ class NoSuchKey extends S3DBError {
753
802
  constructor({ bucket, key, ...rest }) {
754
- super({ ...rest, bucket, message: `Key [${key}] does not exists [bucket:${bucket}/${key}]` });
755
- this.key = key;
803
+ super(`Key [${key}] does not exists [bucket:${bucket}/${key}]`, { bucket, key, ...rest });
756
804
  }
757
805
  }
758
806
  class NotFound extends NoSuchKey {
759
807
  }
760
- class MissingMetadata extends BaseError {
808
+ class MissingMetadata extends S3DBError {
761
809
  constructor({ bucket, ...rest }) {
762
- super({ ...rest, bucket, message: `Missing metadata for bucket [bucket:${bucket}]` });
810
+ super(`Missing metadata for bucket [bucket:${bucket}]`, { bucket, ...rest });
763
811
  }
764
812
  }
765
- class InvalidResourceItem extends BaseError {
813
+ class InvalidResourceItem extends S3DBError {
766
814
  constructor({
767
815
  bucket,
768
816
  resourceName,
769
817
  attributes,
770
818
  validation
771
819
  }) {
772
- super({
820
+ super(`This item is not valid. Resource=${resourceName} [bucket:${bucket}].
821
+ ${JSON.stringify(validation, null, 2)}`, {
773
822
  bucket,
774
- message: `This item is not valid. Resource=${resourceName} [bucket:${bucket}].
775
- ${JSON.stringify(validation, null, 2)}`
823
+ resourceName,
824
+ attributes,
825
+ validation
776
826
  });
777
- this.resourceName = resourceName;
778
- this.attributes = attributes;
779
- this.validation = validation;
780
827
  }
781
828
  }
782
- class UnknownError extends BaseError {
829
+ class UnknownError extends S3DBError {
783
830
  }
784
831
  const ErrorMap = {
785
832
  "NotFound": NotFound,
@@ -808,9 +855,9 @@ class ConnectionString {
808
855
  }
809
856
  }
810
857
  defineS3(uri) {
811
- this.bucket = uri.hostname;
812
- this.accessKeyId = uri.username;
813
- this.secretAccessKey = uri.password;
858
+ this.bucket = decodeURIComponent(uri.hostname);
859
+ this.accessKeyId = decodeURIComponent(uri.username);
860
+ this.secretAccessKey = decodeURIComponent(uri.password);
814
861
  this.endpoint = S3_DEFAULT_ENDPOINT;
815
862
  if (["/", "", null].includes(uri.pathname)) {
816
863
  this.keyPrefix = "";
@@ -822,14 +869,14 @@ class ConnectionString {
822
869
  defineMinio(uri) {
823
870
  this.forcePathStyle = true;
824
871
  this.endpoint = uri.origin;
825
- this.accessKeyId = uri.username;
826
- this.secretAccessKey = uri.password;
872
+ this.accessKeyId = decodeURIComponent(uri.username);
873
+ this.secretAccessKey = decodeURIComponent(uri.password);
827
874
  if (["/", "", null].includes(uri.pathname)) {
828
875
  this.bucket = "s3db";
829
876
  this.keyPrefix = "";
830
877
  } else {
831
878
  let [, bucket, ...subpath] = uri.pathname.split("/");
832
- this.bucket = bucket;
879
+ this.bucket = decodeURIComponent(bucket);
833
880
  this.keyPrefix = [...subpath || []].join("/");
834
881
  }
835
882
  }
@@ -8838,18 +8885,83 @@ function getBehavior(behaviorName) {
8838
8885
  const DEFAULT_BEHAVIOR = "user-management";
8839
8886
 
8840
8887
  class Resource extends EventEmitter {
8841
- constructor({
8842
- name,
8843
- client,
8844
- version = "1",
8845
- options = {},
8846
- attributes = {},
8847
- parallelism = 10,
8848
- passphrase = "secret",
8849
- observers = [],
8850
- behavior = DEFAULT_BEHAVIOR
8851
- }) {
8888
+ /**
8889
+ * Create a new Resource instance
8890
+ * @param {Object} config - Resource configuration
8891
+ * @param {string} config.name - Resource name
8892
+ * @param {Object} config.client - S3 client instance
8893
+ * @param {string} [config.version='v0'] - Resource version
8894
+ * @param {Object} [config.attributes={}] - Resource attributes schema
8895
+ * @param {string} [config.behavior='user-management'] - Resource behavior strategy
8896
+ * @param {string} [config.passphrase='secret'] - Encryption passphrase
8897
+ * @param {number} [config.parallelism=10] - Parallelism for bulk operations
8898
+ * @param {Array} [config.observers=[]] - Observer instances
8899
+ * @param {boolean} [config.cache=false] - Enable caching
8900
+ * @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
8901
+ * @param {boolean} [config.timestamps=false] - Enable automatic timestamps
8902
+ * @param {Object} [config.partitions={}] - Partition definitions
8903
+ * @param {boolean} [config.paranoid=true] - Security flag for dangerous operations
8904
+ * @param {boolean} [config.allNestedObjectsOptional=false] - Make nested objects optional
8905
+ * @param {Object} [config.hooks={}] - Custom hooks
8906
+ * @param {Object} [config.options={}] - Additional options
8907
+ * @example
8908
+ * const users = new Resource({
8909
+ * name: 'users',
8910
+ * client: s3Client,
8911
+ * attributes: {
8912
+ * name: 'string|required',
8913
+ * email: 'string|required',
8914
+ * password: 'secret|required'
8915
+ * },
8916
+ * behavior: 'user-management',
8917
+ * passphrase: 'my-secret-key',
8918
+ * timestamps: true,
8919
+ * partitions: {
8920
+ * byRegion: {
8921
+ * fields: { region: 'string' }
8922
+ * }
8923
+ * },
8924
+ * hooks: {
8925
+ * preInsert: [async (data) => {
8926
+ * console.log('Pre-insert hook:', data);
8927
+ * return data;
8928
+ * }]
8929
+ * }
8930
+ * });
8931
+ */
8932
+ constructor(config) {
8852
8933
  super();
8934
+ const validation = validateResourceConfig(config);
8935
+ if (!validation.isValid) {
8936
+ throw new Error(`Invalid Resource configuration:
8937
+ ${validation.errors.join("\n")}`);
8938
+ }
8939
+ const {
8940
+ name,
8941
+ client,
8942
+ version = "1",
8943
+ attributes = {},
8944
+ behavior = DEFAULT_BEHAVIOR,
8945
+ passphrase = "secret",
8946
+ parallelism = 10,
8947
+ observers = [],
8948
+ cache = false,
8949
+ autoDecrypt = true,
8950
+ timestamps = false,
8951
+ partitions = {},
8952
+ paranoid = true,
8953
+ allNestedObjectsOptional = true,
8954
+ hooks = {},
8955
+ options = {}
8956
+ } = config;
8957
+ const mergedOptions = {
8958
+ cache: typeof options.cache === "boolean" ? options.cache : cache,
8959
+ autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
8960
+ timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
8961
+ paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
8962
+ allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
8963
+ partitions: options.partitions || partitions || {}
8964
+ };
8853
8965
  this.name = name;
8854
8966
  this.client = client;
8855
8967
  this.version = version;
@@ -8857,15 +8969,14 @@ class Resource extends EventEmitter {
8857
8969
  this.observers = observers;
8858
8970
  this.parallelism = parallelism;
8859
8971
  this.passphrase = passphrase ?? "secret";
8860
- this.options = {
8861
- cache: false,
8862
- autoDecrypt: true,
8863
- timestamps: false,
8864
- partitions: {},
8865
- paranoid: true,
8866
- // Security flag for dangerous operations
8867
- allNestedObjectsOptional: options.allNestedObjectsOptional ?? false,
8868
- ...options
8972
+ this.config = {
8973
+ cache: mergedOptions.cache,
8974
+ hooks,
8975
+ paranoid: mergedOptions.paranoid,
8976
+ timestamps: mergedOptions.timestamps,
8977
+ partitions: mergedOptions.partitions,
8978
+ autoDecrypt: mergedOptions.autoDecrypt,
8979
+ allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
8869
8980
  };
8870
8981
  this.hooks = {
8871
8982
  preInsert: [],
@@ -8876,18 +8987,21 @@ class Resource extends EventEmitter {
8876
8987
  afterDelete: []
8877
8988
  };
8878
8989
  this.attributes = attributes || {};
8879
- if (options.timestamps) {
8990
+ if (this.config.timestamps) {
8880
8991
  this.attributes.createdAt = "string|optional";
8881
8992
  this.attributes.updatedAt = "string|optional";
8882
- if (!this.options.partitions.byCreatedDate) {
8883
- this.options.partitions.byCreatedDate = {
8993
+ if (!this.config.partitions) {
8994
+ this.config.partitions = {};
8995
+ }
8996
+ if (!this.config.partitions.byCreatedDate) {
8997
+ this.config.partitions.byCreatedDate = {
8884
8998
  fields: {
8885
8999
  createdAt: "date|maxlength:10"
8886
9000
  }
8887
9001
  };
8888
9002
  }
8889
- if (!this.options.partitions.byUpdatedDate) {
8890
- this.options.partitions.byUpdatedDate = {
9003
+ if (!this.config.partitions.byUpdatedDate) {
9004
+ this.config.partitions.byUpdatedDate = {
8891
9005
  fields: {
8892
9006
  updatedAt: "date|maxlength:10"
8893
9007
  }
@@ -8900,25 +9014,47 @@ class Resource extends EventEmitter {
8900
9014
  passphrase,
8901
9015
  version: this.version,
8902
9016
  options: {
8903
- ...this.options,
8904
- allNestedObjectsOptional: this.options.allNestedObjectsOptional ?? false
9017
+ autoDecrypt: this.config.autoDecrypt,
9018
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
8905
9019
  }
8906
9020
  });
8907
- this.validatePartitions();
8908
9021
  this.setupPartitionHooks();
8909
- if (options.hooks) {
8910
- for (const [event, hooksArr] of Object.entries(options.hooks)) {
9022
+ this.validatePartitions();
9023
+ if (hooks) {
9024
+ for (const [event, hooksArr] of Object.entries(hooks)) {
8911
9025
  if (Array.isArray(hooksArr) && this.hooks[event]) {
8912
9026
  for (const fn of hooksArr) {
8913
- this.hooks[event].push(fn.bind(this));
9027
+ if (typeof fn === "function") {
9028
+ this.hooks[event].push(fn.bind(this));
9029
+ }
8914
9030
  }
8915
9031
  }
8916
9032
  }
8917
9033
  }
8918
9034
  }
9035
+ /**
9036
+ * Get resource options (for backward compatibility with tests)
9037
+ */
9038
+ get options() {
9039
+ return {
9040
+ timestamps: this.config.timestamps,
9041
+ partitions: this.config.partitions || {},
9042
+ cache: this.config.cache,
9043
+ autoDecrypt: this.config.autoDecrypt,
9044
+ paranoid: this.config.paranoid,
9045
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
9046
+ };
9047
+ }
8919
9048
  export() {
8920
9049
  const exported = this.schema.export();
8921
9050
  exported.behavior = this.behavior;
9051
+ exported.timestamps = this.config.timestamps;
9052
+ exported.partitions = this.config.partitions || {};
9053
+ exported.paranoid = this.config.paranoid;
9054
+ exported.allNestedObjectsOptional = this.config.allNestedObjectsOptional;
9055
+ exported.autoDecrypt = this.config.autoDecrypt;
9056
+ exported.cache = this.config.cache;
9057
+ exported.hooks = this.hooks;
8922
9058
  return exported;
8923
9059
  }
8924
9060
  /**
@@ -8928,18 +9064,21 @@ class Resource extends EventEmitter {
8928
9064
  updateAttributes(newAttributes) {
8929
9065
  const oldAttributes = this.attributes;
8930
9066
  this.attributes = newAttributes;
8931
- if (this.options.timestamps) {
9067
+ if (this.config.timestamps) {
8932
9068
  newAttributes.createdAt = "string|optional";
8933
9069
  newAttributes.updatedAt = "string|optional";
8934
- if (!this.options.partitions.byCreatedDate) {
8935
- this.options.partitions.byCreatedDate = {
9070
+ if (!this.config.partitions) {
9071
+ this.config.partitions = {};
9072
+ }
9073
+ if (!this.config.partitions.byCreatedDate) {
9074
+ this.config.partitions.byCreatedDate = {
8936
9075
  fields: {
8937
9076
  createdAt: "date|maxlength:10"
8938
9077
  }
8939
9078
  };
8940
9079
  }
8941
- if (!this.options.partitions.byUpdatedDate) {
8942
- this.options.partitions.byUpdatedDate = {
9080
+ if (!this.config.partitions.byUpdatedDate) {
9081
+ this.config.partitions.byUpdatedDate = {
8943
9082
  fields: {
8944
9083
  updatedAt: "date|maxlength:10"
8945
9084
  }
@@ -8951,10 +9090,13 @@ class Resource extends EventEmitter {
8951
9090
  attributes: newAttributes,
8952
9091
  passphrase: this.passphrase,
8953
9092
  version: this.version,
8954
- options: this.options
9093
+ options: {
9094
+ autoDecrypt: this.config.autoDecrypt,
9095
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
9096
+ }
8955
9097
  });
8956
- this.validatePartitions();
8957
9098
  this.setupPartitionHooks();
9099
+ this.validatePartitions();
8958
9100
  return { oldAttributes, newAttributes };
8959
9101
  }
8960
9102
  /**
@@ -8985,15 +9127,24 @@ class Resource extends EventEmitter {
8985
9127
  * Setup automatic partition hooks
8986
9128
  */
8987
9129
  setupPartitionHooks() {
8988
- const partitions = this.options.partitions;
8989
- if (!partitions || Object.keys(partitions).length === 0) {
9130
+ if (!this.config.partitions) {
9131
+ return;
9132
+ }
9133
+ const partitions = this.config.partitions;
9134
+ if (Object.keys(partitions).length === 0) {
8990
9135
  return;
8991
9136
  }
8992
- this.addHook("afterInsert", async (data) => {
9137
+ if (!this.hooks.afterInsert) {
9138
+ this.hooks.afterInsert = [];
9139
+ }
9140
+ this.hooks.afterInsert.push(async (data) => {
8993
9141
  await this.createPartitionReferences(data);
8994
9142
  return data;
8995
9143
  });
8996
- this.addHook("afterDelete", async (data) => {
9144
+ if (!this.hooks.afterDelete) {
9145
+ this.hooks.afterDelete = [];
9146
+ }
9147
+ this.hooks.afterDelete.push(async (data) => {
8997
9148
  await this.deletePartitionReferences(data);
8998
9149
  return data;
8999
9150
  });
@@ -9018,8 +9169,11 @@ class Resource extends EventEmitter {
9018
9169
  * @throws {Error} If partition fields don't exist in current schema
9019
9170
  */
9020
9171
  validatePartitions() {
9021
- const partitions = this.options.partitions;
9022
- if (!partitions || Object.keys(partitions).length === 0) {
9172
+ if (!this.config.partitions) {
9173
+ return;
9174
+ }
9175
+ const partitions = this.config.partitions;
9176
+ if (Object.keys(partitions).length === 0) {
9023
9177
  return;
9024
9178
  }
9025
9179
  const currentAttributes = Object.keys(this.attributes || {});
@@ -9126,10 +9280,10 @@ class Resource extends EventEmitter {
9126
9280
  * // Returns: null
9127
9281
  */
9128
9282
  getPartitionKey({ partitionName, id, data }) {
9129
- const partition = this.options.partitions[partitionName];
9130
- if (!partition) {
9283
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
9131
9284
  throw new Error(`Partition '${partitionName}' not found`);
9132
9285
  }
9286
+ const partition = this.config.partitions[partitionName];
9133
9287
  const partitionSegments = [];
9134
9288
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
9135
9289
  for (const [fieldName, rule] of sortedFields) {
@@ -9185,6 +9339,12 @@ class Resource extends EventEmitter {
9185
9339
  * name: 'Jane Smith',
9186
9340
  * email: 'jane@example.com'
9187
9341
  * });
9342
+ *
9343
+ * // Insert with auto-generated password for secret field
9344
+ * const user = await resource.insert({
9345
+ * name: 'John Doe',
9346
+ * email: 'john@example.com',
9347
+ * });
9188
9348
  */
9189
9349
  async insert({ id, ...attributes }) {
9190
9350
  if (this.options.timestamps) {
@@ -9280,7 +9440,7 @@ class Resource extends EventEmitter {
9280
9440
  passphrase: this.passphrase,
9281
9441
  version: objectVersion,
9282
9442
  options: {
9283
- ...this.options,
9443
+ ...this.config,
9284
9444
  autoDecrypt: false,
9285
9445
  // Disable decryption
9286
9446
  autoEncrypt: false
@@ -9367,7 +9527,7 @@ class Resource extends EventEmitter {
9367
9527
  */
9368
9528
  async update(id, attributes) {
9369
9529
  const live = await this.get(id);
9370
- if (this.options.timestamps) {
9530
+ if (this.config.timestamps) {
9371
9531
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9372
9532
  }
9373
9533
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
@@ -9489,7 +9649,7 @@ class Resource extends EventEmitter {
9489
9649
  async count({ partition = null, partitionValues = {} } = {}) {
9490
9650
  let prefix;
9491
9651
  if (partition && Object.keys(partitionValues).length > 0) {
9492
- const partitionDef = this.options.partitions[partition];
9652
+ const partitionDef = this.config.partitions[partition];
9493
9653
  if (!partitionDef) {
9494
9654
  throw new Error(`Partition '${partition}' not found`);
9495
9655
  }
@@ -9574,9 +9734,9 @@ class Resource extends EventEmitter {
9574
9734
  return results;
9575
9735
  }
9576
9736
  async deleteAll() {
9577
- if (this.options.paranoid !== false) {
9737
+ if (this.config.paranoid !== false) {
9578
9738
  throw new Error(
9579
- `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.options.paranoid}`
9739
+ `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9580
9740
  );
9581
9741
  }
9582
9742
  const prefix = `resource=${this.name}/v=${this.version}`;
@@ -9593,9 +9753,9 @@ class Resource extends EventEmitter {
9593
9753
  * @returns {Promise<Object>} Deletion report
9594
9754
  */
9595
9755
  async deleteAllData() {
9596
- if (this.options.paranoid !== false) {
9756
+ if (this.config.paranoid !== false) {
9597
9757
  throw new Error(
9598
- `deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.options.paranoid}`
9758
+ `deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9599
9759
  );
9600
9760
  }
9601
9761
  const prefix = `resource=${this.name}`;
@@ -9638,10 +9798,10 @@ class Resource extends EventEmitter {
9638
9798
  async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9639
9799
  let prefix;
9640
9800
  if (partition && Object.keys(partitionValues).length > 0) {
9641
- const partitionDef = this.options.partitions[partition];
9642
- if (!partitionDef) {
9801
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9643
9802
  throw new Error(`Partition '${partition}' not found`);
9644
9803
  }
9804
+ const partitionDef = this.config.partitions[partition];
9645
9805
  const partitionSegments = [];
9646
9806
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9647
9807
  for (const [fieldName, rule] of sortedFields) {
@@ -9732,10 +9892,10 @@ class Resource extends EventEmitter {
9732
9892
  this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9733
9893
  return validResults2;
9734
9894
  }
9735
- const partitionDef = this.options.partitions[partition];
9736
- if (!partitionDef) {
9895
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9737
9896
  throw new Error(`Partition '${partition}' not found`);
9738
9897
  }
9898
+ const partitionDef = this.config.partitions[partition];
9739
9899
  const partitionSegments = [];
9740
9900
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9741
9901
  for (const [fieldName, rule] of sortedFields) {
@@ -10040,16 +10200,14 @@ class Resource extends EventEmitter {
10040
10200
  }
10041
10201
  /**
10042
10202
  * Generate definition hash for this resource
10043
- * @returns {string} SHA256 hash of the schema definition
10203
+ * @returns {string} SHA256 hash of the resource definition (name + attributes)
10044
10204
  */
10045
10205
  getDefinitionHash() {
10046
- const attributes = this.schema.export().attributes;
10047
- const stableAttributes = { ...attributes };
10048
- if (this.options.timestamps) {
10049
- delete stableAttributes.createdAt;
10050
- delete stableAttributes.updatedAt;
10051
- }
10052
- const stableString = jsonStableStringify(stableAttributes);
10206
+ const definition = {
10207
+ attributes: this.attributes,
10208
+ behavior: this.behavior
10209
+ };
10210
+ const stableString = jsonStableStringify(definition);
10053
10211
  return `sha256:${crypto.createHash("sha256").update(stableString).digest("hex")}`;
10054
10212
  }
10055
10213
  /**
@@ -10078,7 +10236,7 @@ class Resource extends EventEmitter {
10078
10236
  passphrase: this.passphrase,
10079
10237
  version,
10080
10238
  options: {
10081
- ...this.options,
10239
+ ...this.config,
10082
10240
  // For older versions, be more lenient with decryption
10083
10241
  autoDecrypt: true,
10084
10242
  autoEncrypt: true
@@ -10095,7 +10253,7 @@ class Resource extends EventEmitter {
10095
10253
  * @param {Object} data - Inserted object data
10096
10254
  */
10097
10255
  async createPartitionReferences(data) {
10098
- const partitions = this.options.partitions;
10256
+ const partitions = this.config.partitions;
10099
10257
  if (!partitions || Object.keys(partitions).length === 0) {
10100
10258
  return;
10101
10259
  }
@@ -10126,7 +10284,7 @@ class Resource extends EventEmitter {
10126
10284
  * @param {Object} data - Deleted object data
10127
10285
  */
10128
10286
  async deletePartitionReferences(data) {
10129
- const partitions = this.options.partitions;
10287
+ const partitions = this.config.partitions;
10130
10288
  if (!partitions || Object.keys(partitions).length === 0) {
10131
10289
  return;
10132
10290
  }
@@ -10218,7 +10376,7 @@ class Resource extends EventEmitter {
10218
10376
  * @param {Object} data - Updated object data
10219
10377
  */
10220
10378
  async updatePartitionReferences(data) {
10221
- const partitions = this.options.partitions;
10379
+ const partitions = this.config.partitions;
10222
10380
  if (!partitions || Object.keys(partitions).length === 0) {
10223
10381
  return;
10224
10382
  }
@@ -10274,10 +10432,10 @@ class Resource extends EventEmitter {
10274
10432
  * });
10275
10433
  */
10276
10434
  async getFromPartition({ id, partitionName, partitionValues = {} }) {
10277
- const partition = this.options.partitions[partitionName];
10278
- if (!partition) {
10435
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
10279
10436
  throw new Error(`Partition '${partitionName}' not found`);
10280
10437
  }
10438
+ const partition = this.config.partitions[partitionName];
10281
10439
  const partitionSegments = [];
10282
10440
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
10283
10441
  for (const [fieldName, rule] of sortedFields) {
@@ -10325,6 +10483,98 @@ class Resource extends EventEmitter {
10325
10483
  return data;
10326
10484
  }
10327
10485
  }
10486
+ function validateResourceConfig(config) {
10487
+ const errors = [];
10488
+ if (!config.name) {
10489
+ errors.push("Resource 'name' is required");
10490
+ } else if (typeof config.name !== "string") {
10491
+ errors.push("Resource 'name' must be a string");
10492
+ } else if (config.name.trim() === "") {
10493
+ errors.push("Resource 'name' cannot be empty");
10494
+ }
10495
+ if (!config.client) {
10496
+ errors.push("S3 'client' is required");
10497
+ }
10498
+ if (!config.attributes) {
10499
+ errors.push("Resource 'attributes' are required");
10500
+ } else if (typeof config.attributes !== "object" || Array.isArray(config.attributes)) {
10501
+ errors.push("Resource 'attributes' must be an object");
10502
+ } else if (Object.keys(config.attributes).length === 0) {
10503
+ errors.push("Resource 'attributes' cannot be empty");
10504
+ }
10505
+ if (config.version !== void 0 && typeof config.version !== "string") {
10506
+ errors.push("Resource 'version' must be a string");
10507
+ }
10508
+ if (config.behavior !== void 0 && typeof config.behavior !== "string") {
10509
+ errors.push("Resource 'behavior' must be a string");
10510
+ }
10511
+ if (config.passphrase !== void 0 && typeof config.passphrase !== "string") {
10512
+ errors.push("Resource 'passphrase' must be a string");
10513
+ }
10514
+ if (config.parallelism !== void 0) {
10515
+ if (typeof config.parallelism !== "number" || !Number.isInteger(config.parallelism)) {
10516
+ errors.push("Resource 'parallelism' must be an integer");
10517
+ } else if (config.parallelism < 1) {
10518
+ errors.push("Resource 'parallelism' must be greater than 0");
10519
+ }
10520
+ }
10521
+ if (config.observers !== void 0 && !Array.isArray(config.observers)) {
10522
+ errors.push("Resource 'observers' must be an array");
10523
+ }
10524
+ const booleanFields = ["cache", "autoDecrypt", "timestamps", "paranoid", "allNestedObjectsOptional"];
10525
+ for (const field of booleanFields) {
10526
+ if (config[field] !== void 0 && typeof config[field] !== "boolean") {
10527
+ errors.push(`Resource '${field}' must be a boolean`);
10528
+ }
10529
+ }
10530
+ if (config.partitions !== void 0) {
10531
+ if (typeof config.partitions !== "object" || Array.isArray(config.partitions)) {
10532
+ errors.push("Resource 'partitions' must be an object");
10533
+ } else {
10534
+ for (const [partitionName, partitionDef] of Object.entries(config.partitions)) {
10535
+ if (typeof partitionDef !== "object" || Array.isArray(partitionDef)) {
10536
+ errors.push(`Partition '${partitionName}' must be an object`);
10537
+ } else if (!partitionDef.fields) {
10538
+ errors.push(`Partition '${partitionName}' must have a 'fields' property`);
10539
+ } else if (typeof partitionDef.fields !== "object" || Array.isArray(partitionDef.fields)) {
10540
+ errors.push(`Partition '${partitionName}.fields' must be an object`);
10541
+ } else {
10542
+ for (const [fieldName, fieldType] of Object.entries(partitionDef.fields)) {
10543
+ if (typeof fieldType !== "string") {
10544
+ errors.push(`Partition '${partitionName}.fields.${fieldName}' must be a string`);
10545
+ }
10546
+ }
10547
+ }
10548
+ }
10549
+ }
10550
+ }
10551
+ if (config.hooks !== void 0) {
10552
+ if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
10553
+ errors.push("Resource 'hooks' must be an object");
10554
+ } else {
10555
+ const validHookEvents = ["preInsert", "afterInsert", "preUpdate", "afterUpdate", "preDelete", "afterDelete"];
10556
+ for (const [event, hooksArr] of Object.entries(config.hooks)) {
10557
+ if (!validHookEvents.includes(event)) {
10558
+ errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
10559
+ } else if (!Array.isArray(hooksArr)) {
10560
+ errors.push(`Resource 'hooks.${event}' must be an array`);
10561
+ } else {
10562
+ for (let i = 0; i < hooksArr.length; i++) {
10563
+ const hook = hooksArr[i];
10564
+ if (typeof hook !== "function") {
10565
+ if (typeof hook === "string") continue;
10566
+ continue;
10567
+ }
10568
+ }
10569
+ }
10570
+ }
10571
+ }
10572
+ }
10573
+ return {
10574
+ isValid: errors.length === 0,
10575
+ errors
10576
+ };
10577
+ }
10328
10578
 
10329
10579
  class Database extends EventEmitter {
10330
10580
  constructor(options) {
@@ -10332,7 +10582,7 @@ class Database extends EventEmitter {
10332
10582
  this.version = "1";
10333
10583
  this.s3dbVersion = (() => {
10334
10584
  try {
10335
- return true ? "4.1.9" : "latest";
10585
+ return true ? "4.1.12" : "latest";
10336
10586
  } catch (e) {
10337
10587
  return "latest";
10338
10588
  }
@@ -10345,10 +10595,21 @@ class Database extends EventEmitter {
10345
10595
  this.plugins = options.plugins || [];
10346
10596
  this.cache = options.cache;
10347
10597
  this.passphrase = options.passphrase || "secret";
10598
+ let connectionString = options.connectionString;
10599
+ if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
10600
+ connectionString = ConnectionString.buildFromParams({
10601
+ bucket: options.bucket,
10602
+ region: options.region,
10603
+ accessKeyId: options.accessKeyId,
10604
+ secretAccessKey: options.secretAccessKey,
10605
+ endpoint: options.endpoint,
10606
+ forcePathStyle: options.forcePathStyle
10607
+ });
10608
+ }
10348
10609
  this.client = options.client || new Client({
10349
10610
  verbose: this.verbose,
10350
10611
  parallelism: this.parallelism,
10351
- connectionString: options.connectionString
10612
+ connectionString
10352
10613
  });
10353
10614
  this.bucket = this.client.bucket;
10354
10615
  this.keyPrefix = this.client.keyPrefix;
@@ -10373,15 +10634,18 @@ class Database extends EventEmitter {
10373
10634
  name,
10374
10635
  client: this.client,
10375
10636
  version: currentVersion,
10376
- options: {
10377
- ...versionData.options,
10378
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {}
10379
- },
10380
10637
  attributes: versionData.attributes,
10381
10638
  behavior: versionData.behavior || "user-management",
10382
10639
  parallelism: this.parallelism,
10383
10640
  passphrase: this.passphrase,
10384
- observers: [this]
10641
+ observers: [this],
10642
+ cache: this.cache,
10643
+ timestamps: versionData.options?.timestamps || false,
10644
+ partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
10645
+ paranoid: versionData.options?.paranoid !== false,
10646
+ allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
10647
+ autoDecrypt: versionData.options?.autoDecrypt !== false,
10648
+ hooks: versionData.options?.hooks || {}
10385
10649
  });
10386
10650
  }
10387
10651
  }
@@ -10450,7 +10714,7 @@ class Database extends EventEmitter {
10450
10714
  generateDefinitionHash(definition, behavior = void 0) {
10451
10715
  const attributes = definition.attributes;
10452
10716
  const stableAttributes = { ...attributes };
10453
- if (definition.options?.timestamps) {
10717
+ if (definition.timestamps) {
10454
10718
  delete stableAttributes.createdAt;
10455
10719
  delete stableAttributes.updatedAt;
10456
10720
  }
@@ -10512,14 +10776,22 @@ class Database extends EventEmitter {
10512
10776
  }
10513
10777
  metadata.resources[name] = {
10514
10778
  currentVersion: version,
10515
- partitions: resourceDef.options?.partitions || {},
10779
+ partitions: resource.config.partitions || {},
10516
10780
  versions: {
10517
10781
  ...existingResource?.versions,
10518
10782
  // Preserve previous versions
10519
10783
  [version]: {
10520
10784
  hash: definitionHash,
10521
10785
  attributes: resourceDef.attributes,
10522
- options: resourceDef.options,
10786
+ options: {
10787
+ timestamps: resource.config.timestamps,
10788
+ partitions: resource.config.partitions,
10789
+ paranoid: resource.config.paranoid,
10790
+ allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
10791
+ autoDecrypt: resource.config.autoDecrypt,
10792
+ cache: resource.config.cache,
10793
+ hooks: resourceDef.hooks || {}
10794
+ },
10523
10795
  behavior: resourceDef.behavior || "user-management",
10524
10796
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10525
10797
  }
@@ -10572,10 +10844,9 @@ class Database extends EventEmitter {
10572
10844
  observers: [],
10573
10845
  client: this.client,
10574
10846
  version: "temp",
10575
- options: {
10576
- cache: this.cache,
10577
- ...options
10578
- }
10847
+ passphrase: this.passphrase,
10848
+ cache: this.cache,
10849
+ ...options
10579
10850
  });
10580
10851
  const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10581
10852
  const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
@@ -10615,7 +10886,7 @@ class Database extends EventEmitter {
10615
10886
  async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
10616
10887
  if (this.resources[name]) {
10617
10888
  const existingResource = this.resources[name];
10618
- Object.assign(existingResource.options, {
10889
+ Object.assign(existingResource.config, {
10619
10890
  cache: this.cache,
10620
10891
  ...options
10621
10892
  });
@@ -10642,10 +10913,9 @@ class Database extends EventEmitter {
10642
10913
  observers: [this],
10643
10914
  client: this.client,
10644
10915
  version,
10645
- options: {
10646
- cache: this.cache,
10647
- ...options
10648
- }
10916
+ passphrase: this.passphrase,
10917
+ cache: this.cache,
10918
+ ...options
10649
10919
  });
10650
10920
  this.resources[name] = resource;
10651
10921
  await this.uploadMetadataFile();
@@ -17347,6 +17617,7 @@ class CachePlugin extends Plugin {
17347
17617
  }
17348
17618
  }
17349
17619
 
17620
+ exports.AuthenticationError = AuthenticationError;
17350
17621
  exports.BaseError = BaseError;
17351
17622
  exports.Cache = Cache;
17352
17623
  exports.CachePlugin = CachePlugin;
@@ -17354,6 +17625,8 @@ exports.Client = Client;
17354
17625
  exports.ConnectionString = ConnectionString;
17355
17626
  exports.CostsPlugin = CostsPlugin;
17356
17627
  exports.Database = Database;
17628
+ exports.DatabaseError = DatabaseError;
17629
+ exports.EncryptionError = EncryptionError;
17357
17630
  exports.ErrorMap = ErrorMap;
17358
17631
  exports.InvalidResourceItem = InvalidResourceItem;
17359
17632
  exports.MemoryCache = MemoryCache;
@@ -17361,20 +17634,28 @@ exports.MissingMetadata = MissingMetadata;
17361
17634
  exports.NoSuchBucket = NoSuchBucket;
17362
17635
  exports.NoSuchKey = NoSuchKey;
17363
17636
  exports.NotFound = NotFound;
17637
+ exports.PermissionError = PermissionError;
17364
17638
  exports.Plugin = Plugin;
17365
17639
  exports.PluginObject = PluginObject;
17366
17640
  exports.ResourceIdsPageReader = ResourceIdsPageReader;
17367
17641
  exports.ResourceIdsReader = ResourceIdsReader;
17642
+ exports.ResourceNotFound = ResourceNotFound;
17643
+ exports.ResourceNotFoundError = ResourceNotFound;
17368
17644
  exports.ResourceReader = ResourceReader;
17369
17645
  exports.ResourceWriter = ResourceWriter;
17370
17646
  exports.S3Cache = S3Cache;
17647
+ exports.S3DB = S3db;
17648
+ exports.S3DBError = S3DBError;
17371
17649
  exports.S3_DEFAULT_ENDPOINT = S3_DEFAULT_ENDPOINT;
17372
17650
  exports.S3_DEFAULT_REGION = S3_DEFAULT_REGION;
17373
17651
  exports.S3db = S3db;
17652
+ exports.S3dbError = S3DBError;
17374
17653
  exports.UnknownError = UnknownError;
17654
+ exports.ValidationError = ValidationError;
17375
17655
  exports.Validator = Validator;
17376
17656
  exports.ValidatorManager = ValidatorManager;
17377
17657
  exports.decrypt = decrypt;
17658
+ exports.default = S3db;
17378
17659
  exports.encrypt = encrypt;
17379
17660
  exports.sha256 = sha256;
17380
17661
  exports.streamToString = streamToString;