@skillswaveca/nova-shared-libraries 4.24.0 → 4.26.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.
@@ -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.24.0",
6
+ "version": "4.26.0",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "pre-release": "pnpm run create-index",
@@ -16,17 +16,20 @@
16
16
  "devDependencies": {
17
17
  "chai": "^5.1.0",
18
18
  "eslint": "^9.0.0",
19
- "mocha": "^10.4.0"
19
+ "mocha": "^10.4.0",
20
+ "sinon": "^21.0.0",
21
+ "sinon-chai": "^4.0.0"
20
22
  },
21
23
  "dependencies": {
22
- "simple-oauth2": "^5.0.0",
23
24
  "@aws-sdk/client-dynamodb": "^3.572.0",
24
25
  "@aws-sdk/client-secrets-manager": "^3.572.0",
25
26
  "@aws-sdk/client-sqs": "^3.572.0",
26
27
  "@aws-sdk/lib-dynamodb": "^3.572.0",
28
+ "@hubspot/api-client": "^13.0.0",
27
29
  "aws-xray-sdk-core": "^3.6.0",
28
30
  "lodash.chunk": "^4.2.0",
29
31
  "node-fetch": "^3.1.0",
30
- "pino": "^9.0.0"
32
+ "pino": "^9.0.0",
33
+ "simple-oauth2": "^5.0.0"
31
34
  }
32
35
  }
@@ -3,6 +3,7 @@ import { Client } from '@hubspot/api-client';
3
3
  import log from '../../nova-utils/src/logger.js';
4
4
  import { NovaDriver } from './nova-driver.js';
5
5
  import { secretsManager } from './aws/secrets-manager.js';
6
+ import { validateEvent, getEventDefinition, getEventMetadata } from '../../nova-utils/src/event-definitions.js';
6
7
 
