How to create Oracle SMS function from scratch

1 Install fn project

curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh

2 Generate boilerplate for the future function called send_sms

In order to generate boilerplate for the javascript function (Node.js platform) execute:

fn init --runtime node <function_name>

Example:
fn init --runtime node send_sms

It gives you the following directory structure:

ls send_sms/

func.js  func.yaml  package.json

File structure of the Oracle function for Node.js:

  • func.js - function code. Can be modified in order to support different SMS/Cloud provider. In order to use other SMS provider it's API or Node.js library shall be used. For example, bandwidth.com SMS provider has it's '@bandwidth/messaging' node.js library. For Twilio SMS provider lib is 'twilio'. Chosen lib has to be included as a dependency to package.json. If provider has only API available any http client for node.js can be used, like native or external (axios).
  • func.yaml - configuration of the function, generated automatically. Can be extended by 'config' key, used to pass environmental variables to the function (TWILIO_LOG_LEVEL in the example). Additional info can be found here: https://fnproject.io/tutorials/basics/UsingRuntimeContext/#
  • package.json - configuration of the Node.js application. Generated automatically. Have to be extended by required dependencies.

Dependencies shall be installed during function development. For example, for Twilio provider available lib is 'twilio'. To install 'twilio' dependency the following command shall be executed:

npm install twilio

After package installation (in our case 'twilio') newest available package version (in our case 3.82.2) is automatically added to package.json, e.g:

"dependencies": { "@fnproject/fdk": "^0.0.41", "twilio": "^3.82.2" }

If  SMS provider provides only HTTP api, axios or native node.js http client can be installed and used.

After installing axios

npm install axios

package.json dependencies will look like this:

"dependencies": { "@fnproject/fdk": "^0.0.41", "axios": "^1.2.1" }

Axios can be used according to its docs: https://www.npmjs.com/package/axios

3 Modify these files according to provided example

Example of the Oracle SMS cloud function for the Twilio provider written in javascript (Node.js)

Provided example of the Oracle function implements described earlier SMS Function interface, e.g. it accepts input and returns output as HTTP response.

func.js
/*
 * Комерційна таємниця / Proprietary and confidential to PortaOne, Inc.
 */

const fdk = require('@fnproject/fdk');


const HTTP_CODE_OK             = 200;
const HTTP_CODE_BAD_REQUEST    = 400;
const HTTP_CODE_UNAUTHORIZED   = 401;
const HTTP_CODE_INTERNAL_ERROR = 500;

/**
 * Send SMS via Twilio
 * @param  input, object, request body
 *         input.auth_info.login, string, The user login. Note: for the SMS provider Twilio, this is the Account SID value that can be found in the Account Info section.
 *         input.auth_info.token, string, The user token. Note: for the SMS provider Twilio it can be found in the Account Info section.
 *         input.src_phone, string, The phone number SMS are sent from. Note: for the SMS provider Twilio, this phone can be found in the Account Info section.
 *         input.dst_phone, string, The phone number of the SMS reciever.
 *         input.message, string, The text of the message to send.
 * @param  ctx, object, context object ot the @fnproject/fdk, docs https://www.npmjs.com/package/@fnproject/fdk
 *
 * @return object, response body:
 *         object.success, boolean, True in case of success, false - otherwise.
 *         object.error, string, Contains error message in case of error.
 */
const sendSMS = async (input, ctx) => {

    const isTwilio = isTwilioInput(input);

    if (isTwilio.error) {
        return setStatus(isTwilio, ctx);
    }

    const { token, login, dst_phone, src_phone, message } = isTwilio;

    try {
        const client = require('twilio')(login, token);
        const result = await client.messages.create({
            body: message,
            from: src_phone,
            to: dst_phone
        });

        if (!result) {
            return setStatus({
                error: 'Internal server error.'
            }, ctx);
        }

        if (result.errorCode) {
            return setStatus({
                error: result.errorMessage,
                status: result.status || HTTP_CODE_INTERNAL_ERROR
            }, ctx);
        }

        return setStatus({
            status: HTTP_CODE_OK
        }, ctx);

    } catch (error) {
        return setStatus({
            error: error.toString(),
            status: error.status || HTTP_CODE_INTERNAL_ERROR
        }, ctx);
    }

    return setStatus(isTwilio, ctx);
}

/**
 * Check whether message body is correct for Twilio
 * @param  input, object, request body, see sendSMS method input.
 * @return object:
 *         object.login, string.
 *         object.token, string.
 *         object.src_phone, string.
 *         object.dst_phone, string.
 *         object.message, string.
 *         object.error, string, Contains error message in case of validation error.
 *         object.status, int, The HTTP error code.
 */
const isTwilioInput = (input) => {
    const { dst_phone, src_phone, message, auth_info } = input;

    if (!auth_info) {
        return {
            error: 'The auth info is missing.',
            status: HTTP_CODE_UNAUTHORIZED
        };
    }

    const errors = {
        token: {
            error: 'The Twilio token is missing.',
            status: HTTP_CODE_UNAUTHORIZED
        },
        login: {
            error: 'The login is missing. The login should be equal to the Twilio account SID.',
            status: HTTP_CODE_UNAUTHORIZED
        },
        dst_phone: {
            error: 'The destination phone number is missing.',
            status: HTTP_CODE_BAD_REQUEST
        },
        src_phone: {
            error: 'The source phone number is missing. This should be the Twilio phone number taken from the personal info of the account.',
            status: HTTP_CODE_BAD_REQUEST
        },
        message: {
            error: 'The message is missing.',
            status: HTTP_CODE_BAD_REQUEST
        }
    };

    for (let key in errors) {
        if ( !input[key] && !auth_info[key] ) {
            return errors[key];
        }
    }

    const { login, token, password } = auth_info;

    return { token, login, dst_phone, src_phone, message };
}

