(async ()=>{constweb=_spPageContextInfo.webAbsoluteUrl;constEMPTY_LISTS_FIRST=false;constSKIP_EXISTING_ITEMS=true;// ---- load JSON via file picker ----constdata=awaitnewPromise((resolve,reject)=>{constinput=document.createElement("input");input.type="file";input.accept="application/json,.json";input.onchange=()=>{constfile=input.files[0];if (!file)returnreject(newError("No file selected"));constreader=newFileReader();reader.onload=()=>{try{resolve(JSON.parse(reader.result));}catch (e){reject(e);}};reader.onerror=()=>reject(reader.error);reader.readAsText(file);};input.click();});console.log(`[Import] Loaded export from ${data.source?.web} with ${data.lists.length} lists`);// ---- REST helpers ----constdigest=(await (awaitfetch(`${web}/_api/contextinfo`,{method:"POST",headers:{Accept:"application/json;odata=verbose"},})).json()).d.GetContextWebInformation.FormDigestValue;constgetJson=async (url)=>{constr=awaitfetch(url,{headers:{Accept:"application/json;odata=verbose"}});if (!r.ok)return{ok:false,status:r.status,body:awaitr.text()};return{ok:true,d:(awaitr.json()).d};};constsend=async (url,body,method="POST",extraHeaders={})=>{constr=awaitfetch(url,{method,headers:{Accept:"application/json;odata=verbose","Content-Type":"application/json;odata=verbose","X-RequestDigest":digest,...extraHeaders,},body:JSON.stringify(body),});if (!r.ok)thrownewError(`${method}${url} -> ${r.status}: ${awaitr.text()}`);constt=awaitr.text();returnt?JSON.parse(t).d:null;};constesc=(s)=>String(s).replace(/'/g,"''");constsleep=(ms)=>newPromise((r)=>setTimeout(r,ms));// Delete every item in a list (used when EMPTY_LISTS_FIRST is true).constclearListItems=async (title)=>{constids=[];leturl=`${web}/_api/web/lists/getbytitle('${esc(title)}')/items?$select=Id&$top=500`;while (url){constr=awaitgetJson(url);if (!r.ok)break;ids.push(...r.d.results.map((x)=>x.Id));url=r.d.__next||null;}for (constidofids){try{awaitsend(`${web}/_api/web/lists/getbytitle('${esc(title)}')/items(${id})`,{},"POST",{"X-HTTP-Method":"DELETE","IF-MATCH":"*"});}catch (e){console.warn(`[Import] ! delete item #${id} in ${title} failed: ${e.message}`);}}console.log(`[Import] cleared ${ids.length} existing items from ${title}`);};constlistByTitle={};// title -> { id, entityType }constidMap={};// listTitle -> { sourceItemId -> newItemId }constuserCache={};// email -> id (or null)constensureUser=async (email)=>{if (!email)returnnull;if (emailinuserCache)returnuserCache[email];try{constd=awaitsend(`${web}/_api/web/ensureuser`,{logonName:email});userCache[email]=d.Id;}catch (e){console.warn(`[Import] ensureUser failed for ${email}: ${e.message}`);userCache[email]=null;}returnuserCache[email];};constgetListInfo=async (title)=>{if (listByTitle[title])returnlistByTitle[title];constr=awaitgetJson(`${web}/_api/web/lists/getbytitle('${esc(title)}')?$select=Id,ListItemEntityTypeFullName`);if (!r.ok)returnnull;listByTitle[title]={id:r.d.Id,entityType:r.d.ListItemEntityTypeFullName};returnlistByTitle[title];};// SharePoint list provisioning is eventually consistent: a getbytitle right// after POST /lists can 404 for a second or two. Poll until the list is// queryable so later passes (lookups, items) never miss a freshly made list.constwaitForList=async (title,tries=10,delayMs=800)=>{for (leti=0;i<tries;i++){deletelistByTitle[title];constinfo=awaitgetListInfo(title);if (info)returninfo;awaitsleep(delayMs);}returnnull;};consthasField=async (title,internalName)=>{constr=awaitgetJson(`${web}/_api/web/lists/getbytitle('${esc(title)}')/fields/getbyinternalnameortitle('${esc(internalName)}')`);returnr.ok;};constaddFieldXml=async (title,schemaXml)=>{// A field add right after the list is created can transiently fail while// the list finishes provisioning; retry a couple of times before giving up.letlastErr;for (leti=0;i<3;i++){try{awaitsend(`${web}/_api/web/lists/getbytitle('${esc(title)}')/fields/createfieldasxml`,{parameters:{__metadata:{type:"SP.XmlSchemaFieldCreationInformation"},SchemaXml:schemaXml,Options:8,// AddFieldInternalNameHint},});return;}catch (e){lastErr=e;awaitsleep(600);}}throwlastErr;};// ============ PASS 0: create EVERY list first ============// Creating all lists up front (and waiting until each is queryable) means// the lookup pass below can always resolve its target list, regardless of// the order lists appear in the export file.for (constlistofdata.lists){if (awaitgetListInfo(list.title)){console.log(`[Import] = list exists: ${list.title}`);continue;}console.log(`[Import] + create list: ${list.title}`);try{awaitsend(`${web}/_api/web/lists`,{__metadata:{type:"SP.List"},BaseTemplate:list.baseTemplate||100,Title:list.title,Description:list.description||"",});}catch (e){console.warn(`[Import] ! create list ${list.title} failed: ${e.message}`);continue;}if (!(awaitwaitForList(list.title))){console.warn(`[Import] ! list ${list.title} not queryable after retries (provisioning slow?)`);}}// ============ PASS 1: non-lookup fields ============for (constlistofdata.lists){if (!(awaitgetListInfo(list.title))){console.warn(`[Import] ! skipping fields, list missing: ${list.title}`);continue;}for (constfoflist.fields){if (f.internalName==="Title"||f.unsupported||f.isLookup)continue;// lookups in pass 2if (awaithasField(list.title,f.internalName))continue;try{awaitaddFieldXml(list.title,f.schemaXml);console.log(`[Import] + field ${list.title}.${f.internalName}`);}catch (e){console.warn(`[Import] ! field ${list.title}.${f.internalName} failed: ${e.message}`);}}}// ============ PASS 2: lookup fields (re-pointed to target GUIDs) ============constskippedLookups=[];for (constlistofdata.lists){for (constfoflist.fields.filter((x)=>x.isLookup&&!x.unsupported)){if (awaithasField(list.title,f.internalName))continue;if (!f.lookupListTitle){console.warn(`[Import] ! lookup ${list.title}.${f.internalName} has no target list, skipped`);skippedLookups.push(`${list.title}.${f.internalName}`);continue;}// waitForList (not getListInfo) so a slow-provisioning target is retried.consttarget=awaitwaitForList(f.lookupListTitle);if (!target){console.warn(`[Import] ! lookup target '${f.lookupListTitle}' not found for ${list.title}.${f.internalName}, skipped`);skippedLookups.push(`${list.title}.${f.internalName} -> ${f.lookupListTitle}`);continue;}consttype=f.multi?"LookupMulti":"Lookup";constmult=f.multi?"Mult='TRUE'":"";// NOTE: not marked Required so the two-pass item import can run.constxml=`<Field Type='${type}' DisplayName='${f.title}' Name='${f.internalName}' `+`StaticName='${f.internalName}' List='{${target.id}}' ShowField='${f.showField}' ${mult} />`;try{awaitaddFieldXml(list.title,xml);console.log(`[Import] + lookup ${list.title}.${f.internalName} -> ${f.lookupListTitle}`);}catch (e){console.warn(`[Import] ! lookup ${list.title}.${f.internalName} failed: ${e.message}`);}}}// ============ PASS 3: items (scalar/choice/url) ============constisMultiChoice=(f)=>f.type==="MultiChoice";constisUrl=(f)=>f.type==="URL";// Fields that actually get written as item values (used for dedup signature).constscalarFieldsOf=(list)=>list.fields.filter((f)=>!f.unsupported&&!f.isLookup&&!f.isUser&&!f.readOnly);// Normalize one field value to a stable string so a source item and an// existing target item produce the SAME signature when they are equal.constnormVal=(f,v)=>{if (v===undefined||v===null)return"";if (isMultiChoice(f)){constarr=Array.isArray(v)?v:(v.results||[]);return[...arr].map(String).sort().join("|");}if (isUrl(f))returnString((v&&v.Url)||"");if (typeofv==="object")returnJSON.stringify(v);returnString(v);};constsignatureOf=(scalarFields,item)=>scalarFields.map((f)=>normVal(f,item[f.internalName])).join("\u00A7");// Read every existing item in a target list and map signature -> existing Id.constfetchExistingSignatures=async (list,scalarFields)=>{constmap=newMap();if (!scalarFields.length)returnmap;constsel=["Id",...scalarFields.map((f)=>f.internalName)].join(",");leturl=`${web}/_api/web/lists/getbytitle('${esc(list.title)}')/items?$select=${sel}&$top=500`;while (url){constr=awaitgetJson(url);if (!r.ok)break;for (constitofr.d.results)map.set(signatureOf(scalarFields,it),it.Id);url=r.d.__next||null;}returnmap;};for (constlistofdata.lists){constinfo=awaitgetListInfo(list.title);idMap[list.title]={};if (EMPTY_LISTS_FIRST)awaitclearListItems(list.title);constscalarFields=scalarFieldsOf(list);constexisting=SKIP_EXISTING_ITEMS?awaitfetchExistingSignatures(list,scalarFields):newMap();if (SKIP_EXISTING_ITEMS){console.log(`[Import] items -> ${list.title} (${list.items.length}); ${existing.size} already in target`);}else{console.log(`[Import] items -> ${list.title} (${list.items.length})`);}letadded=0,skipped=0;for (constitemoflist.items){// De-dup: if an identical item already exists, reuse its Id and skip insert.if (SKIP_EXISTING_ITEMS){constsig=signatureOf(scalarFields,item);if (existing.has(sig)){idMap[list.title][item.__id]=existing.get(sig);skipped++;continue;}}constbody={__metadata:{type:info.entityType}};for (constfoflist.fields){if (f.unsupported||f.isLookup||f.isUser)continue;if (f.readOnly)continue;constv=item[f.internalName];if (v===undefined||v===null)continue;if (isMultiChoice(f))body[f.internalName]={__metadata:{type:"Collection(Edm.String)"},results:Array.isArray(v)?v:(v.results||[])};elseif (isUrl(f))body[f.internalName]={__metadata:{type:"SP.FieldUrlValue"},Url:v.Url,Description:v.Description||v.Url};elsebody[f.internalName]=v;}try{constcreated=awaitsend(`${web}/_api/web/lists/getbytitle('${esc(list.title)}')/items`,body);idMap[list.title][item.__id]=created.Id;// Remember the new signature so duplicate rows in the source file are// also de-duplicated within this same run.if (SKIP_EXISTING_ITEMS)existing.set(signatureOf(scalarFields,item),created.Id);added++;}catch (e){console.warn(`[Import] ! item (src #${item.__id}) in ${list.title} failed: ${e.message}`);}}if (SKIP_EXISTING_ITEMS)console.log(`[Import] ${list.title}: +${added} added, ${skipped} skipped (already existed)`);}// ============ PASS 4: re-link lookups + people ============for (constlistofdata.lists){constlookupFields=list.fields.filter((f)=>(f.isLookup||f.isUser)&&!f.unsupported);if (!lookupFields.length)continue;constinfo=awaitgetListInfo(list.title);console.log(`[Import] relink -> ${list.title}`);for (constitemoflist.items){constnewId=idMap[list.title][item.__id];if (!newId)continue;constbody={__metadata:{type:info.entityType}};letany=false;for (constfoflookupFields){constv=item[f.internalName];if (v===undefined||v===null||(Array.isArray(v)&&!v.length))continue;constn=f.internalName;if (f.isLookup){constmap=idMap[f.lookupListTitle]||{};if (f.multi){constids=v.map((x)=>map[x.id]).filter(Boolean);if (ids.length){body[`${n}Id`]={results:ids};any=true;}}else{constid=map[v.id];if (id){body[`${n}Id`]=id;any=true;}}}else{// personif (f.multi){constids=[];for (constxofv){constid=awaitensureUser(x.email);if (id)ids.push(id);}if (ids.length){body[`${n}Id`]={results:ids};any=true;}}else{constid=awaitensureUser(v.email);if (id){body[`${n}Id`]=id;any=true;}}}}if (!any)continue;try{awaitsend(`${web}/_api/web/lists/getbytitle('${esc(list.title)}')/items(${newId})`,body,"POST",{"X-HTTP-Method":"MERGE","IF-MATCH":"*"});}catch (e){console.warn(`[Import] ! relink item #${newId} in ${list.title} failed: ${e.message}`);}}}if (skippedLookups.length){console.warn(`[Import] ${skippedLookups.length} lookup column(s) were NOT created (target list missing in this run). Re-run the import once all lists exist to create them:`,skippedLookups);}console.log("[Import] DONE.");})().catch((e)=>console.error("[Import] FAILED:",e));
Top comments (0)
Subscribe
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)