Internal Server Error when changing chart font name via API - google-apps-script

I am trying to update formatting of the charts using Sheets API's UpdateChartSpec request.
However, the script returns the error:
"API call to sheets.spreadsheets.batchUpdate failed with error: Internal error encountered"
Here's the snippet of my code that raises the exception:
var request = [{
'updateChartSpec': {
'chartId': chart_id,
'spec': {
'fontName': 'Arial',
'basicChart': { //to update font name, it seems that chart type should be provided
'chartType': 'BAR'
}
}
}
}];
Sheets.Spreadsheets.batchUpdate({'requests': request}, spreadsheet_id);
Can anybody tell, what's wrong with the request, if anything?

Per the "Samples" section on the Google Sheets API description, you cannot perform a partial chart specification update - you must replace the existing spec with a whole new spec.
If you just want to change a small bit of the current spec, then the simplest approach is to
Query the current chartSpec
Change the necessary bits
Issue the update with the (whole) modified spec.
In Apps Script this might be implemented as such:
function getChartSpecs(wkbkId) {
const fields = "sheets(charts(chartId,spec),properties(sheetId,title))";
var resp = Sheets.Spreadsheets.get(wkbkId, { fields: fields });
// return an object mapped by chartId, storing the chart spec and the host sheet.
return resp.sheets.reduce(function (obj, sheet) {
if (sheet.charts) {
sheet.charts.forEach(function (chart) {
obj[chart.chartId] = {
spec: chart.spec,
sheetName: sheet.properties.title,
sheetId: sheet.properties.sheetId
};
});
}
return obj;
}, {});
}
function makeChartUpdateRequest(chartId, newSpec) {
return {
updateChartSpec: {
chartId: chartId,
spec: newSpec
}
};
}
function setNewFontOnChart(newFontName, chartId, chartSpecs) {
const wb = SpreadsheetApp.getActive();
const wbId = wb.getId();
if (!chartSpecs)
chartSpecs = getChartSpecs(wbId);
var requests = [];
if (!chartId) { // Update all charts
requests = Object.keys(chartSpecs).map(function (id) {
var chart = chartSpecs[id];
chart.spec.fontName = newFontName;
return makeChartUpdateRequest(id, chart.spec);
});
} else if (chartSpecs[chartId]) { // Update just one chart.
chartSpecs[chartId].spec.fontName = newFontName;
requests.push(makeChartUpdateRequest(chartId, chartSpecs[chartId].spec));
} else {
// oops, the given chartId is not valid.
}
if (requests.length) {
Sheets.Spreadsheets.batchUpdate({ requests: requests }, wbId);
}
}
Useful links:
Partial Responses / "Fields"
APIs Explorer - spreadsheets#get
APIs Explorer - spreadsheets#batchUpdate
Array#map
Array#forEach
Array#reduce

Related

batchUpdate method throws errors while updating Google Slides

I am trying to create a presentation and update it on Google Apps Scripts. The creation is successful. However when I try to update the title or add a new shape or text it throws errors.
Is there any other update method? Also is it possible to update the presentation after modifying the texts without updating all of the presentation? I don't want to create an add-on I just want to be able to update the slides with executing the scripts.
Code:
function createAndUpdatePresentation() {
const createdPresentation = Slides.Presentations.create({"title": "MyNewPresentation"});
const link = `https://docs.google.com/presentation/d/${createdPresentation.presentationId}/edit`;
Logger.log(`Created presentation is on: ${link}`);
const request = {
requests: [
{
updateTitle: {
title: 'My Updated Presentation'
}
}
]
};
const updatedPresentation =
Slides.Presentations.batchUpdate(request, createdPresentation.presentationId);
const updatedLink = `https://docs.google.com/presentation/d/${updatedPresentation.presentationId}/edit`;
Logger.log(`Updated presentation is on: ${updatedLink}`);
}
Error: GoogleJsonResponseException: API call to slides.presentations.batchUpdate failed with error: Invalid JSON payload received. Unknown name "updateTitle" at 'requests[0]': Cannot find field.
Here are two ways to edit a new presentation, one using SlidesApp and the second using Slides API.
function newPresentation1() {
try {
let presentation = Slides.Presentations.create({'title': 'MyNewPresentation'});
presentation = SlidesApp.openById(presentation.presentationId);
let slide = presentation.getSlides()[0];
let element = slide.getPageElements()[0];
element.asShape().getText().setText("Hello")
}
catch(err) {
console.log(err)
}
}
function newPresentation2() {
try {
let presentation = Slides.Presentations.create({'title': 'MyNewPresentation'});
let pageElement = presentation.slides[0].pageElements[0].objectId;
let request = { insertText: { objectId: pageElement,
text: "Good bye" }
};
Slides.Presentations.batchUpdate( { requests: [ request ] }, presentation.presentationId );
}
catch(err) {
console.log(err)
}
}
Reference
SlidesApp
Slides API

