s3db.js 4.1.2 → 4.1.4

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
@@ -3480,6 +3480,7 @@ class Schema {
3480
3480
  this.attributes = attributes || {};
3481
3481
  this.passphrase = passphrase ?? "secret";
3482
3482
  this.options = merge({}, this.defaultOptions(), options);
3483
+ this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
3483
3484
  const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
3484
3485
  this.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
3485
3486
  { $$async: true },
@@ -3677,11 +3678,17 @@ class Schema {
3677
3678
  const processed = {};
3678
3679
  for (const [key, value] of Object.entries(attributes)) {
3679
3680
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3680
- processed[key] = {
3681
+ const isExplicitRequired = value.$$type && value.$$type.includes("required");
3682
+ const isExplicitOptional = value.$$type && value.$$type.includes("optional");
3683
+ const objectConfig = {
3681
3684
  type: "object",
3682
3685
  properties: this.preprocessAttributesForValidation(value),
3683
3686
  strict: false
3684
3687
  };
3688
+ if (isExplicitRequired) ; else if (isExplicitOptional || this.allNestedObjectsOptional) {
3689
+ objectConfig.optional = true;
3690
+ }
3691
+ processed[key] = objectConfig;
3685
3692
  } else {
3686
3693
  processed[key] = value;
3687
3694
  }
@@ -8843,6 +8850,7 @@ class Resource extends EventEmitter {
8843
8850
  partitions: {},
8844
8851
  paranoid: true,
8845
8852
  // Security flag for dangerous operations
8853
+ allNestedObjectsOptional: options.allNestedObjectsOptional ?? false,
8846
8854
  ...options
8847
8855
  };
8848
8856
  this.hooks = {
@@ -8877,7 +8885,10 @@ class Resource extends EventEmitter {
8877
8885
  attributes: this.attributes,
8878
8886
  passphrase,
8879
8887
  version: this.version,
8880
- options: this.options
8888
+ options: {
8889
+ ...this.options,
8890
+ allNestedObjectsOptional: this.options.allNestedObjectsOptional ?? false
8891
+ }
8881
8892
  });
8882
8893
  this.validatePartitions();
8883
8894
  this.setupPartitionHooks();
@@ -9804,7 +9815,7 @@ class Database extends EventEmitter {
9804
9815
  this.version = "1";
9805
9816
  this.s3dbVersion = (() => {
9806
9817
  try {
9807
- return true ? "4.1.1" : "latest";
9818
+ return true ? "4.1.3" : "latest";
9808
9819
  } catch (e) {
9809
9820
  return "latest";
9810
9821
  }
@@ -9916,16 +9927,21 @@ class Database extends EventEmitter {
9916
9927
  /**
9917
9928
  * Generate a consistent hash for a resource definition
9918
9929
  * @param {Object} definition - Resource definition to hash
9930
+ * @param {string} behavior - Resource behavior
9919
9931
  * @returns {string} SHA256 hash
9920
9932
  */
9921
- generateDefinitionHash(definition) {
9933
+ generateDefinitionHash(definition, behavior = void 0) {
9922
9934
  const attributes = definition.attributes;
9923
9935
  const stableAttributes = { ...attributes };
9924
9936
  if (definition.options?.timestamps) {
9925
9937
  delete stableAttributes.createdAt;
9926
9938
  delete stableAttributes.updatedAt;
9927
9939
  }
9928
- const stableString = jsonStableStringify(stableAttributes);
9940
+ const hashObj = {
9941
+ attributes: stableAttributes,
9942
+ behavior: behavior || definition.behavior || "user-management"
9943
+ };
9944
+ const stableString = jsonStableStringify(hashObj);
9929
9945
  return `sha256:${createHash("sha256").update(stableString).digest("hex")}`;
9930
9946
  }
9931
9947
  /**
@@ -10012,6 +10028,73 @@ class Database extends EventEmitter {
10012
10028
  resources: {}
10013
10029
  };
10014
10030
  }
10031
+ /**
10032
+ * Check if a resource exists by name
10033
+ * @param {string} name - Resource name
10034
+ * @returns {boolean} True if resource exists, false otherwise
10035
+ */
10036
+ resourceExists(name) {
10037
+ return !!this.resources[name];
10038
+ }
10039
+ /**
10040
+ * Check if a resource exists with the same definition hash
10041
+ * @param {string} name - Resource name
10042
+ * @param {Object} attributes - Resource attributes
10043
+ * @param {Object} options - Resource options
10044
+ * @param {string} behavior - Resource behavior
10045
+ * @returns {Object} Object with exists flag and hash information
10046
+ */
10047
+ resourceExistsWithSameHash({ name, attributes, options = {}, behavior = "user-management" }) {
10048
+ if (!this.resources[name]) {
10049
+ return { exists: false, sameHash: false, hash: null };
10050
+ }
10051
+ const tempResource = new Resource({
10052
+ name,
10053
+ attributes,
10054
+ behavior,
10055
+ observers: [],
10056
+ client: this.client,
10057
+ version: "temp",
10058
+ options: {
10059
+ cache: this.cache,
10060
+ ...options
10061
+ }
10062
+ });
10063
+ const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10064
+ const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
10065
+ return {
10066
+ exists: true,
10067
+ sameHash: newHash === existingHash,
10068
+ hash: newHash,
10069
+ existingHash
10070
+ };
10071
+ }
10072
+ /**
10073
+ * Create a resource only if it doesn't exist with the same definition hash
10074
+ * @param {Object} params - Resource parameters
10075
+ * @param {string} params.name - Resource name
10076
+ * @param {Object} params.attributes - Resource attributes
10077
+ * @param {Object} params.options - Resource options
10078
+ * @param {string} params.behavior - Resource behavior
10079
+ * @returns {Object} Object with resource and created flag
10080
+ */
10081
+ async createResourceIfNotExists({ name, attributes, options = {}, behavior = "user-management" }) {
10082
+ const alreadyExists = !!this.resources[name];
10083
+ const hashCheck = this.resourceExistsWithSameHash({ name, attributes, options, behavior });
10084
+ if (hashCheck.exists && hashCheck.sameHash) {
10085
+ return {
10086
+ resource: this.resources[name],
10087
+ created: false,
10088
+ reason: "Resource already exists with same definition hash"
10089
+ };
10090
+ }
10091
+ const resource = await this.createResource({ name, attributes, options, behavior });
10092
+ return {
10093
+ resource,
10094
+ created: !alreadyExists,
10095
+ reason: alreadyExists ? "Resource updated with new definition" : "New resource created"
10096
+ };
10097
+ }
10015
10098
  async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
10016
10099
  if (this.resources[name]) {
10017
10100
  const existingResource = this.resources[name];
@@ -10023,7 +10106,13 @@ class Database extends EventEmitter {
10023
10106
  existingResource.behavior = behavior;
10024
10107
  }
10025
10108
  existingResource.updateAttributes(attributes);
10026
- await this.uploadMetadataFile();
10109
+ const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
10110
+ const existingMetadata2 = this.savedMetadata?.resources?.[name];
10111
+ const currentVersion = existingMetadata2?.currentVersion || "v0";
10112
+ const existingVersionData = existingMetadata2?.versions?.[currentVersion];
10113
+ if (!existingVersionData || existingVersionData.hash !== newHash) {
10114
+ await this.uploadMetadataFile();
10115
+ }
10027
10116
  this.emit("s3db.resourceUpdated", name);
10028
10117
  return existingResource;
10029
10118
  }