DEV Community

Yili Zhang
Yili Zhang

Posted on

Sharepoint daochu test

(async () => {
  // ---- config ----
  const INCLUDE_TEMPLATES = [100];          // add 101 for libraries (metadata only)
  const SKIP_HIDDEN_LISTS = true;
  const PAGE_SIZE = 2000;                    // items per request
  const SKIP_LIST_TITLES = new Set([        // system lists to never export
    "User Information List", "Form Templates", "Site Assets", "Site Pages",
    "Style Library", "Master Page Gallery", "Composed Looks", "Web Part Gallery",
    "Theme Gallery", "Solution Gallery", "List Template Gallery",
    "TaxonomyHiddenList", "appdata", "appfiles", "Maintenance Log Library",
    "Content and Structure Reports", "Reusable Content", "Workflow Tasks",
    "Workflow History", "Converted Forms", "Long Running Operation Status",
    "Quick Deploy Items", "Relationships List", "Variation Labels",
    "Reporting Metadata", "Reporting Templates", "Translation Status",
  ]);

  const isSafeFieldName = (n) =>
    /^[A-Za-z0-9_]+$/.test(n) && !/_x0024_|_x003a_|_x002c_/.test(n);

  const web = _spPageContextInfo.webAbsoluteUrl;
  const getJson = async (url) => {
    const r = await fetch(url, { headers: { Accept: "application/json;odata=verbose" } });
    if (!r.ok) throw new Error(`GET ${url} -> ${r.status}: ${await r.text()}`);
    return (await r.json()).d;
  };
  const pagedAll = async (firstUrl) => {
    let url = firstUrl, out = [];
    while (url) {
      const d = await getJson(url);
      out = out.concat(d.results || []);
      url = d.__next || null;
    }
    return out;
  };

  console.log("[Export] Source web:", web);

  // 1) all lists (unfiltered) so we can resolve lookup target GUIDs -> titles
  const allLists = await pagedAll(
    `${web}/_api/web/lists?$select=Id,Title,BaseTemplate,Hidden,Description,EnableAttachments&$top=500`
  );
  const idToTitle = {};
  for (const l of allLists) idToTitle[String(l.Id).toLowerCase()] = l.Title;

  // 2) pick lists to export
  const targets = allLists.filter((l) =>
    INCLUDE_TEMPLATES.indexOf(l.BaseTemplate) !== -1 &&
    !(SKIP_HIDDEN_LISTS && l.Hidden) &&
    !SKIP_LIST_TITLES.has(l.Title)
  );
  console.log(`[Export] Lists to export: ${targets.length}`, targets.map((l) => l.Title));

  // helpers to read schema xml ------------------------------------------------
  const parseSchema = (xml) => {
    try {
      const doc = new DOMParser().parseFromString(xml, "text/xml");
      const f = doc.documentElement;
      return {
        type: f.getAttribute("Type") || "",
        showField: f.getAttribute("ShowField") || "Title",
        mult: (f.getAttribute("Mult") || "").toUpperCase() === "TRUE",
        listGuid: (f.getAttribute("List") || "").replace(/[{}]/g, "").toLowerCase(),
      };
    } catch {
      return { type: "", showField: "Title", mult: false, listGuid: "" };
    }
  };

  const exportLists = [];

  for (const list of targets) {
   try {
    console.log(`[Export] >>> ${list.Title}`);
    // 3) creatable fields only
    const rawFields = await pagedAll(
      `${web}/_api/web/lists(guid'${list.Id}')/fields` +
      `?$select=InternalName,Title,TypeAsString,Required,ReadOnlyField,Hidden,CanBeDeleted,SchemaXml&$top=500`
    );
    const fields = [];
    for (const f of rawFields) {
      const creatable = !f.Hidden && !f.ReadOnlyField && (f.CanBeDeleted || f.InternalName === "Title");
      if (!creatable) continue;
      if (!isSafeFieldName(f.InternalName)) {
        console.warn(`[Export]     skip field with unsafe internal name: ${list.Title}.${f.InternalName}`);
        continue;
      }
      const sx = parseSchema(f.SchemaXml);
      const isLookup = sx.type === "Lookup" || sx.type === "LookupMulti";
      const isUser = sx.type === "User" || sx.type === "UserMulti";
      const unsupported = sx.type.indexOf("TaxonomyFieldType") === 0;
      fields.push({
        internalName: f.InternalName,
        title: f.Title,
        type: f.TypeAsString,
        required: !!f.Required,
        schemaXml: f.SchemaXml,
        isLookup, isUser,
        multi: sx.mult || sx.type.endsWith("Multi"),
        showField: sx.showField,
        lookupListTitle: isLookup ? (idToTitle[sx.listGuid] || null) : null,
        unsupported,
      });
    }

    // 4) build select/expand for items
    const sel = ["Id"], exp = [];
    for (const f of fields) {
      if (f.unsupported) continue;
      const n = f.internalName;
      if (f.isLookup) { exp.push(n); sel.push(`${n}/Id`, `${n}/${f.showField}`); }
      else if (f.isUser) { exp.push(n); sel.push(`${n}/Id`, `${n}/EMail`, `${n}/Title`); }
      else { sel.push(n); }
    }
    const q =
      `${web}/_api/web/lists(guid'${list.Id}')/items` +
      `?$select=${encodeURIComponent(sel.join(","))}` +
      (exp.length ? `&$expand=${encodeURIComponent(exp.join(","))}` : "") +
      `&$top=${PAGE_SIZE}`;

    const rawItems = await pagedAll(q);

    // 5) flatten items
    const items = rawItems.map((it) => {
      const rec = { __id: it.Id };
      for (const f of fields) {
        if (f.unsupported) continue;
        const n = f.internalName;
        if (f.isLookup) {
          if (f.multi) rec[n] = (it[n]?.results || []).map((r) => ({ id: r.Id, value: r[f.showField] }));
          else rec[n] = it[n] ? { id: it[n].Id, value: it[n][f.showField] } : null;
        } else if (f.isUser) {
          if (f.multi) rec[n] = (it[n]?.results || []).map((r) => ({ id: r.Id, email: r.EMail, title: r.Title }));
          else rec[n] = it[n] ? { id: it[n].Id, email: it[n].EMail, title: it[n].Title } : null;
        } else {
          let v = it[n];
          if (v && typeof v === "object") delete v.__metadata; // URL/value objects
          rec[n] = v ?? null;
        }
      }
      return rec;
    });

    console.log(`[Export]     ${fields.length} fields, ${items.length} items`);
    exportLists.push({
      title: list.Title,
      baseTemplate: list.BaseTemplate,
      description: list.Description || "",
      enableAttachments: !!list.EnableAttachments,
      fields,
      items,
    });
   } catch (e) {
    console.warn(`[Export] !!! Skipped list "${list.Title}" due to error: ${e.message}`);
   }
  }

  // 6) download
  const payload = {
    source: { web, exportedAt: new Date().toISOString() },
    lists: exportLists,
  };
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
  const a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = `sp-export-${(web.split("/").pop() || "site")}-${Date.now()}.json`;
  document.body.appendChild(a);
  a.click();
  a.remove();

  console.log(`[Export] DONE. Lists: ${exportLists.length}. File downloaded.`);
})().catch((e) => console.error("[Export] FAILED:", e));

Enter fullscreen mode Exit fullscreen mode

Top comments (0)