# 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 | ✔      |
