I have this function
exports.webhook = functions.https.onRequest((_req: any, res: { send: (arg0: string) => void; }) => {
// It sends notification to a user
});
I want this function to be executed at 3 PM daily. How do I schedule the function to run daily at this time ?
Edit 1
I have a function which sends notification, name of function is sendNotifications, how do I call this function from a URL and pass payload variables title and body
Sample working code:
exports.sendNotifications = functions.firestore
const payload = {
notification: {
title: no_of_followers2,
body: desc + ' Notification body',
icon: 'https://img.icons8.com/material/4ac144/256/user-male.png',
click_action: `https://google.com`,
}
};
... // some code
const subscriber = doc.data().token;
return admin.messaging().sendToDevice(subscriber, payload);
Edit 2
My function:
exports.sendNoti_cal_log = functions.https.onRequest((_req: any, res: { send: (arg0: string) => void; }) => {
const payload = {
notification: {
title: 'Notification Title',
body: 'Notification body',
icon: 'https://img.icons8.com/material/4ac144/256/user-male.png',
click_action: `https://google.com`,
}
};
const subscriber = "evGBnI_klVQYSBIPMqJbx8:APA91bEV5xOEbPwF4vBJ7mHrOskCTpTRJx0cQrZ_uxa-QH8HLomXdSYixwRIvcA2AuBRh4B_2DDaY8hvj-TsFJG_Hb6LJt9sgbPrWkI-eo0Xtx2ZKttbIuja4NqajofmjgnubraIOb4_";
return admin.messaging().sendToDevice(subscriber, payload)
});
Now when I am calling this function from URL, it works but I don't get any response, see screenshot:
Last thing I need is, how do i pass title from parameter and receive it in function.
Edit 3
My working Https function
//-------notification for calllogger
exports.sendNoti_cal_log = functions.https.onRequest((req: any, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: { status: string; }): void; new(): any; }; }; }) => {
const payload = {
notification: {
title: 'Notification Title',
body: 'Notification body',
icon: 'https://img.icons8.com/material/4ac144/256/user-male.png',
click_action: `https://google.com`,
}
};
const subscriber = "evGBnI_klVQYSBIPMqJbx8:APA91bEV5xOEbPwF4vBJ7mHrOskCTpTRJx0cQrZ_uxa-QH8HLomXdSYixwRIvcA2AuBRh4B_2DDaY8hvj-TsFJG_Hb6LJt9sgbPrWkI-eo0Xtx2ZKttbIuja4NqajofmjgnubraIOb4_";
return admin.messaging().sendToDevice(subscriber, payload).then((messagingResponse: any) => {
res.status(200).send({status: "OK"})
})
});
Edit 4 (Passing Parameters)
I have passed parameter like this in my function:
exports.sendNoti_cal_log = functions.https.onRequest((req: any, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: { status: string; }): void; new(): any; }; }; }) => {
const param1 = req.params.param1;
const payload = {
notification: {
title: 'Notification Title'+param1,
But when I am passing it from URL, it says undefined in the notification:
I am passing it like this -
https://us-central1-fir-crud-5b378.cloudfunctions.net/sendNoti_cal_log?param1=Hello
Update following your second update:
As you will read in the doc:
Always end an HTTP function with send(), redirect(), or end().
Otherwise, your function might continue to run and be forcibly
terminated by the system. See also Sync, Async and Promises.
So you need to do something like:
return admin.messaging().sendToDevice(subscriber, payload)
then(messagingResponse => {
res.status(200).send({status: "OK"});
})
For your question: "how do i pass title from parameter and receive it in function."
It is explained below in my first update ("In order to pass variables to an HTTP Cloud Function....").
Update following your comment and update:
The URL of your Cloud Function will be as follows:
https://us-central1-<project-id>.cloudfunctions.net/webhook
as detailed here in the doc.
In order to pass variables to an HTTP Cloud Function and get them in the CF, you should use the Request object which "gives you access to the properties of the HTTP request sent by the client" (see here in the doc).
For example, you can use the params property as follows to get query string parameters:
exports.webhook = functions.https.onRequest((_req: any, res: { send: (arg0: string) => void; }) => {
const param1 = _req.params.param1;
});
Initial answer:
Your Cloud Function is an HTTPS one which needs to be triggered through an HTTP request made from the "external world" to the Cloud Function platform.
So you need to schedule it from the caller/consumer end (for example a CRON service like https://cron-job.org/en/ which would issue an HTTP request to the corresponding Cloud Function URL, or one of your server, etc.).
If you want the Cloud Function platform to execute a specific Cloud Function at 3 PM daily, you should use a Scheduled Function as follows:
exports.webhook = functions.pubsub.schedule('0 15 * * *')
.timeZone('America/New_York') // Users can choose timezone - default is America/Los_Angeles
.onRun((context) => {
// It sends notification to a user
// return ...;
});
Note that a Scheduled Function is different from an HTTPS one (different trigger mechanism) but the business logic it implements can be the same (in your case, the code to send a notification).
Related
Using: NextJS, Firebase (Auth, DB, etc...), Hosted on Vercel, OVH Domains, (Using next to for the backend (node))
Basically, went I send an email with Sendgrid library or with the API v3 directly, my mail got strange behavior.
When I send a mail to any address, I don't recieve de mail at all, I need to make multiple time THE SAME request to get the answer of the first, isn't that weird at all?
Like I send "1" to "joe#example.com", joe recieve nothing, I make the same request with "2", joe recieve nothing, then I make another "3", then finnally joe has recieved the first mail.
Then when I send other mail, I send the "4" I will recieve the "2", etc....
I feel like there is a Queue within my mail.
At first, if I'm not wrong the mail was always gapped by 1 when I used the library below. Then I moved to the API directly thinking it was a library issue.
Using the library
import { getAuth } from 'firebase-admin/auth'
import { supportConfig, websiteConfig, sendgridConfig } from 'src/config'
import sgMail from '#sendgrid/mail'
import path from 'path'
import fs from 'fs'
/**
* #name verifyEmail
* #description Send email verify link to user email
* #param {string} email
* #param {any} actionCodeSettings
* #returns Promise<Void>
*/
export default async function verifyEmail(
email: string,
actionCodeSettings: any
): Promise<void> {
const pathTemplate = path.join(
process.cwd(),
'src/api/lib/user/actionCode/emailTemplate/verifyEmail.html'
)
return await getAuth()
.generateEmailVerificationLink(email as string)
.then(async (link) => {
sgMail.setApiKey(sendgridConfig.apiKey as string)
fs.readFile(pathTemplate, 'utf8', async function (err, data) {
if (err) {
console.log(err)
throw err
}
const msg = {
to: [email, supportConfig.email as string],
from: supportConfig.email as string,
subject: "Email verification",
text: 'Please click on the link',
html: data.replace('{{link}}', link),
}
await sgMail
.send(msg)
.then(() => {
console.log('Email sent!')
})
.catch((error) => {
console.log(error)
throw error
})
})
})
.catch((error) => {
console.log(error)
throw error
})
}
Using the api directly
const message = {
personalizations: [
{
to: [
{
email: email,
},
],
},
],
from: {
email: 'support#nerap.fr',
name: 'Support',
},
replyTo: {
email: 'support#nerap.fr',
name: 'Support',
},
subject: 'Email verification',
content: [
{
type: 'text/html',
value: data.replace('{{link}}', link),
},
],
}
await axios({
method: 'post',
url: 'https://api.sendgrid.com/v3/mail/send',
headers: {
Authorization: `Bearer ${sendgridConfig.apiKey}`,
'Content-Type': 'application/json',
},
data: message,
})
Nothing fancy, it's like the template that Sendgrid give us.
Honestly I'm lost I don't have any lead to fix it.
Here come the crazy part, IT'S PERFECTLY WORKING ON LOCAL.
So I think my conclusion is this.
Vercel hosting might be limited or restricted by Sengrid this is the only rationnal way.
Like, I don't know, I took the priced version thinking it was a trick to force people to use the pay version.
I'm open to any suggestion thanks !
Thanks for sharing the code, it highlighted an asynchronous piece of code that was allowing your function to complete before it ran. On platforms like Vercel, when a function completes, the event loop is effectively suspended, but continues when the function runs again. In this case your code was not completing before the function ended, which is why later emails would trigger the email to be sent.
The asynchronous code that escaped was the use of fs.readFile within a promise. Calling on fs.readFile started the work to read the email template from the filesystem asynchronously, and would call the callback when done, which would then send the email. However, the execution of the function would continue and complete before that finished. Since you are using promises for everything else in this code, I'd recommend using fs/promises (with the line import { promises as fs } from 'fs') so that you can treat fs.readFile like the rest of your code. I rewrote your function below using the promise version, and hopefully this will work.
import { getAuth } from 'firebase-admin/auth'
import { supportConfig, websiteConfig, sendgridConfig } from 'src/config'
import sgMail from '#sendgrid/mail'
import path from 'path'
import { promises as fs } from 'fs'
/**
* #name verifyEmail
* #description Send email verify link to user email
* #param {string} email
* #param {any} actionCodeSettings
* #returns Promise<Void>
*/
export default async function verifyEmail(
email: string,
actionCodeSettings: any
): Promise<void> {
const pathTemplate = path.join(
process.cwd(),
'src/api/lib/user/actionCode/emailTemplate/verifyEmail.html'
)
return await getAuth()
.generateEmailVerificationLink(email as string)
.then(async (link) => {
sgMail.setApiKey(sendgridConfig.apiKey as string)
try {
const data = await fs.readFile(pathTemplate, 'utf8');
const msg = {
to: [email, supportConfig.email as string],
from: supportConfig.email as string,
subject: "Email verification",
text: 'Please click on the link',
html: data.replace('{{link}}', link),
}
await sgMail
.send(msg)
.then(() => {
console.log('Email sent!')
})
.catch((error) => {
console.log(error)
throw error
})
} catch(error) {
console.log(error)
throw error
}
})
.catch((error) => {
console.log(error)
throw error
})
}
You do have a confusing mix of then/catch and async/await in your code. I'd recommend using just one style in order to simplify things. With just async/await your code could look like this:
import { getAuth } from 'firebase-admin/auth'
import { supportConfig, websiteConfig, sendgridConfig } from 'src/config'
import sgMail from '#sendgrid/mail'
import path from 'path'
import { promises as fs } from 'fs'
/**
* #name verifyEmail
* #description Send email verify link to user email
* #param {string} email
* #param {any} actionCodeSettings
* #returns Promise<Void>
*/
export default async function verifyEmail(
email: string,
actionCodeSettings: any
): Promise<void> {
const pathTemplate = path.join(
process.cwd(),
'src/api/lib/user/actionCode/emailTemplate/verifyEmail.html'
)
try {
const auth = getAuth()
const link = await auth.generateEmailVerificationLink(email as string)
sgMail.setApiKey(sendgridConfig.apiKey as string)
const data = await fs.readFile(pathTemplate, 'utf8')
const msg = {
to: [email, supportConfig.email as string],
from: supportConfig.email as string,
subject: "Email verification",
text: 'Please click on the link',
html: data.replace('{{link}}', link),
}
await sgMail.send(msg)
console.log('Email sent!')
} catch(error) {
console.log(error)
throw error
}
}
Such as described here, I'm using local emulator (on-line) to make tests im my cloud functions.
Index.js:
var status = 200;
exports.saveAndSendMail = functions.https.onCall( async (req, res) => {
try{
let jsons = req.body;
await saveInfirestore(jsons);
await sendMail("Data saved", jsons);
} finally {
closeConnection(res, status);
}
async function saveInfirestore(json) {
//execute business logic and save in firestore (irrelevant for this question)
}
function closeConnection (res, status){
res.sendStatus(status);
res.end();
}
async function sendMail(title, message) {
try {
AWS.config.loadFromPath('./config_mail.json');
// Create sendEmail params
var params = {
Destination: {
ToAddresses: [
'mymail#gmail.com'
]
},
Message: { /* required */
Body: { /* required */
Html: {
Charset: "UTF-8",
Data: JSON.stringfy(message);
}
},
Subject: {
Charset: 'UTF-8',
Data: title
}
},
Source: '"Origin" <origin#gmail.com>',
ReplyToAddresses: [
'origin#gmail.com'
]
};
// Create the promise and SES service object
var sendPromise = new AWS.SES({apiVersion: '2022-17-01'}).sendEmail(params).promise();
}
catch(e){
throw e;
}
// Handle promise's fulfilled/rejected states
sendPromise.then(
function(data) {
console.log(data.MessageId);
}).catch(
function(err) {
console.error(err, err.stack);
});
}
index.test.js
const { expect } = require("chai");
const admin = require("firebase-admin");
const test = require("firebase-functions-test")({
projectId: process.env.GCLOUD_PROJECT,
});
const myFunctions = require("../index");
describe("Unit tests", () => {
after(() => {
test.cleanup();
});
it("test if save is correct", async () => {
const wrapped = test.wrap(myFunctions.saveAndSendMail);
const req = {
body: [{
value: 5,
name: 'mario'
}]
};
const result = await wrapped(req);
let snap = await db.collection("collection_data").get();
expect(snap.size).to.eq(1);
snap.forEach(doc => {
let data = doc.data();
expect(data.value).to.eql(5);
expect(data.name).to.eql('mario');
});
});
I execute it with: firebase emulators:exec "npm run test"
I have 2 problems.
1 - When execute, I receive the error TypeError: res.sendStatus is not a function. If I comment closeConnection call in block finally (index.js), this code run perfectly and all tests and "expect" run with success. But, this correct way is mock this method or mock 'res' calls. I tried mock with something like this:
const res = {
sendStatus: (status) => {
},
end: () => {
}
}
const result = await wrapped(req, res);
But, I receive this error:
Error: Options object {} has invalid key "sendStatus"
at /home/linuxuser/my-project/firebase/functions/myfolder/node_modules/firebase-functions-test/lib/main.js:99:19
at Array.forEach (<anonymous>)
at _checkOptionValidity (node_modules/firebase-functions-test/lib/main.js:97:26)
at wrapped (node_modules/firebase-functions-test/lib/main.js:57:13)
at Context.<anonymous> (test/index.test.js:50:26)
at processImmediate (node:internal/timers:464:21)
Problem 2:
I'm not wish receive an e-mail each time that tests executes. How I mock sendMail function?
Something very important to point out is that you are currently trying to use a Firebase callable function, as shown by the function heading functions.https.onCall(() => {});. Since you want to work with requests and response codes, the correct type of function to use is an HTTP function. You would only need to change the heading in your index.js:
exports.saveAndSendMail = functions.https.onRequest(async (req, res) => {
// function body
});
Now, your first problem can then be solved by correctly mocking the res object that is passed to the function (inside index.test.js). When testing HTTP functions, you must not use test.wrap() when calling the function, nor expect the result as you were doing with const result = await wrapped(req); This is since Wrap being only supported for testing onCall functions. You can see another snippet of how to call an HTTP function for testing in the documentation.
it("test if save is correct", async () => {
const req = {
body: [{
value: 5,
name: 'mario'
}]
};
// mocking the response object that is returned from the function:
const res = {
sendStatus: (code) => {
expect(code).to.eql(200); // asserting that we get 200 back as the response code
},
end: () => {
}
};
const result = await myFunctions.saveAndSendMail(req, res); // mocking a call to an HTTP function, without test.wrap()
// rest of the function…
For your second problem, I haven’t used AWS SES before, but it seems this library offers ways to mock the functions so that you won’t have to actually send emails during your tests.
I've written a cloud task and it works perfectly and triggers the link I gave without any problems, but it won't stop retrying running the link.
How can I make it run it only once?
What I'm trying to do is run a Firestore Function once in the future, on a document write in a collection. I found this tutorial for it.
So far my task creation code works perfectly, and delivers correct payload to the function it's going to call. And the called function works correctly too the first time it runs and exits with status 200. But on the retries I have to exit with error 500 since there's no data to access anymore.
I can see the 200 and 500 logs in firestore function's logs, but Cloud Tasks' logs is empty, even if a method has been run 50 times!
This is the full code
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
const { CloudTasksClient } = require('#google-cloud/tasks')
exports.moveActivityFromPlanToRecord = () =>
functions
.region('europe-west1')
.firestore.document('Users/{userId}/Activities/{activityId}')
.onCreate(async snapshot => {
const moveTime = snapshot.data()! as MoveTime
if (!moveTime || !moveTime.dueTime) {
console.log("DueTime is empty or null: \n" + moveTime)
return
}
// Get the project ID from the FIREBASE_CONFIG env var
const project = JSON.parse(process.env.FIREBASE_CONFIG!).projectId
const location = 'europe-west1'
const queue = 'activityDateEventChecker'
//queuePath is going to be a string that uniquely identifes the task
const tasksClient = new CloudTasksClient()
const queuePath: string =
tasksClient.queuePath(project, location, queue)
// URL to my callback function and the contents of the payload to deliver
const url = `https://${location}-${project}.cloudfunctions.net/activityDateEventCheckerCallback`
const docPath = snapshot.ref.path
const dueTime = moveTime.dueTime
const payload: MoveTaskPayload = { docPath, dueTime }
console.log(payload)
// build up the configuration for the Cloud Task
const task = {
httpRequest: {
httpMethod: 'POST',
url: url,
body: Buffer.from(JSON.stringify(payload)).toString('base64'),
headers: {
'Content-Type': 'application/json',
},
},
scheduleTime: {
seconds: moveTime.dueTime / 1000
}
}
// enqueue the task in the queue
return tasksClient.createTask({ parent: queuePath, task: task })
})
interface MoveTime extends admin.firestore.DocumentData {
dueTime?: number
}
interface MoveTaskPayload {
docPath: string,
dueTime: number
}
exports.activityDateEventCheckerCallback = () =>
functions
.region('europe-west1')
.https.onRequest(async (req, res) => {
const payload = req.body as MoveTaskPayload
try {
// getting the item
const activity = await admin.firestore().doc(payload.docPath).get()
// if time is up for it
if (Date.now() >= payload.dueTime && activity.data() != undefined) {
// getting path to activity to be in record
const pathUser = activity.ref.parent.parent?.path
const pathDocRecord = admin.firestore().doc(`${pathUser}/Record/${activity.id}`)
console.log("RECORD-- ", (await (await pathDocRecord.get()).data())?.subject)
// moving activity into record
await pathDocRecord.set(activity.data()!)
await activity.ref.delete()
// sending notif to user
const fcmPayload = {
notification: {
title: `${activity.data()?.subject}`,
body: " Time for activity. Record how it goes!"
},
data: {
activityId: activity.id
}
}
const user = await admin.firestore().doc(pathUser!).get()
const fcmToken: string = user.data()?.fcmToken
return admin.messaging().sendToDevice(fcmToken, fcmPayload)
}
return null
} catch (error) {
console.error(error)
res.status(500).send(error)
return null
}
})
Tasks in Cloud Task retries when it does not get response code 2XX.
You can config the retry in Cloud Task Queue using maxAttempt paramtere.
Details are mentioned in the doc
I'm refactoring my react/redux app to use redux-observable instead of redux-thunk. Using thunk, I have an api middleware set up to listen for any actions with a CALL_API key and do some manipulation of the data, prepare headers, prepare full url, perform an api call using axios, and also do some additional action dispatches related to an api call.
Importantly, the api middleware dispatches a REQUEST_START action which gives the request an id and sets its status to pending in the network part of my state. When the promise from axios resolves or rejects, the middleware dispatches a REQUEST_END action, updating the state so that the current request is set to resolved or rejected. Then the response is returned to the calling action creator that initially dispatched the CALL_API action.
I have not been able to figure out how to do this with redux-observable. The part about the api middleware described above that I want to replicate is the REQUEST_START and REQUEST_END action dispatches. It's very convenient to have a centralized place where all api call related stuff is handled. I know I can effectively dispatch the REQUEST_START and REQUEST_END actions in each of my epics that does an api call, but I don't want to have to repeat the same code in many places.
I managed to partially solve this by creating an apiCallEpic which listens for actions with type CALL_API and does the above setup for api calls. However, an issue (or rather, something I don't like) is that the epic that initiates the api call (e.g. getCurrentUserEpic) essentially gives up control to apiCallEpic.
So, for example, when the api call succeeds and has a response, I may want to format that response data in some way before dispatching an action to be handled by my reducer. That is, getCurrentUserEpic should do some formatting of data returned from api call before sending to reducer. I was able to achieve something close to this by passing a payloadHandler callback function defined in getCurrentUserEpic that the apiCallEpic can call if/when it gets a successful response. However, I don't like this callback architecture and it seems like there's got to be a better way.
Here is some code that demonstrates my use of api middleware using thunk.
import axios from 'axios';
// actionCreators.js
// action types
const CALL_API = "CALL_API";
const FETCH_CURRENT_USER = "FETCH_CURRENT_USER";
const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER";
// action creators for request start and end
export const reqStart = (params = {}) => (dispatch) => {
const reduxAction = {
type: REQ_START,
status: 'pending',
statusCode: null,
requestId: params.requestId,
}
dispatch(reduxAction);
}
export const reqEnd = (params = {}) => (dispatch) => {
const {
requestId,
response = null,
error = null,
} = params;
let reduxAction = {}
if (response) {
reduxAction = {
type: REQ_END,
status: 'success',
statusCode: response.status,
requestId,
}
}
else if (error) {
if (error.response) {
reduxAction = {
type: REQ_END,
status: 'failed',
statusCode: error.response.status,
requestId,
}
}
else {
reduxAction = {
type: REQ_END,
status: 'failed',
statusCode: 500,
requestId,
}
}
}
dispatch(reduxAction);
}
// some api call to fetch data
export const fetchCurrentUser = (params = {}) => (dispatch) => {
const config = {
url: '/current_user',
method: 'get',
}
const apiCall = {
[CALL_API]: {
config,
requestId: FETCH_CURRENT_USER,
}
}
return dispatch(apiCall)
.then(response => {
dispatch({
type: RECEIVE_CURRENT_USER,
payload: {response},
})
return Promise.resolve({response});
})
.catch(error => {
return Promise.reject({error});
})
}
// apiMiddleware.js
// api endpoint
const API_ENTRY = "https://my-api.com";
// utility functions for request preparation
export const makeFullUrl = (params) => {
// ...prepend endpoint url with API_ENTRY constant
return fullUrl
}
export const makeHeaders = (params) => {
// ...add auth token to headers, etc.
return headers;
}
export default store => next => action => {
const call = action[CALL_API];
if (call === undefined) {
return next(action);
}
const requestId = call.requestId;
store.dispatch(reqStart({requestId}));
const config = {
...call.config,
url: makeFullUrl(call.config),
headers: makeHeaders(call.config);
}
return axios(config)
.then(response => {
store.dispatch(reqEnd({
response,
requestId,
}))
return Promise.resolve(response);
})
.catch(error => {
store.dispatch(reqEnd({
error,
requestId,
}))
return Promise.reject(error);
})
}
// reducers.js
// Not included, but you can imagine reducers handle the
// above defined action types and update the state
// accordingly. Most usefully, components can always
// subscribe to specific api calls and check the request
// status. Showing loading indicators is one
// use case.
Here's the code I've implemented to accomplish a similar thing with redux-observable.
export const fetchCurrentUserEpic = (action$, state$) => {
const requestType = FETCH_CURRENT_USER;
const successType = RECEIVE_CURRENT_USER;
const requestConfig = {
url: "/current_user",
method: "get",
}
const payload = {requestConfig, requestType, successType};
const payloadNormalizer = ({response}) => {
return {currentUser: response.data.data};
}
return action$.ofType(FETCH_CURRENT_USER).pipe(
switchMap((action) => of({
type: CALL_API,
payload: {...payload, requestId: action.requestId, shouldFail: action.shouldFail, payloadNormalizer},
})),
)
}
export const apiEpic = (action$, state$) => {
return action$.ofType(CALL_API).pipe(
mergeMap((action) => (
concat(
of({type: REQ_START, payload: {requestId: action.payload.requestId, requestType: action.payload.requestType}}),
from(callApi(action.payload.requestConfig, action.payload.shouldFail)).pipe(
map(response => {
return {
type: action.payload.successType,
payload: action.payload.payloadNormalizer({response})
}
}),
map(() => {
return {
type: REQ_END,
payload: {status: 'success', requestId: action.payload.requestId, requestType: action.payload.requestType},
}
})
)
)
).pipe(
catchError(error => {
console.log('error', error);
return of({type: REQ_END, payload: {status: 'failed', requestId: action.payload.requestId, requestType: action.payload.requestType}, error});
})
)
)
)
}
Any comments or suggestions are appreciated!
I've found redux-fetch-epic-builder (A lib for building "fetch actions" and generic epics handled by redux-observable) to be similar to what you are trying to achieve here (beware it uses rxjs 5, this guide to rescue). It uses fetch, not axios, but it's easy to replace that. Plus it has transformers for successful/failed actions.
The library is a bit old, but the base idea to overcome boilerplate code is still valid: Generic epic-builder to fetch data with calls to API(s).
I am a novice in React / Redux / RxJS, but the only problem I see with the redux-fetch-epic-builder is the way to configure the client (in axios terms). That is, I am not fully satisfied with (due to it being not FSA or RSAA):
//action creators
const getComments = (id, page = 1) => ({
type: GET_COMMENTS,
host: 'http://myblog.com',
path: `/posts/${id}/comments`,
query: {
page,
},
})
// ...
const epics = [
buildEpic(GET_COMMENTS),
]
but this may still be an elegant way. And the license allow to develop the library further. I have not converted the example from the library documentation to your user-related example, but with react-observable there is certainly no need to introduce a separate "api middleware". (Also, I like /SUBACTION better than _SUBACTION, but it's trivial to change.)
My nodejs app calls nexmo API to send SMS message. Here is the API:
nexmo.message.sendSms(sender, recipient, message, options, callback);
In the app, it is
const nexmo = new Nexmo({
apiKey: "nexmoApiKey",
apiSecret: "nexmoSecret"
}, { debug: true });
nexmo.message.sendSms(nexmo_sender_number, cell_country + to, message, {type: 'unicode'}, async (err, result) => {....});
Is there a way I can convert it to async/await structure like below:
const {err, result} = nexmo.message.sendSms(nexmo_sender_number, cell_country + to, vcode, {type: 'unicode'});
if (err) {.....};
//then process result...
I would like to return the message to parent function after the message was successfully sent out.
The nexmo-node library only supports callbacks for now. You'll need to use something like promisify or bluebird to convert the sendSms function to promises, and the use async/await with it. Here is an example using Node's promisify
const util = require('util');
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
apiKey: "nexmoApiKey",
apiSecret: "nexmoSecret"
}, { debug: true });
const sendSms = util.promisify(nexmo.message.sendSms);
async function sendingSms() {
const {err, result} = await sendSms(nexmo_sender_number, cell_country + to, message, {type: 'unicode'});
if (err) {...} else {
// do something with result
}
}
Though Alex's solution is elegant. It breaks TypeScript and util does some 'hidden logic' on the promises; when it errors stack traces are not clear.
This also allows you to stay true to the API and get auto-fill on the properties.
So instead you can do this (TypeScript)
/**
* Return the verification id needed.
*/
export const sendSMSCode = async (phoneNumber: string): Promise<string> => {
const result = await new Promise(async (resolve: (value: RequestResponse) => void, reject: (value: VerifyError) => void) => {
await nexmo.verify.request({
number: phoneNumber,
brand: `${Config.productName}`,
code_length: 4
}, (err, result) => {
console.log(err ? err : result)
if (err) {
reject(err)
}
resolve(result)
});
})
return result.request_id
}