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.iife.js CHANGED
@@ -236,6 +236,8 @@ var S3DB = (function (exports, nanoid, lodashEs, promisePool, clientS3, crypto,
236
236
  ;
237
237
 
238
238
  const idGenerator = nanoid.customAlphabet(nanoid.urlAlphabet, 22);
239
+ const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
240
+ nanoid.customAlphabet(passwordAlphabet, 12);
239
241
 
240
242
  var domain;
241
243
 
@@ -736,42 +738,85 @@ ${JSON.stringify(rest, null, 2)}`;
736
738
  return `${this.name} | ${this.message}`;
737
739
  }
738
740
  }
739
- class NoSuchBucket extends BaseError {
741
+ class S3DBError extends BaseError {
742
+ constructor(message, details = {}) {
743
+ super({ message, ...details });
744
+ }
745
+ }
746
+ class DatabaseError extends S3DBError {
747
+ constructor(message, details = {}) {
748
+ super(message, details);
749
+ Object.assign(this, details);
750
+ }
751
+ }
752
+ class ValidationError extends S3DBError {
753
+ constructor(message, details = {}) {
754
+ super(message, details);
755
+ Object.assign(this, details);
756
+ }
757
+ }
758
+ class AuthenticationError extends S3DBError {
759
+ constructor(message, details = {}) {
760
+ super(message, details);
761
+ Object.assign(this, details);
762
+ }
763
+ }
764
+ class PermissionError extends S3DBError {
765
+ constructor(message, details = {}) {
766
+ super(message, details);
767
+ Object.assign(this, details);
768
+ }
769
+ }
770
+ class EncryptionError extends S3DBError {
771
+ constructor(message, details = {}) {
772
+ super(message, details);
773
+ Object.assign(this, details);
774
+ }
775
+ }
776
+ class ResourceNotFound extends S3DBError {
777
+ constructor({ bucket, resourceName, id, ...rest }) {
778
+ super(`Resource not found: ${resourceName}/${id} [bucket:${bucket}]`, {
779
+ bucket,
780
+ resourceName,
781
+ id,
782
+ ...rest
783
+ });
784
+ }
785
+ }
786
+ class NoSuchBucket extends S3DBError {
740
787
  constructor({ bucket, ...rest }) {
741
- super({ ...rest, bucket, message: `Bucket does not exists [bucket:${bucket}]` });
788
+ super(`Bucket does not exists [bucket:${bucket}]`, { bucket, ...rest });
742
789
  }
743
790
  }
744
- class NoSuchKey extends BaseError {
791
+ class NoSuchKey extends S3DBError {
745
792
  constructor({ bucket, key, ...rest }) {
746
- super({ ...rest, bucket, message: `Key [${key}] does not exists [bucket:${bucket}/${key}]` });
747
- this.key = key;
793
+ super(`Key [${key}] does not exists [bucket:${bucket}/${key}]`, { bucket, key, ...rest });
748
794
  }
749
795
  }
750
796
  class NotFound extends NoSuchKey {
751
797
  }
752
- class MissingMetadata extends BaseError {
798
+ class MissingMetadata extends S3DBError {
753
799
  constructor({ bucket, ...rest }) {
754
- super({ ...rest, bucket, message: `Missing metadata for bucket [bucket:${bucket}]` });
800
+ super(`Missing metadata for bucket [bucket:${bucket}]`, { bucket, ...rest });
755
801
  }
756
802
  }
757
- class InvalidResourceItem extends BaseError {
803
+ class InvalidResourceItem extends S3DBError {
758
804
  constructor({
759
805
  bucket,
760
806
  resourceName,
761
807
  attributes,
762
808
  validation
763
809
  }) {
764
- super({
810
+ super(`This item is not valid. Resource=${resourceName} [bucket:${bucket}].
811
+ ${JSON.stringify(validation, null, 2)}`, {
765
812
  bucket,
766
- message: `This item is not valid. Resource=${resourceName} [bucket:${bucket}].
767
- ${JSON.stringify(validation, null, 2)}`
813
+ resourceName,
814
+ attributes,
815
+ validation
768
816
  });
769
- this.resourceName = resourceName;
770
- this.attributes = attributes;
771
- this.validation = validation;
772
817
  }
773
818
  }
774
- class UnknownError extends BaseError {
819
+ class UnknownError extends S3DBError {
775
820
  }
776
821
  const ErrorMap = {
777
822
  "NotFound": NotFound,
@@ -800,9 +845,9 @@ ${JSON.stringify(validation, null, 2)}`
800
845
  }
801
846
  }