Converting GraphQL mutation to Google Apps Scripts

I am looking to convert this specific GraphQL code snippet to GAS.
mutation {
createReportExport(input: {
reportId: "XXXX",
fileContentType: CSV,
frequency: ONCE,
reportFilters: [
{
attributeName: "Sale Date",
relativeDateQuery: {
greaterEqual: "P14D"
}
}
]
}) {
reportExport {
id
fileUrl
}
}
}
Below is what I have tried in GAS
var query = 'mutation {createReportExport(input: {reportId: "urn:abc:Report:3318979a-7628-44ab-aa0d-a822f856b908",fileContentType: CSV,frequency: ONCE,reportFilters: [{attributeName: "Sale Date",relativeDateQuery: {greaterEqual: "P10D"}}]}) {reportExport {idfileUrl}}}'
var query2 = {
'operationName': 'Mutation',
'query': {query},
'variables': {}
}
var ql = '{insert URL}';
var content = {
"method": 'POST',
"headers": {"Authorization": httpBasicHeader,
"contentType": "application/json"},
"payload": JSON.stringify(query2)
};
var response = UrlFetchApp.fetch(ql, content);
var data = (response.getContentText());
Logger.log(data);
I have two variables, 'query' and 'query2' that I have tried. I am getting a {"errors":[{"message":"No query document supplied"}]} error message when running it in GAS.
When I run the first code snippet in another environment, it runs successfully. I am looking to keep my project within GAS if possible, since I have figured out the rest of the problems with my project and this is the last thing holding me back.
I think, the query property in query2 should not be wrapped in an object:
var query2 = {
'operationName': 'Mutation',
// no { } here:
'query': query,
'variables': {}
}

Google Data Studio Community Connector getData() not working as expected