7
8
  const HUBSPOT_USER_ATTRIBUTE_MAPPINGS = {
8
9
  firstName: 'firstname',
@@ -22,6 +23,7 @@ export class HubSpotDriver extends NovaDriver {
22
23
  * @property {Object} hubSpot
23
24
  * @property {string} hubSpot.secretName
24
25
  * @property {string} hubSpot.secretKeyId
26
+ * @property {string} hubSpot.accountId
25
27
  * @property {string} region
26
28
  * @property {string} rootEnvironmentAwsAccountId
27
29
  * @property {boolean} enabled
@@ -31,6 +33,7 @@ export class HubSpotDriver extends NovaDriver {
31
33
  this._client = null;
32
34
  this.secretName = config.hubSpot.secretName;
33
35
  this.secretKeyId = config.hubSpot.secretKeyId;
36
+ this.accountId = config.hubSpot.accountId;
34
37
  }
35
38
 
36
39
  /**
@@ -177,4 +180,47 @@ export class HubSpotDriver extends NovaDriver {
177
180
 
178
181
  await this._callHubSpot('crm.contacts.basicApi.archive', user.hubSpotContactId);
179
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
+ }
180
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.24.0",
6
+ "version": "4.26.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.24.0",
6
+ "version": "4.26.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.24.0",
6
+ "version": "4.26.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.25.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@skillswaveca/nova-utils",
9
+ "version": "4.25.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.24.0",
6
+ "version": "4.26.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,237 @@
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 clientOnboardingCompletedEventDefinition = {
15
+ name: 'client_onboarding_completed',
16
+ description: 'Client has finished PLG onboarding and pressed "create workspace."',
17
+ properties: {
18
+ email: {
19
+ type: 'string',
20
+ required: true,
21
+ description: 'Email address of the user',
22
+ },
23
+ tenant_id: {
24
+ type: 'string',
25
+ required: true,
26
+ description: 'The ID of the tenant that was created',
27
+ },
28
+ tenant_name: {
29
+ type: 'string',
30
+ required: true,
31
+ description: 'The name of the tenant that was created',
32
+ },
33
+ },
34
+ validator: yup.object({
35
+ email: yup.string().email('Must be a valid email').required('Email is required'),
36
+ }),
37
+ getMetadata: eventData => {
38
+ return {
39
+ email: eventData.email,
40
+ };
41
+ },
42
+ };
43
+
44
+ const usersInvitedEventDefinition = {
45
+ name: 'users_invited',
46
+ description: 'PLG Admin has invited one or more users to their workspace.',
47
+ properties: {
48
+ email: {
49
+ type: 'string',
50
+ required: true,
51
+ description: 'Email address of the user',
52
+ },
53
+ tenant_id: {
54
+ type: 'string',
55
+ required: true,
56
+ description: 'The ID of the tenant that was created',
57
+ },
58
+ tenant_name: {
59
+ type: 'string',
60
+ required: true,
61
+ description: 'The name of the tenant that was created',
62
+ },
63
+ },
64
+ validator: yup.object({
65
+ email: yup.string().email('Must be a valid email').required('Email is required'),
66
+ }),
67
+ getMetadata: eventData => {
68
+ return {
69
+ email: eventData.email,
70
+ };
71
+ },
72
+ };
73
+
74
+ const creditCardEventDefinition = {
75
+ name: 'credit_card_added',
76
+ description: 'PLG Admin added a credit card in Stripe.',
77
+ properties: {
78
+ email: {
79
+ type: 'string',
80
+ required: true,
81
+ description: 'Email address of the user',
82
+ },
83
+ tenant_id: {
84
+ type: 'string',
85
+ required: true,
86
+ description: 'The ID of the tenant that was created',
87
+ },
88
+ tenant_name: {
89
+ type: 'string',
90
+ required: true,
91
+ description: 'The name of the tenant that was created',
92
+ },
93
+ },
94
+ validator: yup.object({
95
+ email: yup.string().email('Must be a valid email').required('Email is required'),
96
+ }),
97
+ getMetadata: eventData => {
98
+ return {
99
+ email: eventData.email,
100
+ };
101
+ },
102
+ };
103
+
104
+ const firstRequestSubmittedEventDefinition = {
105
+ name: 'first_request_submitted',
106
+ description: 'A user in the PLG workspace has submitted the first request.',
107
+ properties: {
108
+ email: {
109
+ type: 'string',
110
+ required: true,
111
+ description: 'Email address of the user',
112
+ },
113
+ tenant_id: {
114
+ type: 'string',
115
+ required: true,
116
+ description: 'The ID of the tenant that was created',
117
+ },
118
+ tenant_name: {
119
+ type: 'string',
120
+ required: true,
121
+ description: 'The name of the tenant that was created',
122
+ },
123
+ },
124
+ validator: yup.object({
125
+ email: yup.string().email('Must be a valid email').required('Email is required'),
126
+ }),
127
+ getMetadata: eventData => {
128
+ return {
129
+ email: eventData.email,
130
+ };
131
+ },
132
+ };
133
+
134
+ const transactionsThresholdMetEventDefinition = {
135
+ name: 'transactions_threshold_met',
136
+ description: 'The PLG tenant has completed a specified number of transactions.',
137
+ properties: {
138
+ email: {
139
+ type: 'string',
140
+ required: true,
141
+ description: 'Email address of the user',
142
+ },
143
+ tenant_id: {
144
+ type: 'string',
145
+ required: true,
146
+ description: 'The ID of the tenant that was created',
147
+ },
148
+ tenant_name: {
149
+ type: 'string',
150
+ required: true,
151
+ description: 'The name of the tenant that was created',
152
+ },
153
+ },
154
+ validator: yup.object({
155
+ email: yup.string().email('Must be a valid email').required('Email is required'),
156
+ }),
157
+ getMetadata: eventData => {
158
+ return {
159
+ email: eventData.email,
160
+ };
161
+ },
162
+ };
163
+
164
+ /**
165
+ * Map of all available event definitions
166
+ * Key: event name, Value: event definition object
167
+ */
168
+ export const eventDefinitions = {
169
+ [clientOnboardingCompletedEventDefinition.name]: clientOnboardingCompletedEventDefinition,
170
+ [usersInvitedEventDefinition.name]: usersInvitedEventDefinition,
171
+ [creditCardEventDefinition.name]: creditCardEventDefinition,
172
+ [firstRequestSubmittedEventDefinition.name]: firstRequestSubmittedEventDefinition,
173
+ [transactionsThresholdMetEventDefinition.name]: transactionsThresholdMetEventDefinition,
174
+ };
175
+ /**
176
+ * Get event definition by name
177
+ * @param {string} eventName - The name of the event
178
+ * @returns {Object|null} The event definition or null if not found
179
+ */
180
+ export const getEventDefinition = eventName => {
181
+ return eventDefinitions[eventName] || null;
182
+ };
183
+
184
+ /**
185
+ * Get all available event names
186
+ * @returns {string[]} Array of event names
187
+ */
188
+ export const getEventNames = () => {
189
+ return Object.keys(eventDefinitions);
190
+ };
191
+
192
+ /**
193
+ * Validate event data against its schema
194
+ * @param {string} eventName - The name of the event
195
+ * @param {Object} eventData - The event data to validate
196
+ * @returns {Promise<Object>} The validated and transformed event data
197
+ * @throws {Error} If validation fails
198
+ */
199
+ export const validateEvent = async(eventName, eventData) => {
200
+ const eventDef = getEventDefinition(eventName);
201
+
202
+ if (!eventDef) {
203
+ return;
204
+ }
205
+
206
+ const validatedData = await eventDef.validator.validate(eventData, {
207
+ abortEarly: false,
208
+ stripUnknown: true,
209
+ });
210
+
211
+ return validatedData;
212
+ };
213
+
214
+ /**
215
+ * Check if an event name is valid
216
+ * @param {string} eventName - The name of the event
217
+ * @returns {boolean} True if the event exists, false otherwise
218
+ */
219
+ export const isValidEventName = eventName => {
220
+ return Object.hasOwn(eventDefinitions, eventName);
221
+ };
222
+
223
+ /**
224
+ * Gets any event metadata required to send the event to HubSpot.
225
+ * @param {string} eventName The name of the event
226
+ * @param {Object} eventData The custom data for the event
227
+ * @returns {Promise<Object>} The metadata for the event required to send it to HubSpot
228
+ */
229
+ export const getEventMetadata = async(eventName, eventData) => {
230
+ const eventDef = getEventDefinition(eventName);
231
+
232
+ if (!eventDef || typeof eventDef.getMetadata !== 'function') {
233
+ return {};
234
+ }
235
+
236
+ return await eventDef.getMetadata(eventData);
237
+ };