@skillswaveca/nova-shared-libraries 4.23.0 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,31 +1,31 @@
1
1
  export const packageInfo = [
2
2
  {
3
3
  "name": "@skillswaveca/nova-utils",
4
- "version": "4.22.0",
4
+ "version": "4.24.0",
5
5
  "description": "A collection of random utils used in nova repos",
6
6
  "docsPath": "./nova-utils/index.html"
7
7
  },
8
8
  {
9
9
  "name": "@skillswaveca/nova-router",
10
- "version": "4.22.0",
10
+ "version": "4.24.0",
11
11
  "description": "An extended Koa router that enables better validation",
12
12
  "docsPath": "./nova-router/index.html"
13
13
  },
14
14
  {
15
15
  "name": "@skillswaveca/nova-model",
16
- "version": "4.22.0",
16
+ "version": "4.24.0",
17
17
  "description": "Nova model stuff",
18
18
  "docsPath": "./nova-model/index.html"
19
19
  },
20
20
  {
21
21
  "name": "@skillswaveca/nova-middleware",
22
- "version": "4.22.0",
22
+ "version": "4.24.0",
23
23
  "description": "A collection of middleware used by nova projects",
24
24
  "docsPath": "./nova-middleware/index.html"
25
25
  },
26
26
  {
27
27
  "name": "@skillswaveca/nova-drivers",
28
- "version": "4.22.0",
28
+ "version": "4.24.0",
29
29
  "description": "Some helper drivers for AWS services",
30
30
  "docsPath": "./drivers/index.html"
31
31
  }
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './packages/nova-utils/src/tenant-utils.js';
2
2
  export * from './packages/nova-utils/src/logger.js';
3
3
  export * from './packages/nova-utils/src/general.js';
4
+ export * from './packages/nova-utils/src/event-definitions.js';
4
5
  export * from './packages/nova-utils/src/config.js';
5
6
  export * from './packages/nova-router/src/nova-router.js';
6
7
  export * from './packages/nova-middleware/src/oauth-middleware.js';
@@ -9,6 +10,7 @@ export * from './packages/drivers/src/wave.js';
9
10
  export * from './packages/drivers/src/oauth.js';
10
11
  export * from './packages/drivers/src/nova-driver.js';
11
12
  export * from './packages/drivers/src/kraken-driver.js';
13
+ export * from './packages/drivers/src/hubspot.js';
12
14
  export * from './packages/drivers/src/http.js';
13
15
  export * from './packages/drivers/src/config.js';
14
16
  export * from './packages/nova-model/src/stream/nova-stream-task-router.js';
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "@skillswaveca/nova-shared-libraries",
4
4
  "description": "A monorepo of shared libraries for Nova projects.",
5
5
  "repository": "https://github.com/SkillsWave/nova-shared-libraries",
6
- "version": "4.23.0",
6
+ "version": "4.25.0",
7
7
  "main": "index.js",
8
8
  "license": "MIT",
9
9
  "keywords": [],
@@ -14,6 +14,7 @@
14
14
  "@aws-sdk/client-sqs": "^3.682.0",
15
15
  "@aws-sdk/client-ssm": "^3.682.0",
16
16
  "@aws-sdk/lib-dynamodb": "^3.682.0",
17
+ "@hubspot/api-client": "^13.0.0",
17
18
  "@koa/router": "^13.1.0",
18
19
  "aws-xray-sdk-core": "^3.6.0",
19
20
  "jsonwebtoken": "^9.0.2",
@@ -24,7 +25,8 @@
24
25
  "pino": "^9.1.0",
25
26
  "simple-oauth2": "^5.0.0",
26
27
  "uuid": "^9.0.1",
27
- "yn": "^5.0.0"
28
+ "yn": "^5.0.0",
29
+ "yup": "^1.4.0"
28
30
  },