function getData(request){
try{
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(request)
};
response=UrlFetchApp.fetch(getDataUrl, options);
resData = JSON.parse(response.getContentText())
return resData
}catch (e) {
e = (typeof e === 'string') ? new Error(e) : e;
Logger.log("Catch", e);
throw e;
}
}
The the above is my getData() function.
My isAdminUser() returns true.
When I try to visualize my data, I get the following error
Data Set Configuration Error
Data Studio cannot connect to your data set.
There was an error requesting data from the community connector. Please report the issue to the provider of this community connector if this issue persists.
Error ID: 3d11b88b
https://i.stack.imgur.com/x3Hki.png
The error code changes every time I refresh data and I can't find any dictionary to map the error id to an error
I tried debugging by logging the request parameter, response.getContentText() and resData variable to make sure I my data is formatted correctly.
Following are the logs printed in Stackdriver logs
request
{configParams={/Personal config data/}, fields=[{name=LASTNAME}]}
response.getContentText()
{"schema":[{"name":"LASTNAME","dataType":"STRING"}],"rows":[{"values":["test"]},{"values":["test"]},{"values":["Dummy"]},{"values":["One"]},{"values":["Nagargoje"]},{"values":[""]},{"values":[""]},{"values":[""]},{"values":[""]},{"values":[""]}],"filtersApplied":false}
resData
{rows=[{values=[test]}, {values=[test]}, {values=[Dummy]},
{values=[One]}, {values=[Nagargoje]}, {values=[]}, {values=[]},
{values=[]}, {values=[]}, {values=[]}], filtersApplied=false,
schema=[{name=LASTNAME, dataType=STRING}]}
I am not sure what is wrong with my getData() function.
The Object that I am returning seems to match the structure given here https://developers.google.com/datastudio/connector/reference#getdata
So there was no issue with my getData() function, the issue existed in the manifest file.
I was searching about passing parameter via URL and I stumbled upon a field called
dataStudio.useQueryConfig and added that to my manifest file and set its value to true.
Google Data studio was expecting me to return a query Config for getData().
But what I really wanted was this.
Anyways, I was able to debug it thanks to Matthias for suggesting me to take a look at Open-Source implementations
I implemented JSON connect which worked fine, so I Logged what it was returning in getData() and used that format/structure in my code, but my connector still didn't work.
My next assumption was maybe there is something wrong with my getSchema() return value. So I logged that as well and then copy pasted the hard coded value of both getData() and getSchema() return varaibles from JSON connect.
And even that didn't work, so my last bet was there must be something wrong with the manifest file, maybe the dummy links I added in it must be the issue. Then, after carrying out field by comparison I was finally able to get my community connector working.
This would have been easier to debug if the error messages were a bit helpful and didn't seem so generic.
First: You can always check out the Open-Source implementations that others did for custom Google Data Studio connectors. They are a great source if information. Fore more information checkout the documentation on Open Source Community Connectors.
Second: My implementation is for a time tracking system thus having confidential GDPR relevant data. That's why I can not just give you response messages. But I assembled this code. It contains authentifiction, HTTP GET data fetch and data conversions. Explanation is below the code. Again, checkout the open-source connectors if you need further assistance.
var cc = DataStudioApp.createCommunityConnector();
const URL_DATA = 'https://www.myverysecretdomain.com/api';
const URL_PING = 'https://www.myverysecretdomain.com/ping';
const AUTH_USER = 'auth.user'
const AUTH_KEY = 'auth.key';
const JSON_TAG = 'user';
String.prototype.format = function() {
// https://coderwall.com/p/flonoa/simple-string-format-in-javascript
a = this;
for (k in arguments) {
a = a.replace("{" + k + "}", arguments[k])
}
return a
}
function httpGet(user, token, url, params) {
try {
// this depends on the URL you are connecting to
var headers = {
'ApiUser': user,
'ApiToken': token,
'User-Agent': 'my super freaky Google Data Studio connector'
};
var options = {
headers: headers
};
if (params && Object.keys(params).length > 0) {
var params_ = [];
for (const [key, value] of Object.entries(params)) {
var value_ = value;
if (Array.isArray(value))
value_ = value.join(',');
params_.push('{0}={1}'.format(key, encodeURIComponent(value_)))
}
var query = params_.join('&');
url = '{0}?{1}'.format(url, query);
}
var response = UrlFetchApp.fetch(url, options);
return {
code: response.getResponseCode(),
json: JSON.parse(response.getContentText())
}
} catch (e) {
throwConnectorError(e);
}
}
function getCredentials() {
var userProperties = PropertiesService.getUserProperties();
return {
username: userProperties.getProperty(AUTH_USER),
token: userProperties.getProperty(AUTH_KEY)
}
}
function validateCredentials(user, token) {
if (!user || !token)
return false;
var response = httpGet(user, token, URL_PING);
if (response.code == 200)
console.log('API key for the user %s successfully validated', user);
else
console.error('API key for the user %s is invalid. Code: %s', user, response.code);
return response;
}
function getAuthType() {
var cc = DataStudioApp.createCommunityConnector();
return cc.newAuthTypeResponse()
.setAuthType(cc.AuthType.USER_TOKEN)
.setHelpUrl('https://www.myverysecretdomain.com/index.html#authentication')
.build();
}
function resetAuth() {
var userProperties = PropertiesService.getUserProperties();
userProperties.deleteProperty(AUTH_USER);
userProperties.deleteProperty(AUTH_KEY);
console.info('Credentials have been reset.');
}
function isAuthValid() {
var credentials = getCredentials()
if (credentials == null) {
console.info('No credentials found.');
return false;
}
var response = validateCredentials(credentials.username, credentials.token);
return (response != null && response.code == 200);
}
function setCredentials(request) {
var credentials = request.userToken;
var response = validateCredentials(credentials.username, credentials.token);
if (response == null || response.code != 200) return { errorCode: 'INVALID_CREDENTIALS' };
var userProperties = PropertiesService.getUserProperties();
userProperties.setProperty(AUTH_USER, credentials.username);
userProperties.setProperty(AUTH_KEY, credentials.token);
console.info('Credentials have been stored');
return {
errorCode: 'NONE'
};
}
function throwConnectorError(text) {
DataStudioApp.createCommunityConnector()
.newUserError()
.setDebugText(text)
.setText(text)
.throwException();
}
function getConfig(request) {
// ToDo: handle request.languageCode for different languages being displayed
console.log(request)
var params = request.configParams;
var config = cc.getConfig();
// ToDo: add your config if necessary
config.setDateRangeRequired(true);
return config.build();
}
function getDimensions() {
var types = cc.FieldType;
return [
{
id:'id',
name:'ID',
type:types.NUMBER
},
{
id:'name',
name:'Name',
isDefault:true,
type:types.TEXT
},
{
id:'email',
name:'Email',
type:types.TEXT
}
];
}
function getMetrics() {
return [];
}
function getFields(request) {
Logger.log(request)
var fields = cc.getFields();
var dimensions = this.getDimensions();
var metrics = this.getMetrics();
dimensions.forEach(dimension => fields.newDimension().setId(dimension.id).setName(dimension.name).setType(dimension.type));
metrics.forEach(metric => fields.newMetric().setId(metric.id).setName(metric.name).setType(metric.type).setAggregation(metric.aggregations));
var defaultDimension = dimensions.find(field => field.hasOwnProperty('isDefault') && field.isDefault == true);
var defaultMetric = metrics.find(field => field.hasOwnProperty('isDefault') && field.isDefault == true);
if (defaultDimension)
fields.setDefaultDimension(defaultDimension.id);
if (defaultMetric)
fields.setDefaultMetric(defaultMetric.id);
return fields;
}
function getSchema(request) {
var fields = getFields(request).build();
return { schema: fields };
}
function convertValue(value, id) {
// ToDo: add special conversion if necessary
switch(id) {
default:
// value will be converted automatically
return value[id];
}
}
function entriesToDicts(schema, data, converter, tag) {
return data.map(function(element) {
var entry = element[tag];
var row = {};
schema.forEach(function(field) {
// field has same name in connector and original data source
var id = field.id;
var value = converter(entry, id);
// use UI field ID
row[field.id] = value;
});
return row;
});
}
function dictsToRows(requestedFields, rows) {
return rows.reduce((result, row) => ([...result, {'values': requestedFields.reduce((values, field) => ([...values, row[field]]), [])}]), []);
}
function getParams (request) {
var schema = this.getSchema();
var params;
if (request) {
params = {};
// ToDo: handle pagination={startRow=1.0, rowCount=100.0}
} else {
// preview only
params = {
limit: 20
}
}
return params;
}
function getData(request) {
Logger.log(request)
var credentials = getCredentials()
var schema = getSchema();
var params = getParams(request);
var requestedFields; // fields structured as I want them (see above)
var requestedSchema; // fields structured as Google expects them
if (request) {
// make sure the ordering of the requested fields is kept correct in the resulting data
requestedFields = request.fields.filter(field => !field.forFilterOnly).map(field => field.name);
requestedSchema = getFields(request).forIds(requestedFields);
} else {
// use all fields from schema
requestedFields = schema.map(field => field.id);
requestedSchema = api.getFields(request);
}
var filterPresent = request && request.dimensionsFilters;
//var filter = ...
if (filterPresent) {
// ToDo: apply request filters on API level (before the API call) to minimize data retrieval from API (number of rows) and increase speed
// see https://developers.google.com/datastudio/connector/filters
// filter = ... // initialize filter
// filter.preFilter(params); // low-level API filtering if possible
}
// get HTTP response; e.g. check for HTTT RETURN CODE on response.code if necessary
var response = httpGet(credentials.username, credentials.token, URL_DATA, params);
// get JSON data from HTTP response
var data = response.json;
// convert the full dataset including all fields (the full schema). non-requested fields will be filtered later on
var rows = entriesToDicts(schema, data, convertValue, JSON_TAG);
// match rows against filter (high-level filtering)
//if (filter)
// rows = rows.filter(row => filter.match(row) == true);
// remove non-requested fields
var result = dictsToRows(requestedFields, rows);
console.log('{0} rows received'.format(result.length));
//console.log(result);
return {
schema: requestedSchema.build(),
rows: result,
filtersApplied: filter ? true : false
};
}
A sample request that filters for all users with names starting with J.
{
configParams={},
dateRange={
endDate=2020-05-14,
startDate=2020-04-17
},
fields=[
{name=name}
],
scriptParams={
lastRefresh=1589543208040
},
dimensionsFilters=[
[
{
values=[^J.*],
operator=REGEXP_EXACT_MATCH,
type=INCLUDE,
fieldName=name
}
]
]
}
The JSON data returned by the HTTP GET contains all fields (full schema).
[ { user:
{ id: 1,
name: 'Jane Doe',
email: 'jane#doe.com' } },
{ user:
{ id: 2,
name: 'John Doe',
email: 'john#doe.com' } }
]
Once the data is filtered and converted/transformed, you'll get this result, which is perfectly displayed by Google Data Studio:
{
filtersApplied=true,
schema=[
{
isDefault=true,
semantics={
semanticType=TEXT,
conceptType=DIMENSION
},
label=Name,
name=name,
dataType=STRING
}
],
rows=[
{values=[Jane Doe]},
{values=[John Doe]}
]
}
getData should return data for only the requested fields. In request.fields should have the list of all requested fields. Limit your data for those fields only and then send the parsed data back.

