Related
user.js:(In keycloak_add_user, i need to get proper response id of the user added in the keycloak. But user is successfully added, but getting this response, so not able to assign role for the user. Please help if any changes required for my code?)
const payload = {
email: req.payload.email,
firstName: req.payload.first_name,
lastName: req.payload.last_name,
credentials: [
{
type: 'password',
temporary: false,
value: req.payload.password,
},
],
enabled: true,
emailVerified: true,
createdTimestamp: Date.now(),
requiredActions: ['UPDATE_PASSWORD'],
};
const token = req.headers.authorization;
const keycloak_add_user = await fetch(
`http://${process.env.KEYCLOAK_HOST}:${process.env.KEYCLOAK_PORT}/auth/admin/realms/${process.env.REALM_NAME}/users`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify(payload),
}
);
console.log('keycloak_add_user',keycloak_add_user)
if (keycloak_add_user.status !== 201 || !keycloak_add_user) {
return h
.response({ error: keycloak_add_user.statusText })
.code(keycloak_add_user.status);
}
const keycloak_user = await keycloak_add_user.json();
console.log('keycloak_user', keycloak_user)
response:
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: {
body: PassThrough {
_readableState: [ReadableState],
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_writableState: [WritableState],
allowHalfOpen: true,
[Symbol(kCapture)]: false,
[Symbol(kCallback)]: null
},
disturbed: false,
error: null
},
[Symbol(Response internals)]: {
url: 'http://13.213.9.103:8080/auth/admin/realms/Rubick/users',
status: 201,
statusText: 'Created',
headers: Headers { [Symbol(map)]: [Object: null prototype] },
counter: 0
}
}
You need to call a new user GET call after adding user.
The adding user POST call did not response a new user's id. it returned just 201 Created status.
This node java script is example to get new user id.
import fetch from "node-fetch";
const displayUserId = async () => {
const token ='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5VXBWcTVsQ3Ywc2FZRGNCMHNpOWNscjB2Nmk5aGRPeXAtaS1XQk1ydXJFIn0.eyJleHAiOjE2NTU0Mzc0ODAsImlhdCI6MTY1NTQwMTQ4MCwianRpIjoiOGRjNjM0YjMtNjg4Zi00NWIzLWE5ODItYWVhZDFkNGFiNTU2IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MTgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJzdWIiOiI2OTFiMWNkNC1lMzZjLTQ5ZDktOTQ0Zi0yZGZjNjI5MWNlOTciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoiYWU2OTBiNjAtMTY5OS00NjAwLThmNDItMTc1NzNiZDllMzUwIiwiYWNyIjoiMSIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6ImFlNjkwYjYwLTE2OTktNDYwMC04ZjQyLTE3NTczYmQ5ZTM1MCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkZpcnN0TmFtZSBMYXN0TmFtZSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxIiwiZ2l2ZW5fbmFtZSI6IkZpcnN0TmFtZSIsImZhbWlseV9uYW1lIjoiTGFzdE5hbWUiLCJlbWFpbCI6InVzZXIxQHRlc3QuY29tIn0.bCbAgp3uvbQNCWSXWBX00QJl_zSZmspn0o2Jfejrznlf8ocYxcyWEVG4Cg_aEfqNn0lrdfhllftsZTTJlxrE4F19xg9GSdoya-DsY_fhwvtQZ-gHkxbLwjMqa06eSHxUCxhtk0FNjToxnv_5LSic3z09K9BRi5QXG9peMq_BoLpOjwfbROJHEDvByPtwZpxib6iWeXUl1S8ZwMaW3lJ6bqisbd2GVMJqaVMm0zp9tXws_LOP5lTCWuRKXXWJC0V3Oubd-qN_wQoNw9_R9CNlcVkIrJ3MbZqnEbdczU8-eX5lLnydjVa1eKzo6Rfnh4387vyai80kFPUN2mb4x20xOw';
const got = await fetch('http://localhost:8080/auth/admin/realms/test/users/?username=newmyuser',
{
method: 'GET',
headers: {
'Content-Type' : 'application/json',
Authorization: 'Bearer ' + token
}
}
);
const response = await got.json();
console.log(response);
console.log(response.map(({ id }) => ({ id })))
}
displayUserId();
And it's display a response in terminal.
$ node get-api.js
[
{
id: '70fb1f9f-257d-4f11-8e3d-b0e01bafa47f',
createdTimestamp: 1655395295332,
username: 'newmyuser',
enabled: true,
totp: false,
emailVerified: false,
firstName: 'newFirstName',
lastName: 'newLastName',
email: 'newtestuser#test.com',
disableableCredentialTypes: [],
requiredActions: [],
notBefore: 0,
access: {
manageGroupMembership: true,
view: true,
mapRoles: true,
impersonate: false,
manage: true
}
}
]
[ { id: '70fb1f9f-257d-4f11-8e3d-b0e01bafa47f' } ]
There is no response body, so "invalid json response body" is correct = don't expect JSON response body. There is the Location header (with user id parameter), which is just telling you where you can find the resource.
So if you need user id, then parse Location header or you can pass id in the request payload and Keycloak will use it (or it will return an error if that id is already used).
I want to create a pin with Pinterest API, So I followed official docs.
So I created a post request with this code snippet:
const options = {
method: 'POST',
url: 'https://pinterest-media-upload.s3-accelerate.amazonaws.com/',
headers: {},
formData: params,
file: {
value: fs.createReadStream(file.location),
options: {
filename: file.name,
},
},
}
request(options, function (error, response) {
if (error) throw new Error(error)
console.log(response)
console.log(response.statusCode)
})
Btw, my options output like that:
{
method: 'POST',
url: 'https://pinterest-media-upload.s3-accelerate.amazonaws.com/',
headers: {},
formData: {
'x-amz-date': '20220214T010341Z',
'x-amz-signature': '784024caf758eae76b1bfb3f61e31aa6ffaed6882867450c6d777ef5f09defa0',
'x-amz-security-token': 'IQoJb3JpZ2luX2VjEDkaCXVzLWVhc3QtMSJGMEQCIHqkv3HIUURvLPoVv0MBWxUJg8MrB1GTCmpV689cxDewAiAm/8wphXobyHdvSt5XCzB/MVyaf903CTS0BOCMPIk7BiqDBAiC//////////8BEAAaDDk5ODEzMTAzMjk5MCIMmzJXhv+p3w3LzPTpKtcDpGZmPmf4j2GTnYm1Pz/fzOlXX91H7Hjpnd5jqYGLkT4agyHNb/QLpP/0CZUogv1k7cOnjoxGRO7pqtIvshN7CTFyrvdNLkiuGLdKK52atlRzV3wWv+Mz7gmnp09YN0+hufKQ94ueJ5dueigU6Cf7rMvKVxQA+61II4GHj4WFl2dpFv2VLGsHZecg7cDvBITp13QL53z1aWojZjVGdg+0on5LGHYq8qExYQ3oy4gAq6wDTtzIBGKElOTXZYJjgLsX7ilARtm2i/Jv3MFTpCZFsoaqzCqHwsMfcIUbyCR6PKadriDU1qzYvBfUln4KItFvRxOPJ7SkOdOBcfoIq9OY7FRL8atlNh8omY9z+JD2yk8nUF69VmXjhtNjvUMQSLPzjGanHgeuwGfJwbKcc30lNopuw8cBzWbGQDFl69wPNbO0qXKjR9bW9MoPZzhl1eFVbmQ97u5pb5JnSK3/YExJTe/gQ4OgI21/TAWveNv1+j/mzPmE1rQbZ36Hfn2NP0QKulkh8TsnYDQo9Baf0mQoBqdU+WI1eeIPUu4++9viWsgmVMX1hCp8QRBPwgn+DE+lhvAx1roewoxkQGj6JXOv3koaV/f+VDI2tcjTzZ7Vr5ltsh1dyk+4MPnJppAGOqYBoK8YoVFKCclHc408UkZvQtTS4Y6TWmAhOHFpCw2dSON1hjzUQVFDXbDQ2z0YUedUBaWa/Nzspiz9X3K1dqY23mCR46UD57ZyOJ1GjyfnfOthyjwPfTXuvIVWGKd6owE5vSMEhr7WIIlPsom03LRMOmWXTej7RNcLV3tC5z7QXlei6FoflOll0kNC4u290wL+OTtRT1nBjP3a9M6pZ8c69OZVFJ3fRA==',
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
key: 'uploads/ab/14/d3/2:video:1142647874115387016:5215251022217270581',
policy: 'eyJleHBpcmF0aW9uIjoiMjAyMi0wMi0xNFQwMjowMzo0MS4wMDBaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0IjoicGludGVyZXN0LW1lZGlhLXVwbG9hZCJ9LFsic3RhcnRzLXdpdGgiLCIka2V5IiwidXBsb2Fkcy9hYi8xNC9kMyJdLHsieC1hbXotY3JlZGVudGlhbCI6IkFTSUE2UVpKNjRPUERFQVUzRjRBLzIwMjIwMjE0L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QifSx7IngtYW16LWFsZ29yaXRobSI6IkFXUzQtSE1BQy1TSEEyNTYifSx7IngtYW16LWRhdGUiOiIyMDIyMDIxNFQwMTAzNDFaIn0sWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMCwyMTQ3NDgzNjQ4XSxbImVxIiwiJENvbnRlbnQtVHlwZSIsIm11bHRpcGFydC9mb3JtLWRhdGEiXSx7IngtYW16LXNlY3VyaXR5LXRva2VuIjoiSVFvSmIzSnBaMmx1WDJWakVEa2FDWFZ6TFdWaGMzUXRNU0pHTUVRQ0lIcWt2M0hJVVVSdkxQb1Z2ME1CV3hVSmc4TXJCMUdUQ21wVjY4OWN4RGV3QWlBbS84d3BoWG9ieUhkdlN0NVhDekIvTVZ5YWY5MDNDVFMwQk9DTVBJazdCaXFEQkFpQy8vLy8vLy8vLy84QkVBQWFERGs1T0RFek1UQXpNams1TUNJTW16SlhoditwM3czTHpQVHBLdGNEcEdabVBtZjRqMkdUblltMVB6L2Z6T2xYWDkxSDdIanBuZDVqcVlHTGtUNGFneUhOYi9RTHBQLzBDWlVvZ3YxazdjT25qb3hHUk83cHF0SXZzaE43Q1RGeXJ2ZE5Ma2l1R0xkS0s1MmF0bFJ6VjN3V3YrTXo3Z21ucDA5WU4wK2h1ZktROTR1ZUo1ZHVlaWdVNkNmN3JNdktWeFFBKzYxSUk0R0hqNFdGbDJkcEZ2MlZMR3NIWmVjZzdjRHZCSVRwMTNRTDUzejFhV29qWmpWR2RnKzBvbjVMR0hZcThxRXhZUTNveTRnQXE2d0RUdHpJQkdLRWxPVFhaWUpqZ0xzWDdpbEFSdG0yaS9KdjNNRlRwQ1pGc29hcXpDcUh3c01mY0lVYnlDUjZQS2FkcmlEVTFxell2QmZVbG40S0l0RnZSeE9QSjdTa09kT0JjZm9JcTlPWTdGUkw4YXRsTmg4b21ZOXorSkQyeWs4blVGNjlWbVhqaHROanZVTVFTTFB6akdhbkhnZXV3R2ZKd2JLY2MzMGxOb3B1dzhjQnpXYkdRREZsNjl3UE5iTzBxWEtqUjliVzlNb1BaemhsMWVGVmJtUTk3dTVwYjVKblNLMy9ZRXhKVGUvZ1E0T2dJMjEvVEFXdmVOdjErai9telBtRTFyUWJaMzZIZm4yTlAwUUt1bGtoOFRzbllEUW85QmFmMG1Rb0JxZFUrV0kxZWVJUFV1NCsrOXZpV3NnbVZNWDFoQ3A4UVJCUHdnbitERStsaHZBeDFyb2V3b3hrUUdqNkpYT3Yza29hVi9mK1ZESTJ0Y2pUelo3VnI1bHRzaDFkeWsrNE1QbkpwcEFHT3FZQm9LOFlvVkZLQ2NsSGM0MDhVa1p2UXRUUzRZNlRXbUFoT0hGcEN3MmRTT04xaGp6VVFWRkRYYkRRMnowWVVlZFVCYVdhL056c3BpejlYM0sxZHFZMjNtQ1I0NlVENTdaeU9KMUdqeWZuZk90aHlqd1BmVFh1dklWV0dLZDZvd0U1dlNNRWhyN1dJSWxQc29tMDNMUk1PbVdYVGVqN1JOY0xWM3RDNXo3UVhsZWk2Rm9mbE9sbDBrTkM0dTI5MHdMK09UdFJUMW5CalAzYTlNNnBaOGM2OU9aVkZKM2ZSQT09In1dfQ==',
'x-amz-credential': 'ASIA6QZJ64OPDEAU3F4A/20220214/us-east-1/s3/aws4_request',
'Content-Type': 'multipart/form-data'
},
file: {
value: ReadStream {
_readableState: [ReadableState],
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
path: '/Users/akasaa101/Desktop/vas-project/social-service/temp/VIIcz8eR6y.mp4',
fd: null,
flags: 'r',
mode: 438,
start: undefined,
end: Infinity,
autoClose: true,
pos: undefined,
bytesRead: 0,
closed: false,
[Symbol(kFs)]: [Object],
[Symbol(kCapture)]: false,
[Symbol(kIsPerformingIO)]: false
},
options: { filename: 'VIIcz8eR6y.mp4' }
}
}
But I get an error as :
'<Error><Code>InvalidArgument</Code><Message>POST requires exactly one file upload per request.</Message><ArgumentName>file</ArgumentName><ArgumentValue>0</ArgumentValue><RequestId>VZ5RH6P8MD14E6CP</RequestId><HostId>loa5iyjv2L3yRHhK0nsu4pIa1VqmvKn9nxgRs2euKlUaZ2mS5Emb0KUGolNeKDu3upY2VPxN8Zo=</HostId></Error>',
By the way, I tried to send the same request with axios but I get a different error.
But now I get an error as "POST requires exactly one file upload per request.".
I checked my options but I cannot find any error on my code snipped.
How can I solve this problem? Please Help!
I'm working on adapting some React code of mine to use #testing-library/react, react-query, and msw to mock network calls that I make using axios.
So far (after some brainbending) I've got it working with this code! (yay!)
The test in question (simplified):
import React from 'react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { screen, waitFor, waitForElementToBeRemoved } from '#testing-library/react';
import { queryCache } from 'react-query';
import userEvent from '#testing-library/user-event';
import Login from '../Login';
import { render, queryClient } from '~/testhelpers/helpers.integration';
// #endregion imports
const server = setupServer(
// ...default setup for successful request; not relevant here
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('rendering', () => {
describe('error', () => {
it.only('should render error message when request fails', async () => {
server.use(
rest.post(`${API_BASE_URL}api/auth/token`, (req, res, ctx) => {
return res(ctx.status(400), ctx.json('Invalid username or password.'));
})
);
render(<Login />);
const usernameInput = screen.getByLabelText('Username');
const passwordInput = screen.getByLabelText('Password');
const submitInput = screen.getByRole('button', { name: 'Login' });
userEvent.type(usernameInput, 'testUsername');
userEvent.type(passwordInput, 'testPassword');
userEvent.click(submitInput);
await waitFor(() => {
expect(screen.getByText('Invalid username or password.')).toBeInTheDocument();
});
});
});
});
Again, this test works! Hooray! But the issue is that I am making axios think (via msw) that it is receiving a 400 response for this query, and I get this big chunk of error message in my console when I run this test with Jest:
PASS src/display/views/login/__tests__/Login.spec.js
rendering
error
√ should hide loading indicator and render error message when request fails (522 ms)
console.error
Error: Request failed with status code 400
at createError (C:\Users\me\Documents\_Programming\GitHub\my-project\node_modules\axios\lib\core\createError.js:16:15)
at settle (C:\Users\me\Documents\_Programming\GitHub\my-project\node_modules\axios\lib\core\settle.js:18:12)
at XMLHttpRequestOverride.handleLoad [as onreadystatechange] (C:\Users\me\Documents\_Programming\GitHub\my-project\node_modules\axios\lib\adapters\xhr.js:59:7)
at XMLHttpRequestOverride.triggerReadyStateChange (C:\Users\me\Documents\_Programming\GitHub\my-project\node_modules\node-request-interceptor\src\interceptors\XMLHttpRequest\XMLHttpRequestOverride.ts:133:33)
at XMLHttpRequestOverride.trigger (C:\Users\me\Documents\_Programming\GitHub\my-project\node_modules\node-request-interceptor\src\interceptors\XMLHttpRequest\XMLHttpRequestOverride.ts:145:12)
at C:\Users\me\Documents\_Programming\GitHub\my-project\node_modules\node-request-interceptor\src\interceptors\XMLHttpRequest\XMLHttpRequestOverride.ts:306:18
at runNextTicks (internal/process/task_queues.js:62:5)
at processTimers (internal/timers.js:489:9) {
config: {
adapter: [Function: xhrAdapter],
transformRequest: { '0': [Function: transformRequest] },
transformResponse: { '0': [Function: transformResponse] },
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: [Function: validateStatus],
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json;charset=utf-8'
},
method: 'post',
url: 'http://baseurl/api/auth/token',
data: '{"username":"testUsername","password":"testPassword"}'
},
request: XMLHttpRequestOverride {
requestHeaders: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json;charset=utf-8'
},
responseHeaders: { 'x-powered-by': 'msw', 'content-type': 'application/json' },
_events: [],
UNSENT: 0,
OPENED: 1,
HEADERS_RECEIVED: 2,
LOADING: 3,
DONE: 4,
onreadystatechange: [Function: handleLoad],
onabort: null,
onerror: [Function: handleError],
onload: null,
onloadend: null,
onloadstart: null,
onprogress: null,
ontimeout: [Function: handleTimeout],
url: 'http://baseurl/api/auth/token',
method: 'POST',
readyState: 4,
withCredentials: false,
status: 400,
statusText: 'Bad Request',
data: '{"username":"testUsername","password":"testPassword"}',
response: '"Invalid username or password."',
responseType: 'text',
responseText: '"Invalid username or password."',
responseXML: null,
responseURL: '',
upload: null,
timeout: 0,
async: true,
user: undefined,
password: undefined
},
response: {
data: 'Invalid username or password.',
status: 400,
statusText: 'Bad Request',
headers: { 'x-powered-by': 'msw', 'content-type': 'application/json' },
config: {
adapter: [Function: xhrAdapter],
transformRequest: [Object],
transformResponse: [Object],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: [Function: validateStatus],
headers: [Object],
method: 'post',
url: 'http://baseurl/api/auth/token',
data: '{"username":"testUsername","password":"testPassword"}'
},
request: XMLHttpRequestOverride {
requestHeaders: [Object],
responseHeaders: [Object],
_events: [],
UNSENT: 0,
OPENED: 1,
HEADERS_RECEIVED: 2,
LOADING: 3,
DONE: 4,
onreadystatechange: [Function: handleLoad],
onabort: null,
onerror: [Function: handleError],
onload: null,
onloadend: null,
onloadstart: null,
onprogress: null,
ontimeout: [Function: handleTimeout],
url: 'http://baseurl/api/auth/token',
method: 'POST',
readyState: 4,
withCredentials: false,
status: 400,
statusText: 'Bad Request',
data: '{"username":"testUsername","password":"testPassword"}',
response: '"Invalid username or password."',
responseType: 'text',
responseText: '"Invalid username or password."',
responseXML: null,
responseURL: '',
upload: null,
timeout: 0,
async: true,
user: undefined,
password: undefined
}
}
}
at CustomConsole.console.error (node_modules/#testing-library/react/dist/act-compat.js:52:34)
at node_modules/react-query/lib/core/mutation.js:115:32
I think, to a certain extent, this is expected, but it's REALLY unnecessary spam in my tests given that I am expecting and in fact wanting that request to fail. Is there a best practice way to remove this unnecessary log via config for Jest, RTL, msw, or Axios?
react-query logs to the console per default, you can override this with setLogger:
import { setLogger } from 'react-query'
setLogger({
log: printLog,
warn: printWarn,
error: printError,
})
see also: https://react-query.tanstack.com/reference/setLogger
I found strange behaviour in Google Sheets API (is it issue?):
Firebase functions log doesn't show any error, but the values#batchGet function is ignoring ranges. It returns the entire dataRange of the sheet in first range.
batchGet "Try This API"
I edited my code based on code Github: Sheets API batchGet ranges parameter issue
sheets.spreadsheets.values.batchGet({
// your options
}, function (err, data, response) {
console.log(response.req.res.request.url);
});
Node.js code portion:
const sheets = google.sheets({ version: 'v4', access_token });
// set auth as a global default:
google.options({ auth: jwt });
const request = {
auth: jwt,
spreadsheetId: 'xxxxx', //<---------- "Project Checklist" and "Control" sheets
ranges: [
"'Project Checklist'!B:K",
"Control!A:F"
],
majorDimension: "ROWS",
dateTimeRenderOption: "FORMATTED_STRING",
valueRenderOption: "FORMATTED_VALUE"
}
This is wrapped inside a Promise...
sheets.spreadsheets.values.batchGet(request, (err, data, response) => {
console.log("inside: sheets.spreadsheets.values.batchGet() --------");
if (err) {
console.log('The Sheets API returned an error: ' + err);
//The API returned an error: Error: API key not valid. Please pass a valid API key.
reject(err);
};
try {
console.log("response:-------------------------------");
console.log(JSON.stringify(response));
console.log("data.valueRanges:-------------------------------");
console.log(data.valueRanges);
resolve(JSON.stringify(data.valueRanges));
} catch (err) {
console.log("Error processing Sheets API response: " + err);
reject(err);
}
})
Firebase log:
Function execution took 3822 ms, finished with status code: 200
undefined
data.valueRanges:-------------------------------
undefined
response:-------------------------------
getJwt() --------------- OK
index.js
'use strict'
const functions = require('firebase-functions');
const { google } = require('googleapis');
var serviceAccount = require("./credentials/admin-service-account-key.json");
function getJwt() {
// Define the required scopes.
var scopes = [
'https://www.googleapis.com/auth/spreadsheets'
];
return new google.auth.JWT(
serviceAccount.client_email,
null,
serviceAccount.private_key,
scopes
);
}
function getSpreadsheetData(jwt) {
return new Promise((resolve, reject) => {
jwt.authorize((error, access_token) => {
if (error) {
console.log('Error in jwt.authorize: ' + error);
reject(error);
} else {
// access_token ready to use to fetch data and return to client
const sheets = google.sheets({ version: 'v4', access_token });
// set auth as a global default:
google.options({ auth: jwt }); //<----------------------
const request = {
auth: jwt,
spreadsheetId: 'xxxxxxxxxxxxxxxxxxxx', //<---------- Project Checklist / Control
range: 'Control!A:F', //
}
sheets.spreadsheets.values.get(request, (err, response) => {
console.log("inside: sheets.spreadsheets.values.get() -------------------------------");
if (err) {
console.log('The Sheets API returned an error: ' + err);
//The API returned an error: Error: API key not valid. Please pass a valid API key.
reject(err);
};
try {
var numRows = response.data.values ? response.data.values.length : 0;
console.log('%d rows retrieved.', numRows);
console.log("response.data:-------------------------------");
console.log(response.data.values);
resolve(response.data.values);
} catch (err) {
console.log("Error processing Sheets API response: " + err);
reject(err);
}
})
}
})
})
}
function getSheetBatchData(jwt) {
return new Promise((resolve, reject) => {
jwt.authorize((error, access_token) => {
if (error) {
console.log('Error in jwt.authorize: ' + error);
reject(error);
} else {
// access_token ready to use to fetch data and return to client
const sheets = google.sheets({ version: 'v4', access_token });
// set auth as a global default:
google.options({ auth: jwt }); //<----------------------
const request = {
auth: jwt,
spreadsheetId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx', //<---------- Project Checklist / Control
ranges: [
"Project Checklist!B:K",
"Control!A:F"
],
majorDimension: "ROWS",
dateTimeRenderOption: "FORMATTED_STRING",
valueRenderOption: "FORMATTED_VALUE"
}
/*
sheets.spreadsheets.values.batchGet({
// your options
}, function (err, data, response) {
console.log(response.req.res.request.url);
});
*/
//https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchGet
sheets.spreadsheets.values.batchGet(request, (err, data, response) => {
if (err) {
console.log('The Sheets API returned an error: ' + err);
//The API returned an error: Error: API key not valid. Please pass a valid API key.
reject(err);
};
try {
/*
* Returns: Array: data.valueRanges =
* [
* "range": "....",
* "values": []
* ]
*/
console.log("response:-------------------------------");
console.log(JSON.stringify(response));
console.log("data.valueRanges:-------------------------------");
console.log(data.valueRanges);
resolve(JSON.stringify(data.valueRanges));
} catch (err) {
console.log("Error processing Sheets API response: " + err);
reject(err);
}
});
}
})
})
}
/* Working */
exports.getControlSheetData = functions.https.onCall((data, context) => {
console.log("getData()---------------------------");
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + 'while authenticated.');
} else {
console.log("context.auth ------------ OK");
const uid = context.auth.uid;
console.log(uid);
var jwt = getJwt();
console.log("getJwt() --------------- OK");
return getSpreadsheetData(jwt); //<------------ Requested Spreadsheet's Data
}
})
/* Error */
exports.getBatchData = functions.https.onCall((data, context) => {
console.log("getBatchData()---------------------------");
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + 'while authenticated.');
} else {
console.log("context.auth ------------ OK");
const uid = context.auth.uid;
console.log(uid);
var jwt = getJwt();
console.log("getJwt() --------------- OK");
return getSheetBatchData(jwt); //<------------ Requested Spreadsheet's Data
}
})
New test: index.js
'use strict'
const functions = require('firebase-functions');
const { google } = require('googleapis');
var serviceAccount = require("./credentials/admin-service-account-key.json");
function getJwt() {
// Define the required scopes.
var scopes = [
'https://www.googleapis.com/auth/spreadsheets'
];
return new google.auth.JWT(
serviceAccount.client_email,
null,
serviceAccount.private_key,
scopes
);
}
function getDataTest2(jwt) {
jwt.authorize().then(access_token => {
// access_token ready to use to fetch data and return to client
const sheets = google.sheets({ version: 'v4', access_token });
// set auth as a global default:
google.options({ auth: jwt }); //<----------------------
const request = {
auth: jwt,
spreadsheetId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
ranges: [
"Project Checklist!B:K",
"Control!A:F"
],
majorDimension: "ROWS",
dateTimeRenderOption: "FORMATTED_STRING",
valueRenderOption: "FORMATTED_VALUE"
}
function callback(data,resp) {
try {
console.log("inside callback-----------");
console.log("returned data:--------------")
console.log(data);
console.log("returned resp:--------------")
console.log(resp);
console.log("expected data.valueRanges------------------------")
console.log(data.valueRanges); //<--------- expected data.valueRanges
return JSON.stringify(data.valueRanges);
} catch(err) {
console.log('The Sheets API returned an error: ' + err);
//The API returned an error: Error: API key not valid. Please pass a valid API key.
return(err);
}
}
sheets.spreadsheets.values.batchGet(request, callback);
}).catch(error => {
console.log('Error in jwt.authorize: ' + error);
reject(error);
})
}
exports.getBatchData = functions.https.onCall((data, context) => {
console.log("getBatchData()---------------------------");
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + 'while authenticated.');
} else {
console.log("context.auth ------------ OK");
const uid = context.auth.uid;
console.log(uid);
var jwt = getJwt();
console.log("getJwt() --------------- OK");
//return getSheetBatchData(jwt); //<------------ Requested Spreadsheet's Data
return getDataTest2(jwt)
}
})
Firebase log:
getJwt()--------------- OK
Function execution took 965 ms, finished with status code: 200
inside callback-----------
returned data: --------------
null
returned resp: --------------
{
status: 200, statusText: 'OK', headers: { 'content-type': 'application/json; charset=UTF-8', vary: 'Origin, X-Origin, Referer', date: 'Tue, 18 Sep 2018 02:28:15 GMT', server: 'ESF', 'cache-control': 'private', 'x-xss-protection': '1; mode=block', 'x-frame-options': 'SAMEORIGIN', 'alt-svc': 'quic=":443"; ma=2592000; v="44,43,39,35"', connection: 'close', 'transfer-encoding': 'chunked' }, config: { adapter: [Function: httpAdapter], transformRequest: { '0': [Function: transformRequest] }, transformResponse: { '0': [Function: transformResponse] }, timeout: 0, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: 2147483648, validateStatus: [Function], headers: { Accept: 'application/json, text/plain, */*', 'Accept-Encoding': 'gzip', 'User-Agent': 'google-api-nodejs-client/0.2.1 (gzip)', Authorization: 'Bearer ya29.c.ElocBg_A_xxxxxxxxxxxxxxxxxxxx' }, method: 'get', access_token: { access_token: 'ya29.c.ElocBg_A_xxxxxxxxxxxxxxxxxx', token_type: 'Bearer', expiry_date: 1537241286000, id_token: undefined, refresh_token: 'jwt-placeholder' }, url: 'https://sheets.googleapis.com/v4/spreadsheets/xxxxxxxxxxxxxxxxxxxxxxxxxxx/values:batchGet', paramsSerializer: [Function], data: undefined, params: { ranges: [Object], majorDimension: 'ROWS', dateTimeRenderOption: 'FORMATTED_STRING', valueRenderOption: 'FORMATTED_VALUE' } }, request: ClientRequest {
domain: null, _events: { socket: [Function], abort: [Function], aborted: [Function], error: [Function], timeout: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 6, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, upgrading: false, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: false, sendDate: false, _removedHeader: { }, _contentLength: 0, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: TLSSocket { _tlsOptions: [Object], _secureEstablished: true, _securePending: false, _newSessionPending: false, _controlReleased: true, _SNICallback: null, servername: null, npnProtocol: false, alpnProtocol: false, authorized: true, authorizationError: null, encrypted: true, _events: [Object], _eventsCount: 8, connecting: false, _hadError: false, _handle: null, _parent: null, _host: 'sheets.googleapis.com', _readableState: [Object], readable: false, domain: null, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, destroyed: true, _bytesDispatched: 563, _sockname: null, _pendingData: null, _pendingEncoding: '', server: undefined, _server: null, ssl: null, _requestCert: true, _rejectUnauthorized: true, parser: null, _httpMessage: [Circular], read: [Function], _consuming: true, write: [Function: writeAfterFIN], _idleNext: null, _idlePrev: null, _idleTimeout: -1 }, connection: TLSSocket {
_tlsOptions: [Object], _secureEstablished: true, _securePending: false, _newSessionPending: false, _controlReleased: true, _SNICallback: null, servername: null, npnProtocol: false, alpnProtocol: false, authorized: true, authorizationError: null, encrypted: true, _events: [Object], _eventsCount: 8, connecting: false, _hadError: false, _handle: null, _parent: null, _host: 'sheets.googleapis.com', _readableState: [Object], readable: false, domain: null, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, destroyed: true, _bytesDispatched: 563, _sockname: null, _pendingData: null, _pendingEncoding: '', server: undefined, _server: null, ssl: null, _requestCert: true, _rejectUnauthorized: true, parser: null, _httpMessage: [Circular], read: [Function], _consuming: true, write: [Function: writeAfte
...
expected data.valueRanges------------------------
The Sheets API returned an error: TypeError: Cannot read property 'valueRanges' of null
I am struggling to successfully execute create calls on the mautic REST api. In my node project I am using a standard express installation and a wrapper for the node api https://github.com/sambarnes90/node-mautic
I have authenticated my machine with the api and can PULL data without problem. The problem is POSTing data to it.
The documentation for the targeted post request is here: https://developer.mautic.org/#create-dynamic-content
This is my route. It derives from the example in node-mautic which works fine, but putting something into mautic does not work for me:
router.get('/api', function(req, res, next) {
//check auth and create config object
mautic.auth.checkAuth(function(config) {
// if all worked
if (config.auth_object) {
var testbody = {
"name": "apitest2"
}
//create call
mautic.dynamiccontent.createDynamicContent(config, testbody, function(data) {
console.log(data);
return res.send(data);
});
//request call for id=3
/*mautic.dynamiccontent.getDynamicContent(config, 3, function(data) {
console.log(data);
return res.send(data);
});
});*/
}
});
});
I am getting this response from the API:
[ { code: 400,
message: 'name: A name is required.',
details: { name: [Array] } } ]
This is the GitHub project for this: https://github.com/raphaelurban/mautic-api-test
EDIT:
I also tried:
var testbody = new Array();
testbody['name'] = 'apitest2';
with the same result.
Here is the full POST request:
Request {
domain: null,
_events:
{ error: [Function: bound ],
complete: [Function: bound ],
pipe: [Function] },
_eventsCount: 3,
_maxListeners: undefined,
body: '{"name":"apitest2"}',
callback: [Function],
method: 'POST',
readable: true,
writable: true,
explicitMethod: true,
_qs:
Querystring {
request: [Circular],
lib: { formats: [Object], parse: [Function], stringify: [Function] },
useQuerystring: undefined,
parseOptions: {},
stringifyOptions: {} },
_auth:
Auth {
request: [Circular],
hasAuth: false,
sentAuth: false,
bearerToken: null,
user: null,
pass: null },
_oauth: OAuth { request: [Circular], params: null },
_multipart:
Multipart {
request: [Circular],
boundary: 'afa47f83-3327-4d67-982d-4db6daab8a39',
chunked: false,
body: null },
_redirect:
Redirect {
request: [Circular],
followRedirect: true,
followRedirects: true,
followAllRedirects: false,
followOriginalHttpMethod: false,
allowRedirect: [Function],
maxRedirects: 10,
redirects: [],
redirectsFollowed: 0,
removeRefererHeader: false },
_tunnel:
Tunnel {
request: [Circular],
proxyHeaderWhiteList:
[ 'accept',
'accept-charset',
'accept-encoding',
'accept-language',
'accept-ranges',
'cache-control',
'content-encoding',
'content-language',
'content-location',
'content-md5',
'content-range',
'content-type',
'connection',
'date',
'expect',
'max-forwards',
'pragma',
'referer',
'te',
'user-agent',
'via' ],
proxyHeaderExclusiveList: [] },
headers: { host: 'hrutest.mautic.net', 'content-length': 19 },
setHeader: [Function],
hasHeader: [Function],
getHeader: [Function],
removeHeader: [Function],
localAddress: undefined,
pool: {},
dests: [],
__isRequestRequest: true,
_callback: [Function],
uri:
Url {
protocol: 'https:',
slashes: true,
auth: null,
host: 'hrutest.mautic.net',
port: 443,
hostname: 'hrutest.mautic.net',
hash: null,
search: '?access_token=ZTJlOWY3MzI0YTg4OWViNzU3YjY5YjFiZjRlOTU1OWFiYWM1N2ZmOTQ4OWI4NGI2NzZjZGUyMDg3ZWMxZmU5OA',
query: 'access_token=ZTJlOWY3MzI0YTg4OWViNzU3YjY5YjFiZjRlOTU1OWFiYWM1N2ZmOTQ4OWI4NGI2NzZjZGUyMDg3ZWMxZmU5OA',
pathname: '/api/dynamiccontents/new',
path: '/api/dynamiccontents/new?access_token=ZTJlOWY3MzI0YTg4OWViNzU3YjY5YjFiZjRlOTU1OWFiYWM1N2ZmOTQ4OWI4NGI2NzZjZGUyMDg3ZWMxZmU5OA',
href: 'https://hrutest.mautic.net/api/dynamiccontents/new?access_token=ZTJlOWY3MzI0YTg4OWViNzU3YjY5YjFiZjRlOTU1OWFiYWM1N2ZmOTQ4OWI4NGI2NzZjZGUyMDg3ZWMxZmU5OA' },
proxy: null,
tunnel: true,
setHost: true,
originalCookieHeader: undefined,
_disableCookies: true,
_jar: undefined,
port: 443,
host: 'hrutest.mautic.net',
path: '/api/dynamiccontents/new?access_token=ZTJlOWY3MzI0YTg4OWViNzU3YjY5YjFiZjRlOTU1OWFiYWM1N2ZmOTQ4OWI4NGI2NzZjZGUyMDg3ZWMxZmU5OA',
httpModule:
{ Server: { [Function: Server] super_: [Object] },
createServer: [Function: createServer],
globalAgent:
Agent {
domain: null,
_events: [Object],
_eventsCount: 1,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object],
requests: {},
sockets: [Object],
freeSockets: {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
maxCachedSessions: 100,
_sessionCache: [Object] },
Agent: { [Function: Agent] super_: [Object] },
request: [Function: request],
get: [Function: get] },
agentClass:
{ [Function: Agent]
super_: { [Function: Agent] super_: [Object], defaultMaxSockets: Infinity } },
agent:
Agent {
domain: null,
_events: { free: [Function] },
_eventsCount: 1,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: { path: null },
requests: {},
sockets: { 'hrutest.mautic.net:443:::::::::': [Array] },
freeSockets: {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
maxCachedSessions: 100,
_sessionCache: { map: [Object], list: [Array] } } }
I haven't had much chance to test any more of this wrapper in recent months.
I would try sending it as an array though if possible?
Try these two combinations of testbody:
var testbody = [
"name": "apitest2"
]
Or
var testbody = {[
"name": "apitest2"
]}
And see what results you get?
Can you possibly get full logs of the request?
Finally I worked it out. At least it is some solution, I am not sure if this also works with the current function.
I found that all other tutorials use form instead of body when posting data. Additionally, their objects do not get JSON.stringified.
So with my object:
var testbody = {
name: 'apitest99'
};
It works when I change the function in node-mautic:
createDynamicContent: function(config, queryParameters, callback) {
var url = config.api_endpoint + "/dynamiccontents/new?access_token=" + config.auth_object.access_token;
//queryParameters = JSON.stringify(queryParameters);**
request.post({
url: url,
form: queryParameters
}, function(err, res) {
if (err) {
callback(err);
} else {
var asset = JSON.parse(res.body);
callback(asset);
}
})
},
And I am getting the following response from the API:
{ dynamicContent:
{ isPublished: true,
dateAdded: '2018-07-13T09:37:07+00:00',
dateModified: null,
createdBy: 1,
createdByUser: 'Raphael',
modifiedBy: null,
modifiedByUser: null,
id: 10,
name: 'apitest99',
category: null,
publishUp: null,
publishDown: null,
sentCount: 0,
variantParent: null,
variantChildren: [],
content: null,
filters: [],
isCampaignBased: true,
slotName: '' } }