(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));
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)