diff --git a/4-sample-snap-in/code/README.md b/4-sample-snap-in/code/README.md new file mode 100644 index 0000000..6ff582c --- /dev/null +++ b/4-sample-snap-in/code/README.md @@ -0,0 +1,34 @@ +## DevRev Snaps TypeScript Template + +This repository contains a template for the functions that can be deployed as +part of Snap-Ins. + +### Getting started with the template +1. Create a new repository from this template. +2. In the new repository, you can add functions at path `src/functions` where the folder name corresponds to the function name in your manifest file. +3. Each function you add will also need to be mentioned in `src/function-factory.ts` . + +### Testing locally +You can test your code by adding test events under `src/fixtures` similar to the example event provided. You can add keyring values to the event payload to test API calls as well. + +Once you have added the event, you can test your code by running: +``` +npm install +npm run start:watch -- --functionName=function_1 --fixturePath=function_1_event.json +npm run start:watch -- --functionName=function_2 --fixturePath=function_2_event.json +``` + +### Adding external dependencies +You can also add dependencies on external packages in package.json under the "dependencies" key. These dependencies will be made available to your function at runtime and testing. + +### Packaging the code +Once you are done with the testing, +Run +``` +npm install +npm run build +npm run package +``` +and ensure it succeeds. + +You will see a `build.tar.gz` file is created and you can provide it while creating the snap_in_version. diff --git a/4-sample-snap-in/code/babel.config.js b/4-sample-snap-in/code/babel.config.js new file mode 100644 index 0000000..57f9aff --- /dev/null +++ b/4-sample-snap-in/code/babel.config.js @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', + ], +}; diff --git a/4-sample-snap-in/code/jest.config.js b/4-sample-snap-in/code/jest.config.js new file mode 100644 index 0000000..467b67d --- /dev/null +++ b/4-sample-snap-in/code/jest.config.js @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/4-sample-snap-in/code/manifest.yaml b/4-sample-snap-in/code/manifest.yaml new file mode 100644 index 0000000..adb7897 --- /dev/null +++ b/4-sample-snap-in/code/manifest.yaml @@ -0,0 +1,41 @@ +version: '1' + +name: Sample Snap-Ins for DevRev Hackathon +description: Snap In to add Comments for demonstration purpose. + +service_account: + display_name: "DevRev Bot" + +event-sources: + - name: devrev-webhook + description: Event coming from DevRev + display_name: DevRev + type: devrev-webhook + config: + event_types: + - work_created + +functions: + - name: function_1 + description: Function to create a timeline entry comment on a DevRev work item created. + - name: function_2 + description: Function to create a timeline entry comment on a DevRev work item on which comment is added. + +automations: + - name: convergence_automation_devrev + source: devrev-webhook + event_types: + - work_created + function: function_1 + +commands: + - name: comment_here + namespace: devrev + description: Command to trigger function to add comment to this work item. + surfaces: + - surface: discussions + object_types: + - issue + - ticket + usage_hint: "Command to add comment to this work item." + function: function_2 \ No newline at end of file diff --git a/4-sample-snap-in/code/nodemon.json b/4-sample-snap-in/code/nodemon.json new file mode 100644 index 0000000..8e47e98 --- /dev/null +++ b/4-sample-snap-in/code/nodemon.json @@ -0,0 +1,5 @@ +{ + "execMap": { + "ts": "ts-node" + } +} diff --git a/4-sample-snap-in/code/package.json b/4-sample-snap-in/code/package.json new file mode 100644 index 0000000..382f6c5 --- /dev/null +++ b/4-sample-snap-in/code/package.json @@ -0,0 +1,50 @@ +{ + "name": "devrev-snaps-typescript-template", + "version": "1.0.0", + "description": "", + "main": "./dist/index.js", + "scripts": { + "lint": "eslint --ignore-path .gitignore .", + "lint:fix": "eslint --fix --ignore-path .gitignore .", + "build": "rimraf ./dist && tsc", + "build:watch": "tsc --watch", + "prepackage": "npm run build", + "package": "tar -cvzf build.tar.gz dist package.json package-lock.json", + "start": "ts-node ./src/main.ts", + "start:watch": "nodemon ./src/main.ts", + "start:production": "node dist/main.js", + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/core": "^7.20.12", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.18.6", + "@types/jest": "^29.4.0", + "@types/node": "^18.13.0", + "@types/yargs": "^17.0.24", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "babel-jest": "^29.4.2", + "eslint": "^8.33.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.4.2", + "nodemon": "^2.0.20", + "prettier": "^2.8.3", + "prettier-plugin-organize-imports": "^3.2.2", + "rimraf": "^4.1.2", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + }, + "dependencies": { + "@devrev/typescript-sdk": "^1.1.9", + "dotenv": "^16.0.3", + "yargs": "^17.6.2" + } +} diff --git a/4-sample-snap-in/code/src/fixtures/command_event.json b/4-sample-snap-in/code/src/fixtures/command_event.json new file mode 100644 index 0000000..154bfbf --- /dev/null +++ b/4-sample-snap-in/code/src/fixtures/command_event.json @@ -0,0 +1,28 @@ +{ + "payload": { + "actor_id": "don:identity:dvrv-us-1:devo/0:devu/1", + "command_id": "don:integration:dvrv-us-1:devo/0:namespace/cns:command/cname", + "dev_org": "don:identity:dvrv-us-1:devo/0", + "parameters": "commands parameters string passed by user", + "parent_id": "don:integration:dvrv-us-1:devo/0:snap_in/00000001-0001-0001-0001-00000001", + "request_id": "4QtCBSKJcKKqwQhoJKZvRQ", + "source_id": "don:core:dvrv-us-1:devo/0:issue/1" + }, + "context": { + "dev_oid": "don:identity:dvrv-us-1:devo/0", + "source_id": "don:integration:dvrv-us-1:devo/0:namespace/devrev:command/complete_comm_k", + "snap_in_id": "don:integration:dvrv-us-1:devo/0:snap_in/00000001-0001-0001-0001-00000001", + "snap_in_version_id": "don:integration:dvrv-us-1:devo/0:snap_in_package/00000001-0001-0001-0001-00000001:snap_in_version/00000001-0001-0001-0001-00000001" + }, + "execution_metadata": { + "request_id": "4QtCBSKJcKKqwQhoJKZvRQ", + "function_name": "foobar" + }, + "input_data": { + "global_values": {}, + "event_sources": {}, + "keyrings": { + "devrev" : "" + } + } +} diff --git a/4-sample-snap-in/code/src/fixtures/function_1_event.json b/4-sample-snap-in/code/src/fixtures/function_1_event.json new file mode 100644 index 0000000..2020bce --- /dev/null +++ b/4-sample-snap-in/code/src/fixtures/function_1_event.json @@ -0,0 +1,159 @@ +[ + { + "payload": { + "id": "don:integration:dvrv-us-1:devo/XXXXXX:webhook/ovtfa4mg:webhook_event/uzX6Pqe9tQc", + "timestamp": "2023-08-03T06:03:21.932268Z", + "type": "work_created", + "unique_key": "XXXX=", + "webhook_id": "don:integration:dvrv-us-1:devo/XXXXXX:webhook/ovtfa4mg", + "work_created": { + "work": { + "type": "issue", + "age_days": 13.75945837962963, + "applies_to_part": { + "type": "product", + "display_id": "PROD-4", + "id": "don:core:dvrv-us-1:devo/XXXXXX:product/4", + "id_v1": "don:DEV-XXXXXX:product:4", + "name": "Others" + }, + "body": "body of the issue", + "created_by": { + "type": "dev_user", + "display_handle": "john-doe", + "display_id": "DEVU-1046", + "display_name": "john-doe", + "email": "john.doe@gmail.com", + "full_name": "john doe", + "id": "don:identity:dvrv-us-1:devo/XXXXXX:devu/1046", + "id_v1": "don:DEV-XXXXXX:dev_user:DEVU-1046", + "state": "shadow", + "thumbnail": "https://api.devrev.ai/internal/display-picture/john%20doe.svg" + }, + "created_date": "2023-08-04T17:09:39.278Z", + "custom_schema_fragments": [ + "don:core:dvrv-us-1:devo/XXXXXX:custom_type_fragment/18" + ], + "display_id": "ISS-9", + "id": "don:core:dvrv-us-1:devo/XXXXXX:issue/9", + "id_v1": "don:DEV-XXXXXX:issue:9", + "modified_by": { + "type": "dev_user", + "display_handle": "dane-doe", + "display_id": "DEVU-4185", + "display_name": "dane-doe", + "email": "dane.doe@gmail.com", + "full_name": "John Doe doe", + "id": "don:identity:dvrv-us-1:devo/XXXXXX:devu/4185", + "id_v1": "don:DEV-XXXXXX:dev_user:DEVU-4185", + "state": "shadow", + "thumbnail": "https://api.devrev.ai/internal/display-picture/John%20Sai%20doe.svg" + }, + "modified_date": "2023-08-04T22:09:19.164Z", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "jane-doe", + "display_id": "DEVU-2588", + "display_name": "jane-doe", + "email": "jane.doe@gmail.com", + "full_name": "doe jane", + "id": "don:identity:dvrv-us-1:devo/XXXXXX:devu/2588", + "id_v1": "don:DEV-XXXXXX:dev_user:DEVU-2588", + "state": "shadow", + "thumbnail": "https://api.devrev.ai/internal/display-picture/doe%20jane%20.svg" + } + ], + "priority": "p3", + "stage": { + "name": "backlog", + "notes": "Open", + "ordinal": 1000, + "stage": { + "id": "don:core:dvrv-us-1:devo/XXXXXX:custom_stage/1" + }, + "state": { + "id": "don:core:dvrv-us-1:devo/XXXXXX:custom_state/1" + } + }, + "state": "open", + "stock_schema_fragment": "don:core:dvrv-us-1:stock_sf/5597", + "subtype": "jira_gmail.atlassian.net_support", + "tags": [ + { + "id": { + "display_id": "TAG-45", + "id": "don:core:dvrv-us-1:devo/XXXXXX:tag/45", + "id_v1": "don:DEV-XXXXXX:tag:45", + "name": "src_id", + "style": "{\"color\":{\"code\":\"#991199\",\"name\":\"CustomPurple\"}}", + "style_new": { + "color": "#991199" + } + }, + "tag": { + "display_id": "TAG-45", + "id": "don:core:dvrv-us-1:devo/XXXXXX:tag/45", + "id_v1": "don:DEV-XXXXXX:tag:45", + "name": "src_id", + "style": "{\"color\":{\"code\":\"#991199\",\"name\":\"CustomPurple\"}}", + "style_new": { + "color": "#991199" + } + }, + "value": "980305" + }, + { + "id": { + "display_id": "TAG-22", + "id": "don:core:dvrv-us-1:devo/XXXXXX:tag/22", + "id_v1": "don:DEV-XXXXXX:tag:22", + "name": "Jira import", + "style": "{\"color\":{\"code\":\"#AAF0BD\",\"name\":\"Mint\"}}", + "style_new": { + "color": "#AAF0BD" + } + }, + "tag": { + "display_id": "TAG-22", + "id": "don:core:dvrv-us-1:devo/XXXXXX:tag/22", + "id_v1": "don:DEV-XXXXXX:tag:22", + "name": "Jira import", + "style": "{\"color\":{\"code\":\"#AAF0BD\",\"name\":\"Mint\"}}", + "style_new": { + "color": "#AAF0BD" + } + }, + "value": "c6e7ee09-c708-4b42-bfc1-a1d054a66c9e" + } + ], + "title": "Internal Associates Showing up as External Customers" + } + } + }, + "context": { + "dev_oid": "don:identity:dvrv-us-1:devo/XXXXXX", + "automation_id": "don:integration:dvrv-us-1:devo/XXXXXX:automation/d6776732-c90e-4dd0-aa21-3f7115dc965f", + "source_id": "don:integration:dvrv-us-1:devo/XXXXXX:automation/d6776732-c90e-4dd0-aa21-3f7115dc965f", + "snap_in_id": "don:integration:dvrv-us-1:devo/XXXXXX:snap_in/6525667a-7e10-4c13-bb21-a69f779fabcd", + "snap_in_version_id": "don:integration:dvrv-us-1:devo/XXXXXX:snap_in_package/401582f8-c516-4b74-9783-da592989e220:snap_in_version/20b7f15b-3f58-4806-b124-b86953b90bb1", + "service_account_id": "don:identity:dvrv-us-1:devo/XXXXXX:svcacc/10", + "secrets": { + "service_account_token": "XXXXXXXXXX" + } + }, + "execution_metadata": { + "request_id": "", + "function_name": "function_1", + "event_type": "work_created", + "devrev_endpoint": "https://api.devrev.ai" + }, + "input_data": { + "global_values": {}, + "event_sources": { + "devrev-webhook": "don:integration:dvrv-us-1:devo/XXXXXX:event_source/62d7cea0-7529-42a2-949d-550e0c93ac6a" + }, + "keyrings": null, + "resources": { "keyrings": {}, "tags": {} } + } + }] \ No newline at end of file diff --git a/4-sample-snap-in/code/src/fixtures/function_2_event.json b/4-sample-snap-in/code/src/fixtures/function_2_event.json new file mode 100644 index 0000000..25e77e7 --- /dev/null +++ b/4-sample-snap-in/code/src/fixtures/function_2_event.json @@ -0,0 +1,38 @@ +[ + { + "payload": { + "actor_id": "don:identity:dvrv-us-1:devo/XXXXXX:devu/1", + "command_id": "don:integration:dvrv-us-1:devo/XXXXXX:namespace/devrev:command/comment_here", + "dev_org": "don:identity:dvrv-us-1:devo/XXXXXX", + "parameters": "", + "parent_id": "don:integration:dvrv-us-1:devo/XXXXXX:snap_in/1663a15e-8369-4260-8952-6d68bfd316c8", + "request_id": "XXXXXXXXXXX", + "source_id": "don:core:dvrv-us-1:devo/XXXXXX:issue/31" + }, + "context": { + "dev_oid": "don:identity:dvrv-us-1:devo/XXXXXX", + "automation_id": "", + "source_id": "don:integration:dvrv-us-1:devo/XXXXXX:namespace/devrev:command/comment_here", + "snap_in_id": "don:integration:dvrv-us-1:devo/XXXXXX:snap_in/1663a15e-8369-4260-8952-6d68bfd316c8", + "snap_in_version_id": "don:integration:dvrv-us-1:devo/XXXXXX:snap_in_package/80e5139a-8475-4230-ae56-3f2f07702c14:snap_in_version/9b524f10-1f46-4456-a356-73dc5266a4f3", + "service_account_id": "", + "secrets": { + "actor_session_token": "XXXXX.XXXX.XXXX", + "service_account_token": "XXXXX.XXXXXX" + } + }, + "execution_metadata": { + "request_id": "XXXXXXXXXXX", + "function_name": "function_2", + "devrev_endpoint": "https://api.devrev.ai" + }, + "input_data": { + "global_values": {}, + "event_sources": { + "devrev-webhook": "don:integration:dvrv-us-1:devo/XXXXXX:event_source/eaddf652-a78d-4389-8723-786c63360126" + }, + "keyrings": null, + "resources": { "keyrings": {}, "tags": {} } + } + } +] diff --git a/4-sample-snap-in/code/src/function-factory.ts b/4-sample-snap-in/code/src/function-factory.ts new file mode 100644 index 0000000..647c549 --- /dev/null +++ b/4-sample-snap-in/code/src/function-factory.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import function_1 from './functions/function_1/index'; +import function_2 from './functions/function_2/index'; + +export const functionFactory = { + function_1, + function_2, +} as const; + +export type FunctionFactoryType = keyof typeof functionFactory; diff --git a/4-sample-snap-in/code/src/functions/function_1/index.test.ts b/4-sample-snap-in/code/src/functions/function_1/index.test.ts new file mode 100644 index 0000000..84af9c2 --- /dev/null +++ b/4-sample-snap-in/code/src/functions/function_1/index.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { testRunner } from '../../test-runner/test-runner'; +describe('Example Index Test file', () => { + it('Testing the method', () => { + testRunner({ + fixturePath: 'function_1_event.json', + functionName: 'function_1', + }); + }); +}); diff --git a/4-sample-snap-in/code/src/functions/function_1/index.ts b/4-sample-snap-in/code/src/functions/function_1/index.ts new file mode 100644 index 0000000..204c1eb --- /dev/null +++ b/4-sample-snap-in/code/src/functions/function_1/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { client } from "@devrev/typescript-sdk"; + +async function handleEvent( + event: any, +) { + const devrevPAT = event.context.secrets.service_account_token; + const API_BASE = event.execution_metadata.devrev_endpoint; + const devrevSDK = client.setup({ + endpoint: API_BASE, + token: devrevPAT, + }) + const workCreated = event.payload.work_created.work; + const bodyComment = 'Hello World is printed on the work ' + workCreated.display_id + ' from the automation.'; + const body = { + object: workCreated.id, + type: 'timeline_comment', + body: bodyComment, + } + const response = await devrevSDK.timelineEntriesCreate(body as any); + return response; + +} + +export const run = async (events: any[]) => { + console.info('events', JSON.stringify(events), '\n\n\n'); + for (let event of events) { + const resp = await handleEvent(event); + console.log(JSON.stringify(resp.data)); + } +}; + +export default run; diff --git a/4-sample-snap-in/code/src/functions/function_2/index.test.ts b/4-sample-snap-in/code/src/functions/function_2/index.test.ts new file mode 100644 index 0000000..b77bd3a --- /dev/null +++ b/4-sample-snap-in/code/src/functions/function_2/index.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { testRunner } from '../../test-runner/test-runner'; +describe('Example Index Test file', () => { + it('Testing the method', () => { + testRunner({ + fixturePath: 'function_2_event.json', + functionName: 'function_2', + }); + }); +}); diff --git a/4-sample-snap-in/code/src/functions/function_2/index.ts b/4-sample-snap-in/code/src/functions/function_2/index.ts new file mode 100644 index 0000000..a9132d8 --- /dev/null +++ b/4-sample-snap-in/code/src/functions/function_2/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { client } from "@devrev/typescript-sdk"; + +async function handleEvent( + event: any, +) { + const devrevPAT = event.context.secrets.service_account_token; + const API_BASE = event.execution_metadata.devrev_endpoint; + const devrevSDK = client.setup({ + endpoint: API_BASE, + token: devrevPAT, + }) + const workCreated = event.payload.source_id; + const bodyComment = 'Hello World is printed on the work from the command.'; + const body = { + object: workCreated, + type: 'timeline_comment', + body: bodyComment, + } + const response = await devrevSDK.timelineEntriesCreate(body as any); + return response; + +} + +export const run = async (events: any[]) => { + console.info('events', JSON.stringify(events), '\n\n\n'); + for (let event of events) { + const resp = await handleEvent(event); + console.log(JSON.stringify(resp.data)); + } +}; + +export default run; diff --git a/4-sample-snap-in/code/src/index.ts b/4-sample-snap-in/code/src/index.ts new file mode 100644 index 0000000..7c83b87 --- /dev/null +++ b/4-sample-snap-in/code/src/index.ts @@ -0,0 +1,5 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +export * from './function-factory'; diff --git a/4-sample-snap-in/code/src/main.ts b/4-sample-snap-in/code/src/main.ts new file mode 100644 index 0000000..7874c8c --- /dev/null +++ b/4-sample-snap-in/code/src/main.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { FunctionFactoryType } from './function-factory'; +import { testRunner } from './test-runner/test-runner'; + +(async () => { + const argv = await yargs(hideBin(process.argv)).options({ + fixturePath: { + type: 'string', + require: true, + }, + functionName: { + type: 'string', + require: true, + }, + }).argv; + + if (!argv.fixturePath || !argv.functionName) { + console.error( + 'Please make sure you have passed fixturePath & functionName' + ); + } + + await testRunner({ + fixturePath: argv.fixturePath, + functionName: argv.functionName as FunctionFactoryType, + }); +})(); diff --git a/4-sample-snap-in/code/src/test-runner/example.test.ts b/4-sample-snap-in/code/src/test-runner/example.test.ts new file mode 100644 index 0000000..88fa919 --- /dev/null +++ b/4-sample-snap-in/code/src/test-runner/example.test.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { run } from '../functions/function_1'; + +describe('Test some function', () => { + it('Something', () => { + run([]); + }); +}); diff --git a/4-sample-snap-in/code/src/test-runner/test-runner.ts b/4-sample-snap-in/code/src/test-runner/test-runner.ts new file mode 100644 index 0000000..8626f82 --- /dev/null +++ b/4-sample-snap-in/code/src/test-runner/test-runner.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import * as dotenv from 'dotenv'; +import { functionFactory, FunctionFactoryType } from '../function-factory'; + +export interface TestRunnerProps { + functionName: FunctionFactoryType; + fixturePath: string; +} + +export const testRunner = async ({ functionName, fixturePath }: TestRunnerProps) => { + const env = dotenv.config(); + + //TODO: Pass this config to run method + console.info(env.parsed?.APP_SECRET_TEST); + + if (!functionFactory[functionName]) { + console.error(`${functionName} is not found in the functionFactory`); + console.error('Add your function to the function-factory.ts file'); + throw new Error('Function is not found in the functionFactory'); + } + + const run = functionFactory[functionName]; + + const eventFixture = require(`../fixtures/${fixturePath}`); + + await run(eventFixture); +}; diff --git a/4-sample-snap-in/code/tsconfig.eslint.json b/4-sample-snap-in/code/tsconfig.eslint.json new file mode 100644 index 0000000..c8722d7 --- /dev/null +++ b/4-sample-snap-in/code/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["./**/*.ts", "./**/*.js", "./.*.js"] +} diff --git a/4-sample-snap-in/code/tsconfig.json b/4-sample-snap-in/code/tsconfig.json new file mode 100644 index 0000000..5f0d03c --- /dev/null +++ b/4-sample-snap-in/code/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "baseUrl": "./", + "paths": { + "*": ["./src/*"] + }, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}