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.iife.js CHANGED
@@ -9651,7 +9651,7 @@ ${JSON.stringify(validation, null, 2)}`,
9651
9651
  }) {
9652
9652
  super();
9653
9653
  this.verbose = verbose;
9654
- this.id = id ?? idGenerator();
9654
+ this.id = id ?? idGenerator(77);
9655
9655
  this.parallelism = parallelism;
9656
9656
  this.config = new ConnectionString(connectionString);
9657
9657
  this.httpClientOptions = {
@@ -11238,10 +11238,18 @@ ${JSON.stringify(validation, null, 2)}`,
11238
11238
  */
11239
11239
  constructor(config = {}) {
11240
11240
  super();
11241
- this._instanceId = Math.random().toString(36).slice(2, 8);
11241
+ this._instanceId = idGenerator(7);
11242
11242
  const validation = validateResourceConfig(config);
11243
11243
  if (!validation.isValid) {
11244
- throw new ResourceError(`Invalid Resource ${config.name} configuration`, { resourceName: config.name, validation: validation.errors, operation: "constructor", suggestion: "Check resource config and attributes." });
11244
+ const errorDetails = validation.errors.map((err) => ` \u2022 ${err}`).join("\n");
11245
+ throw new ResourceError(
11246
+ `Invalid Resource ${config.name || "[unnamed]"} configuration:
11247
+ ${errorDetails}`,
11248
+ {
11249
+ resourceName: config.name,
11250
+ validation: validation.errors
11251
+ }
11252
+ );
11245
11253
  }
11246
11254
  const {
11247
11255
  name,
@@ -13412,9 +13420,10 @@ ${JSON.stringify(validation, null, 2)}`,
13412
13420
  class Database extends EventEmitter {
13413
13421
  constructor(options) {
13414
13422
  super();
13423
+ this.id = idGenerator(7);
13415
13424
  this.version = "1";
13416
13425
  this.s3dbVersion = (() => {
13417
- const [ok, err, version] = try_fn_default(() => true ? "8.0.2" : "latest");
13426
+ const [ok, err, version] = try_fn_default(() => true ? "8.1.0" : "latest");
13418
13427
  return ok ? version : "latest";
13419
13428
  })();
13420
13429
  this.resources = {};
@@ -13427,6 +13436,7 @@ ${JSON.stringify(validation, null, 2)}`,
13427
13436
  this.cache = options.cache;
13428
13437
  this.passphrase = options.passphrase || "secret";
13429
13438
  this.versioningEnabled = options.versioningEnabled || false;
13439
+ this.persistHooks = options.persistHooks || false;
13430
13440
  this._initHooks();
13431
13441
  let connectionString = options.connectionString;
13432
13442
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
@@ -13473,13 +13483,42 @@ ${JSON.stringify(validation, null, 2)}`,
13473
13483
  async connect() {
13474
13484
  await this.startPlugins();
13475
13485
  let metadata = null;
13486
+ let needsHealing = false;
13487
+ let healingLog = [];
13476
13488
  if (await this.client.exists(`s3db.json`)) {
13477
- const request = await this.client.getObject(`s3db.json`);
13478
- metadata = JSON.parse(await streamToString(request?.Body));
13489
+ try {
13490
+ const request = await this.client.getObject(`s3db.json`);
13491
+ const rawContent = await streamToString(request?.Body);
13492
+ try {
13493
+ metadata = JSON.parse(rawContent);
13494
+ } catch (parseError) {
13495
+ healingLog.push("JSON parsing failed - attempting recovery");
13496
+ needsHealing = true;
13497
+ metadata = await this._attemptJsonRecovery(rawContent, healingLog);
13498
+ if (!metadata) {
13499
+ await this._createCorruptedBackup(rawContent);
13500
+ healingLog.push("Created backup of corrupted file - starting with blank metadata");
13501
+ metadata = this.blankMetadataStructure();
13502
+ }
13503
+ }
13504
+ const healedMetadata = await this._validateAndHealMetadata(metadata, healingLog);
13505
+ if (healedMetadata !== metadata) {
13506
+ metadata = healedMetadata;
13507
+ needsHealing = true;
13508
+ }
13509
+ } catch (error) {
13510
+ healingLog.push(`Critical error reading s3db.json: ${error.message}`);
13511
+ await this._createCorruptedBackup();
13512
+ metadata = this.blankMetadataStructure();
13513
+ needsHealing = true;
13514
+ }
13479
13515
  } else {
13480
13516
  metadata = this.blankMetadataStructure();
13481
13517
  await this.uploadMetadataFile();
13482
13518
  }
13519
+ if (needsHealing) {
13520
+ await this._uploadHealedMetadata(metadata, healingLog);
13521
+ }
13483
13522
  this.savedMetadata = metadata;
13484
13523
  const definitionChanges = this.detectDefinitionChanges(metadata);
13485
13524
  for (const [name, resourceMetadata] of Object.entries(metadata.resources || {})) {
@@ -13515,7 +13554,7 @@ ${JSON.stringify(validation, null, 2)}`,
13515
13554
  paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
13516
13555
  allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
13517
13556
  autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
13518
- hooks: versionData.hooks || {},
13557
+ hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : versionData.hooks || {},
13519
13558
  versioningEnabled: this.versioningEnabled,
13520
13559
  map: versionData.map,
13521
13560
  idGenerator: restoredIdGenerator,
@@ -13610,6 +13649,73 @@ ${JSON.stringify(validation, null, 2)}`,
13610
13649
  const maxVersion = versionNumbers.length > 0 ? Math.max(...versionNumbers) : -1;
13611
13650
  return `v${maxVersion + 1}`;
13612
13651
  }
13652
+ /**
13653
+ * Serialize hooks to strings for JSON persistence
13654
+ * @param {Object} hooks - Hooks object with event names as keys and function arrays as values
13655
+ * @returns {Object} Serialized hooks object
13656
+ * @private
13657
+ */
13658
+ _serializeHooks(hooks) {
13659
+ if (!hooks || typeof hooks !== "object") return hooks;
13660
+ const serialized = {};
13661
+ for (const [event, hookArray] of Object.entries(hooks)) {
13662
+ if (Array.isArray(hookArray)) {
13663
+ serialized[event] = hookArray.map((hook) => {
13664
+ if (typeof hook === "function") {
13665
+ try {
13666
+ return {
13667
+ __s3db_serialized_function: true,
13668
+ code: hook.toString(),
13669
+ name: hook.name || "anonymous"
13670
+ };
13671
+ } catch (err) {
13672
+ if (this.verbose) {
13673
+ console.warn(`Failed to serialize hook for event '${event}':`, err.message);
13674
+ }
13675
+ return null;
13676
+ }
13677
+ }
13678
+ return hook;
13679
+ });
13680
+ } else {
13681
+ serialized[event] = hookArray;
13682
+ }
13683
+ }
13684
+ return serialized;
13685
+ }
13686
+ /**
13687
+ * Deserialize hooks from strings back to functions
13688
+ * @param {Object} serializedHooks - Serialized hooks object
13689
+ * @returns {Object} Deserialized hooks object
13690
+ * @private
13691
+ */
13692
+ _deserializeHooks(serializedHooks) {
13693
+ if (!serializedHooks || typeof serializedHooks !== "object") return serializedHooks;
13694
+ const deserialized = {};
13695
+ for (const [event, hookArray] of Object.entries(serializedHooks)) {
13696
+ if (Array.isArray(hookArray)) {
13697
+ deserialized[event] = hookArray.map((hook) => {
13698
+ if (hook && typeof hook === "object" && hook.__s3db_serialized_function) {
13699
+ try {
13700
+ const fn = new Function("return " + hook.code)();
13701
+ if (typeof fn === "function") {
13702
+ return fn;
13703
+ }
13704
+ } catch (err) {
13705
+ if (this.verbose) {
13706
+ console.warn(`Failed to deserialize hook '${hook.name}' for event '${event}':`, err.message);
13707
+ }
13708
+ }
13709
+ return null;
13710
+ }
13711
+ return hook;
13712
+ }).filter((hook) => hook !== null);
13713
+ } else {
13714
+ deserialized[event] = hookArray;
13715
+ }
13716
+ }
13717
+ return deserialized;
13718
+ }
13613
13719
  async startPlugins() {
13614
13720
  const db = this;
13615
13721
  if (!lodashEs.isEmpty(this.pluginList)) {
@@ -13679,7 +13785,7 @@ ${JSON.stringify(validation, null, 2)}`,
13679
13785
  allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
13680
13786
  autoDecrypt: resource.config.autoDecrypt,
13681
13787
  cache: resource.config.cache,
13682
- hooks: resource.config.hooks,
13788
+ hooks: this.persistHooks ? this._serializeHooks(resource.config.hooks) : resource.config.hooks,
13683
13789
  idSize: resource.idSize,
13684
13790
  idGenerator: resource.idGeneratorType,
13685
13791
  createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
@@ -13703,9 +13809,269 @@ ${JSON.stringify(validation, null, 2)}`,
13703
13809
  return {
13704
13810
  version: `1`,
13705
13811
  s3dbVersion: this.s3dbVersion,
13812
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
13706
13813
  resources: {}
13707
13814
  };
13708
13815
  }
13816
+ /**
13817
+ * Attempt to recover JSON from corrupted content
13818
+ */
13819
+ async _attemptJsonRecovery(content, healingLog) {
13820
+ if (!content || typeof content !== "string") {
13821
+ healingLog.push("Content is empty or not a string");
13822
+ return null;
13823
+ }
13824
+ const fixes = [
13825
+ // Remove trailing commas
13826
+ () => content.replace(/,(\s*[}\]])/g, "$1"),
13827
+ // Add missing quotes to keys
13828
+ () => content.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":'),
13829
+ // Fix incomplete objects by adding closing braces
13830
+ () => {
13831
+ let openBraces = 0;
13832
+ let openBrackets = 0;
13833
+ let inString = false;
13834
+ let escaped = false;
13835
+ for (let i = 0; i < content.length; i++) {
13836
+ const char = content[i];
13837
+ if (escaped) {
13838
+ escaped = false;
13839
+ continue;
13840
+ }
13841
+ if (char === "\\") {
13842
+ escaped = true;
13843
+ continue;
13844
+ }
13845
+ if (char === '"') {
13846
+ inString = !inString;
13847
+ continue;
13848
+ }
13849
+ if (!inString) {
13850
+ if (char === "{") openBraces++;
13851
+ else if (char === "}") openBraces--;
13852
+ else if (char === "[") openBrackets++;
13853
+ else if (char === "]") openBrackets--;
13854
+ }
13855
+ }
13856
+ let fixed = content;
13857
+ while (openBrackets > 0) {
13858
+ fixed += "]";
13859
+ openBrackets--;
13860
+ }
13861
+ while (openBraces > 0) {
13862
+ fixed += "}";
13863
+ openBraces--;
13864
+ }
13865
+ return fixed;
13866
+ }
13867
+ ];
13868
+ for (const [index, fix] of fixes.entries()) {
13869
+ try {
13870
+ const fixedContent = fix();
13871
+ const parsed = JSON.parse(fixedContent);
13872
+ healingLog.push(`JSON recovery successful using fix #${index + 1}`);
13873
+ return parsed;
13874
+ } catch (error) {
13875
+ }
13876
+ }
13877
+ healingLog.push("All JSON recovery attempts failed");
13878
+ return null;
13879
+ }
13880
+ /**
13881
+ * Validate and heal metadata structure
13882
+ */
13883
+ async _validateAndHealMetadata(metadata, healingLog) {
13884
+ if (!metadata || typeof metadata !== "object") {
13885
+ healingLog.push("Metadata is not an object - using blank structure");
13886
+ return this.blankMetadataStructure();
13887
+ }
13888
+ let healed = { ...metadata };
13889
+ let changed = false;
13890
+ if (!healed.version || typeof healed.version !== "string") {
13891
+ if (healed.version && typeof healed.version === "number") {
13892
+ healed.version = String(healed.version);
13893
+ healingLog.push("Converted version from number to string");
13894
+ changed = true;
13895
+ } else {
13896
+ healed.version = "1";
13897
+ healingLog.push("Added missing or invalid version field");
13898
+ changed = true;
13899
+ }
13900
+ }
13901
+ if (!healed.s3dbVersion || typeof healed.s3dbVersion !== "string") {
13902
+ if (healed.s3dbVersion && typeof healed.s3dbVersion !== "string") {
13903
+ healed.s3dbVersion = String(healed.s3dbVersion);
13904
+ healingLog.push("Converted s3dbVersion to string");
13905
+ changed = true;
13906
+ } else {
13907
+ healed.s3dbVersion = this.s3dbVersion;
13908
+ healingLog.push("Added missing s3dbVersion field");
13909
+ changed = true;
13910
+ }
13911
+ }
13912
+ if (!healed.resources || typeof healed.resources !== "object" || Array.isArray(healed.resources)) {
13913
+ healed.resources = {};
13914
+ healingLog.push("Fixed invalid resources field");
13915
+ changed = true;
13916
+ }
13917
+ if (!healed.lastUpdated) {
13918
+ healed.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
13919
+ healingLog.push("Added missing lastUpdated field");
13920
+ changed = true;
13921
+ }
13922
+ const validResources = {};
13923
+ for (const [name, resource] of Object.entries(healed.resources)) {
13924
+ const healedResource = this._healResourceStructure(name, resource, healingLog);
13925
+ if (healedResource) {
13926
+ validResources[name] = healedResource;
13927
+ if (healedResource !== resource) {
13928
+ changed = true;
13929
+ }
13930
+ } else {
13931
+ healingLog.push(`Removed invalid resource: ${name}`);
13932
+ changed = true;
13933
+ }
13934
+ }
13935
+ healed.resources = validResources;
13936
+ return changed ? healed : metadata;
13937
+ }
13938
+ /**
13939
+ * Heal individual resource structure
13940
+ */
13941
+ _healResourceStructure(name, resource, healingLog) {
13942
+ if (!resource || typeof resource !== "object") {
13943
+ healingLog.push(`Resource ${name}: invalid structure`);
13944
+ return null;
13945
+ }
13946
+ let healed = { ...resource };
13947
+ let changed = false;
13948
+ if (!healed.currentVersion) {
13949
+ healed.currentVersion = "v0";
13950
+ healingLog.push(`Resource ${name}: added missing currentVersion`);
13951
+ changed = true;
13952
+ }
13953
+ if (!healed.versions || typeof healed.versions !== "object" || Array.isArray(healed.versions)) {
13954
+ healed.versions = {};
13955
+ healingLog.push(`Resource ${name}: fixed invalid versions object`);
13956
+ changed = true;
13957
+ }
13958
+ if (!healed.partitions || typeof healed.partitions !== "object" || Array.isArray(healed.partitions)) {
13959
+ healed.partitions = {};
13960
+ healingLog.push(`Resource ${name}: fixed invalid partitions object`);
13961
+ changed = true;
13962
+ }
13963
+ const currentVersion = healed.currentVersion;
13964
+ if (!healed.versions[currentVersion]) {
13965
+ const availableVersions = Object.keys(healed.versions);
13966
+ if (availableVersions.length > 0) {
13967
+ healed.currentVersion = availableVersions[0];
13968
+ healingLog.push(`Resource ${name}: changed currentVersion from ${currentVersion} to ${healed.currentVersion}`);
13969
+ changed = true;
13970
+ } else {
13971
+ healingLog.push(`Resource ${name}: no valid versions found - removing resource`);
13972
+ return null;
13973
+ }
13974
+ }
13975
+ const versionData = healed.versions[healed.currentVersion];
13976
+ if (!versionData || typeof versionData !== "object") {
13977
+ healingLog.push(`Resource ${name}: invalid version data - removing resource`);
13978
+ return null;
13979
+ }
13980
+ if (!versionData.attributes || typeof versionData.attributes !== "object") {
13981
+ healingLog.push(`Resource ${name}: missing or invalid attributes - removing resource`);
13982
+ return null;
13983
+ }
13984
+ if (versionData.hooks) {
13985
+ const healedHooks = this._healHooksStructure(versionData.hooks, name, healingLog);
13986
+ if (healedHooks !== versionData.hooks) {
13987
+ healed.versions[healed.currentVersion].hooks = healedHooks;
13988
+ changed = true;
13989
+ }
13990
+ }
13991
+ return changed ? healed : resource;
13992
+ }
13993
+ /**
13994
+ * Heal hooks structure
13995
+ */
13996
+ _healHooksStructure(hooks, resourceName, healingLog) {
13997
+ if (!hooks || typeof hooks !== "object") {
13998
+ healingLog.push(`Resource ${resourceName}: invalid hooks structure - using empty hooks`);
13999
+ return {};
14000
+ }
14001
+ const healed = {};
14002
+ let changed = false;
14003
+ for (const [event, hookArray] of Object.entries(hooks)) {
14004
+ if (Array.isArray(hookArray)) {
14005
+ const validHooks = hookArray.filter(
14006
+ (hook) => hook !== null && hook !== void 0 && hook !== ""
14007
+ );
14008
+ healed[event] = validHooks;
14009
+ if (validHooks.length !== hookArray.length) {
14010
+ healingLog.push(`Resource ${resourceName}: cleaned invalid hooks for event ${event}`);
14011
+ changed = true;
14012
+ }
14013
+ } else {
14014
+ healingLog.push(`Resource ${resourceName}: hooks for event ${event} is not an array - removing`);
14015
+ changed = true;
14016
+ }
14017
+ }
14018
+ return changed ? healed : hooks;
14019
+ }
14020
+ /**
14021
+ * Create backup of corrupted file
14022
+ */
14023
+ async _createCorruptedBackup(content = null) {
14024
+ try {
14025
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
14026
+ const backupKey = `s3db.json.corrupted.${timestamp}.backup`;
14027
+ if (!content) {
14028
+ try {
14029
+ const request = await this.client.getObject(`s3db.json`);
14030
+ content = await streamToString(request?.Body);
14031
+ } catch (error) {
14032
+ content = "Unable to read corrupted file content";
14033
+ }
14034
+ }
14035
+ await this.client.putObject({
14036
+ key: backupKey,
14037
+ body: content,
14038
+ contentType: "application/json"
14039
+ });
14040
+ if (this.verbose) {
14041
+ console.warn(`S3DB: Created backup of corrupted s3db.json as ${backupKey}`);
14042
+ }
14043
+ } catch (error) {
14044
+ if (this.verbose) {
14045
+ console.warn(`S3DB: Failed to create backup: ${error.message}`);
14046
+ }
14047
+ }
14048
+ }
14049
+ /**
14050
+ * Upload healed metadata with logging
14051
+ */
14052
+ async _uploadHealedMetadata(metadata, healingLog) {
14053
+ try {
14054
+ if (this.verbose && healingLog.length > 0) {
14055
+ console.warn("S3DB Self-Healing Operations:");
14056
+ healingLog.forEach((log) => console.warn(` - ${log}`));
14057
+ }
14058
+ metadata.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
14059
+ await this.client.putObject({
14060
+ key: "s3db.json",
14061
+ body: JSON.stringify(metadata, null, 2),
14062
+ contentType: "application/json"
14063
+ });
14064
+ this.emit("metadataHealed", { healingLog, metadata });
14065
+ if (this.verbose) {
14066
+ console.warn("S3DB: Successfully uploaded healed metadata");
14067
+ }
14068
+ } catch (error) {
14069
+ if (this.verbose) {
14070
+ console.error(`S3DB: Failed to upload healed metadata: ${error.message}`);
14071
+ }
14072
+ throw error;
14073
+ }
14074
+ }
13709
14075
  /**
13710
14076
  * Check if a resource exists by name
13711
14077
  * @param {string} name - Resource name