802
847
  defineS3(uri) {
803
- this.bucket = uri.hostname;
804
- this.accessKeyId = uri.username;
805
- this.secretAccessKey = uri.password;
848
+ this.bucket = decodeURIComponent(uri.hostname);
849
+ this.accessKeyId = decodeURIComponent(uri.username);
850
+ this.secretAccessKey = decodeURIComponent(uri.password);
806
851
  this.endpoint = S3_DEFAULT_ENDPOINT;
807
852
  if (["/", "", null].includes(uri.pathname)) {
808
853
  this.keyPrefix = "";
@@ -814,14 +859,14 @@ ${JSON.stringify(validation, null, 2)}`
814
859
  defineMinio(uri) {
815
860
  this.forcePathStyle = true;
816
861
  this.endpoint = uri.origin;
817
- this.accessKeyId = uri.username;
818
- this.secretAccessKey = uri.password;
862
+ this.accessKeyId = decodeURIComponent(uri.username);
863
+ this.secretAccessKey = decodeURIComponent(uri.password);
819
864
  if (["/", "", null].includes(uri.pathname)) {
820
865
  this.bucket = "s3db";
821
866
  this.keyPrefix = "";
822
867
  } else {
823
868
  let [, bucket, ...subpath] = uri.pathname.split("/");
824
- this.bucket = bucket;
869
+ this.bucket = decodeURIComponent(bucket);
825
870
  this.keyPrefix = [...subpath || []].join("/");
826
871
  }
827
872
  }
@@ -8830,18 +8875,83 @@ ${JSON.stringify(validation, null, 2)}`
8830
8875
  const DEFAULT_BEHAVIOR = "user-management";
8831
8876
 
8832
8877
  class Resource extends EventEmitter {
8833
- constructor({
8834
- name,
8835
- client,
8836
- version = "1",
8837
- options = {},
8838
- attributes = {},
8839
- parallelism = 10,
8840
- passphrase = "secret",
8841
- observers = [],
8842
- behavior = DEFAULT_BEHAVIOR
8843
- }) {
8878
+ /**
8879
+ * Create a new Resource instance
8880
+ * @param {Object} config - Resource configuration
8881
+ * @param {string} config.name - Resource name
8882
+ * @param {Object} config.client - S3 client instance
8883
+ * @param {string} [config.version='v0'] - Resource version
8884
+ * @param {Object} [config.attributes={}] - Resource attributes schema
8885
+ * @param {string} [config.behavior='user-management'] - Resource behavior strategy
8886
+ * @param {string} [config.passphrase='secret'] - Encryption passphrase
8887
+ * @param {number} [config.parallelism=10] - Parallelism for bulk operations
8888
+ * @param {Array} [config.observers=[]] - Observer instances
8889
+ * @param {boolean} [config.cache=false] - Enable caching
8890
+ * @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
8891
+ * @param {boolean} [config.timestamps=false] - Enable automatic timestamps
8892
+ * @param {Object} [config.partitions={}] - Partition definitions
8893
+ * @param {boolean} [config.paranoid=true] - Security flag for dangerous operations
8894
+ * @param {boolean} [config.allNestedObjectsOptional=false] - Make nested objects optional
8895
+ * @param {Object} [config.hooks={}] - Custom hooks
8896
+ * @param {Object} [config.options={}] - Additional options
8897
+ * @example
8898
+ * const users = new Resource({
8899
+ * name: 'users',
8900
+ * client: s3Client,
8901
+ * attributes: {
8902
+ * name: 'string|required',
8903
+ * email: 'string|required',
8904
+ * password: 'secret|required'
8905
+ * },
8906
+ * behavior: 'user-management',
8907
+ * passphrase: 'my-secret-key',
8908
+ * timestamps: true,
8909
+ * partitions: {
8910
+ * byRegion: {
8911
+ * fields: { region: 'string' }
8912
+ * }
8913
+ * },
8914
+ * hooks: {
8915
+ * preInsert: [async (data) => {
8916
+ * console.log('Pre-insert hook:', data);
8917
+ * return data;
8918
+ * }]
8919
+ * }
8920
+ * });
8921
+ */
8922
+ constructor(config) {
8844
8923
  super();
8924
+ const validation = validateResourceConfig(config);
8925
+ if (!validation.isValid) {
8926
+ throw new Error(`Invalid Resource configuration:
8927
+ ${validation.errors.join("\n")}`);
8928
+ }
8929
+ const {
8930
+ name,
8931
+ client,
8932
+ version = "1",
8933
+ attributes = {},
8934
+ behavior = DEFAULT_BEHAVIOR,
8935
+ passphrase = "secret",
8936
+ parallelism = 10,
8937
+ observers = [],
8938
+ cache = false,
8939
+ autoDecrypt = true,
8940
+ timestamps = false,
8941
+ partitions = {},
8942
+ paranoid = true,
8943
+ allNestedObjectsOptional = true,
8944
+ hooks = {},
8945
+ options = {}
8946
+ } = config;
8947
+ const mergedOptions = {
8948
+ cache: typeof options.cache === "boolean" ? options.cache : cache,
8949
+ autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
8950
+ timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
8951
+ paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
8952
+ allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
8953
+ partitions: options.partitions || partitions || {}
8954
+ };
8845
8955
  this.name = name;
8846
8956
  this.client = client;
8847
8957
  this.version = version;
@@ -8849,15 +8959,14 @@ ${JSON.stringify(validation, null, 2)}`
8849
8959
  this.observers = observers;
8850
8960
  this.parallelism = parallelism;
8851
8961
  this.passphrase = passphrase ?? "secret";
8852
- this.options = {
8853
- cache: false,
8854
- autoDecrypt: true,
8855
- timestamps: false,
8856
- partitions: {},
8857
- paranoid: true,
8858
- // Security flag for dangerous operations
8859
- allNestedObjectsOptional: options.allNestedObjectsOptional ?? false,
8860
- ...options
8962
+ this.config = {
8963
+ cache: mergedOptions.cache,
8964
+ hooks,
8965
+ paranoid: mergedOptions.paranoid,
8966
+ timestamps: mergedOptions.timestamps,
8967
+ partitions: mergedOptions.partitions,
8968
+ autoDecrypt: mergedOptions.autoDecrypt,
8969
+ allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
8861
8970
  };
8862
8971
  this.hooks = {
8863
8972
  preInsert: [],
@@ -8868,18 +8977,21 @@ ${JSON.stringify(validation, null, 2)}`
8868
8977
  afterDelete: []
8869
8978
  };
8870
8979
  this.attributes = attributes || {};
8871
- if (options.timestamps) {
8980
+ if (this.config.timestamps) {
8872
8981
  this.attributes.createdAt = "string|optional";
8873
8982
  this.attributes.updatedAt = "string|optional";
8874
- if (!this.options.partitions.byCreatedDate) {
8875
- this.options.partitions.byCreatedDate = {
8983
+ if (!this.config.partitions) {
8984
+ this.config.partitions = {};
8985
+ }
8986
+ if (!this.config.partitions.byCreatedDate) {
8987
+ this.config.partitions.byCreatedDate = {
8876
8988
  fields: {
8877
8989
  createdAt: "date|maxlength:10"
8878
8990
  }
8879
8991
  };
8880
8992
  }
8881
- if (!this.options.partitions.byUpdatedDate) {
8882
- this.options.partitions.byUpdatedDate = {
8993
+ if (!this.config.partitions.byUpdatedDate) {
8994
+ this.config.partitions.byUpdatedDate = {
8883
8995
  fields: {
8884
8996
  updatedAt: "date|maxlength:10"
8885
8997
  }
@@ -8892,25 +9004,47 @@ ${JSON.stringify(validation, null, 2)}`
8892
9004
  passphrase,
8893
9005
  version: this.version,
8894
9006
  options: {
8895
- ...this.options,
8896
- allNestedObjectsOptional: this.options.allNestedObjectsOptional ?? false
9007
+ autoDecrypt: this.config.autoDecrypt,
9008
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
8897
9009
  }
8898
9010
  });
8899
- this.validatePartitions();
8900
9011
  this.setupPartitionHooks();
8901
- if (options.hooks) {
8902
- for (const [event, hooksArr] of Object.entries(options.hooks)) {
9012
+ this.validatePartitions();
9013
+ if (hooks) {
9014
+ for (const [event, hooksArr] of Object.entries(hooks)) {
8903
9015
  if (Array.isArray(hooksArr) && this.hooks[event]) {
8904
9016
  for (const fn of hooksArr) {
8905
- this.hooks[event].push(fn.bind(this));
9017
+ if (typeof fn === "function") {
9018
+ this.hooks[event].push(fn.bind(this));
9019
+ }
8906
9020
  }
8907
9021
  }
8908
9022
  }
8909
9023
  }
8910
9024
  }
9025
+ /**
9026
+ * Get resource options (for backward compatibility with tests)
9027
+ */
9028
+ get options() {
9029
+ return {
9030
+ timestamps: this.config.timestamps,
9031
+ partitions: this.config.partitions || {},
9032
+ cache: this.config.cache,
9033
+ autoDecrypt: this.config.autoDecrypt,
9034
+ paranoid: this.config.paranoid,
9035
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
9036
+ };
9037
+ }
8911
9038
  export() {
8912
9039
  const exported = this.schema.export();
8913
9040
  exported.behavior = this.behavior;
9041
+ exported.timestamps = this.config.timestamps;
9042
+ exported.partitions = this.config.partitions || {};
9043
+ exported.paranoid = this.config.paranoid;
9044
+ exported.allNestedObjectsOptional = this.config.allNestedObjectsOptional;
9045
+ exported.autoDecrypt = this.config.autoDecrypt;
9046
+ exported.cache = this.config.cache;
9047
+ exported.hooks = this.hooks;
8914
9048
  return exported;
8915
9049
  }
8916
9050
  /**
@@ -8920,18 +9054,21 @@ ${JSON.stringify(validation, null, 2)}`
8920
9054
  updateAttributes(newAttributes) {
8921
9055
  const oldAttributes = this.attributes;
8922
9056
  this.attributes = newAttributes;
8923
- if (this.options.timestamps) {
9057
+ if (this.config.timestamps) {
8924
9058
  newAttributes.createdAt = "string|optional";
8925
9059
  newAttributes.updatedAt = "string|optional";
8926
- if (!this.options.partitions.byCreatedDate) {
8927
- this.options.partitions.byCreatedDate = {
9060
+ if (!this.config.partitions) {
9061
+ this.config.partitions = {};
9062
+ }
9063
+ if (!this.config.partitions.byCreatedDate) {
9064
+ this.config.partitions.byCreatedDate = {
8928
9065
  fields: {
8929
9066
  createdAt: "date|maxlength:10"
8930
9067
  }
8931
9068
  };
8932
9069
  }
8933
- if (!this.options.partitions.byUpdatedDate) {
8934
- this.options.partitions.byUpdatedDate = {
9070
+ if (!this.config.partitions.byUpdatedDate) {
9071
+ this.config.partitions.byUpdatedDate = {
8935
9072
  fields: {
8936
9073
  updatedAt: "date|maxlength:10"
8937
9074
  }
@@ -8943,10 +9080,13 @@ ${JSON.stringify(validation, null, 2)}`
8943
9080
  attributes: newAttributes,
8944
9081
  passphrase: this.passphrase,
8945
9082
  version: this.version,
8946
- options: this.options
9083
+ options: {
9084
+ autoDecrypt: this.config.autoDecrypt,
9085
+ allNestedObjectsOptional: this.config.allNestedObjectsOptional
9086
+ }
8947
9087
  });
8948
- this.validatePartitions();
8949
9088
  this.setupPartitionHooks();
9089
+ this.validatePartitions();
8950
9090
  return { oldAttributes, newAttributes };
8951
9091
  }
8952
9092
  /**
@@ -8977,15 +9117,24 @@ ${JSON.stringify(validation, null, 2)}`
8977
9117
  * Setup automatic partition hooks
8978
9118
  */
8979
9119
  setupPartitionHooks() {
8980
- const partitions = this.options.partitions;
8981
- if (!partitions || Object.keys(partitions).length === 0) {
9120
+ if (!this.config.partitions) {
8982
9121
  return;
8983
9122
  }
8984
- this.addHook("afterInsert", async (data) => {
9123
+ const partitions = this.config.partitions;
9124
+ if (Object.keys(partitions).length === 0) {
9125
+ return;
9126
+ }
9127
+ if (!this.hooks.afterInsert) {
9128
+ this.hooks.afterInsert = [];
9129
+ }
9130
+ this.hooks.afterInsert.push(async (data) => {
8985
9131
  await this.createPartitionReferences(data);
8986
9132
  return data;
8987
9133
  });
8988
- this.addHook("afterDelete", async (data) => {
9134
+ if (!this.hooks.afterDelete) {
9135
+ this.hooks.afterDelete = [];
9136
+ }
9137
+ this.hooks.afterDelete.push(async (data) => {
8989
9138
  await this.deletePartitionReferences(data);
8990
9139
  return data;
8991
9140
  });
@@ -9010,8 +9159,11 @@ ${JSON.stringify(validation, null, 2)}`
9010
9159
  * @throws {Error} If partition fields don't exist in current schema
9011
9160
  */
9012
9161
  validatePartitions() {
9013
- const partitions = this.options.partitions;
9014
- if (!partitions || Object.keys(partitions).length === 0) {
9162
+ if (!this.config.partitions) {
9163
+ return;
9164
+ }
9165
+ const partitions = this.config.partitions;
9166
+ if (Object.keys(partitions).length === 0) {
9015
9167
  return;
9016
9168
  }
9017
9169
  const currentAttributes = Object.keys(this.attributes || {});
@@ -9118,10 +9270,10 @@ ${JSON.stringify(validation, null, 2)}`
9118
9270
  * // Returns: null
9119
9271
  */
9120
9272
  getPartitionKey({ partitionName, id, data }) {
9121
- const partition = this.options.partitions[partitionName];
9122
- if (!partition) {
9273
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
9123
9274
  throw new Error(`Partition '${partitionName}' not found`);
9124
9275
  }
9276
+ const partition = this.config.partitions[partitionName];
9125
9277
  const partitionSegments = [];
9126
9278
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
9127
9279
  for (const [fieldName, rule] of sortedFields) {
@@ -9177,6 +9329,12 @@ ${JSON.stringify(validation, null, 2)}`
9177
9329
  * name: 'Jane Smith',
9178
9330
  * email: 'jane@example.com'
9179
9331
  * });
9332
+ *
9333
+ * // Insert with auto-generated password for secret field
9334
+ * const user = await resource.insert({
9335
+ * name: 'John Doe',
9336
+ * email: 'john@example.com',
9337
+ * });
9180
9338
  */
9181
9339
  async insert({ id, ...attributes }) {
9182
9340
  if (this.options.timestamps) {
@@ -9272,7 +9430,7 @@ ${JSON.stringify(validation, null, 2)}`
9272
9430
  passphrase: this.passphrase,
9273
9431
  version: objectVersion,
9274
9432
  options: {
9275
- ...this.options,
9433
+ ...this.config,
9276
9434
  autoDecrypt: false,
9277
9435
  // Disable decryption
9278
9436
  autoEncrypt: false
@@ -9359,7 +9517,7 @@ ${JSON.stringify(validation, null, 2)}`
9359
9517
  */
9360
9518
  async update(id, attributes) {
9361
9519
  const live = await this.get(id);
9362
- if (this.options.timestamps) {
9520
+ if (this.config.timestamps) {
9363
9521
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9364
9522
  }
9365
9523
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
@@ -9481,7 +9639,7 @@ ${JSON.stringify(validation, null, 2)}`
9481
9639
  async count({ partition = null, partitionValues = {} } = {}) {
9482
9640
  let prefix;
9483
9641
  if (partition && Object.keys(partitionValues).length > 0) {
9484
- const partitionDef = this.options.partitions[partition];
9642
+ const partitionDef = this.config.partitions[partition];
9485
9643
  if (!partitionDef) {
9486
9644
  throw new Error(`Partition '${partition}' not found`);
9487
9645
  }
@@ -9566,9 +9724,9 @@ ${JSON.stringify(validation, null, 2)}`
9566
9724
  return results;
9567
9725
  }
9568
9726
  async deleteAll() {
9569
- if (this.options.paranoid !== false) {
9727
+ if (this.config.paranoid !== false) {
9570
9728
  throw new Error(
9571
- `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.options.paranoid}`
9729
+ `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9572
9730
  );
9573
9731
  }
9574
9732
  const prefix = `resource=${this.name}/v=${this.version}`;
@@ -9585,9 +9743,9 @@ ${JSON.stringify(validation, null, 2)}`
9585
9743
  * @returns {Promise<Object>} Deletion report
9586
9744
  */
9587
9745
  async deleteAllData() {
9588
- if (this.options.paranoid !== false) {
9746
+ if (this.config.paranoid !== false) {
9589
9747
  throw new Error(
9590
- `deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.options.paranoid}`
9748
+ `deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9591
9749
  );
9592
9750
  }
9593
9751
  const prefix = `resource=${this.name}`;
@@ -9630,10 +9788,10 @@ ${JSON.stringify(validation, null, 2)}`
9630
9788
  async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9631
9789
  let prefix;
9632
9790
  if (partition && Object.keys(partitionValues).length > 0) {
9633
- const partitionDef = this.options.partitions[partition];
9634
- if (!partitionDef) {
9791
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9635
9792
  throw new Error(`Partition '${partition}' not found`);
9636
9793
  }
9794
+ const partitionDef = this.config.partitions[partition];
9637
9795
  const partitionSegments = [];
9638
9796
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9639
9797
  for (const [fieldName, rule] of sortedFields) {
@@ -9724,10 +9882,10 @@ ${JSON.stringify(validation, null, 2)}`
9724
9882
  this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9725
9883
  return validResults2;
9726
9884
  }
9727
- const partitionDef = this.options.partitions[partition];
9728
- if (!partitionDef) {
9885
+ if (!this.config.partitions || !this.config.partitions[partition]) {
9729
9886
  throw new Error(`Partition '${partition}' not found`);
9730
9887
  }
9888
+ const partitionDef = this.config.partitions[partition];
9731
9889
  const partitionSegments = [];
9732
9890
  const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9733
9891
  for (const [fieldName, rule] of sortedFields) {
@@ -10032,16 +10190,14 @@ ${JSON.stringify(validation, null, 2)}`
10032
10190
  }
10033
10191
  /**
10034
10192
  * Generate definition hash for this resource
10035
- * @returns {string} SHA256 hash of the schema definition
10193
+ * @returns {string} SHA256 hash of the resource definition (name + attributes)
10036
10194
  */
10037
10195
  getDefinitionHash() {
10038
- const attributes = this.schema.export().attributes;
10039
- const stableAttributes = { ...attributes };
10040
- if (this.options.timestamps) {
10041
- delete stableAttributes.createdAt;
10042
- delete stableAttributes.updatedAt;
10043
- }
10044
- const stableString = jsonStableStringify(stableAttributes);
10196
+ const definition = {
10197
+ attributes: this.attributes,
10198
+ behavior: this.behavior
10199
+ };
10200
+ const stableString = jsonStableStringify(definition);
10045
10201
  return `sha256:${crypto.createHash("sha256").update(stableString).digest("hex")}`;
10046
10202
  }
10047
10203
  /**
@@ -10070,7 +10226,7 @@ ${JSON.stringify(validation, null, 2)}`
10070
10226
  passphrase: this.passphrase,
10071
10227
  version,
10072
10228
  options: {
10073
- ...this.options,
10229
+ ...this.config,
10074
10230
  // For older versions, be more lenient with decryption
10075
10231
  autoDecrypt: true,
10076
10232
  autoEncrypt: true
@@ -10087,7 +10243,7 @@ ${JSON.stringify(validation, null, 2)}`
10087
10243
  * @param {Object} data - Inserted object data
10088
10244
  */
10089
10245
  async createPartitionReferences(data) {
10090
- const partitions = this.options.partitions;
10246
+ const partitions = this.config.partitions;
10091
10247
  if (!partitions || Object.keys(partitions).length === 0) {
10092
10248
  return;
10093
10249
  }
@@ -10118,7 +10274,7 @@ ${JSON.stringify(validation, null, 2)}`
10118
10274
  * @param {Object} data - Deleted object data
10119
10275
  */
10120
10276
  async deletePartitionReferences(data) {
10121
- const partitions = this.options.partitions;
10277
+ const partitions = this.config.partitions;
10122
10278
  if (!partitions || Object.keys(partitions).length === 0) {
10123
10279
  return;
10124
10280
  }
@@ -10210,7 +10366,7 @@ ${JSON.stringify(validation, null, 2)}`
10210
10366
  * @param {Object} data - Updated object data
10211
10367
  */
10212
10368
  async updatePartitionReferences(data) {
10213
- const partitions = this.options.partitions;
10369
+ const partitions = this.config.partitions;
10214
10370
  if (!partitions || Object.keys(partitions).length === 0) {
10215
10371
  return;
10216
10372
  }
@@ -10266,10 +10422,10 @@ ${JSON.stringify(validation, null, 2)}`
10266
10422
  * });
10267
10423
  */
10268
10424
  async getFromPartition({ id, partitionName, partitionValues = {} }) {
10269
- const partition = this.options.partitions[partitionName];
10270
- if (!partition) {
10425
+ if (!this.config.partitions || !this.config.partitions[partitionName]) {
10271
10426
  throw new Error(`Partition '${partitionName}' not found`);
10272
10427
  }
10428
+ const partition = this.config.partitions[partitionName];
10273
10429
  const partitionSegments = [];
10274
10430
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
10275
10431
  for (const [fieldName, rule] of sortedFields) {
@@ -10317,6 +10473,98 @@ ${JSON.stringify(validation, null, 2)}`
10317
10473
  return data;
10318
10474
  }
10319
10475
  }
10476
+ function validateResourceConfig(config) {
10477
+ const errors = [];
10478
+ if (!config.name) {
10479
+ errors.push("Resource 'name' is required");
10480
+ } else if (typeof config.name !== "string") {
10481
+ errors.push("Resource 'name' must be a string");
10482
+ } else if (config.name.trim() === "") {
10483
+ errors.push("Resource 'name' cannot be empty");
10484
+ }
10485
+ if (!config.client) {
10486
+ errors.push("S3 'client' is required");
10487
+ }
10488
+ if (!config.attributes) {
10489
+ errors.push("Resource 'attributes' are required");
10490
+ } else if (typeof config.attributes !== "object" || Array.isArray(config.attributes)) {
10491
+ errors.push("Resource 'attributes' must be an object");
10492
+ } else if (Object.keys(config.attributes).length === 0) {
10493
+ errors.push("Resource 'attributes' cannot be empty");
10494
+ }
10495
+ if (config.version !== void 0 && typeof config.version !== "string") {
10496
+ errors.push("Resource 'version' must be a string");
10497
+ }
10498
+ if (config.behavior !== void 0 && typeof config.behavior !== "string") {
10499
+ errors.push("Resource 'behavior' must be a string");
10500
+ }
10501
+ if (config.passphrase !== void 0 && typeof config.passphrase !== "string") {
10502
+ errors.push("Resource 'passphrase' must be a string");
10503
+ }
10504
+ if (config.parallelism !== void 0) {
10505
+ if (typeof config.parallelism !== "number" || !Number.isInteger(config.parallelism)) {
10506
+ errors.push("Resource 'parallelism' must be an integer");
10507
+ } else if (config.parallelism < 1) {
10508
+ errors.push("Resource 'parallelism' must be greater than 0");
10509
+ }
10510
+ }
10511
+ if (config.observers !== void 0 && !Array.isArray(config.observers)) {
10512
+ errors.push("Resource 'observers' must be an array");
10513
+ }
10514
+ const booleanFields = ["cache", "autoDecrypt", "timestamps", "paranoid", "allNestedObjectsOptional"];
10515
+ for (const field of booleanFields) {
10516
+ if (config[field] !== void 0 && typeof config[field] !== "boolean") {
10517
+ errors.push(`Resource '${field}' must be a boolean`);
10518
+ }
10519
+ }
10520
+ if (config.partitions !== void 0) {
10521
+ if (typeof config.partitions !== "object" || Array.isArray(config.partitions)) {
10522
+ errors.push("Resource 'partitions' must be an object");
10523
+ } else {
10524
+ for (const [partitionName, partitionDef] of Object.entries(config.partitions)) {
10525
+ if (typeof partitionDef !== "object" || Array.isArray(partitionDef)) {
10526
+ errors.push(`Partition '${partitionName}' must be an object`);
10527
+ } else if (!partitionDef.fields) {
10528
+ errors.push(`Partition '${partitionName}' must have a 'fields' property`);
10529
+ } else if (typeof partitionDef.fields !== "object" || Array.isArray(partitionDef.fields)) {
10530
+ errors.push(`Partition '${partitionName}.fields' must be an object`);
10531
+ } else {
10532
+ for (const [fieldName, fieldType] of Object.entries(partitionDef.fields)) {
10533
+ if (typeof fieldType !== "string") {
10534
+ errors.push(`Partition '${partitionName}.fields.${fieldName}' must be a string`);
10535
+ }
10536
+ }
10537
+ }
10538
+ }
10539
+ }
10540
+ }
10541
+ if (config.hooks !== void 0) {
10542
+ if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
10543
+ errors.push("Resource 'hooks' must be an object");
10544
+ } else {
10545
+ const validHookEvents = ["preInsert", "afterInsert", "preUpdate", "afterUpdate", "preDelete", "afterDelete"];
10546
+ for (const [event, hooksArr] of Object.entries(config.hooks)) {
10547
+ if (!validHookEvents.includes(event)) {
10548
+ errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
10549
+ } else if (!Array.isArray(hooksArr)) {
10550
+ errors.push(`Resource 'hooks.${event}' must be an array`);
10551
+ } else {
10552
+ for (let i = 0; i < hooksArr.length; i++) {
10553
+ const hook = hooksArr[i];
10554
+ if (typeof hook !== "function") {
10555
+ if (typeof hook === "string") continue;
10556
+ continue;
10557
+ }
10558
+ }
10559
+ }
10560
+ }
10561
+ }
10562
+ }
10563
+ return {
10564
+ isValid: errors.length === 0,
10565
+ errors
10566
+ };
10567
+ }
10320
10568
 
10321
10569
  class Database extends EventEmitter {
10322
10570
  constructor(options) {
@@ -10324,7 +10572,7 @@ ${JSON.stringify(validation, null, 2)}`
10324
10572
  this.version = "1";
10325
10573
  this.s3dbVersion = (() => {
10326
10574
  try {
10327
- return true ? "4.1.9" : "latest";
10575
+ return true ? "4.1.12" : "latest";
10328
10576
  } catch (e) {
10329
10577
  return "latest";
10330
10578
  }
@@ -10337,10 +10585,21 @@ ${JSON.stringify(validation, null, 2)}`
10337
10585
  this.plugins = options.plugins || [];
10338
10586
  this.cache = options.cache;
10339
10587
  this.passphrase = options.passphrase || "secret";
10588
+ let connectionString = options.connectionString;
10589
+ if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
10590
+ connectionString = ConnectionString.buildFromParams({
10591
+ bucket: options.bucket,
10592
+ region: options.region,
10593
+ accessKeyId: options.accessKeyId,
10594
+ secretAccessKey: options.secretAccessKey,
10595
+ endpoint: options.endpoint,
10596
+ forcePathStyle: options.forcePathStyle
10597
+ });
10598
+ }
10340
10599
  this.client = options.client || new Client({
10341
10600
  verbose: this.verbose,
10342
10601
  parallelism: this.parallelism,
10343
- connectionString: options.connectionString
10602
+ connectionString
10344
10603
  });
10345
10604
  this.bucket = this.client.bucket;
10346
10605
  this.keyPrefix = this.client.keyPrefix;
@@ -10365,15 +10624,18 @@ ${JSON.stringify(validation, null, 2)}`
10365
10624
  name,
10366
10625
  client: this.client,
10367
10626
  version: currentVersion,
10368
- options: {
10369
- ...versionData.options,
10370
- partitions: resourceMetadata.partitions || versionData.options?.partitions || {}
10371
- },
10372
10627
  attributes: versionData.attributes,
10373
10628
  behavior: versionData.behavior || "user-management",
10374
10629
  parallelism: this.parallelism,
10375
10630
  passphrase: this.passphrase,
10376
- observers: [this]
10631
+ observers: [this],
10632
+ cache: this.cache,
10633
+ timestamps: versionData.options?.timestamps || false,
10634
+ partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
10635
+ paranoid: versionData.options?.paranoid !== false,
10636
+ allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
10637
+ autoDecrypt: versionData.options?.autoDecrypt !== false,
10638
+ hooks: versionData.options?.hooks || {}
10377
10639
  });
10378
10640
  }
10379
10641
  }
@@ -10442,7 +10704,7 @@ ${JSON.stringify(validation, null, 2)}`
10442
10704
  generateDefinitionHash(definition, behavior = void 0) {
10443
10705
  const attributes = definition.attributes;
10444
10706
  const stableAttributes = { ...attributes };
10445
- if (definition.options?.timestamps) {
10707
+ if (definition.timestamps) {
10446
10708
  delete stableAttributes.createdAt;
10447
10709
  delete stableAttributes.updatedAt;
10448
10710
  }
@@ -10504,14 +10766,22 @@ ${JSON.stringify(validation, null, 2)}`
10504
10766
  }
10505
10767
  metadata.resources[name] = {
10506
10768
  currentVersion: version,
10507
- partitions: resourceDef.options?.partitions || {},
10769
+ partitions: resource.config.partitions || {},
10508
10770
  versions: {
10509
10771
  ...existingResource?.versions,
10510
10772
  // Preserve previous versions
10511
10773
  [version]: {
10512
10774
  hash: definitionHash,
10513
10775
  attributes: resourceDef.attributes,
10514
- options: resourceDef.options,
10776
+ options: {
10777
+ timestamps: resource.config.timestamps,
10778
+ partitions: resource.config.partitions,
10779
+ paranoid: resource.config.paranoid,
10780
+ allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
10781
+ autoDecrypt: resource.config.autoDecrypt,
10782
+ cache: resource.config.cache,
10783
+ hooks: resourceDef.hooks || {}
10784
+ },
10515
10785
  behavior: resourceDef.behavior || "user-management",
10516
10786
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
10517
10787
  }
@@ -10564,10 +10834,9 @@ ${JSON.stringify(validation, null, 2)}`
10564
10834
  observers: [],
10565
10835
  client: this.client,
10566
10836
  version: "temp",
10567
- options: {
10568
- cache: this.cache,
10569
- ...options
10570
- }
10837
+ passphrase: this.passphrase,
10838
+ cache: this.cache,
10839
+ ...options
10571
10840
  });
10572
10841
  const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10573
10842
  const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
@@ -10607,7 +10876,7 @@ ${JSON.stringify(validation, null, 2)}`
10607
10876
  async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
10608
10877
  if (this.resources[name]) {
10609
10878
  const existingResource = this.resources[name];
10610
- Object.assign(existingResource.options, {
10879
+ Object.assign(existingResource.config, {
10611
10880
  cache: this.cache,
10612
10881
  ...options
10613
10882
  });
@@ -10634,10 +10903,9 @@ ${JSON.stringify(validation, null, 2)}`
10634
10903
  observers: [this],
10635
10904
  client: this.client,
10636
10905
  version,
10637
- options: {
10638
- cache: this.cache,
10639
- ...options
10640
- }
10906
+ passphrase: this.passphrase,
10907
+ cache: this.cache,
10908
+ ...options
10641
10909
  });
10642
10910
  this.resources[name] = resource;
10643
10911
  await this.uploadMetadataFile();
@@ -17339,6 +17607,7 @@ ${JSON.stringify(validation, null, 2)}`
17339
17607
  }
17340
17608
  }
17341
17609
 
17610
+ exports.AuthenticationError = AuthenticationError;
17342
17611
  exports.BaseError = BaseError;
17343
17612
  exports.Cache = Cache;
17344
17613
  exports.CachePlugin = CachePlugin;
@@ -17346,6 +17615,8 @@ ${JSON.stringify(validation, null, 2)}`
17346
17615
  exports.ConnectionString = ConnectionString;
17347
17616
  exports.CostsPlugin = CostsPlugin;
17348
17617
  exports.Database = Database;
17618
+ exports.DatabaseError = DatabaseError;
17619
+ exports.EncryptionError = EncryptionError;
17349
17620
  exports.ErrorMap = ErrorMap;
17350
17621
  exports.InvalidResourceItem = InvalidResourceItem;
17351
17622
  exports.MemoryCache = MemoryCache;
@@ -17353,24 +17624,34 @@ ${JSON.stringify(validation, null, 2)}`
17353
17624
  exports.NoSuchBucket = NoSuchBucket;
17354
17625
  exports.NoSuchKey = NoSuchKey;
17355
17626
  exports.NotFound = NotFound;
17627
+ exports.PermissionError = PermissionError;
17356
17628
  exports.Plugin = Plugin;
17357
17629
  exports.PluginObject = PluginObject;
17358
17630
  exports.ResourceIdsPageReader = ResourceIdsPageReader;
17359
17631
  exports.ResourceIdsReader = ResourceIdsReader;
17632
+ exports.ResourceNotFound = ResourceNotFound;
17633
+ exports.ResourceNotFoundError = ResourceNotFound;
17360
17634
  exports.ResourceReader = ResourceReader;
17361
17635
  exports.ResourceWriter = ResourceWriter;
17362
17636
  exports.S3Cache = S3Cache;
17637
+ exports.S3DB = S3db;
17638
+ exports.S3DBError = S3DBError;
17363
17639
  exports.S3_DEFAULT_ENDPOINT = S3_DEFAULT_ENDPOINT;
17364
17640
  exports.S3_DEFAULT_REGION = S3_DEFAULT_REGION;
17365
17641
  exports.S3db = S3db;
17642
+ exports.S3dbError = S3DBError;
17366
17643
  exports.UnknownError = UnknownError;
17644
+ exports.ValidationError = ValidationError;
17367
17645
  exports.Validator = Validator;
17368
17646
  exports.ValidatorManager = ValidatorManager;
17369
17647
  exports.decrypt = decrypt;
17648
+ exports.default = S3db;
17370
17649
  exports.encrypt = encrypt;
17371
17650
  exports.sha256 = sha256;
17372
17651
  exports.streamToString = streamToString;
17373
17652
 
17653
+ Object.defineProperty(exports, '__esModule', { value: true });
17654
+
17374
17655
  return exports;
17375
17656
 
17376
17657
  })({}, nanoid, lodashEs, promisePool, clientS3, crypto, flat, FastestValidator, web);