Google Data Studio Connector with KEY Auth Type not working

I am creating a Google Data Studio Connector with KEY Auth Type. As per Google Documentation, I have programmed as below
function getAuthType() {
return {
type: 'KEY',
helpUrl: 'https://integra.jivrus.com/data-studio-connectors/insightly'
};
}
However, Data studio is not prompting the user to enter KEY anywhere. So it is resulting in an authentication error as the API requires KEY to be supplied.
How do I solve this? Is there any working sample code for KEY Auth Type?
My full code related to KEY Auth Type is below for reference.
var KEY_SIGNATURE = "dscc.key";
function getAuthType() {
return {
type: 'KEY',
helpUrl: 'https://integra.jivrus.com/data-studio-connectors/insightly'
};
}
function resetAuth() {
var userProperties = PropertiesService.getUserProperties();
userProperties.deleteProperty(KEY_SIGNATURE);
}
function isAuthValid() {
var userProperties = PropertiesService.getUserProperties();
var key = userProperties.getProperty(KEY_SIGNATURE);
return validateKey(key);
}
function setCredentials(request) {
var key = request.key;
var validKey = validateKey(key);
if (!validKey) {
return {
errorCode: 'INVALID_CREDENTIALS'
};
}
var userProperties = PropertiesService.getUserProperties();
userProperties.setProperty(KEY_SIGNATURE, key);
return {
errorCode: 'NONE'
};
}
function validateKey(key) {
return true;
}
Appreciate your help.
If isAuthValid() always returns true, then the prompt will never be shown. If you change validateKey(key) in your code to return false, you'll start seeing the prompt.

