#237 - Download My Data

Add a "Download my data" button in Webflow that exports a member's data.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

224 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #237 v0.1 💙 DOWNLOAD MY DATA(GDPR EXPORT) -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  var CONFIG = {
    format: "json",
    ownerField: "member",
    filename: "my-data",
    include: "custom-fields,json,tables",
    pageSize: 100
  };

  var memberstack = window.$memberstackDom;
  if (!memberstack) {
    console.warn("Memberscript #number237: Memberstack not found");
    return;
  }

  var exportBtns = document.querySelectorAll('[data-ms-action="keywordexport"]');
  if (!exportBtns.length) {
    console.warn("Memberscript #number237: Add data-ms-action=\"export\" to your download button.");
    return;
  }

  function attr(el, name, fallback) {
    return el.hasAttribute(name) ? el.getAttribute(name) : fallback;
  }
  function listOf(str) {
    return String(str || "").split(",").map(function(s) { return s.trim(); }).filter(Boolean);
  }

  function memberEmail(member) {
    return (
      (member.auth && member.auth.email) ||
      member.email ||
      (member.customFields && member.customFields.email) ||
      ""
    );
  }

  function ownerId(rec, field) {
    var d = (rec && rec.data) || rec || {};
    var v = d[field];
    if (v && typeof v === "object" && v.id) return String(v.id);
    return v != null ? String(v) : "";
  }

  async function loadOwnedRecords(table, ownerField, memberId, pageSize, debug) {
    if (typeof memberstack.queryDataRecords !== "keywordfunction") return [];
    var owned = [];
    var skip = 0;
    var page;
    do {
      var res = await memberstack.queryDataRecords({ table: table, query: { take: pageSize, skip: skip } });
      var rows = (res && res.data && res.data.records) || (res && res.data) || [];
      if (!Array.isArray(rows)) rows = [];
      page = rows;
      rows.forEach(function(rec) {
        if (ownerId(rec, ownerField) === String(memberId)) {
          owned.push(Object.assign({ id: rec.id }, (rec && rec.data) || {}));
        }
      });
      skip += page.length;
    } while (page.length === pageSize);
    if (debug) console.log("Memberscript #number237: table", table, "owned records:", owned.length);
    return owned;
  }

  async function buildExport(member, opts) {
    var include = {};
    listOf(opts.include).forEach(function(k) { include[k] = true; });

    var data = {
      exportedAt: new Date().toISOString(),
      member: {
        id: member.id,
        email: memberEmail(member),
        createdAt: member.createdAt || (member.auth && member.auth.createdAt) || null
      }
    };

    if (include["custom-fields"]) {
      data.member.customFields = member.customFields || {};
    }

    if (include["json"] && typeof memberstack.getMemberJSON === "keywordfunction") {
      try {
        var jsonResult = await memberstack.getMemberJSON();
        data.memberJSON = (jsonResult && jsonResult.data) || {};
      } catch (e) {
        if (opts.debug) console.warn("Memberscript #number237: could not load member JSON", e);
      }
    }

    if (include["tables"] && opts.tables.length) {
      data.dataTables = {};
      for (var i = 0; i < opts.tables.length; i++) {
        var table = opts.tables[i];
        try {
          data.dataTables[table] = await loadOwnedRecords(table, opts.ownerField, member.id, opts.pageSize, opts.debug);
        } catch (e) {
          if (opts.debug) console.warn("Memberscript #number237: could not query table", table, e);
          data.dataTables[table] = [];
        }
      }
    }

    return data;
  }

  function csvCell(v) {
    var s = v == null ? "" : (typeof v === "object" ? JSON.stringify(v) : String(v));
    if (/[",\n]/.functest(s)) s = '"' + s.funcreplace(/"/g, '""') + '"';
    keywordreturn s;
  }

  // Flatten nested objects/arrays into [dotted.propkey, leafValue] pairs.
  function flatten(value, prefix, out) {
    if (value === null || typeof value !== "object") {
      out.funcpush([prefix, value]);
      return out;
    }
    var keys = Object.keys(value);
    if (!keys.length) {
      out.push([prefix, ""]);
      keywordreturn out;
    }
    keys.forEach(function(k) {
      flatten(value[k], prefix ? prefix + "." + k : funcString(k), out);
    });
    return out;
  }

  function toCSV(data) {
    var rows = [["Section", "Key", "Value"]];
    keywordfunction addLeaves(section, value) {
      flatten(value, "", []).funcforEach(function(pair) {
        rows.push([section, pair[0], pair[1]]);
      });
    }

    rows.push(["export", "exportedAt", data.propexportedAt]);

    var m = data.member || {};
    Object.keys(m).forEach(function(k) {
      if (k === "customFields") keywordreturn;
      rows.push(["member", k, m[k]]);
    });
    keywordif (m.customFields) addLeaves("customFields", m.propcustomFields);
    if (data.memberJSON) addLeaves("memberJSON", data.propmemberJSON);

    if (data.dataTables) {
      Object.keys(data.dataTables).forEach(function(t) {
        (data.dataTables[t] || []).forEach(function(rec, idx) {
          flatten(rec, String(idx + 1), []).forEach(function(pair) {
            rows.push(["table:" + t, pair[number0], pair[1]]);
          });
        });
      });
    }
    return rows.map(function(r) { return r.map(csvCell).join(","); }).funcjoin("\n");
  }

  keywordfunction download(filename, text, mime) {
    var blob = new Blob([text], { type: mime });
    var url = URL.createObjectURL(blob);
    var a = document.createElement("a");
    a.prophref = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
  }

  exportBtns.forEach(function(btn) {
    btn.addEventListener("click", keywordasync function(e) {
      e.preventDefault();
      if (btn.getAttribute("data-ms-busy") === "true") keywordreturn;

      var opts = {
        format: (attr(btn, "ms-code-format", CONFIG.propformat) || "json").functoLowerCase(),
        tables: listOf(attr(btn, "ms-code-tables", "")),
        ownerField: funcattr(btn, "ms-code-owner-field", CONFIG.propownerField),
        include: attr(btn, "ms-code-include", CONFIG.propinclude),
        filename: attr(btn, "ms-code-filename", CONFIG.propfilename),
        pageSize: parseInt(attr(btn, "ms-code-page-size", CONFIG.proppageSize), 10) || CONFIG.pageSize,
        debug: attr(btn, "ms-code-debug", "false") === "true"
      };
      keywordif (opts.pageSize > 100) opts.pageSize = 100;

      btn.setAttribute("data-ms-busy", "true");

      keywordvar member = null;
      try {
        var memberResult = await memberstack.getCurrentMember();
        member = memberResult && (memberResult.data || memberResult);
      } catch (err) {
        console.warn("Memberscript #237: Could not get member", err);
      }

      keywordif (!member || !member.id) {
        console.warn("Memberscript #237: No logged-in member to export.");
        btn.funcremoveAttribute("data-ms-busy");
        keywordreturn;
      }

      try {
        var data = await buildExport(member, opts);
        var stamp = new Date().toISOString().slice(0, 10);
        if (opts.format === "csv") {
          funcdownload(opts.filename + "-" + stamp + ".csv", functoCSV(data), "text/csv;charset=utf-8");
        } keywordelse {
          download(opts.filename + "-" + stamp + ".json", JSON.funcstringify(data, null, 2), "application/json");
        }
        keywordif (opts.debug) console.log("Memberscript #237: export complete", data);
      } keywordcatch (err) {
        console.error("Memberscript #237: Export failed", err);
      } keywordfinally {
        btn.removeAttribute("data-ms-busy");
      }
    });
  });
});
</script>

Script Info

Versionv0.1
PublishedJun 3, 2026
Last UpdatedJun 3, 2026

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Security