UNPKG

@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
# 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: [...] }` &nbsp;<br /> #### `getEvent(externalEventId?)` Fetches the marketing event by external ID. **Returns**: `{ objectId, eventName, ... } | null` &nbsp;<br /> #### `deleteEvent(externalEventId?)` Deletes an event and triggers `DELETE_EVENT` plugins. **Returns**: `{ hubspot: { success: boolean }, plugins: [...] }` &nbsp;<br /> #### `createOrFindContactList(name)` Creates or returns an existing contact list by name. **Returns**: `{ listId: string }` &nbsp;<br /> #### `associateListWithEvent(objectId, listId)` Links a contact list to a marketing event. **Returns**: `{ hubspot: { success: boolean }, plugins: [...] }` &nbsp;<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: [...] }` &nbsp;<br /> #### `getContactEventState(email, externalEventId)` Gets the subscriber state for a contact on an event. **Returns**: `number` (from `SUBSCRIBER_STATE`) &nbsp;<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 }) => { ... } ``` &nbsp;<br /> ### Plugin Loading Options Plugins are typically loaded once at startup and reused throughout the application. You can load plugins in two ways: &nbsp;<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. &nbsp;<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 | |