{"id":15350162,"url":"https://github.com/mars/event-driven-functions","last_synced_at":"2025-04-15T04:22:17.104Z","repository":{"id":139297758,"uuid":"128978140","full_name":"mars/event-driven-functions","owner":"mars","description":"🚇 Invoke javascript functions in a Heroku app via Salesforce Platform Events","archived":false,"fork":false,"pushed_at":"2018-07-25T21:00:25.000Z","size":229,"stargazers_count":9,"open_issues_count":1,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-28T15:51:47.335Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mars.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-10T18:27:01.000Z","updated_at":"2021-02-15T18:39:16.000Z","dependencies_parsed_at":null,"dependency_job_id":"36860f59-2430-4dff-bbc4-8c4575b2bbfa","html_url":"https://github.com/mars/event-driven-functions","commit_stats":{"total_commits":69,"total_committers":2,"mean_commits":34.5,"dds":"0.13043478260869568","last_synced_commit":"149d8eeb98e5fc571344801acc42b2d7fd21e375"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mars%2Fevent-driven-functions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mars%2Fevent-driven-functions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mars%2Fevent-driven-functions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mars%2Fevent-driven-functions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mars","download_url":"https://codeload.github.com/mars/event-driven-functions/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249004381,"owners_count":21196865,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-01T11:57:46.952Z","updated_at":"2025-04-15T04:22:17.088Z","avatar_url":"https://github.com/mars.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Event-driven Functions\n\nInvoke javascript functions in a [Heroku app](https://www.heroku.com/platform) via [Salesforce Platform Events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_define_ui.htm).\n\n💻👩‍🔬 *This project is a exploration into a new pattern for extending the compute capabilities of Salesforce. **Shared freely via [MIT license](LICENSE). Use at your own risk.***\n\n* 📐 [Design](#user-content-design)\n* 🏙 [Architecture](#user-content-architecture)\n* 🌱 [Example](#user-content-example)\n  * [Heroku function](#user-content-heroku-function)\n  * [Account object](#user-content-account-object)\n  * [Platform Events](#user-content-platform-events)\n  * [Process Builder flows](#user-content-process-builder-flows)\n* ✅ [Requirements](#user-content-requirements)\n* ⚡️ [Usage](#user-content-usage)\n  * [First time setup](#user-content-first-time-setup)\n  * [Salesforce setup](#user-content-salesforce-setup)\n  * [Run locally](#user-content-run-locally)\n  * [Developing more functions](#user-content-developing-more-functions)\n* 🎛 [Configuration](#user-content-configuration) via environment variables\n  * [Authentication](#user-content-configure-authentication)\n  * [Runtime behavior](#user-content-configure-runtime-behavior)\n* 🔬 [Testing](#user-content-testing)\n* 🚀 [Deployment](#user-content-deployment)\n  * [Salesforce components](#user-content-deploy-salesforce-components)\n  * [Heroku app](#user-content-deploy-heroku-app)\n\nDesign\n------\n\nThe high-level flow is:\n\n\u003e Platform Event → **this app** → Platform Event\n\nThis flow maps specific **Invoke events** (topics) to function calls that return values by producing **Return events**.\n\n\u003e `Heroku_Function_*_Invoke__e` → Node.js function call → `Heroku_Function_*_Return__e`\n\nThese functions are composed in a Heroku app. Each function's arguments, return values, and their types must be encoded in the Invoke and Return events' fields.\n\nArchitecture\n------------\n\nThis event-driven functions app is a **Node.js** app, along with an **sfdx** project providing the Salesforce customizations.\n\nBased on [improvements to the **jsforce** Streaming module](https://github.com/jsforce/jsforce/pull/740) to support durable consumption of the Salesforce Streaming API. Those changes were merged to become **jsforce 1.9.0**.\n\nExample\n-------\n\nThis repo contains an example implementation of a UUID generator for Salesforce Accounts. This could generate UUIDs for any Salesforce object by implementing a Process Builder flow for each desired object type.\n\n### Heroku function\n\nImplemented in Node.js, an event-driven function is defined in the [JavaScript module `Generate_UUID`](lib/functions/Generate_UUID.js) and [registered as a function export](lib/functions/index.js).\n\n### Account object\n\nIn **Salesforce Setup** → **Object Manager**, a custom field `UUID__c` is defined for Account.\n\nThis [UUID field](force-app/main/default/objects/Account/fields/UUID__c.field-meta.xml) is blank by default, to be filled with a universally unique external indentifier generated by the Heroku app/function.\n\n### Platform Events\n\nIn **Salesforce Setup** → **Platform Events**, two events are defined.\n\n#### Invoke event\n\nSalesforce Platform Event [`Heroku_Function_Generate_UUID_Invoke__e`](force-app/main/default/objects/Heroku_Function_Generate_UUID_Invoke__e)\n\n```json\n{\n  \"Context_Id__c\": \"xxxxx\"\n}\n```\n\n* `Context_Id` should be passed-through unchanged from Invoke to Return. It provides an identifier to associate the return value with the original invocation\n* This example  is a minimal Invoke event payload with no additional fields for function arguments. When defining the Platform Event in Salesforce Setup, it may contain as many fields as necessary\n* This object is included in the Salesforce [Permission Set](force-app/main/default/permissionsets/Heroku_Function_Generate_UUID.permissionset-meta.xml) for this app.\n\n#### Return event\n\nSalesforce Platform Event  [`Heroku_Function_Generate_UUID_Return__e`](https://github.com/mars/event-driven-functions/tree/master/force-app/main/default/objects/Heroku_Function_Generate_UUID_Return__e)\n\n```json\n{\n  \"Context_Id__c\": \"xxxxx\",\n  \"Value__c\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx\"\n}\n```\n\n* `Context_Id__c` should be passed-through unchanged from Invoke to Return\n* `Value__c` is a minimal Return event payload. In this example it contains the string UUID\n* This object is included in the Salesforce [Permission Set](force-app/main/default/permissionsets/Heroku_Function_Generate_UUID.permissionset-meta.xml) for this app.\n\n👓 *About the suffixes in Salesforce identifiers: `__e` is appended to Platform Event names, and `__c` is appended to custom object \u0026 field names.*\n\n### Process Builder flows\n\nIn **Salesforce Setup** → **Process Builder**, two flows are defined.\n\n#### Generate UUID for Account\n\n[The first flow](force-app/main/default/flows/Generate_UUID_for_Account-1.flow-meta.xml) triggers when creating or updating an Account that does not yet have a UUID. This publishes an [Invoke event](#user-content-invoke-event).\n\n#### Set UUID for Account\n\n[The second flow](force-app/main/default/flows/Set_UUID_for_Account-1.flow-meta.xml) triggers when a [Return event](#user-content-return-event) is received. This updates the Account with the returned UUID.\n\n\nRequirements\n------------\n\n* [Node.js](https://nodejs.org/) 8.11 with npm 5\n* [redis](https://redis.io)\n\nUsage\n-----\n\n### First time setup\n\n```bash\ngit clone https://github.com/mars/event-driven-functions.git\ncd event-driven-functions/\nnpm install\ncp .env.sample .env\n```\n\n### Salesforce setup\n\nNext, we'll use [`sfdx`](https://developer.salesforce.com/docs/atlas.en-us.212.0.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm) to deploy the Salesforce customizations. If you don't yet have access to a Dev Hub org, or this is your first time using `sfdx`, then see [**Setup Salesforce DX** in Trailhead](https://trailhead.salesforce.com/trails/sfdx_get_started/modules/sfdx_app_dev/units/sfdx_app_dev_setup_dx).\n\nDeploy the included `force-app` code to a scratch org:\n\n```bash\nsfdx force:org:create -s -f config/project-scratch-def.json -a EventDrivenFunctions\nsfdx force:source:push\nsfdx force:user:permset:assign -n Heroku_Function_Generate_UUID\n```\n\nView the scratch org description:\n\n```bash\nsfdx force:user:display\n```\n\nThen, update `.env` file with the **Instance Url** \u0026 **Access Token** values from the scratch org description:\n\n```\nSALESFORCE_INSTANCE_URL=xxxxx\nSALESFORCE_ACCESS_TOKEN=yyyyy\n```\n\n⚠️ *Scratch orgs and their authorizations expire, so this setup may need to be repeated whenever beginning local development work. View the current status of the orgs with `sfdx force:org:list`.*\n\n### Run locally\n\nOpen the scratch org's Accounts:\n\n```bash\nsfdx force:org:open --path one/one.app#/sObject/Account/list\n```\n\nRun this `node` command in a shell terminal:\n\n```bash\nREAD_MODE=changes \\\nPLUGIN_NAMES=invoke-functions \\\nOBSERVE_SALESFORCE_TOPIC_NAMES=/event/Heroku_Function_Generate_UUID_Invoke__e \\\nnode lib/exec\n```\n\n🔁 *This command runs continuously, listening for the Platform Event.*\n\n▶️ Watch this command's output as your create Accounts in the scratch org.\n\n### Developing more functions\n\nFor a given function, three identifiers are used.\n\n* **Function Name**\n  * example `Generate_UUID`\n  * used for the JavaScript module file \u0026 its `functions` export\n* **Invoke Event Name**\n  * example `Heroku_Function_Generate_UUID_Invoke__e`\n  * used for the Platform Event that runs the function\n* **Return Event Name**\n  * example `Heroku_Function_Generate_UUID_Return__e`\n  * used for the Platform Event that receives the function's result\n\nNote: the **Function Name** must be embedded exactly in both **Event Names**.\n\nTo implement a new function:\n\n1. create the new Platform Events using `sfdx` workflow\n   * develop in a scratch org and pull changes into this repo\n   * define each **Invoke \u0026 Return Event** and its schema (fields \u0026 their types)\n   * define a new **Permission Set** for access or add to an existing Set\n1. create the function as a default export in `lib/functions/`\n   * use **Function Name** for the module file \u0026 its `functions` export\n   * the function receives the Invoke event's payload and must honor the Return event's schema\n   * see: [example `Generate_UUID.js`](lib/functions/Generate_UUID.js) \u0026 [the export](lib/functions/index.js)\n1. include the new Invoke Event Name in `OBSERVE_SALESFORCE_TOPIC_NAMES` env var\n   * example: `OBSERVE_SALESFORCE_TOPIC_NAMES=/event/Heroku_Function_Generate_UUID_Invoke__e,/event/Heroku_Function_Generate_Haiku_Invoke__e`\n\nConfiguration\n-------------\n\n### Configure Authentication\n\nPerformed based on environment variables. Either of the following authentication methods may be used:\n\n* Username + password\n  * `SALESFORCE_USERNAME`\n  * `SALESFORCE_PASSWORD` (password+securitytoken)\n  * `SALESFORCE_LOGIN_URL` (optional; defaults to **login.salesforce.com**)\n* Existing OAuth token\n  * `SALESFORCE_INSTANCE_URL`\n  * `SALESFORCE_ACCESS_TOKEN`\n  * Retrieve from an [sfdx](https://developer.salesforce.com/docs/atlas.en-us.212.0.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm) scratch org with:\n\n    ```bash\n    sfdx force:org:create -s -f config/project-scratch-def.json -a EventDrivenFunctions\n    sfdx force:org:display\n    ```\n* OAuth client\n  * `SALESFORCE_URL`\n    * *Must include oAuth client ID, secret, \u0026 refresh token*\n    * Example: `force://{client-id}:{secret}:{refresh-token}@{instance-name}.salesforce.com`\n\n### Configure Runtime Behavior\n\n* `VERBOSE`\n  * enable detailed runtime logging to stderr\n  * example: `VERBOSE=true`\n  * default value: unset, no log output\n* `PLUGIN_NAMES`\n  * configure the consumers/observers of the Salesforce data streams\n  * example: `PLUGIN_NAMES=invoke-functions`\n  * default value: `console-output`\n* `OBSERVE_SALESFORCE_TOPIC_NAMES`\n  * the path part of a Streaming API URL\n  * a comma-delimited list\n  * example: `OBSERVE_SALESFORCE_TOPIC_NAMES=/event/Heroku_Function_Generate_UUID_Invoke__e`\n  * default value: no Salesforce observer\n* `REDIS_URL`\n  * connection config to Redis datastore\n  * example: `REDIS_URL=redis://localhost:6379`\n  * default: unset, no Redis\n* `REPLAY_ID`\n  * force a specific replayId for Salesforce Streaming API\n  * ensure to unset this after usage to prevent the stream from sticking\n  * example: `REPLAY_ID=5678` (or `-2` for all possible events)\n  * default: unset, receive all new events\n\nTesting\n-------\n\nImplemented with [AVA](https://github.com/avajs/ava), concurrent test runner.\n\n`npm test` runs only unit tests. It skips integration tests, because Salesforce and AWS config is not automated.\n\n### Unit Tests\n\n* `npm run test:unit`\n* Defined in `lib/` alongside source files\n* Salesforce API calls are mocked by way of [Episode 7](https://github.com/mars/episode-7)\n\n\nDeployment\n----------\n\n### Deploy Salesforce components\n\n#### Example, ready-to-install package from this repo\n\n```bash\nsfdx force:auth:web:login -a AnotherOrg\nsfdx force:package:install --id 04tf4000001ft4hAAA -u AnotherOrg\n```\n\n#### Custom, package it yourself\n\n##### 2nd-generation unnamespaced, unlocked package (2GP)\n\nBased on [Salesforce DX 2GP docs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_dev2gp_workflow.htm):\n\n```bash\nsfdx force:package2:create --name Event_Driven_Functions_Generate_UUID_2GP_sans_NS --description \"Integration with event-driven-functions Heroku app\" --containeroptions Unlocked --nonamespace\n```\n\nIn `sfdx-project.json`, update `packageDirectories`.`0`.`id` with the output `Package2 Id`:\n\n```json\n{\n  \"packageDirectories\": [\n    {\n      \"path\": \"force-app\",\n      \"default\": true,\n      \"id\": \"0Hof4000000blNuCAI\",\n      \"versionName\": \"Initial 2GP\",\n      \"versionNumber\": \"1.0.0\"\n    }\n  ],\n  \"namespace\": \"\",\n  \"sfdcLoginUrl\": \"https://login.salesforce.com\",\n  \"sourceApiVersion\": \"42.0\"\n}\n```\n\n```bash\nsfdx force:package2:version:create --directory force-app --wait 10\nsfdx force:package:install --id \u003cSubscriber Package2 Version Id\u003e -u OrgAliasOrUserId --wait 10 --publishwait 10\nsfdx force:org:open -u EventDrivenFunctions\n```\n\n\n\n##### 1st-generation unmanaged (unnamespaced) package\n\nFollow [Build and Release Your App with Managed Packages](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_build_man_pack.htm) to prepare a packaging org.\n\nDiverging from those directions, we'll prepare an unmanaged package without a namespace. We have to skip namespacing for now, because of problems with Process Builder + Platform Events embedding namespaces in the metadata (evidence [1](NOTES.md#user-content-push-to-scratch-org-error), [2](NOTES.md#user-content-manually-pruned-package-fails-to-install)). \u003cdel\u003e[Link its namespace with your Hub org](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_reg_namespace.htm), and set the established `\"namespace\"` in [sfdx-project.json](sfdx-project.json), and then [provision \u0026 push to a fresh scratch org](#user-content-salesforce-setup).\u003c/del\u003e\n\nNow, pull the Salesforce customizations back out of the scratch org in the Metadata API format:\n\n```bash\nsfdx force:source:convert --outputdir mdapi_output_dir --packagename Event_Driven_Functions_Generate_UUID\n```\n\nLogin to the packaging org and create the Beta package:\n\n```bash\nsfdx force:org:list\nsfdx force:auth:web:login -a PkgFunctions\n\nsfdx force:mdapi:deploy --deploydir mdapi_output_dir --targetusername PkgFunctions\n\n# Find the package ID in the URL of the packaging org:\n#   Setup → Package Manager → View/Edit the Package\nsfdx force:package1:version:create --packageid XXXXX --name r00000 -u PkgFunctions\n\nsfdx force:package1:version:list -u PkgFunctions\n```\n\nInstall the beta package into another org by its id `METADATAPACKAGEVERSIONID`:\n\n```bash\nsfdx force:auth:web:login -a AnotherOrg\nsfdx force:package:install --id YYYYY -u AnotherOrg\n```\n\n\n### Deploy Heroku app\n\n```bash\nheroku create\n\nheroku config:set \\\n  SALESFORCE_USERNAME=mmm@mmm.mmm \\\n  SALESFORCE_PASSWORD=nnnnnttttt \\\n  VERBOSE=true \\\n  PLUGIN_NAMES=invoke-functions \\\n  OBSERVE_SALESFORCE_TOPIC_NAMES=/event/Heroku_Function_Generate_UUID_Invoke__e \\\n  READ_MODE=changes\n\nheroku addons:create heroku-redis:premium-0\n\ngit push heroku master\n\nheroku ps:scale web=0:Standard-1x worker=1:Standard-1x\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmars%2Fevent-driven-functions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmars%2Fevent-driven-functions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmars%2Fevent-driven-functions/lists"}