AngularJS $http and filters

I have a JSON file, which contains:
{
"/default.aspx": "headerBg",
"/about.aspx": "aboutBg",
"/contact.aspx": "contactBg",
"/registration.aspx": "regBg",
"/clients.aspx": "clientsBg",
"/onlinesessions.aspx": "bg-white-box",
"/ondemamdsessions.aspx": "bg-grey"
}
Now I am reading this json file using $http, but I want to add a filter in below fashion:
Using window.location.pathname, I am reading path of the current page, suppose the current page is /about.aspx
Then I want to add a filter in $http response by which I want to read only aboutBg.
The code I wrote can retrieve all the values, but unable to filter that. Please help.
User this function where you receive the response.
function getPageBgClass(currentPage, responseData) {
if (responseData.hasOwnProperty(currentPage))
return responseData[currentPage]
else
return "none"
}
Here is how it should be used in your promise then function
function(response) {
var bg = getPageBgClass(window.location.pathname, response.data);
//Your code here ...
}
there is no direct method to get key using value from json.
you should make sure that there are no 2 keys having same value for below code to work
function swapJsonKeyValues(input) {
var one, output = {};
for (one in input) {
if (input.hasOwnProperty(one)) {
output[input[one]] = one;
}
}
return output;
}
var originaJSON = {
"/default.aspx": "headerBg",
"/about.aspx": "aboutBg",
"/contact.aspx": "contactBg",
"/registration.aspx": "regBg",
"/clients.aspx": "clientsBg",
"/onlinesessions.aspx": "bg-white-box",
"/ondemamdsessions.aspx": "bg-grey"
}
var invertedJSON = swapJsonKeyValues(originaJSON);
var samplepathname = "aboutBg";
var page = invertedJSON[samplepathname];
[function swapJsonKeyValues from https://stackoverflow.com/a/1970193/1006780 ]