29
31
  "devDependencies": {
30
32
  "@alpha-lambda/eslint-config": "^2.0.0",
@@ -2,6 +2,7 @@ export * from './src/wave.js';
2
2
  export * from './src/oauth.js';
3
3
  export * from './src/nova-driver.js';
4
4
  export * from './src/kraken-driver.js';
5
+ export * from './src/hubspot.js';
5
6
  export * from './src/http.js';
6
7
  export * from './src/config.js';
7
8
  export * from './src/aws/sqs.js';
@@ -3,7 +3,7 @@
3
3
  "name": "@skillswaveca/nova-drivers",
4
4
  "description": "Some helper drivers for AWS services",
5
5
  "repository": "https://github.com/SkillsWave/nova-shared-libraries",
6
- "version": "4.23.0",
6
+ "version": "4.25.0",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "pre-release": "pnpm run create-index",
@@ -0,0 +1,226 @@
1
+ import { Client } from '@hubspot/api-client';
2
+
3
+ import log from '../../nova-utils/src/logger.js';
4
+ import { NovaDriver } from './nova-driver.js';
5
+ import { secretsManager } from './aws/secrets-manager.js';
6
+ import { validateEvent, getEventDefinition, getEventMetadata } from '../../nova-utils/src/event-definitions.js';
7
+
8
+ const HUBSPOT_USER_ATTRIBUTE_MAPPINGS = {
9
+ firstName: 'firstname',
10
+ lastName: 'lastname',
11
+ email: 'email',
12
+ guid: 'external_id',
13
+ tenantId: 'tenant_id',
14
+ };
15
+
16
+ /**
17
+ * HubSpotDriver class provides methods to interact with HubSpot's CRM API using the HubSpot API client.
18
+ */
19
+ export class HubSpotDriver extends NovaDriver {
20
+
21
+ /**
22
+ * @param {Object} config
23
+ * @property {Object} hubSpot
24
+ * @property {string} hubSpot.secretName
25
+ * @property {string} hubSpot.secretKeyId
26
+ * @property {string} hubSpot.accountId
27
+ * @property {string} region
28
+ * @property {string} rootEnvironmentAwsAccountId
29
+ * @property {boolean} enabled
30
+ */
31
+ constructor(config) {
32
+ super(config);
33
+ this._client = null;
34
+ this.secretName = config.hubSpot.secretName;
35
+ this.secretKeyId = config.hubSpot.secretKeyId;
36
+ this.accountId = config.hubSpot.accountId;
37
+ }
38
+
39
+ /**
40
+ * Maps a skillswave user to a hubSpot user object.
41
+ * @param {Object} user
42
+ * @returns {Object} The mapped HubSpot user object.
43
+ * @private
44
+ */
45
+ _mapToHubSpotUser(user) {
46
+ const hubSpotUser = {};
47
+ for (const [key, value] of Object.entries(user)) {
48
+ if (HUBSPOT_USER_ATTRIBUTE_MAPPINGS[key]) {
49
+ hubSpotUser[HUBSPOT_USER_ATTRIBUTE_MAPPINGS[key]] = value;
50
+ }
51
+ }
52
+
53
+ return hubSpotUser;
54
+ }
55
+
56
+ /**
57
+ * This method retrieves the access token from AWS Secrets Manager and creates a new HubSpot client instance. It will return the existing client if it has already been created.
58
+ * @returns {Promise<Client>} The HubSpot client instance.
59
+ * @private
60
+ */
61
+ async _getClient() {
62
+ if (!this._client) {
63
+ const accessToken = await secretsManager.getSecret(this.secretName, this.secretKeyId, this.config.region, this.config.rootEnvironmentAwsAccountId);
64
+ const hubSpotClient = new Client({ accessToken });
65
+
66
+ this._client = hubSpotClient;
67
+ }
68
+
69
+ return this._client;
70
+ }
71
+
72
+ /**
73
+ * Calls a HubSpot API method based on the provided method path and arguments.
74
+ * @param {string|Array<string>} methodPath
75
+ * @param {...any} args
76
+ * @returns {Promise<any>} Returns the raw result from the HubSpot client method.
77
+ */
78
+ async _callHubSpot(methodPath, ...args) {
79
+ if (!this.enabled) {
80
+ log.warn('HubSpot driver is disabled');
81
+ return;
82
+ }
83
+
84
+ let client = await this._getClient();
85
+ if (!client) {
86
+ log.error('Failed to create HubSpot client');
87
+ return;
88
+ }
89
+
90
+ const path = Array.isArray(methodPath) ? methodPath : methodPath.split('.');
91
+ let parent = null;
92
+ for (const key of path) {
93
+ if (typeof client[key] === 'undefined') {
94
+ log.error(`Invalid HubSpot client method path: ${methodPath}`);
95
+ return;
96
+ }
97
+ parent = client;
98
+ client = client[key];
99
+ }
100
+
101
+ if (typeof client !== 'function') {
102
+ log.error(`Target at path ${methodPath} is not a function`);
103
+ return;
104
+ }
105
+
106
+ try {
107
+ return await client.apply(parent, args.length === 1 ? [args[0]] : args);
108
+ } catch (e) {
109
+ log.error({ error: e.message }, `Error calling HubSpot method ${methodPath}`);
110
+ throw e;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Updates a HubSpot deal with the given properties.
116
+ * @param {string} dealId
117
+ * @param {object} properties
118
+ * @returns The updated deal object.
119
+ */
120
+ async updateDeal(dealId, properties) {
121
+ return await this._callHubSpot('crm.deals.basicApi.update', dealId, { properties });
122
+ }
123
+
124
+ /**
125
+ * Looks up a HubSpot contact ID for a given user. May return multiple IDs.
126
+ * @param {Object} user
127
+ * @returns {Promise<string[]|null>} An array of HubSpot contact IDs or null if no contact is found.
128
+ */
129
+ async lookupUserContactId(user) {
130
+ if (!user.email) {
131
+ log.error('User email is required for lookup');
132
+ return;
133
+ }
134
+
135
+ try {
136
+ const response = await this._callHubSpot('crm.contacts.searchApi.doSearch', {
137
+ filterGroups: [{
138
+ filters: [{
139
+ propertyName: 'email',
140
+ operator: 'EQ',
141
+ value: user.email,
142
+ }]
143
+ }]
144
+ });
145
+
146
+ return response.results.length > 0 ? response.results.map(result => result.id) : [];
147
+ } catch (error) {
148
+ log.error({ error }, `Error looking up user ${user.email} in HubSpot`);
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Creates or updates a HubSpot contact. If the provided user has a `hubSpotContactId`, it will update the existing contact; otherwise, it will create a new one.
155
+ * @param {object} user
156
+ * @returns {object} The created/updated HubSpot user.
157
+ */
158
+ async upsertUser(user) {
159
+ const hubSpotUser = this._mapToHubSpotUser(user);
160
+ let response;
161
+ if (user.hubSpotContactId) {
162
+ response = await this._callHubSpot('crm.contacts.basicApi.update', user.hubSpotContactId, { properties: hubSpotUser });
163
+ } else {
164
+ response = await this._callHubSpot('crm.contacts.basicApi.create', { properties: hubSpotUser });
165
+ }
166
+
167
+ return { hubSpotContactId: response.id, ...user };
168
+ }
169
+
170
+ /**
171
+ * Deletes a HubSpot user.
172
+ * @param {object} user
173
+ * @returns {Promise<void>}
174
+ */
175
+ async deleteUser(user) {
176
+ if (!user.hubSpotContactId) {
177
+ log.error(`User ${user.email} does not have a HubSpot contact ID, skipping deletion.`);
178
+ return;
179
+ }
180
+
181
+ await this._callHubSpot('crm.contacts.basicApi.archive', user.hubSpotContactId);
182
+ }
183
+
184
+ /**
185
+ * Sends a custom event to HubSpot using the event definitions.
186
+ * This method validates the event data against the predefined schema before sending.
187
+ *
188
+ * @param {string} eventName - The name of the event to send
189
+ * @param {object} eventData - The event data to send
190
+ * @returns {Promise<object>} The response from HubSpot API
191
+ * @throws {Error} If validation fails or HubSpot API returns an error
192
+ */
193
+ async sendEvent(eventName, eventData) {
194
+ try {
195
+ // Validate the event exists and get its definition
196
+ const eventDefinition = getEventDefinition(eventName);
197
+ if (!eventDefinition) {
198
+ log.error({ eventName }, 'Event definition not found');
199
+ return;
200
+ }
201
+
202
+ const validatedData = await validateEvent(eventName, eventData);
203
+
204
+ const eventMetadata = await getEventMetadata(eventName, validatedData);
205
+
206
+ const response = await this._callHubSpot('events.send.basicApi.send', {
207
+ eventName: `pe${this.accountId}_${eventName}`,
208
+ properties: validatedData,
209
+ occurredAt: new Date(),
210
+ ...eventMetadata,
211
+ });
212
+
213
+ log.debug({ eventName, response }, 'Event sent to HubSpot successfully');
214
+ return response;
215
+ } catch (error) {
216
+ log.error({
217
+ eventName,
218
+ eventData,
219
+ error: error.message,
220
+ stack: error.stack
221
+ }, 'Unexpected error while sending event');
222
+
223
+ throw error;
224
+ }
225
+ }
226
+ }
@@ -3,7 +3,7 @@
3
3
  "name": "@skillswaveca/nova-middleware",
4
4
  "description": "A collection of middleware used by nova projects",
5
5
  "repository": "https://github.com/SkillsWave/nova-shared-libraries",
6
- "version": "4.23.0",
6
+ "version": "4.25.0",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "pre-release": "pnpm run create-index",
@@ -3,7 +3,7 @@
3
3
  "name": "@skillswaveca/nova-model",
4
4
  "description": "Nova model stuff",
5
5
  "repository": "https://github.com/SkillsWave/nova-shared-libraries",
6
- "version": "4.23.0",
6
+ "version": "4.25.0",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "pre-release": "pnpm run create-index",
@@ -3,7 +3,7 @@
3
3
  "name": "@skillswaveca/nova-router",
4
4
  "description": "An extended Koa router that enables better validation",
5
5
  "repository": "https://github.com/SkillsWave/nova-shared-libraries",
6
- "version": "4.23.0",
6
+ "version": "4.25.0",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "pre-release": "pnpm run create-index",
@@ -1,4 +1,5 @@
1
1
  export * from './src/tenant-utils.js';
2
2
  export * from './src/logger.js';
3
3
  export * from './src/general.js';
4
+ export * from './src/event-definitions.js';
4
5
  export * from './src/config.js';
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@skillswaveca/nova-utils",
3
+ "version": "4.24.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@skillswaveca/nova-utils",
9
+ "version": "4.24.0",
10
+ "license": "UNLICENSED",
11
+ "dependencies": {
12
+ "yup": "^1.4.0"
13
+ },
14
+ "devDependencies": {}
15
+ },
16
+ "node_modules/property-expr": {
17
+ "version": "2.0.6",
18
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
19
+ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
20
+ },
21
+ "node_modules/tiny-case": {
22
+ "version": "1.0.3",
23
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
24
+ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
25
+ },
26
+ "node_modules/toposort": {
27
+ "version": "2.0.2",
28
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
29
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
30
+ },
31
+ "node_modules/type-fest": {
32
+ "version": "2.19.0",
33
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
34
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
35
+ "engines": {
36
+ "node": ">=12.20"
37
+ },
38
+ "funding": {
39
+ "url": "https://github.com/sponsors/sindresorhus"
40
+ }
41
+ },
42
+ "node_modules/yup": {
43
+ "version": "1.6.1",
44
+ "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
45
+ "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
46
+ "dependencies": {
47
+ "property-expr": "^2.0.5",
48
+ "tiny-case": "^1.0.3",
49
+ "toposort": "^2.0.2",
50
+ "type-fest": "^2.19.0"
51
+ }
52
+ }
53
+ }
54
+ }
@@ -3,7 +3,7 @@
3
3
  "name": "@skillswaveca/nova-utils",
4
4
  "description": "A collection of random utils used in nova repos",
5
5
  "repository": "https://github.com/SkillsWave/nova-shared-libraries",
6
- "version": "4.23.0",
6
+ "version": "4.25.0",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "pre-release": "pnpm run create-index",
@@ -14,5 +14,7 @@
14
14
  "author": "SkillsWave",
15
15
  "license": "UNLICENSED",
16
16
  "devDependencies": {},
17
- "dependencies": {}
17
+ "dependencies": {
18
+ "yup": "^1.4.0"
19
+ }
18
20
  }
@@ -0,0 +1,104 @@
1
+ import * as yup from 'yup';
2
+
3
+ /**
4
+ * HubSpot Event Definitions
5
+ *
6
+ * This module contains the schema definitions for custom events that can be sent to HubSpot.
7
+ * Each event definition includes:
8
+ * - name: The event name used in HubSpot
9
+ * - description: Human-readable description of the event
10
+ * - properties: Schema definition for event properties
11
+ * - validator: Yup validation schema for the event data
12
+ */
13
+
14
+ const testEventDefinition = {
15
+ name: 'hubspot_test_event',
16
+ description: 'Test event for HubSpot integration',
17
+ properties: {
18
+ email: {
19
+ type: 'string',
20
+ required: true,
21
+ description: 'Email address of the user',
22
+ },
23
+ },
24
+ validator: yup.object({
25
+ email: yup.string().email('Must be a valid email').required('Email is required'),
26
+ }),
27
+ getMetadata: eventData => {
28
+ return {
29
+ email: eventData.email,
30
+ };
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Map of all available event definitions
36
+ * Key: event name, Value: event definition object
37
+ */
38
+ export const eventDefinitions = {
39
+ [testEventDefinition.name]: testEventDefinition,
40
+ };
41
+
42
+ /**
43
+ * Get event definition by name
44
+ * @param {string} eventName - The name of the event
45
+ * @returns {Object|null} The event definition or null if not found
46
+ */
47
+ export const getEventDefinition = eventName => {
48
+ return eventDefinitions[eventName] || null;
49
+ };
50
+
51
+ /**
52
+ * Get all available event names
53
+ * @returns {string[]} Array of event names
54
+ */
55
+ export const getEventNames = () => {
56
+ return Object.keys(eventDefinitions);
57
+ };
58
+
59
+ /**
60
+ * Validate event data against its schema
61
+ * @param {string} eventName - The name of the event
62
+ * @param {Object} eventData - The event data to validate
63
+ * @returns {Promise<Object>} The validated and transformed event data
64
+ * @throws {Error} If validation fails
65
+ */
66
+ export const validateEvent = async(eventName, eventData) => {
67
+ const eventDef = getEventDefinition(eventName);
68
+
69
+ if (!eventDef) {
70
+ return;
71
+ }
72
+
73
+ const validatedData = await eventDef.validator.validate(eventData, {
74
+ abortEarly: false,
75
+ stripUnknown: true,
76
+ });
77
+
78
+ return validatedData;
79
+ };
80
+
81
+ /**
82
+ * Check if an event name is valid
83
+ * @param {string} eventName - The name of the event
84
+ * @returns {boolean} True if the event exists, false otherwise
85
+ */
86
+ export const isValidEventName = eventName => {
87
+ return eventDefinitions.hasOwnProperty(eventName);
88
+ };
89
+
90
+ /**
91
+ * Gets any event metadata required to send the event to HubSpot.
92
+ * @param {string} eventName The name of the event
93
+ * @param {Object} eventData The custom data for the event
94
+ * @returns {Promise<Object>} The metadata for the event required to send it to HubSpot
95
+ */
96
+ export const getEventMetadata = async(eventName, eventData) => {
97
+ const eventDef = getEventDefinition(eventName);
98
+
99
+ if (!eventDef || typeof eventDef.getMetadata !== 'function') {
100
+ return {};
101
+ }
102
+
103
+ return await eventDef.getMetadata(eventData);
104
+ };