diff --git a/samples/oci-apigw-nosql-node/README.md b/samples/oci-apigw-nosql-node/README.md new file mode 100644 index 0000000..538f601 --- /dev/null +++ b/samples/oci-apigw-nosql-node/README.md @@ -0,0 +1,158 @@ +# Function that reads data using the OCI Node.js for Oracle NoSQL Database + +This function uses Resource Principals to securely authorize a function to make +API calls to Oracle NoSQL Database. You can query all tables in a compartment + +As you make your way through this tutorial, look out for this icon ![user input icon](../../images/userinput.png). +Whenever you see it, it's time for you to perform an action. + + +## Prerequisites + +1. Before you deploy this sample function, make sure you have run steps A, B +and C of the [Oracle Functions Quick Start Guide for Cloud Shell](https://docs.oracle.com/en-us/iaas/Content/Functions/Tasks/functionsquickstartcloudshell.htm) + * A - Set up your tenancy + * B - Create application + * C - Set up your Cloud Shell dev environment + + +## List Applications + +Assuming you have successfully completed the prerequisites, you should see your +application in the list of applications. + +``` +fn ls apps +``` + + +## Create or Update your Dynamic Group + +In order to use other OCI Services, your function must be part of a dynamic +group. For information on how to create a dynamic group, refer to the +[documentation](https://docs.cloud.oracle.com/iaas/Content/Identity/Tasks/managingdynamicgroups.htm#To). + +![user input icon](../../images/userinput.png) + + +When specifying the *Matching Rules*, we suggest matching all functions in a compartment with: + +``` +ALL {resource.type = 'fnfunc', resource.compartment.id = 'ocid1.compartment.oc1..aaaaaxxxxx'} +``` + + +## Create or Update IAM Policies + +Create a new policy that allows the dynamic group to `manage objects` in the functions related compartment. + +![user input icon](../../images/userinput.png) + +Your policy should look something like this: +``` +Allow dynamic-group to manage nosql-family in compartment +``` +e.g. +``` +Allow dynamic-group demo-func-dyn-group to manage nosql-family in compartment demo-func-compartment +``` +For more information on how to create policies, go [here](https://docs.cloud.oracle.com/iaas/Content/Identity/Concepts/policysyntax.htm). + + +## Review and customize the function + +Review the following files in the current folder: + +- [package.json](./package.json) specifies all the dependencies for your function +- [func.yaml](./func.yaml) that contains metadata about your function and declares properties +- [func.js](./func.js) which is the Node.js function + +## Deploy the function + +In Cloud Shell, run the `fn deploy` command to build the function and its dependencies as a Docker image, +push the image to the specified Docker registry, and deploy the function to Oracle Functions +in the application created earlier: + +![user input icon](../../images/userinput.png) + +``` +COMP_ID="" +fn config app NOSQL_COMPARTMENT_ID $COMP_ID +fn config app NOSQL_REGION $OCI_REGION +fn config app FN_API_KEY + +``` + +e.g. +``` +COMP_ID=`oci iam compartment list --all --name demo-func-compartment | jq -r '."data"[].id'` +fn config app myapp NOSQL_COMPARTMENT_ID $COMP_ID +fn config app myapp NOSQL_REGION $OCI_REGION +fn config app myapp FN_API_KEY "MY_FN_API_KEY_VALUE" +``` + + + +``` +fn -v deploy --app +``` +e.g. +``` +fn -v deploy --app myapp +``` + + +## Create Nosql Tables + +![user input icon](../../images/userinput.png) + + +```` +COMP_ID=`oci iam compartment list --all --name demo-func-compartment | jq -r '."data"[].id'` + +DDL_TABLE="CREATE TABLE IF NOT EXISTS Tutorial (id LONG GENERATED BY DEFAULT AS IDENTITY (NO CYCLE), kv_json_ JSON, PRIMARY KEY( id ))" +echo $DDL_TABLE + +oci nosql table create --compartment-id "$COMP_ID" \ +--name Tutorial --ddl-statement "$DDL_TABLE" \ +--table-limits="{\"maxReadUnits\": 50, \"maxStorageInGBs\": 25, \"maxWriteUnits\": 50 }" \ +--wait-for-state SUCCEEDED --wait-for-state FAILED + +oci nosql row update --compartment-id "$COMP_ID" --table-name-or-id Tutorial \ +--value '{"kv_json_": { "author": { "name": "Dario VEGA"}, "title": "Oracle Functions Samples with NOSQL DB"}}' +```` + +## Test + +![user input icon](../../images/userinput.png) +``` +echo -n | fn invoke +``` +e.g. +``` +echo '{"tableName":"Tutorial"}' | fn invoke myapp oci-apigw-nosql-node | jq +``` + +You should see the following JSON document appear in the terminal. +``` +[ + { + "id": 1, + "kv_json_": { + "author": { + "name": "Dario VEGA" + }, + "title": "Oracle Functions Samples with NOSQL DB" + } + } +] +``` + + +## Clean Up + +``` +oci nosql table delete --compartment-id "$COMP_ID" --table-name-or-id Tutorial +``` + + diff --git a/samples/oci-apigw-nosql-node/func.js b/samples/oci-apigw-nosql-node/func.js new file mode 100644 index 0000000..7abda22 --- /dev/null +++ b/samples/oci-apigw-nosql-node/func.js @@ -0,0 +1,262 @@ +// Copyright (c) 2023 Oracle, Inc. All rights reserved. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +// + +const fdk=require('@fnproject/fdk'); +const process = require('process'); +const NoSQLClient = require('oracle-nosqldb').NoSQLClient; +const Region = require('oracle-nosqldb').Region; +const ServiceType = require('oracle-nosqldb').ServiceType; +const url = require('url'); + +let client; +let lim = 15; + +process.on('exit', function(code) { + if (client) { + console.log("\close client on exit"); + client.close(); + } + return code; +}); + +fdk.handle(async function(input, ctx){ + + const apiKey=process.env.FN_API_KEY; + const scopeRead ="read" + const scopeWrite="write" + let apiKeyHeader; + let authScope = []; + + let tableName; + let id; + let descTable=false; + + // Reading parameters from standard input for TEST purposes + // using invoke allows only to execute getAllRecords + if (input && input.tableName) + tableName = input.tableName; + if (input) { + method = 'GET'; + apiKeyHeader = [apiKey] + authScope = ["read", "write"] + } + + + // Reading parameters sent by the httpGateway + // When an API Gateway is configured, you can execute all the defined actions + let hctx = ctx.httpGateway + if (hctx && hctx.requestURL) { + var adr = hctx.requestURL; + var q = url.parse(adr, true); + tableName = q.pathname.split('/')[2] + id = q.pathname.split('/')[3] + if (id && id === 'desc') + descTable=true; + method = hctx.method + body = ctx.body + apiKeyHeader = hctx.headers["X-Api-Key"] + if (hctx.headers["X-Scope"]) + authScope.push(...hctx.headers["X-Scope"]) + } + + // Validating apiKey - only if FN_API_KEY was configured at application/function level + if (apiKey) { + if (! (apiKeyHeader)) { + hctx.statusCode = 401 + return {"Api Key Validation":false, comment:"noApiKeyHeader"} + } + else if (! (apiKeyHeader.includes(apiKey))) { + hctx.statusCode = 401 + return {"Api Key Validation":false, debug:apiKeyHeader} + } + } + // Validating Scope if it was setup. + if (authScope) { + if ( (! (authScope.includes(scopeRead)) ) && method==='GET'){ + hctx.statusCode = 401 + return {"Scope Validation":false, debug:authScope} + } + if ( (! (authScope.includes(scopeWrite)) ) && method!=='GET'){ + hctx.statusCode = 401 + return {"Scope Validation":false} + } + } + + // API Implementation + + if ( !client ) { + client = createClientResource(); + } + + if ((method === 'GET') && !tableName){ + return showAll(); + } + + if ((method === 'GET') && descTable){ + return describeTable(tableName); + } + + if ((method === 'GET') && id && !descTable){ + return getRecord(tableName, id); + } + + if ((method === 'GET') && !id){ + return getAllRecords(tableName, q); + } + + if ((method === 'POST') && !id){ + return createRecord(tableName, body); + } + + if ((method === 'DELETE') ){ + return deleteRecord(tableName, id) + } + + if ((method === 'PUT') && id ){ + return updateRecord (tableName, id, body); + } + + return showAll(); +}, {}); + + +// Show all tables + +async function showAll () { + + try { + let varListTablesResult = await client.listTables(); + return varListTablesResult; + } catch (err){ + console.error('failed to show tables', err); + return { error: err }; + } finally { + } +} + +// Show the structure of the table tablename + +async function describeTable (tablename) { + try { + let resExistingTab = await client.getTable(tablename); + await client.forCompletion(resExistingTab); + return Object.assign(resExistingTab, { "schema": JSON.parse(resExistingTab.schema)}); + } catch (err){ + console.error('failed to show tables', err); + return { error: err }; + } finally { + } +} + + +// Create a new record in the table tablename +async function createRecord (tablename, record) { + try { + const result = await client.put(tablename, record, {exactMatch:true} ); + return { result: result}; + } catch (err) { + console.error('failed to insert data', err); + return { error: err }; + } +} + +// Update a record in the table tablename +async function updateRecord (tablename, id, record) { + try { + const result = await client.putIfPresent(tablename, Object.assign(record, {id}) ); + return { result: result}; + } catch (err) { + console.error('failed to insert data', err); + return { error: err }; + } +} + +// Get a record from the table tablename by id +// Currently the id is hardcoded as key of the table +async function getRecord (tablename, id) { + try { + const result = await client.get(tablename, { id }) + if (result.row) + return result.row; + else + return {} + } catch (err) { + console.error('failed to get data', err); + return { error: err }; + } +} + +// Delete a record from the table tablename by id +// Currently the id is hardcoded as key of the table +async function deleteRecord (tablename, id) { + try { + const result = await client.delete(tablename, { id }); + return { result: result}; + } catch (err) { + console.error('failed to delete data', err); + return { error: err }; + } +} + +// Get all records for the table tablename +async function getAllRecords (tablename, req) { + let statement = "SELECT * FROM " + tablename; + let offset; + let page; + let limit; + let orderby; + let result; + + if (req && req.query ) { + page = parseInt(req.query.page); + limit = parseInt(req.query.limit); + orderby = req.query.orderby; + } + + if (orderby ) + statement = statement + " ORDER BY " + orderby; + if (limit) + statement = statement + " LIMIT " + limit; + if (page && limit) { + offset = page*limit; + statement = statement + " OFFSET " + offset; + } + + console.log (statement) + result =executeQuery (statement) + if (result) + return result; + else + return {} + +} + +async function executeQuery (statement) { + const rows = []; + let cnt ; + let res; + try { + do { + res = await client.query(statement, { continuationKey:cnt}); + rows.push.apply(rows, res.rows); + cnt = res.continuationKey; + } while(res.continuationKey != null); + } + catch(err) { + return err; + } + return rows; +} + +function createClientResource() { + return new NoSQLClient({ + region: process.env.NOSQL_REGION, + compartment:process.env.NOSQL_COMPARTMENT_ID, + auth: { + iam: { + useResourcePrincipal: true + } + } + }); +} diff --git a/samples/oci-apigw-nosql-node/func.yaml b/samples/oci-apigw-nosql-node/func.yaml new file mode 100644 index 0000000..bbb80fe --- /dev/null +++ b/samples/oci-apigw-nosql-node/func.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2023 Oracle, Inc. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +# +schema_version: 20180708 +name: oci-apigw-nosql-node +version: 0.0.1 +runtime: node +build_image: fnproject/node:14-dev +run_image: fnproject/node:14 +entrypoint: node func.js diff --git a/samples/oci-apigw-nosql-node/package.json b/samples/oci-apigw-nosql-node/package.json new file mode 100644 index 0000000..daf66a5 --- /dev/null +++ b/samples/oci-apigw-nosql-node/package.json @@ -0,0 +1,13 @@ +{ + "name": "oci-apigw-nosql-node", + "version": "1.0.0", + "description": "example function", + "main": "func.js", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@fnproject/fdk": ">=0.0.45", + "url": "^0.11.0", + "oracle-nosqldb": "^5.3.4" + } +}