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/README.md CHANGED
@@ -830,6 +830,48 @@ const resource = await s3db.createResource({
830
830
  });
831
831
  ```
832
832
 
833
+ #### Check Resource Existence
834
+
835
+ ```javascript
836
+ // Check if a resource exists by name
837
+ const exists = s3db.resourceExists("users");
838
+ console.log(exists); // true or false
839
+ ```
840
+
841
+ #### Create Resource If Not Exists
842
+
843
+ ```javascript
844
+ // Create a resource only if it doesn't exist with the same definition hash
845
+ const result = await s3db.createResourceIfNotExists({
846
+ name: "users",
847
+ attributes: {
848
+ name: "string|required",
849
+ email: "email|required"
850
+ },
851
+ options: { timestamps: true },
852
+ behavior: "user-management"
853
+ });
854
+
855
+ console.log(result);
856
+ // {
857
+ // resource: Resource,
858
+ // created: true, // or false if already existed
859
+ // reason: "New resource created" // or "Resource already exists with same definition hash"
860
+ // }
861
+
862
+ // If the resource already exists with the same hash, it returns the existing resource
863
+ const result2 = await s3db.createResourceIfNotExists({
864
+ name: "users",
865
+ attributes: {
866
+ name: "string|required",
867
+ email: "email|required"
868
+ }
869
+ });
870
+
871
+ console.log(result2.created); // false
872
+ console.log(result2.reason); // "Resource already exists with same definition hash"
873
+ ```
874
+
833
875
  #### Get Resource Reference
834
876
 
835
877
  ```javascript