/**
 * Set HTTP response status and success flag
 * @param  result, object, Response body
 *         result.status, int, Status code of the resulting response.
 *         result.error, string, The text of the error message in case of error.
 * @param  ctx, object, Context object ot the @fnproject/fdk, docs https://www.npmjs.com/package/@fnproject/fdk.
 * @return result:
 *         result.success, boolean, True in case of success, false - otherwise.
 *         result.error, string, The text of the error message in case of error.
 */
const setStatus = (result, ctx) => {
    const gateway = ctx.httpGateway;

    if (result.error) {
        gateway.statusCode = result.status || HTTP_CODE_BAD_REQUEST;
        result.success = false;
        delete result.status;
    }
    else {
        gateway.statusCode = result.status || HTTP_CODE_OK;
        result.success = true;
        delete result.status;
    }

    return result;
}


fdk.handle(sendSMS);
func.yaml
schema_version: 20180708
name: send_sms
version: 0.0.1
runtime: node
build_image: fnproject/node:14-dev
run_image: fnproject/node:14
entrypoint: node func.js
config:
  TWILIO_LOG_LEVEL: none
package.json
{
    "name": "send_sms",
    "version": "0.0.1",
    "description": "Function to send SMS via Twilio provider",
    "main": "func.js",
    "author": "PortaOne, Inc.",
    "license": "Apache-2.0",
    "dependencies": {
        "@fnproject/fdk": "^0.0.41",
        "twilio": "^3.82.2"
    }
}

How to test created Oracle Function locally

Created Oracle function can be tested locally without having oci (Oracle Cloud Infrastructure) tool installed.

In order to test Oracle function docker must be installed. It can be done according to its doc: https://docs.docker.com/engine/install/ubuntu/ .

Oracle use fn project to run its functions. Docs: https://fnproject.io/tutorials/#TestandMonitorFunctions

Generic steps is following:

1 Check available contexts
fn list context

CURRENT    NAME            PROVIDER    API URL                            REGISTRY
*    PortaOne-Development    oracle        https://functions.eu-frankfurt-1.oraclecloud.com    fra.ocir.io/porta1saas1production/twilio
    default            default        http://localhost:8080
2 Use default context:
fn use context default
3 Start local fn server in the background (or in the separate terminal window fn start, without &):
fn start &
4 Create local app
fn create app <app_name>

Example:
fn create app test_app
5 Go to the directory with created function
cd send_sms/
6 Deploy current function where you are located in the test_app locally
fn --verbose deploy --app <app_name> --local

Example:
fn --verbose deploy --app test_app --local
7 Check apps
fn list apps

NAME		ID				
test_app	01GMAS0Y65NG8G00GZJ0000001
8 Check function availability
fn list functions <app_name>

Example:
fn list functions test_app

NAME		IMAGE		ID
send_sms	send_sms:0.0.3	01GMAS56Y9NG8G00GZJ0000001
9 Invoke created function via fn project cmd:
echo -n '{json input}' | fn invoke <app_name> <function_name> --content-type application/json

Example:
echo -n '{ "auth_info" : { "token": "bbaabe3cc16e3578ec10d047a7d00000", "password": null }, "src_phone": "+12069658727", "dst_phone": "+380663333333", "message": "Hello from oracle" }' | fn invoke test_app send_sms --content-type application/json

{"error":"The login is missing. The login should be equal to the Twilio account SID.","success":false}
10 Invoke the same function via local endpoint.
10.1 Get endpoint
fn inspect function <app_name> <function_name>

Example:
fn inspect function test_app send_sms

{
	"annotations": {
		"fnproject.io/fn/invokeEndpoint": "http://localhost:8080/invoke/01GMAS56Y9NG8G00GZJ0000001"
	},
	"app_id": "01GMAS0Y65NG8G00GZJ0000001",
	"config": {
		"TWILIO_LOG_LEVEL": "none"
	},
	"created_at": "2022-12-15T11:36:47.817Z",
	"id": "01GMAS56Y9NG8G00GZJ0000001",
	"idle_timeout": 30,
	"image": "send_sms:0.0.3",
	"memory": 128,
	"name": "send_sms",
	"timeout": 30,
	"updated_at": "2022-12-15T11:36:47.817Z"
}
10.2 Invoke function via endpoint
curl -X "POST" -H "Content-Type: application/json" <function_endpoint> -d'{json_input}'

Example:
curl -X "POST" -H "Content-Type: application/json" http://localhost:8080/invoke/01GMAS56Y9NG8G00GZJ0000001 -d'{ "auth_info" : { "token": "bbaabe3cc16e3578ec10d047a7d08728", "password": null }, "src_phone": "+12069658727", "dst_phone": "+380995000000", "message": "Hello from oracle" }'

{"error":"The login is missing. The login should be equal to the Twilio account SID.","success":false}
11 Stop local fn server:
fn stop