From b6a59d92a684b437f3e20a21c5f262774f7183f6 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Wed, 12 Feb 2025 08:13:31 -0500 Subject: [PATCH 01/13] creating swagger-docs --- package-lock.json | 245 ++++++++++++++++++++++++++++++++++++- package.json | 6 +- src/app.ts | 6 + src/swagger.ts | 30 +++++ src/swagger/definitions.ts | 240 ++++++++++++++++++++++++++++++++++++ 5 files changed, 522 insertions(+), 5 deletions(-) create mode 100644 src/swagger.ts create mode 100644 src/swagger/definitions.ts diff --git a/package-lock.json b/package-lock.json index b715597..dea6da0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "axios": "^1.4.0", "dotenv": "^16.1.4", "express": "^4.17.1", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@faker-js/faker": "^8.4.0", @@ -25,6 +27,8 @@ "@types/node": "^14.18.63", "@types/prompts": "^2.4.4", "@types/supertest": "^6.0.2", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "eslint": "^8.56.0", @@ -62,6 +66,50 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2997,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@ngrok/ngrok": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-0.5.2.tgz", @@ -3335,6 +3389,13 @@ "@prisma/debug": "5.9.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3539,8 +3600,7 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/methods": { "version": "1.1.4", @@ -3688,6 +3748,24 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", + "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4842,6 +4920,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5071,6 +5155,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -5501,7 +5594,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -10812,6 +10904,13 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -10824,6 +10923,12 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -11501,6 +11606,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -13367,6 +13479,83 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", + "integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14021,6 +14210,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -14258,6 +14456,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -14316,6 +14523,36 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/package.json b/package.json index 8b6f7f6..44ec165 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "axios": "^1.4.0", "dotenv": "^16.1.4", "express": "^4.17.1", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@faker-js/faker": "^8.4.0", @@ -40,6 +42,8 @@ "@types/node": "^14.18.63", "@types/prompts": "^2.4.4", "@types/supertest": "^6.0.2", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "eslint": "^8.56.0", diff --git a/src/app.ts b/src/app.ts index 0eb40c8..2b79e4f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,6 +8,10 @@ import { prisma } from './clients'; import handleError from './utils/error'; import { logger } from './utils/logger'; import { Server } from 'http'; +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; +import { commonSchemas, apiEndpoints } from './swagger/definitions'; +import { specs } from './swagger'; const app: Application = express(); app.use(express.json()); @@ -100,6 +104,8 @@ app.get('/initial-contacts-sync', async (req: Request, res: Response) => { } }); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); + let server: Server | null = null; function startServer() { diff --git a/src/swagger.ts b/src/swagger.ts new file mode 100644 index 0000000..3f96f77 --- /dev/null +++ b/src/swagger.ts @@ -0,0 +1,30 @@ +import { apiEndpoints, commonSchemas } from './swagger/definitions'; +import { PORT } from './utils/utils'; +import swaggerJsdoc from 'swagger-jsdoc'; + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'HubSpot Contact Sync API', + version: '1.0.0', + description: + 'API for syncing and managing contacts between the local database and HubSpot' + }, + servers: [ + { + url: `http://localhost:${PORT}`, + description: 'Local development server' + } + ], + components: { + schemas: commonSchemas + }, + paths: apiEndpoints + }, + apis: ['./src/app.ts'] // Since all routes are in app.ts +}; + +const specs = swaggerJsdoc(options); + +export { specs }; diff --git a/src/swagger/definitions.ts b/src/swagger/definitions.ts new file mode 100644 index 0000000..9e9f9bb --- /dev/null +++ b/src/swagger/definitions.ts @@ -0,0 +1,240 @@ +export const commonSchemas = { + Contact: { + type: 'object', + properties: { + id: { + type: 'string' + }, + email: { + type: 'string' + }, + firstName: { + type: 'string' + }, + lastName: { + type: 'string' + }, + hubspotId: { + type: 'string' + }, + createdAt: { + type: 'string', + format: 'date-time' + }, + updatedAt: { + type: 'string', + format: 'date-time' + } + } + }, + Error: { + type: 'object', + properties: { + message: { + type: 'string', + description: 'Error message' + } + } + }, + SyncResults: { + type: 'object', + properties: { + success: { + type: 'boolean' + }, + message: { + type: 'string' + }, + syncedCount: { + type: 'number' + }, + errors: { + type: 'array', + items: { + type: 'string' + } + } + } + } +}; + +export const apiEndpoints = { + '/contacts': { + get: { + summary: 'Get all contacts', + description: 'Retrieves all contacts from the local database', + responses: { + '200': { + description: 'Successfully retrieved contacts', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Contact' + } + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/api/install': { + get: { + summary: 'Get HubSpot installation URL', + description: + 'Returns an HTML page with the HubSpot OAuth installation link', + responses: { + '200': { + description: 'HTML page with installation link', + content: { + 'text/html': { + schema: { + type: 'string' + } + } + } + } + } + } + }, + '/sync-contacts': { + get: { + summary: 'Sync contacts to HubSpot', + description: 'Synchronizes contacts from local database to HubSpot', + responses: { + '200': { + description: 'Sync results', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SyncResults' + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/': { + get: { + summary: 'Get access token', + description: + 'Retrieves the HubSpot access token for the current customer', + responses: { + '200': { + description: 'Access token retrieved successfully', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/oauth-callback': { + get: { + summary: 'OAuth callback endpoint', + description: 'Handles the OAuth callback from HubSpot', + parameters: [ + { + in: 'query', + name: 'code', + schema: { + type: 'string' + }, + required: true, + description: 'OAuth authorization code' + } + ], + responses: { + '302': { + description: 'Redirect to home page after successful OAuth' + }, + '400': { + description: 'Missing code parameter', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/initial-contacts-sync': { + get: { + summary: 'Initial contacts sync from HubSpot', + description: + 'Performs initial synchronization of contacts from HubSpot to local database', + responses: { + '200': { + description: 'Sync results', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SyncResults' + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + } +}; From 864d826714de1c536aaa06049606b39e573b8490 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Wed, 12 Feb 2025 08:13:43 -0500 Subject: [PATCH 02/13] updating readme with template --- README.md | 138 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index c6efef9..30b57ea 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ # CRM Object Sync +A demonstration of best integration practices for syncing CRM contact records between HubSpot and external applications for product management use cases. + +## Table of Contents + +- [What this project does](#what-this-project-does) +- [Why is this project useful?](#why-is-this-project-useful) +- [Setup](#setup) +- [Scopes](#scopes) +- [Endpoints](#endpoints) + - [Authentication](#authentication) + - [Contact Management](#contact-management) +- [Available Scripts](#available-scripts) +- [Project Structure](#project-structure) +- [Dependencies](#dependencies) + - [Core](#core) + - [Development](#development) +- [Support](#support) +- [Contributors](#contributors) + ## What this project does: -This CRM Object Sync repository demonstrates best integration practices for syncing CRM  contact records between HubSpot and external applications for a product management use case. +This CRM Object Sync repository demonstrates best integration practices for syncing CRM contact records between HubSpot and external applications for a product management use case. ## Why is this project useful: @@ -12,70 +31,109 @@ This project demonstrates how to: - Create and seed a PostgreSQL database with contact records -- Sync of seeded contact records from the database to HubSpot, saving the generated `hs_object_id`  back to the database +- Sync seeded contact records from the database to HubSpot, saving the generated hs_object_id back to the database - Sync contact records from HubSpot to the database: - - The default sync option uses the Prisma upsert, matching by email. If there is a record match, it just adds the `hs_object_id` to the existing record. If the contact has no email, it creates a new record in the database. The job results will indicate how many records are upsert and the number of new records without email that were created. + - The default sync option uses the Prisma upsert, matching by email. If there is a record match, it just adds the hs_object_id to the existing record. If the contact has no email, it creates a new record in the database. The job results will indicate how many records are upsert and the number of new records without email that were created. + + - The second option has more verbose reporting. It tries to create a new record in the database. If there's already a record with a matching email, it adds the hs_object_id to the existing record. Contacts without email are just created as normal. The results will indicate the number of records created (with or without email) and the number of existing records that the hs_object_id was added to. + +## Setup - - The second option has more verbose reporting. It tries to create a new record in the database. If there's already a record with a matching email, it adds the `hs_object_id` to the existing record. Contacts without email are just created as normal. The results will indicate the number of records created (with or without email) and the number of existing records that the `hs_object_id` was added to. +1. **Prerequisites** -## Endpoints: + - Install [PostgreSQL](https://www.postgresql.org/download/) + - Create an empty database + - Have HubSpot public app credentials ready -- **GET /api/install**: Sends a simple HTML response containing a link (authUrl) for users to authenticate. The link opens in a new tab when clicked. This should be the first step a new user or client performs to initiate the OAuth2 authorization process. +2. **Install Dependencies** -- **GET /oauth-callback**: It processes the authorization code to obtain an access token for the user and any failure in retrieving it redirects with an error message. +```bash +npm install +``` -- **GET /** : Once authenticated, the access token can be retrieved using this endpoint. This ensures that any subsequent API operations requiring authentication can be performed. +- Download and install PostgreSQL, make sure it's running, and create an empty database. You need the username and password (defaults username is postgres and no password) +- Clone the repo +- Create the .env file with these entries: + - DATABASE_URL the (local) url to the postgres database (e.g. postgresql://{username}:{password}@localhost:5432/{database name}) + - CLIENT_ID from Hubspot public app + - CLIENT_SECRET from Hubspot public app +- Run `npm install` to install the required Node packages. +- In your HubSpot public app, add `localhost:3001/api/install/oauth-callback` as a redirect URL + Run npm run dev to start the server + Visit http://localhost:3001/api/install in a browser to get the OAuth install link + -Run `npm run seed` to seed the database with test data, select an industry for the data examples + -Once the server is running, you can access the application and API documentation at http://localhost:3001/api-docs. -- **GET /initial-contacts-sync**: After establishing authentication and obtaining an access token, the initial **synchronization of contacts from HubSpot to the local database** can occur. +## Endpoints -- **GET /contacts**: This endpoint fetches contacts from the local database. +### Authentication -- **GET /sync-contacts**: This is used to **synchronize any updates or new contact data from the local database to HubSpot**. Email is used as a primary key for logical deduplication, making it crucial that email addresses are correctly managed and non-null where possible. To minimize errors, we first retrieve existing contacts from HubSpot and exclude those already known from our batch. The following methods are employed to send new contacts to HubSpot and to store their HubSpot object IDs back in our local database. +- `GET /api/install` - Returns installation page with HubSpot OAuth link +- `GET /oauth-callback` - Processes OAuth authorization code +- `GET /` - Retrieves access token for authenticated user -## Getting started with the project: +### Contact Management -Setup: +- `GET /contacts` - Fetches contacts from local database +- `GET /initial-contacts-sync` - Syncs contacts from HubSpot to local database +- `GET /sync-contacts` - Syncs contacts from local database to HubSpot + - Uses email as primary key for deduplication + - Excludes existing HubSpot contacts from sync batch -1. Download and install [PostgreSQL](https://www.postgresql.org/download/), make sure it's running, and create an empty database. You need the username and password (defaults username is postgres and no password) +Each scope enables specific functionality for managing contacts and companies in HubSpot CRM. -2. Clone the repo +## Endpoints -3. Create the .env file with these entries (see examples in the [.env.example](./.env.example) file): +### Authentication -- DATABASE_URL the (local) url to the postgres database (e.g. `postgresql://{username}:{password}@localhost:5432/{database name}` +- `GET /api/install` - Returns installation page with HubSpot OAuth link +- `GET /oauth-callback` - Processes OAuth authorization code +- `GET /` - Retrieves access token for authenticated user -- CLIENT_ID from Hubspot public app +### Contact Management -- CLIENT_SECRET from Hubspot public app +- `GET /contacts` - Fetches contacts from local database +- `GET /initial-contacts-sync` - Syncs contacts from HubSpot to local database +- `GET /sync-contacts` - Syncs contacts from local database to HubSpot + - Uses email as primary key for deduplication + - Excludes existing HubSpot contacts from sync batch -4. Run `npm install` to install the required Node packages. +## Scopes -5. Run `npm run db-init` to create the necessary tables in PostgreSQL +- `crm.schemas.companies.write` +- `crm.schemas.contacts.write` +- `crm.schemas.companies.read` +- `crm.schemas.contacts.read` +- `crm.objects.companies.write` +- `crm.objects.contacts.write` +- `crm.objects.companies.read` +- `crm.objects.contacts.read` -6. Optional: Run `npm run db-seed` to seed the database with test data +## Available Scripts -7. In your [HubSpot public app](https://developers.hubspot.com/docs/api/creating-an-app), add `localhost:3000/oauth-callback` as a redirect URL +- `npm run dev` - Start development server +- `npm run db-init` - Initialize database tables +- `npm run db-seed` - Seed database with test data +- `npm test` - Run tests +- `npm run test:coverage` - Generate test coverage report -8. The app uses the following scopes: +## Dependencies -- crm.objects.contacts.read -- crm.objects.contacts.write -- crm.objects.companies.read -- crm.objects.companies.write -- crm.schemas.contacts.read -- crm.schemas.contacts.write -- crm.schemas.companies.read -- crm.schemas.companies.write +### Core -9. Run `npm run dev` to start the server +- Express +- Prisma +- PostgreSQL +- HubSpot Client Libraries -10. Visit `http://localhost:3000/api/install` in a browser to get the OAuth install link +### Development -## Testing: -To execute the tests, use the following command `npm test`. -To check the test coverage report use `npm run test:coverage`. +- Jest +- TypeScript +- ESLint +- Prettier ## Where to get help? @@ -83,4 +141,8 @@ If you encounter any bugs or issues, please report them by opening a GitHub issu ## Who maintains and contributes to this project -Various teams at HubSpot that focus on developer experience and app marketplace quality maintain and contribute to this project. In particular, this project was made possible by @therealdadams, @rahmona-henry and @zman81988 +Various teams at HubSpot that focus on developer experience and app marketplace quality maintain and contribute to this project. In particular, this project was made possible by @therealdadams, @rahmona-henry, @zman81988, @natalijabujevic0708, and @zradford + +## License + +MIT From f1224d80aa952cd6410727e0d0daf31a7a2de669 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Thu, 13 Feb 2025 17:40:41 -0500 Subject: [PATCH 03/13] implementing setAccessToken --- dist/prisma/seed.js | 59 ++++++++-------- dist/src/app.js | 132 ++++++++++++++++++++++++++---------- dist/src/auth.js | 113 ++++++++++++++++-------------- package.json | 5 +- src/app.ts | 2 - src/auth.ts | 24 ++++++- src/initialSyncToHubSpot.ts | 8 +-- 7 files changed, 215 insertions(+), 128 deletions(-) diff --git a/dist/prisma/seed.js b/dist/prisma/seed.js index a5929f0..7239f67 100644 --- a/dist/prisma/seed.js +++ b/dist/prisma/seed.js @@ -1,38 +1,33 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.main = void 0; +const faker_1 = require("@faker-js/faker"); const client_1 = require("@prisma/client"); -const contacts_1 = require("./contacts"); const prisma = new client_1.PrismaClient(); -async function main() { - for (let contact of contacts_1.contacts) { - await prisma.contacts.create({ - data: contact - }); - } -} -main().catch(e => { - console.log(e); - process.exit(1); -}).finally(() => { - prisma.$disconnect(); -}); /*Create dataset, mapping over an array*/ -// const data = Array.from({ length:100 }).map(() => ({ -// firstName: faker.person.firstName(), -// lastName: faker.person.lastName(), -// email: faker.internet.email() -// })); +const data = Array.from({ length: 1000 }).map(() => ({ + first_name: faker_1.faker.person.firstName(), + last_name: faker_1.faker.person.lastName(), + email: faker_1.faker.internet.email().toLowerCase() //normalize before adding to db +})); /*Run seed command and the function below inserts data in the database*/ -// async function main(){ -// await prisma.contacts.createMany({ -// data -// }); -// } -// main() -// .catch((e) => { -// console.log(e); -// process.exit(1) -// }) -// .finally(() => { -// prisma.$disconnect(); -// }) +async function main() { + console.log(`=== Generated ${data.length} contacts ===`); + await prisma.contacts.createMany({ + data, + skipDuplicates: true // fakerjs will repeat emails + }); +} +exports.main = main; +// Only run if this file is being executed directly +if (require.main === module) { + main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +} +exports.default = prisma; diff --git a/dist/src/app.js b/dist/src/app.js index b80a51f..e1b433c 100644 --- a/dist/src/app.js +++ b/dist/src/app.js @@ -3,57 +3,121 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.startServer = exports.server = exports.app = void 0; const express_1 = __importDefault(require("express")); -const client_1 = require("@prisma/client"); const auth_1 = require("./auth"); require("dotenv/config"); -const utils_1 = require("./utils"); -const prisma = new client_1.PrismaClient(); +const utils_1 = require("./utils/utils"); +const initialSyncFromHubSpot_1 = require("./initialSyncFromHubSpot"); +const initialSyncToHubSpot_1 = require("./initialSyncToHubSpot"); +const clients_1 = require("./clients"); +const error_1 = __importDefault(require("./utils/error")); +const logger_1 = require("./utils/logger"); +const swagger_ui_express_1 = __importDefault(require("swagger-ui-express")); +const swagger_1 = require("./swagger"); const app = (0, express_1.default)(); +exports.app = app; app.use(express_1.default.json()); app.use(express_1.default.urlencoded({ extended: true })); app.get('/contacts', async (req, res) => { - const prisma = new client_1.PrismaClient(); - const contacts = await prisma.contacts.findMany({}); - res.send(contacts); + try { + const contacts = await clients_1.prisma.contacts.findMany({}); + res.send(contacts); + } + catch (error) { + (0, error_1.default)(error, 'Error fetching contacts'); + res + .status(500) + .json({ message: 'An error occurred while fetching contacts.' }); + } +}); +app.get('/api/install', (req, res) => { + res.send(`${auth_1.authUrl}`); +}); +app.get('/sync-contacts', async (req, res) => { + try { + const syncResults = await (0, initialSyncToHubSpot_1.syncContactsToHubSpot)(); + res.send(syncResults); + } + catch (error) { + (0, error_1.default)(error, 'Error syncing contacts'); + res + .status(500) + .json({ message: 'An error occurred while syncing contacts.' }); + } }); -app.get("/api/install", (req, res) => { - res.send(auth_1.authUrl); +app.get('/', async (req, res) => { + try { + const accessToken = await (0, auth_1.getAccessToken)((0, utils_1.getCustomerId)()); + res.send(accessToken); + } + catch (error) { + (0, error_1.default)(error, 'Error fetching access token'); + res + .status(500) + .json({ message: 'An error occurred while fetching the access token.' }); + } }); -app.get("/oauth-callback", async (req, res) => { +app.get('/oauth-callback', async (req, res) => { const code = req.query.code; if (code) { try { const authInfo = await (0, auth_1.redeemCode)(code.toString()); const accessToken = authInfo.accessToken; - res.redirect(`http://localhost:${utils_1.PORT - 1}/`); + logger_1.logger.info({ + type: 'HubSpot', + logMessage: { + message: 'OAuth complete!' + } + }); + res.redirect(`http://localhost:${utils_1.PORT}/`); } catch (error) { - res.redirect(`/?errMessage=${error.message}`); + (0, error_1.default)(error, 'Error redeeming code during OAuth'); + res.redirect(`/?errMessage=${error.message || 'An error occurred during the OAuth process.'}`); } } + else { + logger_1.logger.error({ + type: 'HubSpot', + logMessage: { + message: 'Error: code parameter is missing.' + } + }); + res + .status(400) + .json({ message: 'Code parameter is missing in the query string.' }); + } }); -// app.post("/addcontacts", async (req, res) => { -// try{ -// const {id, email, first_name, last_name, hs_object_id} = req.body -// const newContact = await prisma.contacts.create({ -// data: { -// id, -// email, -// first_name, -// last_name, -// hs_object_id -// } -// }) -// res.json(newContact) -// } -// catch (error:any) { -// console.log(error.message) -// res.status(500).json({ -// message: "Internal Server Error", -// }) -// } -// }) -app.listen(utils_1.PORT, function () { - console.log('App is listening on port ${port}'); +app.get('/initial-contacts-sync', async (req, res) => { + try { + const syncResults = await (0, initialSyncFromHubSpot_1.initialContactsSync)(); + res.send(syncResults); + } + catch (error) { + (0, error_1.default)(error, 'Error during initial contacts sync'); + res + .status(500) + .json({ message: 'An error occurred during the initial contacts sync.' }); + } }); +app.use('/api-docs', swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup(swagger_1.specs)); +let server = null; +exports.server = server; +function startServer() { + if (!server) { + exports.server = server = app.listen(utils_1.PORT, function (err) { + if (err) { + console.error('Error starting server:', err); + return; + } + console.log(`App is listening on port ${utils_1.PORT}`); + }); + } + return server; +} +exports.startServer = startServer; +// Start the server only if this file is run directly +if (require.main === module) { + startServer(); +} diff --git a/dist/src/auth.js b/dist/src/auth.js index b1b7324..49910fd 100644 --- a/dist/src/auth.js +++ b/dist/src/auth.js @@ -1,51 +1,47 @@ "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAccessToken = exports.redeemCode = exports.exchangeForTokens = exports.authUrl = void 0; +exports.setAccessToken = exports.getAccessToken = exports.redeemCode = exports.exchangeForTokens = exports.authUrl = void 0; require("dotenv/config"); -const hubspot = __importStar(require("@hubspot/api-client")); const client_1 = require("@prisma/client"); -const utils_1 = require("./utils"); -const CLIENT_ID = process.env.CLIENT_ID || "CLIENT_ID required"; -const CLIENT_SECRET = process.env.CLIENT_SECRET || "CLIENT_SECRET required"; +const utils_1 = require("./utils/utils"); +const clients_1 = require("./clients"); +const error_1 = __importDefault(require("./utils/error")); +class MissingRequiredError extends Error { + constructor(message, options) { + message = message + 'is missing, please add it to your .env file'; + super(message, options); + } +} +const CLIENT_ID = process.env.CLIENT_ID; +const CLIENT_SECRET = process.env.CLIENT_SECRET; +if (!CLIENT_ID) { + throw new MissingRequiredError('CLIENT_ID ', undefined); +} +if (!CLIENT_SECRET) { + throw new MissingRequiredError('CLIENT_SECRET ', undefined); +} const REDIRECT_URI = `http://localhost:${utils_1.PORT}/oauth-callback`; const SCOPES = [ - "crm.schemas.companies.write", - "crm.schemas.contacts.write", - "crm.schemas.companies.read", - "crm.schemas.contacts.read", + 'crm.schemas.companies.write', + 'crm.schemas.contacts.write', + 'crm.schemas.companies.read', + 'crm.schemas.contacts.read', + 'crm.objects.companies.write', + 'crm.objects.contacts.write', + 'crm.objects.companies.read', + 'crm.objects.contacts.read' ]; const EXCHANGE_CONSTANTS = { redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, + client_secret: CLIENT_SECRET }; -const hubspotClient = new hubspot.Client(); const prisma = new client_1.PrismaClient(); -const scopeString = SCOPES.toString().replaceAll(",", " "); -const authUrl = hubspotClient.oauth.getAuthorizationUrl(CLIENT_ID, REDIRECT_URI, scopeString); +const scopeString = SCOPES.toString().replaceAll(',', ' '); +const authUrl = clients_1.hubspotClient.oauth.getAuthorizationUrl(CLIENT_ID, REDIRECT_URI, scopeString); exports.authUrl = authUrl; const getExpiresAt = (expiresIn) => { const now = new Date(); @@ -55,23 +51,23 @@ const redeemCode = async (code) => { return await exchangeForTokens({ ...EXCHANGE_CONSTANTS, code, - grant_type: "authorization_code", + grant_type: 'authorization_code' }); }; exports.redeemCode = redeemCode; const getHubSpotId = async (accessToken) => { - hubspotClient.setAccessToken(accessToken); - const hubspotAccountInfoResponse = await hubspotClient.apiRequest({ - path: "/account-info/v3/details", - method: "GET", + clients_1.hubspotClient.setAccessToken(accessToken); + const hubspotAccountInfoResponse = await clients_1.hubspotClient.apiRequest({ + path: '/account-info/v3/details', + method: 'GET' }); const hubspotAccountInfo = await hubspotAccountInfoResponse.json(); const hubSpotportalId = hubspotAccountInfo.portalId; return hubSpotportalId.toString(); }; const exchangeForTokens = async (exchangeProof) => { - const { code, redirect_uri, client_id, client_secret, grant_type, refresh_token, } = exchangeProof; - const tokenResponse = await hubspotClient.oauth.tokensApi.createToken(grant_type, code, redirect_uri, client_id, client_secret, refresh_token); + const { code, redirect_uri, client_id, client_secret, grant_type, refresh_token } = exchangeProof; + const tokenResponse = await clients_1.hubspotClient.oauth.tokensApi.create(grant_type, code, redirect_uri, client_id, client_secret, refresh_token); try { const accessToken = tokenResponse.accessToken; const refreshToken = tokenResponse.refreshToken; @@ -81,14 +77,14 @@ const exchangeForTokens = async (exchangeProof) => { const hsPortalId = await getHubSpotId(accessToken); const tokenInfo = await prisma.authorization.upsert({ where: { - customerId: customerId, + customerId: customerId }, update: { refreshToken, accessToken, expiresIn, expiresAt, - hsPortalId, + hsPortalId }, create: { refreshToken, @@ -96,8 +92,8 @@ const exchangeForTokens = async (exchangeProof) => { expiresIn, expiresAt, hsPortalId, - customerId, - }, + customerId + } }); return tokenInfo; } @@ -114,11 +110,11 @@ const getAccessToken = async (customerId) => { select: { accessToken: true, expiresAt: true, - refreshToken: true, + refreshToken: true }, where: { - customerId, - }, + customerId + } })); if (currentCreds?.expiresAt && currentCreds?.expiresAt > new Date()) { return currentCreds?.accessToken; @@ -126,8 +122,8 @@ const getAccessToken = async (customerId) => { else { const updatedCreds = await exchangeForTokens({ ...EXCHANGE_CONSTANTS, - grant_type: "refresh_token", - refresh_token: currentCreds?.refreshToken, + grant_type: 'refresh_token', + refresh_token: currentCreds?.refreshToken }); if (updatedCreds instanceof Error) { throw updatedCreds; @@ -143,3 +139,18 @@ const getAccessToken = async (customerId) => { } }; exports.getAccessToken = getAccessToken; +async function setAccessToken() { + try { + const accessToken = await getAccessToken((0, utils_1.getCustomerId)()); + if (!accessToken) { + throw new Error('No access token returned'); + } + clients_1.hubspotClient.setAccessToken(accessToken); + return clients_1.hubspotClient; + } + catch (error) { + (0, error_1.default)(error, 'Error setting access token'); + throw new Error('Failed to authenticate HubSpot client'); + } +} +exports.setAccessToken = setAccessToken; diff --git a/package.json b/package.json index 44ec165..1b9777e 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,7 @@ "dotenv": "^16.1.4", "express": "^4.17.1", "prompts": "^2.4.2", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1" + "swagger-jsdoc": "^6.2.8" }, "devDependencies": { "@faker-js/faker": "^8.4.0", @@ -42,8 +41,6 @@ "@types/node": "^14.18.63", "@types/prompts": "^2.4.4", "@types/supertest": "^6.0.2", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "eslint": "^8.56.0", diff --git a/src/app.ts b/src/app.ts index 2b79e4f..5f34914 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,9 +8,7 @@ import { prisma } from './clients'; import handleError from './utils/error'; import { logger } from './utils/logger'; import { Server } from 'http'; -import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; -import { commonSchemas, apiEndpoints } from './swagger/definitions'; import { specs } from './swagger'; const app: Application = express(); diff --git a/src/auth.ts b/src/auth.ts index 4b5bf88..660f21c 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,6 +3,8 @@ import 'dotenv/config'; import { Authorization, PrismaClient } from '@prisma/client'; import { PORT, getCustomerId } from './utils/utils'; import { hubspotClient } from './clients'; +import { Client } from '@hubspot/api-client'; +import handleError from './utils/error'; interface ExchangeProof { grant_type: string; @@ -175,4 +177,24 @@ const getAccessToken = async (customerId: string): Promise => { } }; -export { authUrl, exchangeForTokens, redeemCode, getAccessToken }; +async function setAccessToken(): Promise { + try { + const accessToken = await getAccessToken(getCustomerId()); + if (!accessToken) { + throw new Error('No access token returned'); + } + hubspotClient.setAccessToken(accessToken); + return hubspotClient; + } catch (error) { + handleError(error, 'Error setting access token'); + throw new Error('Failed to authenticate HubSpot client'); + } +} + +export { + authUrl, + exchangeForTokens, + redeemCode, + getAccessToken, + setAccessToken +}; diff --git a/src/initialSyncToHubSpot.ts b/src/initialSyncToHubSpot.ts index b6cf18f..7cdf5d0 100644 --- a/src/initialSyncToHubSpot.ts +++ b/src/initialSyncToHubSpot.ts @@ -2,7 +2,7 @@ import 'dotenv/config'; import { Contacts, PrismaClient } from '@prisma/client'; import { Client } from '@hubspot/api-client'; -import { exchangeForTokens, getAccessToken } from './auth'; +import { setAccessToken } from './auth'; import { getCustomerId } from './utils/utils'; import { BatchReadInputSimplePublicObjectId, @@ -111,9 +111,9 @@ class BatchToBeSynced { } async batchRead() { - const accessToken = await getAccessToken(customerId); - this.hubspotClient.setAccessToken(accessToken); - + // const accessToken = await getAccessToken(customerId); + // this.hubspotClient.setAccessToken(accessToken); + setAccessToken(); try { const response = await this.hubspotClient.crm.contacts.batchApi.read( this.#batchReadInputs From 832706dc36239cc70970d547138edc83ab233ca6 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Thu, 13 Feb 2025 17:50:39 -0500 Subject: [PATCH 04/13] updating readme --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 30b57ea..a68bfc3 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,22 @@ This project demonstrates how to: 1. **Prerequisites** + - Go to [HubSpot Developer Portal](https://developers.hubspot.com/) + - Create a new public app + - Configure the following scopes: + - `crm.objects.contacts.read` + - `crm.objects.contacts.write` + - `crm.objects.companies.read` + - `crm.objects.companies.write` + - `crm.schemas.contacts.read` + - `crm.schemas.contacts.write` + - `crm.schemas.companies.read` + - `crm.schemas.companies.write` + - Add `http://localhost:3001/oauth-callback` as a redirect URL + - Save your Client ID and Client Secret for the next steps - Install [PostgreSQL](https://www.postgresql.org/download/) - Create an empty database - - Have HubSpot public app credentials ready + - Have HubSpot app credentials ready 2. **Install Dependencies** From 9cbba2693657ff0cf7b86ea17b4e6a4b95e947d6 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Fri, 14 Feb 2025 13:16:21 -0500 Subject: [PATCH 05/13] getting rid of duplicate endpoints section and npm command --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index a68bfc3..6c7ecdb 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,6 @@ This project demonstrates how to: 2. **Install Dependencies** -```bash -npm install -``` - - Download and install PostgreSQL, make sure it's running, and create an empty database. You need the username and password (defaults username is postgres and no password) - Clone the repo - Create the .env file with these entries: @@ -89,24 +85,6 @@ npm install ### Contact Management -- `GET /contacts` - Fetches contacts from local database -- `GET /initial-contacts-sync` - Syncs contacts from HubSpot to local database -- `GET /sync-contacts` - Syncs contacts from local database to HubSpot - - Uses email as primary key for deduplication - - Excludes existing HubSpot contacts from sync batch - -Each scope enables specific functionality for managing contacts and companies in HubSpot CRM. - -## Endpoints - -### Authentication - -- `GET /api/install` - Returns installation page with HubSpot OAuth link -- `GET /oauth-callback` - Processes OAuth authorization code -- `GET /` - Retrieves access token for authenticated user - -### Contact Management - - `GET /contacts` - Fetches contacts from local database - `GET /initial-contacts-sync` - Syncs contacts from HubSpot to local database - `GET /sync-contacts` - Syncs contacts from local database to HubSpot From a9febe41b16ce17577fcb63349a89e44a724e011 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Fri, 14 Feb 2025 13:28:31 -0500 Subject: [PATCH 06/13] increasing accesstoken prisma schema --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7d98b4e..30ac5f0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,7 +29,7 @@ model Companies { model Authorization { customerId String @id @db.VarChar(255) hsPortalId String @db.VarChar(255) - accessToken String @db.VarChar(255) + accessToken String @db.VarChar(512) refreshToken String @db.VarChar(255) expiresIn Int? expiresAt DateTime? @db.Timestamp(6) From 39fd8ce3c980c3e24b0e10acbf9907be6f78ca2c Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Tue, 18 Feb 2025 13:31:49 -0500 Subject: [PATCH 07/13] getting rid of unused imports + formatting --- src/initialSyncFromHubSpot.ts | 7 +++---- src/initialSyncToHubSpot.ts | 9 +++------ src/utils/shutdown.ts | 26 +++++++++++++------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/initialSyncFromHubSpot.ts b/src/initialSyncFromHubSpot.ts index ee2e3c6..8919747 100644 --- a/src/initialSyncFromHubSpot.ts +++ b/src/initialSyncFromHubSpot.ts @@ -3,8 +3,7 @@ import 'dotenv/config'; import { Contacts, Prisma } from '@prisma/client'; import { SimplePublicObject } from '@hubspot/api-client/lib/codegen/crm/contacts'; -import { getAccessToken } from './auth'; -import { getCustomerId } from './utils/utils'; + import { hubspotClient, prisma } from './clients'; // Use verbose (but slower) create or update functionality @@ -163,8 +162,8 @@ const verboseCreateOrUpdate = async (contactData: SimplePublicObject) => { // Initial sync FROM HubSpot contacts TO (local) database const initialContactsSync = async () => { console.log('started sync'); - const customerId = getCustomerId(); - const accessToken = await getAccessToken(customerId); + // const customerId = getCustomerId(); + // const accessToken = await getAccessToken(customerId); // Track created/updated/upserted/any errors let jobRunResults: JobRunResults = { diff --git a/src/initialSyncToHubSpot.ts b/src/initialSyncToHubSpot.ts index 7cdf5d0..8d81e04 100644 --- a/src/initialSyncToHubSpot.ts +++ b/src/initialSyncToHubSpot.ts @@ -3,11 +3,10 @@ import 'dotenv/config'; import { Contacts, PrismaClient } from '@prisma/client'; import { Client } from '@hubspot/api-client'; import { setAccessToken } from './auth'; -import { getCustomerId } from './utils/utils'; +// import { getCustomerId } from './utils/utils'; import { BatchReadInputSimplePublicObjectId, BatchResponseSimplePublicObjectStatusEnum, - SimplePublicObjectBatchInput, SimplePublicObjectInputForCreate, BatchResponseSimplePublicObjectWithErrors, BatchResponseSimplePublicObject, @@ -19,7 +18,7 @@ interface KeyedContacts extends Contacts { [key: string]: any; } -const customerId = getCustomerId(); +// const customerId = getCustomerId(); const MAX_BATCH_SIZE = 100; @@ -111,8 +110,6 @@ class BatchToBeSynced { } async batchRead() { - // const accessToken = await getAccessToken(customerId); - // this.hubspotClient.setAccessToken(accessToken); setAccessToken(); try { const response = await this.hubspotClient.crm.contacts.batchApi.read( @@ -266,7 +263,7 @@ const syncContactsToHubSpot = async () => { if (syncCohort.mapOfEmailsToNativeIds.size === 0) { // take the next set of 100 contacts - console.log('all contacts where known, no need to create'); + console.log('all contacts were known, no need to create'); } else { await syncCohort.sendNetNewContactsToHubspot(); diff --git a/src/utils/shutdown.ts b/src/utils/shutdown.ts index d70ea35..da42611 100644 --- a/src/utils/shutdown.ts +++ b/src/utils/shutdown.ts @@ -1,21 +1,21 @@ -import disconnectPrisma from "../../prisma/disconnect"; -import { server } from "../app"; +import disconnectPrisma from '../../prisma/disconnect'; +import { server } from '../app'; async function shutdown(): Promise { try { - console.log("Initiating graceful shutdown..."); + console.log('Initiating graceful shutdown...'); const closeServerPromise = new Promise((resolve, reject) => { if (!server) { - console.log("No server instance to close."); + console.log('No server instance to close.'); resolve(); return; } server.close((err) => { - console.log("Server close callback called."); + console.log('Server close callback called.'); if (err) { - console.error("Error closing the server:", err); + console.error('Error closing the server:', err); reject(err); } else { resolve(); @@ -24,7 +24,7 @@ async function shutdown(): Promise { // Set a timeout in case the server does not close within a reasonable time setTimeout(() => { - console.warn("Forcing server shutdown after timeout."); + console.warn('Forcing server shutdown after timeout.'); resolve(); }, 5000); }); @@ -32,20 +32,20 @@ async function shutdown(): Promise { await Promise.all([ closeServerPromise .then(() => { - console.log("HTTP server closed successfully."); + console.log('HTTP server closed successfully.'); }) .catch((err) => { - console.error("Error during server close:", err); + console.error('Error during server close:', err); }), disconnectPrisma().catch((err) => - console.error("Error during Prisma disconnection:", err), - ), + console.error('Error during Prisma disconnection:', err) + ) ]); - console.log("Graceful shutdown complete."); + console.log('Graceful shutdown complete.'); process.exit(0); } catch (err) { - console.error("Error during shutdown:", err); + console.error('Error during shutdown:', err); process.exit(1); } } From cba1d7d0566b6b76b36814976dc434425462e15f Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Wed, 26 Feb 2025 08:41:07 -0500 Subject: [PATCH 08/13] committing for merge, moved tests --- {src/__tests__ => tests}/app.test.ts | 62 ++++++++++------------------ 1 file changed, 21 insertions(+), 41 deletions(-) rename {src/__tests__ => tests}/app.test.ts (82%) diff --git a/src/__tests__/app.test.ts b/tests/app.test.ts similarity index 82% rename from src/__tests__/app.test.ts rename to tests/app.test.ts index ce60cba..c305085 100644 --- a/src/__tests__/app.test.ts +++ b/tests/app.test.ts @@ -6,21 +6,19 @@ import { jest, beforeEach, afterAll, - afterEach, beforeAll } from '@jest/globals'; import request from 'supertest'; import { Server } from 'http'; -import { app } from '../app'; -import { prisma } from '../clients'; -import { syncContactsToHubSpot } from '../initialSyncToHubSpot'; -import { initialContactsSync } from '../initialSyncFromHubSpot'; -import { redeemCode, getAccessToken, authUrl } from '../auth'; -import { getCustomerId } from '../utils/utils'; -import shutdown from '../utils/shutdown'; +import { app } from '../src/app'; +import { prisma } from '../src/clients'; +import { syncContactsToHubSpot } from '../src/initialSyncToHubSpot'; +import { initialContactsSync } from '../src/initialSyncFromHubSpot'; +import { redeemCode, getAccessToken, authUrl } from '../src/auth'; +import { getCustomerId } from '../src/utils/utils'; // Mock all external dependencies -jest.mock('../clients', () => ({ +jest.mock('../src/clients', () => ({ prisma: { contacts: { findMany: jest.fn() @@ -36,14 +34,14 @@ jest.mock('../clients', () => ({ } })); -jest.mock('../initialSyncToHubSpot'); -jest.mock('../initialSyncFromHubSpot'); -jest.mock('../auth', () => ({ +jest.mock('../src/initialSyncToHubSpot'); +jest.mock('../src/initialSyncFromHubSpot'); +jest.mock('../src/auth', () => ({ authUrl: 'https://app.hubspot.com/oauth/authorize?mock=true', redeemCode: jest.fn(), getAccessToken: jest.fn() })); -jest.mock('../utils/utils'); +jest.mock('../src/utils/utils'); // Mock environment variables process.env.HUBSPOT_CLIENT_ID = 'test-client-id'; @@ -53,7 +51,7 @@ process.env.REDIRECT_URI = 'http://localhost:3000/oauth-callback'; let server: Server; -beforeAll(done => { +beforeAll((done) => { // Find a free port const port = 3001; // or any other port different from 3000 server = app.listen(port, () => { @@ -94,9 +92,7 @@ describe('Express App', () => { mockContacts ); - const response = await request(server) - .get('/contacts') - .expect(200); + const response = await request(server).get('/contacts').expect(200); expect(response.body).toEqual(mockContacts); }); @@ -106,9 +102,7 @@ describe('Express App', () => { [] ); - const response = await request(server) - .get('/contacts') - .expect(200); + const response = await request(server).get('/contacts').expect(200); expect(response.body).toEqual([]); }); @@ -118,9 +112,7 @@ describe('Express App', () => { new Error('Database connection failed') ); - const response = await request(server) - .get('/contacts') - .expect(500); + const response = await request(server).get('/contacts').expect(500); expect(response.body).toEqual({ message: 'An error occurred while fetching contacts.' @@ -132,9 +124,7 @@ describe('Express App', () => { const mockUrl = 'https://app.hubspot.com/oauth/authorize?mock=true'; it('should return installation URL', async () => { - const response = await request(server) - .get('/api/install') - .expect(200); + const response = await request(server).get('/api/install').expect(200); expect(response.text).toContain(mockUrl); expect(response.text).toMatch( @@ -154,9 +144,7 @@ describe('Express App', () => { mockSyncResults ); - const response = await request(server) - .get('/sync-contacts') - .expect(200); + const response = await request(server).get('/sync-contacts').expect(200); expect(response.body).toEqual(mockSyncResults); }); @@ -166,9 +154,7 @@ describe('Express App', () => { new Error('Sync failed') ); - const response = await request(server) - .get('/sync-contacts') - .expect(500); + const response = await request(server).get('/sync-contacts').expect(500); expect(response.body).toEqual({ message: 'An error occurred while syncing contacts.' @@ -185,9 +171,7 @@ describe('Express App', () => { mockAccessToken ); - const response = await request(server) - .get('/') - .expect(200); + const response = await request(server).get('/').expect(200); expect(response.text).toBe(mockAccessToken); }); @@ -198,9 +182,7 @@ describe('Express App', () => { new Error('Token retrieval failed') ); - const response = await request(server) - .get('/') - .expect(500); + const response = await request(server).get('/').expect(500); expect(response.body).toEqual({ message: 'An error occurred while fetching the access token.' @@ -228,9 +210,7 @@ describe('Express App', () => { }); it('should handle missing code parameter', async () => { - const response = await request(server) - .get('/oauth-callback') - .expect(400); + const response = await request(server).get('/oauth-callback').expect(400); expect(response.body).toEqual({ message: 'Code parameter is missing in the query string.' From bd48f48d8f10636db659278aefcfd11f557b63d0 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Wed, 26 Feb 2025 08:48:41 -0500 Subject: [PATCH 09/13] changing public to private in the readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 32472c8..0f96a1f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This project demonstrates how to: 1. **Prerequisites** - Go to [HubSpot Developer Portal](https://developers.hubspot.com/) - - Create a new public app + - Create a new private app - Configure the following scopes: - `crm.objects.contacts.read` - `crm.objects.contacts.write` @@ -67,10 +67,10 @@ This project demonstrates how to: - Clone the repo - Create the .env file with these entries: - DATABASE_URL the (local) url to the postgres database (e.g. postgresql://{username}:{password}@localhost:5432/{database name}) - - CLIENT_ID from Hubspot public app - - CLIENT_SECRET from Hubspot public app + - CLIENT_ID from Hubspot private app + - CLIENT_SECRET from Hubspot private app - Run `npm install` to install the required Node packages. -- In your HubSpot public app, add `localhost:3001/api/install/oauth-callback` as a redirect URL +- In your HubSpot private app, add `localhost:3001/api/install/oauth-callback` as a redirect URL Run npm run dev to start the server Visit http://localhost:3001/api/install in a browser to get the OAuth install link -Run `npm run seed` to seed the database with test data, select an industry for the data examples From 56436734d5d546d1f9d503c79721af225e5e951e Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Wed, 26 Feb 2025 08:51:13 -0500 Subject: [PATCH 10/13] adding swagger docs endpoint --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0f96a1f..0aba016 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ This project demonstrates how to: - Uses email as primary key for deduplication - Excludes existing HubSpot contacts from sync batch +### Documentation + +- `GET /api-docs` - Returns API documentation + ## Scopes - `crm.schemas.companies.write` From 6f3397f74e027f6528388926096817b815d02df4 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Wed, 26 Feb 2025 08:57:40 -0500 Subject: [PATCH 11/13] changing private to public --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0aba016..0c49aa4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This project demonstrates how to: 1. **Prerequisites** - Go to [HubSpot Developer Portal](https://developers.hubspot.com/) - - Create a new private app + - Create a new public app - Configure the following scopes: - `crm.objects.contacts.read` - `crm.objects.contacts.write` @@ -67,10 +67,10 @@ This project demonstrates how to: - Clone the repo - Create the .env file with these entries: - DATABASE_URL the (local) url to the postgres database (e.g. postgresql://{username}:{password}@localhost:5432/{database name}) - - CLIENT_ID from Hubspot private app - - CLIENT_SECRET from Hubspot private app + - CLIENT_ID from Hubspot public app + - CLIENT_SECRET from Hubspot public app - Run `npm install` to install the required Node packages. -- In your HubSpot private app, add `localhost:3001/api/install/oauth-callback` as a redirect URL +- In your HubSpot public app, add `localhost:3001/api/install/oauth-callback` as a redirect URL Run npm run dev to start the server Visit http://localhost:3001/api/install in a browser to get the OAuth install link -Run `npm run seed` to seed the database with test data, select an industry for the data examples From 2cbbc8a53da4186b5c0d8e4cf787e631b5b9f0f7 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Tue, 25 Mar 2025 17:56:18 -0400 Subject: [PATCH 12/13] updating setAccessToken to authenticateHubspotClient --- src/auth.ts | 34 ++++++++++++++++++++++++++-------- src/initialSyncToHubSpot.ts | 7 ++----- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 660f21c..8fd4895 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -177,17 +177,35 @@ const getAccessToken = async (customerId: string): Promise => { } }; -async function setAccessToken(): Promise { +function applyHubSpotAccessToken(accessToken: string): Client { try { - const accessToken = await getAccessToken(getCustomerId()); - if (!accessToken) { - throw new Error('No access token returned'); - } hubspotClient.setAccessToken(accessToken); return hubspotClient; } catch (error) { - handleError(error, 'Error setting access token'); - throw new Error('Failed to authenticate HubSpot client'); + handleError(error, 'Error setting HubSpot access token'); + throw new Error( + `Failed to apply access token: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +} + +async function authenticateHubspotClient(): Promise { + try { + const customerId = getCustomerId(); + const accessToken = await getAccessToken(customerId); + if (!accessToken) { + throw new Error( + `No access token returned for customer ID: ${customerId}` + ); + } + return applyHubSpotAccessToken(accessToken); + } catch (error) { + handleError(error, 'Error retrieving HubSpot access token'); + throw error instanceof Error + ? new Error(`Failed to authenticate HubSpot client: ${error.message}`) + : new Error( + 'Failed to authenticate HubSpot client due to an unknown error' + ); } } @@ -196,5 +214,5 @@ export { exchangeForTokens, redeemCode, getAccessToken, - setAccessToken + authenticateHubspotClient }; diff --git a/src/initialSyncToHubSpot.ts b/src/initialSyncToHubSpot.ts index 8d81e04..8606989 100644 --- a/src/initialSyncToHubSpot.ts +++ b/src/initialSyncToHubSpot.ts @@ -2,8 +2,7 @@ import 'dotenv/config'; import { Contacts, PrismaClient } from '@prisma/client'; import { Client } from '@hubspot/api-client'; -import { setAccessToken } from './auth'; -// import { getCustomerId } from './utils/utils'; +import { authenticateHubspotClient } from './auth'; import { BatchReadInputSimplePublicObjectId, BatchResponseSimplePublicObjectStatusEnum, @@ -18,8 +17,6 @@ interface KeyedContacts extends Contacts { [key: string]: any; } -// const customerId = getCustomerId(); - const MAX_BATCH_SIZE = 100; const splitBatchByMaxBatchSize = (contacts: Contacts[], start: number) => { @@ -110,7 +107,7 @@ class BatchToBeSynced { } async batchRead() { - setAccessToken(); + await authenticateHubspotClient(); try { const response = await this.hubspotClient.crm.contacts.batchApi.read( this.#batchReadInputs From 5eea176dea85cb8255d2d65325937b3342f28cd2 Mon Sep 17 00:00:00 2001 From: Zach Radford Date: Mon, 31 Mar 2025 22:13:56 -0400 Subject: [PATCH 13/13] adding github actions --- .github/workflows/main.yml | 29 ++++++++++++++++++ .../workflows/trigger-submodule-update.yml | 27 +++++++++++++++++ dist/src/auth.js | 30 +++++++++++++------ 3 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/trigger-submodule-update.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c4be042 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: Trigger Meta Repo Update + +on: + pull_request: + branches: + - github-action-test # Instead of main + types: + - closed + +jobs: + trigger-meta-repo: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - name: Send repository update event + run: | + curl -X POST -H "Authorization: token ${{ secrets.GH_PAT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/hubspotdev/CODE-Hub/actions/workflows/update-meta-repo.yml/dispatches \ + -d '{ + "ref": "main", + "inputs": { + "repo": "'"${{ github.repository }}"'", + "commit": "'"${{ github.sha }}"'", + "pr_title": "'"${{ github.event.pull_request.title }}"'", + "pr_url": "'"${{ github.event.pull_request.html_url }}"'" + } + }' diff --git a/.github/workflows/trigger-submodule-update.yml b/.github/workflows/trigger-submodule-update.yml new file mode 100644 index 0000000..36dd124 --- /dev/null +++ b/.github/workflows/trigger-submodule-update.yml @@ -0,0 +1,27 @@ +name: Trigger Meta Repo Update + +on: + pull_request: + types: + - closed + +jobs: + trigger-meta-repo: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - name: Send repository update event + run: | + curl -X POST -H "Authorization: token ${{ secrets.GH_PAT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/hubspotdev/CODE-Hub/dispatches \ + -d '{ + "event_type": "update-meta-repo", + "client_payload": { + "repo": "'"${{ github.repository }}"'", + "commit": "'"${{ github.sha }}"'", + "pr_title": "'"${{ github.event.pull_request.title }}"'", + "pr_url": "'"${{ github.event.pull_request.html_url }}"'" + } + }' diff --git a/dist/src/auth.js b/dist/src/auth.js index 49910fd..6649b5b 100644 --- a/dist/src/auth.js +++ b/dist/src/auth.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.setAccessToken = exports.getAccessToken = exports.redeemCode = exports.exchangeForTokens = exports.authUrl = void 0; +exports.authenticateHubspotClient = exports.getAccessToken = exports.redeemCode = exports.exchangeForTokens = exports.authUrl = void 0; require("dotenv/config"); const client_1 = require("@prisma/client"); const utils_1 = require("./utils/utils"); @@ -139,18 +139,30 @@ const getAccessToken = async (customerId) => { } }; exports.getAccessToken = getAccessToken; -async function setAccessToken() { +function applyHubSpotAccessToken(accessToken) { try { - const accessToken = await getAccessToken((0, utils_1.getCustomerId)()); - if (!accessToken) { - throw new Error('No access token returned'); - } clients_1.hubspotClient.setAccessToken(accessToken); return clients_1.hubspotClient; } catch (error) { - (0, error_1.default)(error, 'Error setting access token'); - throw new Error('Failed to authenticate HubSpot client'); + (0, error_1.default)(error, 'Error setting HubSpot access token'); + throw new Error(`Failed to apply access token: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} +async function authenticateHubspotClient() { + try { + const customerId = (0, utils_1.getCustomerId)(); + const accessToken = await getAccessToken(customerId); + if (!accessToken) { + throw new Error(`No access token returned for customer ID: ${customerId}`); + } + return applyHubSpotAccessToken(accessToken); + } + catch (error) { + (0, error_1.default)(error, 'Error retrieving HubSpot access token'); + throw error instanceof Error + ? new Error(`Failed to authenticate HubSpot client: ${error.message}`) + : new Error('Failed to authenticate HubSpot client due to an unknown error'); } } -exports.setAccessToken = setAccessToken; +exports.authenticateHubspotClient = authenticateHubspotClient;