package/dist/s3db.cjs.js CHANGED
@@ -3482,6 +3482,7 @@ class Schema {
3482
3482
  this.attributes = attributes || {};
3483
3483
  this.passphrase = passphrase ?? "secret";
3484
3484
  this.options = lodashEs.merge({}, this.defaultOptions(), options);
3485
+ this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
3485
3486
  const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
3486
3487
  this.validator = new ValidatorManager({ autoEncrypt: false }).compile(lodashEs.merge(
3487
3488
  { $$async: true },
@@ -3679,11 +3680,17 @@ class Schema {
3679
3680
  const processed = {};
3680
3681
  for (const [key, value] of Object.entries(attributes)) {
3681
3682
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3682
- processed[key] = {
3683
+ const isExplicitRequired = value.$$type && value.$$type.includes("required");
3684
+ const isExplicitOptional = value.$$type && value.$$type.includes("optional");
3685
+ const objectConfig = {
3683
3686
  type: "object",
3684
3687
  properties: this.preprocessAttributesForValidation(value),
3685
3688
  strict: false
3686
3689
  };
3690
+ if (isExplicitRequired) ; else if (isExplicitOptional || this.allNestedObjectsOptional) {
3691
+ objectConfig.optional = true;
3692
+ }
3693
+ processed[key] = objectConfig;
3687
3694
  } else {
3688
3695
  processed[key] = value;
3689
3696
  }
@@ -8845,6 +8852,7 @@ class Resource extends EventEmitter {
8845
8852
  partitions: {},
8846
8853
  paranoid: true,
8847
8854
  // Security flag for dangerous operations
8855
+ allNestedObjectsOptional: options.allNestedObjectsOptional ?? false,
8848
8856
  ...options
8849
8857
  };
8850
8858
  this.hooks = {
@@ -8879,7 +8887,10 @@ class Resource extends EventEmitter {
8879
8887
  attributes: this.attributes,
8880
8888
  passphrase,
8881
8889
  version: this.version,
8882
- options: this.options
8890
+ options: {
8891
+ ...this.options,
8892
+ allNestedObjectsOptional: this.options.allNestedObjectsOptional ?? false
8893
+ }
8883
8894
  });
8884
8895
  this.validatePartitions();
8885
8896
  this.setupPartitionHooks();
@@ -9806,7 +9817,7 @@ class Database extends EventEmitter {
9806
9817
  this.version = "1";
9807
9818
  this.s3dbVersion = (() => {
9808
9819
  try {
9809
- return true ? "4.1.1" : "latest";
9820
+ return true ? "4.1.3" : "latest";
9810
9821
  } catch (e) {
9811
9822
  return "latest";
9812
9823
  }
@@ -9918,16 +9929,21 @@ class Database extends EventEmitter {
9918
9929
  /**
9919
9930
  * Generate a consistent hash for a resource definition
9920
9931
  * @param {Object} definition - Resource definition to hash
9932
+ * @param {string} behavior - Resource behavior
9921
9933
  * @returns {string} SHA256 hash
9922
9934
  */
9923
- generateDefinitionHash(definition) {
9935
+ generateDefinitionHash(definition, behavior = void 0) {
9924
9936
  const attributes = definition.attributes;
9925
9937
  const stableAttributes = { ...attributes };
9926
9938
  if (definition.options?.timestamps) {
9927
9939
  delete stableAttributes.createdAt;
9928
9940
  delete stableAttributes.updatedAt;
9929
9941
  }
9930
- const stableString = jsonStableStringify(stableAttributes);
9942
+ const hashObj = {
9943
+ attributes: stableAttributes,
9944
+ behavior: behavior || definition.behavior || "user-management"
9945
+ };
9946
+ const stableString = jsonStableStringify(hashObj);
9931
9947
  return `sha256:${crypto.createHash("sha256").update(stableString).digest("hex")}`;
9932
9948
  }
9933
9949
  /**
@@ -10014,6 +10030,73 @@ class Database extends EventEmitter {
10014
10030
  resources: {}
10015
10031
  };
10016
10032
  }
10033
+ /**
10034
+ * Check if a resource exists by name
10035
+ * @param {string} name - Resource name
10036
+ * @returns {boolean} True if resource exists, false otherwise
10037
+ */
10038
+ resourceExists(name) {
10039
+ return !!this.resources[name];
10040
+ }
10041
+ /**
10042
+ * Check if a resource exists with the same definition hash
10043
+ * @param {string} name - Resource name
10044
+ * @param {Object} attributes - Resource attributes
10045
+ * @param {Object} options - Resource options
10046
+ * @param {string} behavior - Resource behavior
10047
+ * @returns {Object} Object with exists flag and hash information
10048
+ */
10049
+ resourceExistsWithSameHash({ name, attributes, options = {}, behavior = "user-management" }) {
10050
+ if (!this.resources[name]) {
10051
+ return { exists: false, sameHash: false, hash: null };
10052
+ }
10053
+ const tempResource = new Resource({
10054
+ name,
10055
+ attributes,
10056
+ behavior,
10057
+ observers: [],
10058
+ client: this.client,
10059
+ version: "temp",
10060
+ options: {
10061
+ cache: this.cache,
10062
+ ...options
10063
+ }
10064
+ });
10065
+ const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
10066
+ const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
10067
+ return {
10068
+ exists: true,
10069
+ sameHash: newHash === existingHash,
10070
+ hash: newHash,
10071
+ existingHash
10072
+ };
10073
+ }
10074
+ /**
10075
+ * Create a resource only if it doesn't exist with the same definition hash
10076
+ * @param {Object} params - Resource parameters
10077
+ * @param {string} params.name - Resource name
10078
+ * @param {Object} params.attributes - Resource attributes
10079
+ * @param {Object} params.options - Resource options
10080
+ * @param {string} params.behavior - Resource behavior
10081
+ * @returns {Object} Object with resource and created flag
10082
+ */
10083
+ async createResourceIfNotExists({ name, attributes, options = {}, behavior = "user-management" }) {
10084
+ const alreadyExists = !!this.resources[name];
10085
+ const hashCheck = this.resourceExistsWithSameHash({ name, attributes, options, behavior });
10086
+ if (hashCheck.exists && hashCheck.sameHash) {
10087
+ return {
10088
+ resource: this.resources[name],
10089
+ created: false,
10090
+ reason: "Resource already exists with same definition hash"
10091
+ };
10092
+ }
10093
+ const resource = await this.createResource({ name, attributes, options, behavior });
10094
+ return {
10095
+ resource,
10096
+ created: !alreadyExists,
10097
+ reason: alreadyExists ? "Resource updated with new definition" : "New resource created"
10098
+ };
10099
+ }
10017
10100
  async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
10018
10101
  if (this.resources[name]) {
10019
10102
  const existingResource = this.resources[name];
@@ -10025,7 +10108,13 @@ class Database extends EventEmitter {
10025
10108
  existingResource.behavior = behavior;
10026
10109
  }
10027
10110
  existingResource.updateAttributes(attributes);
10028
- await this.uploadMetadataFile();
10111
+ const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
10112
+ const existingMetadata2 = this.savedMetadata?.resources?.[name];
10113
+ const currentVersion = existingMetadata2?.currentVersion || "v0";
10114
+ const existingVersionData = existingMetadata2?.versions?.[currentVersion];
10115
+ if (!existingVersionData || existingVersionData.hash !== newHash) {
10116
+ await this.uploadMetadataFile();
10117
+ }
10029
10118
  this.emit("s3db.resourceUpdated", name);
10030
10119
  return existingResource;
10031
10120
  }