I was using below script to create copies of Google Sheets (files) as XLSX and the script was working perfectly.
Now with the pasaage of time numbers of Google Sheets (files) has been increased.
I want to Make a copy of Entire folder as XLSX format with all Google sheets. Your help will be much appreciated in this regards
function GoogleSpreadsheetAsExcel() {
try {
var ss = SpreadsheetApp.getActive();
var url = 'https://docs.google.com/feeds/download/spreadsheets/Export?key=' + ss.getId() + '&exportFormat=xlsx';
var folder = DriveApp.getFolderById('folderID');
var params = {
method: 'get',
headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
muteHttpExceptions: true,
};
var blob = UrlFetchApp.fetch(url, params).getBlob();
blob.setName(ss.getName() + '.xlsx');
folder.createFile(blob)
} catch (f) {
Logger.log(f.toString());
}
}
Your current function works on an active Sheets file context and only exports one file at the time.
The export logic is already implemented, you only need to read the files in a folder and export them in a loop.
To preserve existing logic and to accordingly run in this context, some values became constants (source and target Folder IDs) and file name and id are on the existing function signature to be provided on the loop iteration context.
Here is my suggestion:
Sample code:
const TARGET_FOLDER = "<TARGET_ID>"; //Folder ID where the exported XLSX files Will be saved
const SOURCE_FOLDER = "<SOURCE_FOLDER>"; //Folder ID where the Sheet files are located
function GoogleSpreadsheetAsExcel(fileId, fileName) {
try {
var url = 'https://docs.google.com/feeds/download/spreadsheets/Export?key=' + fileId + '&exportFormat=xlsx';
var folder = DriveApp.getFolderById(TARGET_FOLDER);
var params = {
method: 'get',
headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
muteHttpExceptions: true,
};
var blob = UrlFetchApp.fetch(url, params).getBlob();
blob.setName(fileName + '.xlsx');
folder.createFile(blob)
} catch (f) {
Logger.log(f.toString());
}
}
function exportFiles(){
var folder = DriveApp.getFolderById(SOURCE_FOLDER);
var files = folder.getFiles();
while (files.hasNext()){ //Loop to read all files in SOURCE_FOLDER
file = files.next(); //Keep reference of the file in the iteration
if (file.getMimeType() == "application/vnd.google-apps.spreadsheet"){ //If the iteration file is Sheets
GoogleSpreadsheetAsExcel(file.getId(), file.getName()); //Export file as XLSX
}
}
}
Related
I have a simple Apps Script need, but can't find a near enough sample from the community.
I need to convert 2 sheets from a Google Spreadsheet, individually, as CSV files then attach them in an email.
So far, I found a script to convert a sheet into a CSV format and file it in a folder.
I looked for a script to add that will instead attach the CSV file to an email, but can't find anything which I can use based on my novice level of Apps Script knowledge.
Any help will be greatly appreciated. Thank you very much.
Script:
function sheet1ToCsv()
{
var ssID = SpreadsheetApp.getActiveSpreadsheet().getId();
var sheet_Name = "xxxx"
var requestData = {"method": "GET", "headers":{"Authorization":"Bearer "+ScriptApp.getOAuthToken()}};
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheet_Name)
var sheetNameId = sheet.getSheetId().toString();
params= ssID+"/export?gid="+sheetNameId +"&format=csv"
var url = "https://docs.google.com/spreadsheets/d/"+ params
var result = UrlFetchApp.fetch(url, requestData);
var folderId = "yyyy";
var csvContent = {
title: sheet_Name+".csv",
mimeType: "application/vnd.csv",
parents: [{id: folderId}]
}
var fileJson = Drive.Files.insert(csvContent,result)
}
In your showing script, an email is not sent. And, only one CSV file is created to Google Drive. From I need to convert 2 sheets from a Google Spreadsheet, individually, as CSV files then attach them in an email. and your showing script, when your showing script is modified, how about the following modification?
Modified script:
function myFunction() {
var emailAddress = "###"; // Please set your email address.
var sheetNames = ["Sheet1", "Sheet2"]; // Please set your 2 sheet names you want to use.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssID = ss.getId();
var requestData = { "method": "GET", "headers": { "Authorization": "Bearer " + ScriptApp.getOAuthToken() } };
var blobs = sheetNames.map(s => {
var params = ssID + "/export?gid=" + ss.getSheetByName(s).getSheetId() + "&format=csv";
var url = "https://docs.google.com/spreadsheets/d/" + params;
return UrlFetchApp.fetch(url, requestData).getBlob().setName(s); // <--- Modified
});
MailApp.sendEmail({ to: emailAddress, subject: "sample subject", body: "sample body", attachments: blobs });
}
When this script is run, an email is sent by including 2 CSV files as the attachment files.
References:
map()
sendEmail(message)
function myfunk() {
const folder = DriveApp.getFolderById("folderid");
const ss = SpreadsheetApp.getActive();
const names = ["Sheet1", "Sheet2"];
const params = { "method": "GET", "headers": { "Authorization": "Bearer " + ScriptApp.getOAuthToken() } };
let bA = [];
names.forEach(n => {
let sh = ss.getSheetByName(n);
let url = "https://docs.google.com/spreadsheets/d/" + ss.getId() + "/export?gid=" + sh.getSheetId() + "&format=csv";
let r = UrlFetchApp.fetch(url, params);
let csv = r.getContentText();
let file = folder.createFile(n, csv, MimeType.CSV);
bA.push(file.getBlob());
})
GmailApp.sendEmail(recipient, subject, body, { attachments: bA })
}
I am trying to export a hidden sheet as a PDF to Google Drive. When the sheet is hidden, the file cannot be properly created. When the sheet is not hidden, the PDF export works as expected.
Is this the expected behavior with hidden sheets? If so, what's the best workaround? Do I need to unhide the sheet, create the file, and then hide the sheet again?
Code.gs:
function exportProfiles() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Profiles");
var sheetUrl = SpreadsheetApp.getActive().getUrl();
var sheetId = '&gid=' + sheet.getSheetId();
var exportUrl = sheetUrl.replace(/\/edit.*$/, '')
+'/export?'+'format=pdf'+'&size=letter'+'&portrait=false'+'&fitw=true'
+'&top_margin=0.75'+'&bottom_margin=0.75'+'&left_margin=0.75'+'&right_margin=0.75'
+'&sheetnames=false'+'&printtitle=false'+'&pagenum=center'+'&gridlines=false'+'&fzr=false'
+ sheetId;
var blob = getFileAsBlob(exportUrl);
blob.setName("My Test File");
DriveApp.createFile(blob);
}
function getFileAsBlob(exportUrl) {
let response = UrlFetchApp.fetch(exportUrl, {
muteHttpExceptions: true,
headers: {
Authorization: 'Bearer '+ScriptApp.getOAuthToken(),
},
});
return response.getBlob();
}
I'm trying to convert all CSV and Excel files in a folder to Google Sheet format. The code works, but debugging it produces an error.
function fileToSheet(fileId) {
var file = DriveApp.getFileById(fileId);
var fileInfo = {
title: "(" + Utilities.formatDate(new Date(), "IST", "yyyy.mm.dd") + ") " + file.getName(),
mimeType: MimeType.GOOGLE_SHEETS,
parents: [{id: file.getParents().next().getId()}]
}
var blob = file.getBlob();
var mimeType = blob.getContentType();
if (mimeType == MimeType.CSV || mimeType == MimeType.MICROSOFT_EXCEL || mimeType == MimeType.MICROSOFT_EXCEL_LEGACY) {
var spreadsheet = Drive.Files.insert(fileInfo, blob);
Logger.log(spreadsheet.id)
}
return ""
}
function convertFiles(folderId) {
var folder = DriveApp.getFolderById(folderId);
var files = folder.getFiles();
Logger.log(files.length)
while (files.hasNext()) {
var file = files.next()
var fileId = file.getId()
fileToSheet(fileId)
}
}
Debugging and running it to the end still produces an error (Error Exception: Invalid argument: id fileToSheet # Code.gs:2), but the code still works when I use it on folders.
Is there any way to shorten the code to make it more concise? (still new to coding)
Thank you!
Insert the following in the beginning of the fileToSheet() function:
function fileToSheet(fileId) {
if (!fileId) {
throw new Error(
'Please do not run the fileToSheet() function in the script editor window. '
+ 'See https://stackoverflow.com/a/46860052/13045193 to learn how to '
+ 'debug the function properly.'
);
}
var file = DriveApp.getFileById(fileId);
// ...
can anyone help me with my problem?
Need to export a specific range (A1:J39), not the full sheet.
function SavePDFtoDrive() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ltrsht = ss.getSheetByName("Schichtübergabe-Protokoll");
var datasheet = ss.getSheetByName("Daten");
var sheets=SpreadsheetApp.getActiveSpreadsheet().getSheets();
for(var i =0;i<sheets.length;i++){
if(sheets[i].getName()!="Schichtübergabe-Protokoll"){ sheets[i].hideSheet() }
}
var pdf = DriveApp.getFileById(ss.getId());
var theBlob = pdf.getBlob().getAs('application/pdf').setName(datasheet.getRange("I8").getValue()+".pdf");
var folderID = "1dyKFNvQWrSiNFA8N5PyUMPJQpWSVhsLf"; // Folder id to save in a folder
var folder = DriveApp.getFolderById(folderID);
var newFile = folder.createFile(theBlob);
}
Answer:
Rather than getting your blob using DriveApp, you can obtain your blob from an export URL using UrlFetchApp.
More Information:
When you use DriveApp to create a blob of a file, it doesn't have the functionality to read the specifics of the file. In this case, you can't specify the range of a Sheet you wish to export with DriveApp.
You can, however, build a URL with all your export parameters and create your export in Drive by creating a blob of the response and creating it in Drive.
Code:
function SavePDFtoDrive() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ltrsht = ss.getSheetByName("Schichtübergabe-Protokoll");
var datasheet = ss.getSheetByName("Daten");
// build your URL:
var url = "https://docs.google.com/spreadsheets/d/" + ss.getId() + "/export?"
//define your parameters:
var params = 'exportFormat=pdf&format=pdf'
+ '&portrait=false'
+ '&fitw=true'
+ '&gid=' + ltrsht.getSheetId()
+ '&range=A1:J39';
//get token for UrlFetch:
var token = ScriptApp.getOAuthToken();
var response = UrlFetchApp.fetch(url + params, {
'headers': {
'Authorization': 'Bearer ' + token
}
});
var fileName = datasheet.getRange("I8").getValue() + ".pdf";
var theBlob = response.getBlob().setName(fileName);
var folderID = "1Hp-P1dzWaKHhhS2FTPzKib6pwJibS12t";
var folder = DriveApp.getFolderById(folderID);
folder.createFile(theBlob);
}
References:
Class UrlFetchApp | Apps Script | Google Developers
Related Questions/Answers:
how to print a long Google sheets column in fewer columns
Problem
My Google Script triggers aren't running because it "uses to much CPU time", according to error emails I receive. How can I optimize my scripts to use less CPU time?
I've separated sortGsheetFiles into different trigger, but it still uses to much time. It used to be combined with the importXLSXtoGsheet function.
Scripts explained
I've got 52 folders, each containing one spreadsheet file.
Each folder is shared with different colleagues.
During the day, people make changes to the files.
At the end of the day, all files are collected in one folder (gsheetFolder) and converted to XLSX files, using the function collectAndExportXLS.
These files are copied to a local server in the evening (using batch script and drive sync) which updates other information in the file and are copied back to the importXLSXfolder.
In the morning the importXLSXtoGsheet function runs and converts all XLSX files in the importXLSXfolder folder to Gsheet files in the gsheetFolder.
After that sortGsheetFiles runs, sorting and moving every Gsheet file in one of the 52 folders (using an array list from the current spreadsheet).
Other actions include cleaning the folders with the deleteFolder function.
Triggers
importXLSXtoGsheet - every day - between 6 am and 7 am
sortGsheetFiles - every day - between 7 am and 8 am
collectAndExportXLS - every day - between 10 pm and 11 pm
Script
var gsheetFolder = 'xxx';
var XLSXfolder = 'xxxxx';
var importXLSXfolder = 'xxxxx';
function checkEmptyFolder() {
var folders = DocsList.getAllFolders()
for(n=0;n<folders.length;++n){
if(folders[n].getFiles().length==0 && folders[n].getFolders().length==0){
folders[n].setTrashed(true)
Logger.log(folders[n].getName())
}
}
}
function importXLSXtoGsheet(){
// ========= convert all XLS files in XLS folder to GSheet and put in the general gsheet folder - after that sort in gsheet filiaal folders =========
// cleanup exportXLS folder first
deleteFolder(XLSXfolder);
var files = DriveApp.getFolderById(importXLSXfolder).searchFiles('title contains ".xlsx"');
while(files.hasNext()) {
var xFile = files.next();
var name = xFile.getName();
if (name.indexOf('.xlsx')) {
var ID = xFile.getId();
var xBlob = xFile.getBlob();
var newFile = {
title : name + ('.xlsx'),
key : ID,
parents: [{"id": gsheetFolder}]
}
file = Drive.Files.insert(newFile, xBlob, {convert: true});
}
}
deleteFolder(importXLSXfolder);
}
function sortGsheetFiles() {
// ========= sort Gsheet folder and move to corresponding filiaal folders =========
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var myArrayFileName = sheet.getRange("A2:A53").getValues();
var myArrayFolderId = sheet.getRange("B2:B53").getValues();
var a = myArrayFileName.join().split(',').filter(Boolean);
var b = myArrayFolderId.join().split(',').filter(Boolean);
var folderId = gsheetFolder;
// Log the name of every file in the folder.
var files = DriveApp.getFolderById(folderId).getFiles();
while (files.hasNext()) {
var file = files.next();
for (var i in a) {
var id = file.getId();
if (file.getName() == a[i]) {
moveFiles(id, b[i]); // Match found and move to corresponding folder
}
}
}
deleteFolder(importXLSXfolder);
}
function collectAndExportXLS() {
// ========= collect all Gsheet files, copy to gsheet folder and convert to xlsx and move to xlsx folder =========
// cleanup gsheet folder
deleteFolder(gsheetFolder);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var myArrayFileName = sheet.getRange("A2:A53").getValues();
var myArrayFolderId = sheet.getRange("B2:B53").getValues();
var a = myArrayFileName.join().split(',').filter(Boolean);
var b = myArrayFolderId.join().split(',').filter(Boolean);
var folderId = gsheetFolder;
for (var i in b) {
var files = DriveApp.getFolderById(b[i]).getFiles();
while (files.hasNext()) {
var file = files.next();
var id = file.getId();
moveFiles(id , folderId);
}
}
ConvertBackToXLS()
deleteFolder(gsheetFolder);
}
function moveFiles(sourceFileId, targetFolderId) {
var file = DriveApp.getFileById(sourceFileId);
file.getParents().next().removeFile(file);
DriveApp.getFolderById(targetFolderId).addFile(file);
}
function deleteFolder(folder) {
//delete files in a folder without sending to trash!
var eachFile, idToDLET, myFolder, rtrnFromDLET, thisFile, files;
files = DriveApp.getFolderById(folder).getFiles();
while (files.hasNext()) {//If there is another element in the iterator
eachFile = files.next();
idToDLET = eachFile.getId();
//Logger.log('idToDLET: ' + idToDLET);
rtrnFromDLET = Drive.Files.remove(idToDLET);
};
Logger.log('folder deleted');
}
function ConvertBackToXLS() {
// Log the name of every file in the folder.
var files = DriveApp.getFolderById(gsheetFolder).getFiles();
var dir = DriveApp.getFolderById(XLSXfolder);
while (files.hasNext()) {
try {
var file = files.next();
var ss = SpreadsheetApp.openById(file.getId());
Logger.log(file.getId());
var url = "https://docs.google.com/feeds/download/spreadsheets/Export?key=" + file.getId() + "&exportFormat=xlsx";
var params = {
method : "get",
headers : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
muteHttpExceptions: true
};
var blob = UrlFetchApp.fetch(url, params).getBlob();
blob.setName(ss.getName());
var newfile = dir.createFile(blob);
} catch (f) {
Logger.log(f.toString());
}
}
}
How about this modification? Please think of this as just one of several answers.
And as an important point, please test the script using a test situation, before you run with your actual situation.
About your script:
collectAndExportXLS(): Abou this function name, I didn't modify collectAndExportXLS(). Because I thought that you might be using this function name at other script or triggers.
Delete all files in gsheetFolder.
Convert a Google Spreadsheet in each folder ID retrieved from "B2:B53" of the sheet with the 1st index in the active Spreadsheet to XLSX format.
Filename is like sample.xlsx.
All converted files are put in XLSXfolder.
Delete all files in all folder IDs.
XLSX files in XLSXfolder are put in importXLSXfolder by other script.
importXLSXtoGsheet(): Abou this function name, I didn't modify collectAndExportXLS(). Because I thought that you might be using this function name at other script or triggers.
Delete all files in XLSXfolder.
Convert all XLSX files in importXLSXfolder to Google Spreadsheet.
Filename is like sample.xlsx.
Converted Google Spreadsheets are put in gsheetFolder.
Delete all files in importXLSXfolder.
sortGsheetFiles()
Move Google Spreadsheets in gsheetFolder to each folder ID retrieved from "B2:B53" of the sheet with the 1st index in the active Spreadsheet.
In order to match the folder ID, the filenames of Google Spreadsheet and the values retrieved from "A2:A53".
Delete all files in importXLSXfolder.
I understood that from your question, the filenames of column "A2:A53" of the active Spreadsheet are the same with the filenames of Google Spreadsheets which were put in the folders of folder IDs of the column "B2:B53".
I understood that the number of all files is less than 100.
I understand like above. If my understanding is correct, how about this modification? In my modification, I used the Batch request of Drive API and the fetchAll method of UrlFetchApp with the type of multipart/form-data for your situation. The batch request and fetchAll method can work with the asynchronous process. By this, I thought that your process cost might be reduced.
In order to use these methods, I used 2 GAS libraries. Before you run the script, please install these 2 libraries for your script. You can see how to install the library as follows.
Install a library for running the fetchAll method of UrlFetchApp with the type of multipart/form-data.
Install a library for running batch request.
Modification points:
collectAndExportXLS()
File IDs in each folder are retrieved by the batch request.
Blobs (XLSX format) from each file ID are retrieved by the fetchAll method of UrlFetchApp.
Files of XLSX format are created by FetchApp.
importXLSXtoGsheet()
File list is retrieved by the files.list method of Drive API.
Files of XLSX format are converted to Google Spreadsheet by the batch request.
sortGsheetFiles()
File list is retrieved by the files.list method of Drive API.
Files of Google Spreadsheet are moved to each folder ID retrieved from the column "B2:B53" of the active Spreadsheet using the batch request.
deleteFolder()
Files in the folder are deleted by the batch request.
When above points are reflected to your script, it becomes as follows.
Modified script:
After installed 2 libraries, please run the following script.
var gsheetFolder = '###';
var XLSXfolder = '###';
var importXLSXfolder = '###';
// Modified
function deleteFolder(folderId) {
var url = "https://www.googleapis.com/drive/v3/files?q='" + folderId + "'+in+parents+and+trashed%3Dfalse&fields=files%2Fid&access_token=" + ScriptApp.getOAuthToken();
var res = UrlFetchApp.fetch(url);
var obj = JSON.parse(res.getContentText());
var reqs = obj.files.map(function(e) {return {method: "DELETE", endpoint: "https://www.googleapis.com/drive/v3/files/" + e.id}});
var requests = {batchPath: "batch/drive/v3", requests: reqs};
if (requests.requests.length > 0) BatchRequest.Do(requests);
}
// Added
function deleteFiles(files) {
var reqs = files.map(function(e) {return {method: "DELETE", endpoint: "https://www.googleapis.com/drive/v3/files/" + e.id}});
var requests = {batchPath: "batch/drive/v3", requests: reqs};
if (requests.requests.length > 0) BatchRequest.Do(requests);
}
// Added
function getValuesFromSpreadsheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
return sheet.getRange("A2:B53").getValues();
}
// Modified
function sortGsheetFiles() {
var url = "https://www.googleapis.com/drive/v3/files?q='" + gsheetFolder + "'+in+parents+and+mimeType%3D'" + MimeType.GOOGLE_SHEETS + "'+and+trashed%3Dfalse&fields=files(id%2Cname)&access_token=" + ScriptApp.getOAuthToken();
var res = UrlFetchApp.fetch(url);
var obj = JSON.parse(res.getContentText());
var values = getValuesFromSpreadsheet();
var reqs = values.reduce(function(ar, e) {
for (var i = 0; i < obj.files.length; i++) {
if (obj.files[i].name == e[0]) {
ar.push({
method: "PATCH",
endpoint: "https://www.googleapis.com/drive/v3/files/" + obj.files[i].id + "?addParents=" + e[1] + "&removeParents=" + gsheetFolder,
});
break;
}
}
return ar;
}, []);
var requests = {batchPath: "batch/drive/v3", requests: reqs};
if (requests.requests.length > 0) BatchRequest.Do(requests);
deleteFolder(importXLSXfolder);
}
// Modified
function importXLSXtoGsheet(){
deleteFolder(XLSXfolder);
var url = "https://www.googleapis.com/drive/v3/files?q='" + importXLSXfolder + "'+in+parents+and+mimeType%3D'" + MimeType.MICROSOFT_EXCEL + "'+and+trashed%3Dfalse&fields=files(id%2Cname)&access_token=" + ScriptApp.getOAuthToken();
var res = UrlFetchApp.fetch(url);
var obj = JSON.parse(res.getContentText());
var reqs = obj.files.map(function(e) {return {
method: "POST",
endpoint: "https://www.googleapis.com/drive/v3/files/" + e.id + "/copy",
requestBody: {mimeType: MimeType.GOOGLE_SHEETS, name: e.name + ".xlsx", parents: [gsheetFolder]},
}
});
var requests = {batchPath: "batch/drive/v3", requests: reqs};
if (requests.requests.length > 0) BatchRequest.Do(requests);
deleteFolder(importXLSXfolder);
}
// Modified
function ConvertBackToXLS(fileList) {
var token = ScriptApp.getOAuthToken();
var reqs1 = fileList.map(function(e) {return {
method: "GET",
url: "https://docs.google.com/spreadsheets/export?id=" + e.id + "&exportFormat=xlsx&access_token=" + token,
}
});
var res = UrlFetchApp.fetchAll(reqs1);
var reqs2 = res.map(function(e, i) {
var metadata = {name: fileList[i].name, parents: [XLSXfolder]};
var form = FetchApp.createFormData(); // Create form data
form.append("metadata", Utilities.newBlob(JSON.stringify(metadata), "application/json"));
form.append("file", e.getBlob());
var url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
return {url: url, method: "POST", headers: {Authorization: "Bearer " + token}, body: form};
});
FetchApp.fetchAll(reqs2);
}
// Modified
function collectAndExportXLS() {
deleteFolder(gsheetFolder);
var values = getValuesFromSpreadsheet();
var reqs1 = values.reduce(function(ar, e) {
if (e[0] && e[1]) {
ar.push({
method: "GET",
endpoint: "https://www.googleapis.com/drive/v3/files?q='" + e[1] + "'+in+parents+and+trashed%3Dfalse&fields=files(id%2Cname)",
});
}
return ar;
}, []);
var resForReq1 = BatchRequest.Do({batchPath: "batch/drive/v3", requests: reqs1});
var temp = resForReq1.getContentText().split("--batch");
var files = temp.slice(1, temp.length - 1).map(function(e) {return JSON.parse(e.match(/{[\S\s]+}/g)[0])});
var fileList = files.reduce(function(ar, e) {return ar.concat(e.files.map(function(f) {return f}))}, []);
ConvertBackToXLS(fileList);
deleteFiles(fileList);
}
Note:
In this modification, the error handling is not reflected, because I couldn't test your situation. So please add it, if you are required.
If the file size of XLSX files is large, the error might occur.
References:
Batch request of Drive API
BatchRequest
FetchApp