s3db.js 8.0.2 → 8.1.0

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
@@ -9665,7 +9665,7 @@ class Client extends EventEmitter {
9665
9665
  }) {
9666
9666
  super();
9667
9667
  this.verbose = verbose;
9668
- this.id = id ?? idGenerator();
9668
+ this.id = id ?? idGenerator(77);
9669
9669
  this.parallelism = parallelism;
9670
9670
  this.config = new ConnectionString(connectionString);
9671
9671
  this.httpClientOptions = {
@@ -11252,10 +11252,18 @@ class Resource extends EventEmitter {
11252
11252
  */
11253
11253
  constructor(config = {}) {
11254
11254
  super();
11255
- this._instanceId = Math.random().toString(36).slice(2, 8);
11255
+ this._instanceId = idGenerator(7);
11256
11256
  const validation = validateResourceConfig(config);
11257
11257
  if (!validation.isValid) {
11258
- throw new ResourceError(`Invalid Resource ${config.name} configuration`, { resourceName: config.name, validation: validation.errors, operation: "constructor", suggestion: "Check resource config and attributes." });
11258
+ const errorDetails = validation.errors.map((err) => ` \u2022 ${err}`).join("\n");
11259
+ throw new ResourceError(
11260
+ `Invalid Resource ${config.name || "[unnamed]"} configuration:
11261
+ ${errorDetails}`,
11262
+ {
11263
+ resourceName: config.name,
11264
+ validation: validation.errors
11265
+ }
11266
+ );
11259
11267
  }
11260
11268
  const {
11261
11269
  name,
@@ -13426,9 +13434,10 @@ var resource_class_default = Resource;
13426
13434
  class Database extends EventEmitter {
13427
13435
  constructor(options) {
13428
13436
  super();
13437
+ this.id = idGenerator(7);
13429
13438
  this.version = "1";
13430
13439
  this.s3dbVersion = (() => {
13431
- const [ok, err, version] = try_fn_default(() => true ? "8.0.2" : "latest");
13440
+ const [ok, err, version] = try_fn_default(() => true ? "8.1.0" : "latest");
13432
13441
  return ok ? version : "latest";
13433
13442
  })();
13434
13443
  this.resources = {};
@@ -13441,6 +13450,7 @@ class Database extends EventEmitter {
13441
13450
  this.cache = options.cache;
13442
13451
  this.passphrase = options.passphrase || "secret";
13443
13452
  this.versioningEnabled = options.versioningEnabled || false;
13453
+ this.persistHooks = options.persistHooks || false;
13444
13454
  this._initHooks();
13445
13455
  let connectionString = options.connectionString;
13446
13456
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
@@ -13487,13 +13497,42 @@ class Database extends EventEmitter {
13487
13497
  async connect() {
13488
13498
  await this.startPlugins();
13489
13499
  let metadata = null;
13500
+ let needsHealing = false;
13501
+ let healingLog = [];
13490
13502
  if (await this.client.exists(`s3db.json`)) {
13491
- const request = await this.client.getObject(`s3db.json`);
13492
- metadata = JSON.parse(await streamToString(request?.Body));
13503
+ try {
13504
+ const request = await this.client.getObject(`s3db.json`);
13505
+ const rawContent = await streamToString(request?.Body);
13506
+ try {
13507
+ metadata = JSON.parse(rawContent);
13508
+ } catch (parseError) {
13509
+ healingLog.push("JSON parsing failed - attempting recovery");
13510
+ needsHealing = true;
13511
+ metadata = await this._attemptJsonRecovery(rawContent, healingLog);
13512
+ if (!metadata) {
13513
+ await this._createCorruptedBackup(rawContent);
13514
+ healingLog.push("Created backup of corrupted file - starting with blank metadata");
13515
+ metadata = this.blankMetadataStructure();
13516
+ }
13517
+ }
13518
+ const healedMetadata = await this._validateAndHealMetadata(metadata, healingLog);
13519
+ if (healedMetadata !== metadata) {
13520
+ metadata = healedMetadata;
13521
+ needsHealing = true;
13522
+ }
13523
+ } catch (error) {
13524
+ healingLog.push(`Critical error reading s3db.json: ${error.message}`);
13525
+ await this._createCorruptedBackup();
13526
+ metadata = this.blankMetadataStructure();
13527
+ needsHealing = true;
13528
+ }
13493
13529
  } else {
13494
13530
  metadata = this.blankMetadataStructure();
13495
13531
  await this.uploadMetadataFile();
13496
13532
  }
13533
+ if (needsHealing) {
13534
+ await this._uploadHealedMetadata(metadata, healingLog);
13535
+ }
13497
13536
  this.savedMetadata = metadata;
13498
13537
  const definitionChanges = this.detectDefinitionChanges(metadata);
13499
13538
  for (const [name, resourceMetadata] of Object.entries(metadata.resources || {})) {
@@ -13529,7 +13568,7 @@ class Database extends EventEmitter {
13529
13568
  paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
13530
13569
  allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
13531
13570
  autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
13532
- hooks: versionData.hooks || {},
13571
+ hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : versionData.hooks || {},
13533
13572
  versioningEnabled: this.versioningEnabled,
13534
13573
  map: versionData.map,
13535
13574
  idGenerator: restoredIdGenerator,
@@ -13624,6 +13663,73 @@ class Database extends EventEmitter {
13624
13663
  const maxVersion = versionNumbers.length > 0 ? Math.max(...versionNumbers) : -1;
13625
13664
  return `v${maxVersion + 1}`;
13626
13665
  }
13666
+ /**
13667
+ * Serialize hooks to strings for JSON persistence
13668
+ * @param {Object} hooks - Hooks object with event names as keys and function arrays as values
13669
+ * @returns {Object} Serialized hooks object
13670
+ * @private
13671
+ */
13672
+ _serializeHooks(hooks) {
13673
+ if (!hooks || typeof hooks !== "object") return hooks;
13674
+ const serialized = {};
13675
+ for (const [event, hookArray] of Object.entries(hooks)) {
13676
+ if (Array.isArray(hookArray)) {
13677
+ serialized[event] = hookArray.map((hook) => {
13678
+ if (typeof hook === "function") {
13679
+ try {
13680
+ return {
13681
+ __s3db_serialized_function: true,
13682
+ code: hook.toString(),
13683
+ name: hook.name || "anonymous"
13684
+ };
13685
+ } catch (err) {
13686
+ if (this.verbose) {
13687
+ console.warn(`Failed to serialize hook for event '${event}':`, err.message);
13688
+ }
13689
+ return null;
13690
+ }
13691
+ }
13692
+ return hook;
13693
+ });
13694
+ } else {
13695
+ serialized[event] = hookArray;
13696
+ }
13697
+ }
13698
+ return serialized;
13699
+ }
13700
+ /**
13701
+ * Deserialize hooks from strings back to functions
13702
+ * @param {Object} serializedHooks - Serialized hooks object
13703
+ * @returns {Object} Deserialized hooks object
13704
+ * @private
13705
+ */
13706
+ _deserializeHooks(serializedHooks) {
13707
+ if (!serializedHooks || typeof serializedHooks !== "object") return serializedHooks;
13708
+ const deserialized = {};
13709
+ for (const [event, hookArray] of Object.entries(serializedHooks)) {
13710
+ if (Array.isArray(hookArray)) {
13711
+ deserialized[event] = hookArray.map((hook) => {
13712
+ if (hook && typeof hook === "object" && hook.__s3db_serialized_function) {
13713
+ try {
13714
+ const fn = new Function("return " + hook.code)();
13715
+ if (typeof fn === "function") {
13716
+ return fn;
13717
+ }
13718
+ } catch (err) {
13719
+ if (this.verbose) {
13720
+ console.warn(`Failed to deserialize hook '${hook.name}' for event '${event}':`, err.message);
13721
+ }
13722
+ }
13723
+ return null;
13724
+ }
13725
+ return hook;
13726
+ }).filter((hook) => hook !== null);
13727
+ } else {
13728
+ deserialized[event] = hookArray;
13729
+ }
13730
+ }
13731
+ return deserialized;
13732
+ }
13627
13733
  async startPlugins() {
13628
13734
  const db = this;
13629
13735
  if (!lodashEs.isEmpty(this.pluginList)) {
@@ -13693,7 +13799,7 @@ class Database extends EventEmitter {
13693
13799
  allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
13694
13800
  autoDecrypt: resource.config.autoDecrypt,
13695
13801
  cache: resource.config.cache,
13696
- hooks: resource.config.hooks,
13802
+ hooks: this.persistHooks ? this._serializeHooks(resource.config.hooks) : resource.config.hooks,
13697
13803
  idSize: resource.idSize,
13698
13804
  idGenerator: resource.idGeneratorType,
13699
13805
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
@@ -13717,9 +13823,269 @@ class Database extends EventEmitter {
13717
13823
  return {
13718
13824
  version: `1`,
13719
13825
  s3dbVersion: this.s3dbVersion,
13826
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
13720
13827
  resources: {}
13721
13828
  };
13722
13829
  }
13830
+ /**
13831
+ * Attempt to recover JSON from corrupted content
13832
+ */
13833
+ async _attemptJsonRecovery(content, healingLog) {
13834
+ if (!content || typeof content !== "string") {
13835
+ healingLog.push("Content is empty or not a string");
13836
+ return null;
13837
+ }
13838
+ const fixes = [
13839
+ // Remove trailing commas
13840
+ () => content.replace(/,(\s*[}\]])/g, "$1"),
13841
+ // Add missing quotes to keys
13842
+ () => content.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":'),
13843
+ // Fix incomplete objects by adding closing braces
13844
+ () => {
13845
+ let openBraces = 0;
13846
+ let openBrackets = 0;
13847
+ let inString = false;
13848
+ let escaped = false;
13849
+ for (let i = 0; i < content.length; i++) {
13850
+ const char = content[i];
13851
+ if (escaped) {
13852
+ escaped = false;
13853
+ continue;
13854
+ }
13855
+ if (char === "\\") {
13856
+ escaped = true;
13857
+ continue;
13858
+ }
13859
+ if (char === '"') {
13860
+ inString = !inString;
13861
+ continue;
13862
+ }
13863
+ if (!inString) {
13864
+ if (char === "{") openBraces++;
13865
+ else if (char === "}") openBraces--;
13866
+ else if (char === "[") openBrackets++;
13867
+ else if (char === "]") openBrackets--;
13868
+ }
13869
+ }
13870
+ let fixed = content;
13871
+ while (openBrackets > 0) {
13872
+ fixed += "]";
13873
+ openBrackets--;
13874
+ }
13875
+ while (openBraces > 0) {
13876
+ fixed += "}";
13877
+ openBraces--;
13878
+ }
13879
+ return fixed;
13880
+ }
13881
+ ];
13882
+ for (const [index, fix] of fixes.entries()) {
13883
+ try {
13884
+ const fixedContent = fix();
13885
+ const parsed = JSON.parse(fixedContent);
13886
+ healingLog.push(`JSON recovery successful using fix #${index + 1}`);
13887
+ return parsed;
13888
+ } catch (error) {
13889
+ }
13890
+ }
13891
+ healingLog.push("All JSON recovery attempts failed");
13892
+ return null;
13893
+ }
13894
+ /**
13895
+ * Validate and heal metadata structure
13896
+ */
13897
+ async _validateAndHealMetadata(metadata, healingLog) {
13898
+ if (!metadata || typeof metadata !== "object") {
13899
+ healingLog.push("Metadata is not an object - using blank structure");
13900
+ return this.blankMetadataStructure();
13901
+ }
13902
+ let healed = { ...metadata };
13903
+ let changed = false;
13904
+ if (!healed.version || typeof healed.version !== "string") {
13905
+ if (healed.version && typeof healed.version === "number") {
13906
+ healed.version = String(healed.version);
13907
+ healingLog.push("Converted version from number to string");
13908
+ changed = true;
13909
+ } else {
13910
+ healed.version = "1";
13911
+ healingLog.push("Added missing or invalid version field");
13912
+ changed = true;
13913
+ }
13914
+ }
13915
+ if (!healed.s3dbVersion || typeof healed.s3dbVersion !== "string") {
13916
+ if (healed.s3dbVersion && typeof healed.s3dbVersion !== "string") {
13917
+ healed.s3dbVersion = String(healed.s3dbVersion);
13918
+ healingLog.push("Converted s3dbVersion to string");
13919
+ changed = true;
13920
+ } else {
13921
+ healed.s3dbVersion = this.s3dbVersion;
13922
+ healingLog.push("Added missing s3dbVersion field");
13923
+ changed = true;
13924
+ }
13925
+ }
13926
+ if (!healed.resources || typeof healed.resources !== "object" || Array.isArray(healed.resources)) {
13927
+ healed.resources = {};
13928
+ healingLog.push("Fixed invalid resources field");
13929
+ changed = true;
13930
+ }
13931
+ if (!healed.lastUpdated) {
13932
+ healed.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
13933
+ healingLog.push("Added missing lastUpdated field");
13934
+ changed = true;
13935
+ }
13936
+ const validResources = {};
13937
+ for (const [name, resource] of Object.entries(healed.resources)) {
13938
+ const healedResource = this._healResourceStructure(name, resource, healingLog);
13939
+ if (healedResource) {
13940
+ validResources[name] = healedResource;
13941
+ if (healedResource !== resource) {
13942
+ changed = true;
13943
+ }
13944
+ } else {
13945
+ healingLog.push(`Removed invalid resource: ${name}`);
13946
+ changed = true;
13947
+ }
13948
+ }
13949
+ healed.resources = validResources;
13950
+ return changed ? healed : metadata;
13951
+ }
13952
+ /**
13953
+ * Heal individual resource structure
13954
+ */
13955
+ _healResourceStructure(name, resource, healingLog) {
13956
+ if (!resource || typeof resource !== "object") {
13957
+ healingLog.push(`Resource ${name}: invalid structure`);
13958
+ return null;
13959
+ }
13960
+ let healed = { ...resource };
13961
+ let changed = false;
13962
+ if (!healed.currentVersion) {
13963
+ healed.currentVersion = "v0";
13964
+ healingLog.push(`Resource ${name}: added missing currentVersion`);
13965
+ changed = true;
13966
+ }
13967
+ if (!healed.versions || typeof healed.versions !== "object" || Array.isArray(healed.versions)) {
13968
+ healed.versions = {};
13969
+ healingLog.push(`Resource ${name}: fixed invalid versions object`);
13970
+ changed = true;
13971
+ }
13972
+ if (!healed.partitions || typeof healed.partitions !== "object" || Array.isArray(healed.partitions)) {
13973
+ healed.partitions = {};
13974
+ healingLog.push(`Resource ${name}: fixed invalid partitions object`);
13975
+ changed = true;
13976
+ }
13977
+ const currentVersion = healed.currentVersion;
13978
+ if (!healed.versions[currentVersion]) {
13979
+ const availableVersions = Object.keys(healed.versions);
13980
+ if (availableVersions.length > 0) {
13981
+ healed.currentVersion = availableVersions[0];
13982
+ healingLog.push(`Resource ${name}: changed currentVersion from ${currentVersion} to ${healed.currentVersion}`);
13983
+ changed = true;
13984
+ } else {
13985
+ healingLog.push(`Resource ${name}: no valid versions found - removing resource`);
13986
+ return null;
13987
+ }
13988
+ }
13989
+ const versionData = healed.versions[healed.currentVersion];
13990
+ if (!versionData || typeof versionData !== "object") {
13991
+ healingLog.push(`Resource ${name}: invalid version data - removing resource`);
13992
+ return null;
13993
+ }
13994
+ if (!versionData.attributes || typeof versionData.attributes !== "object") {
13995
+ healingLog.push(`Resource ${name}: missing or invalid attributes - removing resource`);
13996
+ return null;
13997
+ }
13998
+ if (versionData.hooks) {
13999
+ const healedHooks = this._healHooksStructure(versionData.hooks, name, healingLog);
14000
+ if (healedHooks !== versionData.hooks) {
14001
+ healed.versions[healed.currentVersion].hooks = healedHooks;
14002
+ changed = true;
14003
+ }
14004
+ }
14005
+ return changed ? healed : resource;
14006
+ }
14007
+ /**
14008
+ * Heal hooks structure
14009
+ */
14010
+ _healHooksStructure(hooks, resourceName, healingLog) {
14011
+ if (!hooks || typeof hooks !== "object") {
14012
+ healingLog.push(`Resource ${resourceName}: invalid hooks structure - using empty hooks`);
14013
+ return {};
14014
+ }
14015
+ const healed = {};
14016
+ let changed = false;
14017
+ for (const [event, hookArray] of Object.entries(hooks)) {
14018
+ if (Array.isArray(hookArray)) {
14019
+ const validHooks = hookArray.filter(
14020
+ (hook) => hook !== null && hook !== void 0 && hook !== ""
14021
+ );
14022
+ healed[event] = validHooks;
14023
+ if (validHooks.length !== hookArray.length) {
14024
+ healingLog.push(`Resource ${resourceName}: cleaned invalid hooks for event ${event}`);
14025
+ changed = true;
14026
+ }
14027
+ } else {
14028
+ healingLog.push(`Resource ${resourceName}: hooks for event ${event} is not an array - removing`);
14029
+ changed = true;
14030
+ }
14031
+ }
14032
+ return changed ? healed : hooks;
14033
+ }
14034
+ /**
14035
+ * Create backup of corrupted file
14036
+ */
14037
+ async _createCorruptedBackup(content = null) {
14038
+ try {
14039
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
14040
+ const backupKey = `s3db.json.corrupted.${timestamp}.backup`;
14041
+ if (!content) {
14042
+ try {
14043
+ const request = await this.client.getObject(`s3db.json`);
14044
+ content = await streamToString(request?.Body);
14045
+ } catch (error) {
14046
+ content = "Unable to read corrupted file content";
14047
+ }
14048
+ }
14049
+ await this.client.putObject({
14050
+ key: backupKey,
14051
+ body: content,
14052
+ contentType: "application/json"
14053
+ });
14054
+ if (this.verbose) {
14055
+ console.warn(`S3DB: Created backup of corrupted s3db.json as ${backupKey}`);
14056
+ }
14057
+ } catch (error) {
14058
+ if (this.verbose) {
14059
+ console.warn(`S3DB: Failed to create backup: ${error.message}`);
14060
+ }
14061
+ }
14062
+ }
14063
+ /**
14064
+ * Upload healed metadata with logging
14065
+ */
14066
+ async _uploadHealedMetadata(metadata, healingLog) {
14067
+ try {
14068
+ if (this.verbose && healingLog.length > 0) {
14069
+ console.warn("S3DB Self-Healing Operations:");
14070
+ healingLog.forEach((log) => console.warn(` - ${log}`));
14071
+ }
14072
+ metadata.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
14073
+ await this.client.putObject({
14074
+ key: "s3db.json",
14075
+ body: JSON.stringify(metadata, null, 2),
14076
+ contentType: "application/json"
14077
+ });
14078
+ this.emit("metadataHealed", { healingLog, metadata });
14079
+ if (this.verbose) {
14080
+ console.warn("S3DB: Successfully uploaded healed metadata");
14081
+ }
14082
+ } catch (error) {
14083
+ if (this.verbose) {
14084
+ console.error(`S3DB: Failed to upload healed metadata: ${error.message}`);
14085
+ }
14086
+ throw error;
14087
+ }
14088
+ }
13723
14089
  /**
13724
14090
  * Check if a resource exists by name
13725
14091
  * @param {string} name - Resource name