@hoover-institution/hubspot-lib
Version:
A toolkit for deep integration with HubSpot's Marketing Events API with a plugin-based architecture.
470 lines (323 loc) • 13 kB
Markdown
# HubSpot SDK (hoover-institution/hubspot-lib)
This toolkit enables deep integration with HubSpot's Marketing Events API and introduces a flexible plugin system to customize behavior across the event lifecycle.
> ⚠️ **This is version 2.0.0+** — includes breaking changes to method return structures. See [Returning Plugin Information](#returning-plugin-information) for details.
> 💡 Plugins are automatically cached after the first call to `loadPlugins()`. You can reuse the same plugin map across your app without manually caching or exporting anything.
## Table of Contents
- [What's New](#whats-new)
- [Installation](#installation)
- [MarketingEvent Class](#marketingevent-class)
- [Constructor](#constructor)
- [Lifecycle Hook Events (`EVENTS`)](#lifecycle-hook-events-events)
- [Methods](#methods)
- [Plugin System Overview](#plugin-system-overview)
- [Plugin Payloads](#plugin-payloads)
- [Plugin Loading Options](#plugin-loading-options)
- [Returning Plugin Information](#returning-plugin-information)
- [Creating Inline Plugins](#creating-inline-plugins)
- [Creating File-Based Plugins](#creating-file-based-plugins)
- [Native Plugins](#native-plugins)
- [CLI: Generate Plugin Templates](#cli-generate-plugin-templates)
- [Full Testing Example with Plugins](#full-testing-example-with-plugins)
- [Feature Overview](#feature-overview)
## What's New
This release introduces a full-featured plugin system that includes:
- ⚠️ **Update 2.1.0**. Some library methods pass additional context or data to plugins, allowing them to access more information about the operation being performed.
- ✅ **Plugins can now return values**. Each plugin handler may return a result (or error), and the SDK will return these values alongside the core HubSpot operation output. This allows plugins to report inserted IDs, custom status, debug info, and more.
- 🔁 **MarketingEvent method return values have changed** — `createEvent`, `deleteEvent`, and others now return a `{ hubspot, plugins }` object instead of a flat response. This is a breaking change.
- ⚡ **Plugin loading is now cached by default** — the first call to `loadPlugins()` stores the plugin map. All future calls return the same result for consistency and performance. Manual caching is no longer required.
- 🧩 **Built-in `PLUGINS.ALL` constant** — Pass `PLUGINS.ALL` when creating a `MarketingEvent` instance to activate every loaded plugin automatically, without manually listing them.
- Inline plugins defined in code
- File-based plugins loaded from disk
- Bitmask-based lifecycle hooks for efficient execution
- A CLI to scaffold plugin templates
- Typed payloads and full IDE integration
## Installation
```bash
npm install @hoover-institution/hubspot-lib
```
To load file-based plugins, define the plugin directory in your consumer `package.json`:
```json
"hubspot-lib": {
"pluginDir": "src/plugins"
}
```
## MarketingEvent Class
The `MarketingEvent` class provides the core API for creating, managing, and tracking events and their associated contacts. It is the heart of the SDK.
### Constructor
```js
new MarketingEvent(accountId, { plugins: [...] })
```
- `accountId`: Your HubSpot portal's account ID.
- `plugins`: A list of plugin identifiers from the PLUGINS object, such as `PLUGINS.MY_PLUGIN`
⚠️ You must call `loadPlugins()` before instantiating the `MarketingEvent` class. If you don't, the plugins will not be available.
### Lifecycle Hook Events (`EVENTS`)
```ts
CREATE_EVENT;
GET_EVENT;
GET_EVENTS;
DELETE_EVENT;
REGISTER_EMAIL;
GET_CONTACTS_BY_STATE;
CREATE_OR_FIND_CONTACT_LIST;
GET_CONTACT_EVENT_STATE;
ADD_CONTACT_TO_LIST;
REMOVE_CONTACT_FROM_LIST;
ASSOCIATE_LIST_WITH_EVENT;
MARKETING_EVENT_ERROR;
```
### Methods
#### `createEvent(payload)`
Creates a marketing event. Triggers `CREATE_EVENT` plugins.
**Returns**: `{ hubspot: { objectId, status, ... }, plugins: [...] }`
<br />
#### `getEvent(externalEventId?)`
Fetches the marketing event by external ID.
**Returns**: `{ objectId, eventName, ... } | null`
<br />
#### `deleteEvent(externalEventId?)`
Deletes an event and triggers `DELETE_EVENT` plugins.
**Returns**: `{ hubspot: { success: boolean }, plugins: [...] }`
<br />
#### `createOrFindContactList(name)`
Creates or returns an existing contact list by name.
**Returns**: `{ listId: string }`
<br />
#### `associateListWithEvent(objectId, listId)`
Links a contact list to a marketing event.
**Returns**: `{ hubspot: { success: boolean }, plugins: [...] }`
<br />
#### `registerEmail(email, externalEventId, subscriberState [, fullName])`
Adds a contact to the appropriate list. If a contact does not exist, it will automatically create one with the provided email and optional full name.
**Returns**: `{ hubspot: { status: string, listId: string }, plugins: [...] }`
<br />
#### `getContactEventState(email, externalEventId)`
Gets the subscriber state for a contact on an event.
**Returns**: `number` (from `SUBSCRIBER_STATE`)
<br />
#### `getSubscriberStateName(code)`
Maps a subscriber state code to a label.
**Returns**: `"REGISTERED" | "CANCELED" | "ATTENDED"`
## Plugin System Overview
Plugins can be attached to lifecycle hook points such as:
- `CREATE_EVENT`
- `GET_EVENT`
- `DELETE_EVENT`
- `REGISTER_EMAIL`
- `MARKETING_EVENT_ERROR`
Plugins can be either:
- Inline (defined directly in JS)
- File-based (loaded from a directory)
### Plugin Payloads
Each hook receives a typed payload. For example:
```js
[EVENTS.CREATE_EVENT]: ({ eventName, externalEventId, status }) => { ... }
```
<br />
### Plugin Loading Options
Plugins are typically loaded once at startup and reused throughout the application.
You can load plugins in two ways:
<br />
#### 1. Load specific plugins by name:
```js
const PLUGINS = loadPlugins(["MY_PLUGIN"], { useDirectory: false });
```
#### 2. Automatically discover all inline and file-based plugins:
```js
const PLUGINS = loadPlugins([], { useDirectory: true });
```
By default, `loadPlugins()` caches the plugin map the first time it's called. Subsequent calls return the same result, regardless of `names` or `useDirectory`. To force a reload, pass `{ cache: false }`.
> 💡 You can safely call `loadPlugins()` multiple times in your server code — the system ensures plugins are loaded only once.
<br />
### Returning Plugin Information
When a plugin function returns a value, the SDK automatically wraps that value in a structured result object as part of the main return payload. You do **not** need to return metadata like `pluginId`, `pluginName`, or `success` yourself — the system adds those for you.
Each plugin's result is included in the `plugins` array of the overall return value:
> ℹ️ Avoid returning full HubSpot responses inside plugins — the core `hubspot` field already includes that. Plugin results should be minimal (status codes, flags, debug info).
```ts
{
hubspot: { ... },
plugins: [
{
pluginId: number,
pluginName: string,
success: boolean,
result?: any,
error?: string
},
...
]
}
```
A plugin is considered `success: true` if it completes without throwing. If it throws an error, `success` will be `false`, and the `error` field will contain the message. You can return any custom shape from your plugin — it will be captured under `result`.
#### Example plugin return:
```js
return {
pluginEventId: "abc-123",
statusNote: "Successfully synced to MongoDB",
};
```
To indicate failure, use `throw`:
```js
throw new Error("MongoDB insert failed.");
```
This will be returned as:
```js
{
success: false,
error: "MongoDB insert failed."
}
```
If you return a shape like `{ ok: false }`, but do **not** throw, the SDK will still treat the plugin as successful from an execution standpoint. Use this only for soft-failures or metadata.
#### Accessing Individual Plugin Results
You can access each plugin's return using the `getPluginResults()` utility, which converts the plugin array into a fast lookup map:
```js
import { getPluginResults } from "@hoover-institution/hubspot-lib";
const result = await instance.createEvent(payload);
const pluginResults = getPluginResults(result.plugins);
// Access result of a specific plugin by name
const mongo = pluginResults[PLUGINS.MONGO_SYNC];
if (mongo?.success) {
console.log("✅ Mongo inserted:", mongo.result.pluginEventId);
} else {
console.error("❌ Mongo plugin failed:", mongo.error);
}
```
> 💡 All plugin results are wrapped and indexed. Using `getPluginResults()` helps you avoid `.find()` calls and access specific plugin outcomes by `pluginId`.
## Creating Inline Plugins
```js
import { createPlugin, EVENTS } from "@hoover-institution/hubspot-lib";
createPlugin("INLINE_PLUGIN", {
[EVENTS.CREATE_EVENT]: ({ eventName }) => {
console.log(`[INLINE_PLUGIN] Created: ${eventName}`);
return { note: "Event processed by INLINE_PLUGIN" };
},
});
```
## Creating File-Based Plugins
You must define your plugin directory in your consumer `package.json`:
```json
"hubspot-lib": {
"pluginDir": "src/plugins"
}
```
Then add files in that folder:
```js
// src/plugins/MY_PLUGIN.js
import { EVENTS, createPlugin } from "@hoover-institution/hubspot-lib";
export default createPlugin("MY_PLUGIN", {
[EVENTS.DELETE_EVENT]: ({ externalEventId }) => {
console.log(`[MY_PLUGIN] Deleted event: ${externalEventId}`);
return { deleted: true };
},
});
```
### Native Plugins
Currently available native plugins:
- `LOG_TO_CONSOLE` — Logs all lifecycle event activity to the console.
- `ALL` — Loads and registers all plugins found. Use this to automatically include every plugin you’ve added, without specifying each one individually.
## CLI: Generate Plugin Templates
You can scaffold new plugins interactively using the built-in CLI.
### Step 1: Configure CLI in `package.json`
```json
"bin": {
"create-plugin": "./node_modules/@hoover-institution/hubspot-lib/lib/cli/hook-init.js"
}
```
### Step 2: Run the CLI
```bash
npx create-plugin
```
The CLI is interactive:
```
🚀 HubSpot Plugin Generator
? 📂 How do you want to select the plugin directory? (Use arrow keys)
❯ Browse folders
Manually enter path
? 🔌 Select your plugin directory: ./plugins
? 📁 Do you want to create a new subfolder? No
? 🪝 Plugin names (comma-separated, use ALL_CAPS)
📌 Usage: PLUGIN_1, PLUGIN_2
MY_CUSTOM_PLUGIN
✅ Created: plugins/MY_CUSTOM_PLUGIN.js
✅ Scaffolding complete
```
## Full Testing Example with Plugins
```js
import dotenv from "dotenv";
dotenv.config();
import {
createPlugin,
EVENTS,
loadPlugins,
SUBSCRIBER_STATE,
getPluginResults,
} from "@hoover-institution/hubspot-lib";
import { MarketingEvent } from "@hoover-institution/hubspot-lib";
import { payload } from "./payload.js";
createPlugin("INLINE_FULL", {
[EVENTS.CREATE_EVENT]: ({ eventName }) => {
console.log(`[INLINE_FULL][CreateEvent] ${eventName}`);
return { inline: true };
},
[EVENTS.GET_EVENT]: ({ externalEventId, found }) => {
console.log(
`[INLINE_FULL][GetEvent] ID: ${externalEventId}, Found: ${found}`
);
},
[EVENTS.DELETE_EVENT]: ({ externalEventId, success }) => {
console.log(
`[INLINE_FULL][DeleteEvent] ID: ${externalEventId}, Deleted: ${success}`
);
},
[EVENTS.MARKETING_EVENT_ERROR]: ({ action, error }) => {
console.error(`[INLINE_FULL][Error] ${action}:`, error.message);
},
});
const PLUGINS = await loadPlugins([], { useDirectory: true });
const instance = new MarketingEvent(process.env.HUBSPOT_ACCOUNT_ID, {
plugins: [PLUGINS.TEST_PLUGIN, PLUGINS.LOG_TO_CONSOLE, PLUGINS.INLINE_FULL],
});
const found = await instance.getEvent(payload.externalEventId);
console.log(`Get event result: ${found ? "Found" : "Not Found"}`);
const result = await instance.createEvent(payload);
console.log("✅ Event created:", result.hubspot.eventName);
const pluginResults = getPluginResults(result.plugins);
const inline = pluginResults[PLUGINS.INLINE_FULL];
if (inline?.success) console.log("🔌 Inline plugin returned:", inline.result);
await instance.deleteEvent();
console.log("✅ Event deleted:", payload.eventName);
```
### payload.js
```js
export const payload = {
eventName: "My Webinar",
eventOrganizer: "Dante Ielceanu",
externalAccountId: process.env.HUBSPOT_ACCOUNT_ID,
externalEventId: 1234567890,
startDateTime: new Date().toISOString(),
eventType: "WEBINAR",
url: "https://example.com/event",
};
```
## Feature Overview
| Feature | Status |
| -------------------------- | ------ |
| MarketingEvent Core API | ✔ |
| Inline Plugins | ✔ |
| File-Based Plugins | ✔ |
| Bitmask Hook Engine | ✔ |
| CLI Plugin Scaffolding | ✔ |
| Full IDE Support via Types | ✔ |