Skip to content

Adding Enrichment Vendors

This guide covers the end-to-end process of adding a new third-party data vendor to the CRED platform. Adding a vendor requires changes across multiple projects.

Overview

When adding a new enrichment vendor (e.g., Apollo, Lusha, Cognism), you need to update:

Project Repository Purpose
CRED Model cred-model Database tables for storing provider data
DBT cred-dbt DataDescription exposure and field mappings
Model API cred-model-api API integration and data fetching
Commercial cred-api-commercial UI integration, waterfall configuration

Process Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CRED Model  β”‚ β†’  β”‚    DBT      β”‚ β†’  β”‚  Model API  β”‚ β†’  β”‚ Commercial  β”‚
β”‚  (Tables)   β”‚    β”‚  (Schema)   β”‚    β”‚ (Endpoints) β”‚    β”‚    (UI)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Quick Start Summary

Step Project Repository What to Do Key Files
1 CRED Model cred-model Create migration + sync config migrations/*.ts, sync-providers.config.ts
2 DBT cred-dbt Add to provider tables data_description_providers_tables.sql
3 Model API cred-model-api Domain, services, adapter, factory, GraphQL ~15 files across layers
4 Commercial cred-api-commercial Enum, feature, seed, provider list, validation 5-6 files

Naming Conventions

Critical: Use Consistent Naming Across All Projects

All projects are interrelated. Using inconsistent names will break the integration.

Element Convention Example
Abbreviation UPPERCASE, no spaces AP, COG, LUSHA, ROCKETREACH
Table Name (Person) {ABBR}Person APPerson, CGPerson, LSPerson, RRPerson
Table Name (Company) {ABBR}Company APCompany
Provider Unique ID {abbr}Id or providerUniqueId apolloId, cognismId
Feature Constant FEATURE_WATERFALL_DATASOURCE_{ABBR} FEATURE_WATERFALL_DATASOURCE_AP

When adding a new vendor, decide on:

  1. Abbreviation (e.g., NV for "NewVendor") - use this EVERYWHERE
  2. Full name (e.g., "NewVendor") - for display purposes only

Cross-Reference Table:

Project Repository Where Abbreviation is Used
CRED Model cred-model Table name: NVPerson, column: nvId or providerUniqueId
DBT cred-dbt dataSourceAbbreviation: "NV", tableName: "NVPerson"
Model API cred-model-api ProviderDataSourceAbbreviation.NV, folder: newvendor/
Commercial cred-api-commercial CustomFieldRecordDataSourceEnum.NV, feature constant

1. CRED Model (Database Tables)

Repository: cred-model

Adding a new provider table (e.g., NVPerson, NVCompany) uses a single migration that handles everything: table creation, seeding, and replication setup.

Single Migration Pattern

File: src/data/migrations/{timestamp}-create-nv-person.ts

import * as Knex from "knex";
import addSetUpdatedAtTrigger from "./utils/add-set-updated-at-trigger";
import addToReplicationSet from "./utils/add-to-replication-set";
import addToStitchSync from "./utils/add-to-stitch-sync";
import seedIfEmpty from "./utils/seed-if-empty";

export async function up(knex: Knex): Promise<void> {
    // 1. Create table
    await knex.schema.createTable("NVPerson", (table) => {
        table.increments("id").notNullable().primary();
        table.string("providerUniqueId").notNullable().unique();
        table.integer("personId"); // or "companyId" for company tables
        table.foreign("personId").references("Person.id").onDelete("cascade");

        // Provider-specific columns...
        table.string("firstName");
        table.string("lastName");
        table.jsonb("someJsonField");

        // Standard timestamps
        table.dateTime("createdAt").notNullable().defaultTo(knex.raw("NOW()"));
        table.dateTime("updatedAt").notNullable().defaultTo(knex.raw("NOW()"));
        table.dateTime("deletedAt");
    });
    await addSetUpdatedAtTrigger(knex, "NVPerson");

    // 2. Insert seed row with ALL columns populated
    await seedIfEmpty(
        knex,
        "NVPerson",
        {
            providerUniqueId: "seed-nv-person-001",
            personId: null,
            firstName: "Seed",
            lastName: "Record",
            someJsonField: { example: "data" },
            deletedAt: null,
        },
        "providerUniqueId",
        "seed-nv-person-001",
    );

    // 3. Add to pglogical replication
    await addToReplicationSet(knex, "NVPerson");

    // 4. Enable Stitch sync for BigQuery
    await addToStitchSync("NVPerson", "updatedAt");
}

export async function down(knex: Knex): Promise<void> {
    await knex.schema.dropTable("NVPerson");
}

What Each Step Does

Step Utility Purpose
1 addSetUpdatedAtTrigger() Auto-updates updatedAt column on row changes
2 seedIfEmpty() Inserts seed row if table is empty (idempotent). Seed row has ALL columns populated so DBT can detect the full schema.
3 addToReplicationSet() Adds table to pglogical replication set. Data (including seed row) replicates to model-api automatically. Skips in local dev where pglogical is not installed.
4 addToStitchSync() Enables Stitch replication via API for BigQuery sync. Configures incremental sync using the specified replication key (default: updatedAt). Skips gracefully when Stitch credentials are not configured.

Seed Row Requirements

Requirement Description
ALL columns populated Required for DBT schema detection
Foreign keys = null e.g., personId: null, companyId: null
Recognizable seed ID e.g., "seed-nv-person-001"
deletedAt = null If soft-delete column exists

Benefits

Benefit Description
Single source of truth Migration in CRED Model creates everything
No seeding in model-api Data replicates via pglogical automatically
Works locally Replication step skips gracefully if pglogical not installed
Idempotent Safe to run multiple times

Add Sync Provider Configuration

File: src/data/config/sync-providers.config.ts

export const PROVIDER_TABLES: ProviderTableConfig[] = [
    // ... existing providers ...

    {
        tableName: "NVPerson", // Must match migration table name
        primaryKey: "providerUniqueId",
        description: "NewVendor person data",
    },
];

Required Elements

Element Required Purpose
id (auto-increment) βœ… Internal primary key
Provider unique ID βœ… e.g., providerUniqueId - for sync
createdAt βœ… Timestamp
updatedAt βœ… Required for sync time filtering
deletedAt βœ… Soft delete support
addSetUpdatedAtTrigger() βœ… Auto-updates updatedAt on changes
seedIfEmpty() βœ… Seeds ALL columns for DBT detection
addToReplicationSet() βœ… Enables pglogical replication
addToStitchSync() βœ… Enables Stitch sync for BigQuery
Sync config entry βœ… Enables cross-environment sync

Optional Elements

Element Notes
personId / companyId FK Links to main Person or Company table
Indexes Improves query performance

What You Do NOT Need

Not Required Reason
❌ TypeORM entity files Tables use raw Knex queries
❌ Dedicated service files Data accessed via sync mechanism
❌ Manual SQL sync code System auto-discovers columns
❌ Seeding in model-api Seed row replicates via pglogical automatically

Single Migration = Everything

One migration file handles table creation, seeding, and replication setup. Data automatically syncs to model-api via pglogical.


2. DBT (DataDescription)

Repository: cred-dbt

To expose a new enrichment source table in DataDescription, you need to add it to the provider tables configuration. The enrichment source tables are PostgreSQL tables stored in the credmodel_google schema.

Step 1: Ensure Source Table Exists

Prerequisite

The PostgreSQL table must exist in the credmodel_google schema and be defined in sources_datasets/credmodel_google.yml.

Examples of existing tables:

Table Description
APPerson Apollo Person
CGPerson Cognism Person
RRPerson RocketReach Person
LSPerson Lusha Person

Step 2: Add Entry to Provider Tables List

File: models/credentity/data_description/intermadiate/data_description/data_description_providers_tables.sql

Add a new entry to the providerTables list:

{"tableName": "NVPerson", "parentDataName": "Person", "displayName": "NewVendor Person", "dataSourceAbbreviation": "NV"},

Parameters:

Parameter Description Example
tableName Exact table name from CRED Model migration NVPerson (must match exactly!)
parentDataName Top-level entity: "Person" or "Company" "Person"
displayName Human-readable name "NewVendor Person"
dataSourceAbbreviation System abbreviation (same across projects) "NV"

Example - Adding a new source:

{% set providerTables = [
    {"tableName": "APPerson", "parentDataName": "Person", "displayName": "Apollo Person", "dataSourceAbbreviation": "AP"},
    {"tableName": "RRPerson", "parentDataName": "Person", "displayName": "RocketReach Person", "dataSourceAbbreviation": "ROCKETREACH"},
    {"tableName": "CGPerson", "parentDataName": "Person", "displayName": "Cognism Person", "dataSourceAbbreviation": "COG"},
    {"tableName": "LSPerson", "parentDataName": "Person", "displayName": "Lusha Person", "dataSourceAbbreviation": "LUSHA"},
    {"tableName": "NVPerson", "parentDataName": "Person", "displayName": "NewVendor Person", "dataSourceAbbreviation": "NV"},  # ← Add here
] %}

Step 3: Verify Integration

After adding the entry, the provider table will automatically:

  1. Generate an ID: Using to_hex(md5(concat('PROVIDER-TABLE-', tableName)))
  2. Link to Parent Entity: Via data_description_cred_entity
  3. Appear in DataDescription: Available for metadata queries

Step 4: Add Metadata (Optional)

If you want to add display metadata for the provider table, add an entry to the DataDescriptionMetadata seed table in BigQuery:

Location: cred-1556636033881.cred_seed.DataDescriptionMetadata

Add a row with:

  • id: The generated ID (calculate: to_hex(md5(concat('PROVIDER-TABLE-', 'NVPerson'))))
  • name: Display name (e.g., "NewVendor Person")
  • description: Description of the table
  • tooltip: Tooltip text

ID Generation

The ID is auto-generated from the table name. For NVPerson, the ID would be the MD5 hash of 'PROVIDER-TABLE-NVPerson'.

Testing

# Run the dbt model
dbt run --select data_description_providers_tables

# Verify the table appears in output
# Check it's linked to correct parent entity
# Verify it appears in DataDescription model

Current Provider Tables

Table Name Parent Entity Display Name Abbreviation Notes
APPerson Person Apollo Person AP Also has APCompany
RRPerson Person RocketReach Person ROCKETREACH Person only
CGPerson Person Cognism Person COG Person only
LSPerson Person Lusha Person LUSHA Person only

Pattern

Notice how abbreviations match: AP β†’ APPerson table, CustomFieldRecordDataSourceEnum.AP, ProviderDataSourceAbbreviation.AP


3. Model API

Repository: cred-model-api

The Provider Enrichment system provides a unified interface for enriching entity data (Person, Company) from multiple external data providers.

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      ProviderEnrichmentOrchestrator                        β”‚
β”‚                    (Entry point for all enrichment requests)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
                                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                            ProviderFactory                                  β”‚
β”‚              createProvider(providerAbbreviation, entityType)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β–Ό                                   β–Ό
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚  PERSON        β”‚                  β”‚  COMPANY       β”‚
           β”‚  Adapters      β”‚                  β”‚  Adapters      β”‚
           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚                                   β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
    β–Ό       β–Ό       β–Ό       β–Ό       β–Ό                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Apolloβ”‚β”‚Lusha β”‚β”‚Cognisβ”‚β”‚Rocketβ”‚              β”‚ Apollo Company β”‚
β”‚Personβ”‚β”‚Personβ”‚β”‚Personβ”‚β”‚Reach β”‚              β”‚   Adapter      β”‚
β””β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Supported Providers & Entity Types

Provider Abbreviation Person Company
Apollo AP βœ… βœ…
Lusha LUSHA βœ… ❌
Cognism COG βœ… ❌
RocketReach ROCKETREACH βœ… ❌

Component Responsibilities

Component Responsibility
ProviderEnrichmentOrchestrator Entry point, routes requests to appropriate provider
ProviderFactory Creates adapter based on providerAbbreviation + entityType
Enrichment Adapters Implement IProviderEnrichmentService, adapt provider to unified interface
Default Services Cache-first lookup, falls back to API, persists results
API Services Pure HTTP calls to external APIs, response mapping
Repositories Redis caching + PostgreSQL persistence

Data Flow

Request: enrichById(companyId=123, provider=AP, entityType=COMPANY)
                           β”‚
                           β–Ό
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β”‚ ProviderFactory         β”‚
             β”‚ β†’ ApolloCompanyAdapter  β”‚
             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β”‚ DefaultApolloCompany    β”‚
             β”‚ Service                 β”‚
             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Cache/DB Lookup │──── HIT ────▢│ Return cached   β”‚
β”‚ (Repository)    β”‚               β”‚ data            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ MISS
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ApiApolloCompanyβ”‚
β”‚ Service (HTTP)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Persist to DB   β”‚
β”‚ (Repository)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Return enriched β”‚
β”‚ data            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Files Structure

src/
β”œβ”€β”€ domain/
β”‚   β”œβ”€β”€ {vendor}/
β”‚   β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”‚   β”œβ”€β”€ {abbr}-person.ts
β”‚   β”‚   β”‚   └── {abbr}-company.ts
β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”‚   β”œβ”€β”€ {vendor}-person-repository.ts
β”‚   β”‚   β”‚   └── {vendor}-company-repository.ts
β”‚   β”‚   └── service/
β”‚   β”‚       β”œβ”€β”€ {vendor}-person-service.ts
β”‚   β”‚       └── {vendor}-company-service.ts
β”‚   └── provider-enrichment/
β”‚       β”œβ”€β”€ entity/provider-types.ts
β”‚       └── service/provider-enrichment-service.ts
β”œβ”€β”€ data/repos/{vendor}/
β”‚   β”œβ”€β”€ db-{vendor}-person-repository.ts
β”‚   β”œβ”€β”€ cache-{vendor}-person-repository.ts
β”‚   β”œβ”€β”€ db-{vendor}-company-repository.ts
β”‚   └── cache-{vendor}-company-repository.ts
└── services/
    β”œβ”€β”€ {vendor}/
    β”‚   β”œβ”€β”€ api-{vendor}-person-service.ts
    β”‚   β”œβ”€β”€ default-{vendor}-person-service.ts
    β”‚   β”œβ”€β”€ api-{vendor}-company-service.ts
    β”‚   └── default-{vendor}-company-service.ts
    └── provider-enrichment/
        β”œβ”€β”€ provider-factory.ts
        β”œβ”€β”€ provider-enrichment-orchestrator.ts
        └── {vendor}-enrichment-adapter.ts

Step 1: Define the Domain Layer

Create the domain entities and service interface in src/domain/newvendor/.

a) Create Entity Types

File: src/domain/newvendor/entity/nv-person.ts

import { EntityData } from "../../base/entity";

export interface NVPerson extends EntityData {
    nvId: string; // Provider unique ID (matches DB column)
    personId?: number | null;
    name?: string;
    // ... vendor-specific fields
}

b) Create Repository Interface

File: src/domain/newvendor/repository/newvendor-person-repository.ts

export default interface NewVendorPersonRepository {
    lookupPerson(linkedinUrl: string): Promise<NVPersonCreateData | null>;
    // ... other repository methods
}

c) Create Service Interface

File: src/domain/newvendor/service/newvendor-person-service.ts

export type NewVendorPersonInput = {
    personId?: number;
    linkedinUrl?: string;
    cacheOnly?: boolean;
    freshOnly?: boolean;
};

export type NewVendorPersonCreateData = {
    personData: Omit<NVPerson, "id" | "createdAt" | "updatedAt">;
    // ... nested data
};

export default interface NewVendorPersonService {
    lookupPerson(
        input: NewVendorPersonInput,
    ): Promise<NewVendorPersonCreateData | null>;
}

Step 2: Implement the Services Layer

Create services in src/services/newvendor/.

a) Create API Service (external API calls)

File: src/services/newvendor/api-newvendor-person-service.ts

export default class ApiNewVendorPersonService
    extends RestService
    implements NewVendorPersonService
{
    constructor(private apiKey: string, private storage: CacheStorage) {}

    async lookupPerson(
        input: NewVendorPersonInput,
    ): Promise<NewVendorPersonCreateData | null> {
        // Call external vendor API
        // Map response to internal data structure
    }
}

b) Create Default Service (orchestrates cache + API)

File: src/services/newvendor/default-newvendor-person-service.ts

export default class DefaultNewVendorPersonService
    implements NewVendorPersonService
{
    private static instance: DefaultNewVendorPersonService;

    static getInstance(
        personService: PersonService,
    ): DefaultNewVendorPersonService {
        // Singleton pattern
    }

    async lookupPerson(
        input: NewVendorPersonInput,
    ): Promise<NewVendorPersonCreateData | null> {
        // Check cache first, then call API, persist results
    }
}

Step 3: Create the Provider Enrichment Adapter

This is the key integration point for the unified waterfall logic.

File: src/services/provider-enrichment/newvendor-enrichment-adapter.ts

import { BaseProviderEnrichmentService } from "../../domain/provider-enrichment/service/provider-enrichment-service";
import {
    ProviderDataSourceAbbreviation,
    ProviderOptions,
} from "../../domain/provider-enrichment/entity/provider-types";

export class NewVendorEnrichmentAdapter extends BaseProviderEnrichmentService<
    NewVendorPersonCreateData,
    NVPerson
> {
    constructor(
        private readonly newvendorService: NewVendorPersonService,
        dataDescriptionService: DataDescriptionService,
        cacheStorage?: CacheStorage,
    ) {
        super(
            ProviderDataSourceAbbreviation.NV, // Must match enum value
            dataDescriptionService,
            cacheStorage,
        );
    }

    protected validateOptions(_options: ProviderOptions): string | null {
        // Return null if valid, error message if invalid
        return null;
    }

    protected async lookupPersonData(
        personId: number,
        options: ProviderOptions,
    ): Promise<NewVendorPersonCreateData | null> {
        return this.newvendorService.lookupPerson({
            personId,
            freshOnly: options.freshOnly,
        });
    }

    protected extractPersonData(
        result: NewVendorPersonCreateData,
    ): NVPerson | null {
        return result.personData as NVPerson;
    }

    protected getProviderName(): string {
        return "NewVendor";
    }
}

Step 4: Register the Provider in the Factory

a) Add to ProviderDataSourceAbbreviation enum

File: src/domain/provider-enrichment/entity/provider-types.ts

export enum ProviderDataSourceAbbreviation {
    AP = "AP",
    LUSHA = "LUSHA",
    COG = "COG",
    ROCKETREACH = "ROCKETREACH",
    NV = "NV", // ← Add new vendor (must match Commercial abbreviation)
}

b) Update ProviderFactory

File: src/services/provider-enrichment/provider-factory.ts

export class ProviderFactory {
    constructor(
        private readonly apolloService: ApolloPersonService,
        private readonly lushaService: LushaPersonService,
        private readonly cognismService: CognismPersonService,
        private readonly rocketreachService: RocketReachPersonService,
        private readonly newvendorService: NewVendorPersonService, // ← Add
        public readonly dataDescriptionService: DataDescriptionService,
        private readonly cacheStorage?: CacheStorage,
    ) {}

    createProvider(
        providerAbbreviation: ProviderDataSourceAbbreviation,
    ): IProviderEnrichmentService {
        switch (providerAbbreviation) {
            // ... existing cases
            case ProviderDataSourceAbbreviation.NV:
                return new NewVendorEnrichmentAdapter(
                    this.newvendorService,
                    this.dataDescriptionService,
                    this.cacheStorage,
                );
            default:
                throw new Error(
                    `Unknown provider type: ${providerAbbreviation}`,
                );
        }
    }
}

Step 5: Register in the Container Layer

a) Update types.ts

File: src/container/types.ts

export type ApiServicesInput = {
    // ... existing services
    newvendor?: NewVendorPersonService;
};

b) Update services.ts

File: src/container/services.ts

export type ApiServices = {
  // ... existing services
  newvendor: NewVendorPersonService;
};

const createServices = (...) => {
  // ... existing service creation
  const newvendor = input?.newvendor ?? DefaultNewVendorPersonService.getInstance(person);

  const providerFactory = new ProviderFactory(
    apollo,
    lusha,
    cognism,
    rocketreach,
    newvendor,  // ← Add new vendor
    dataDescription,
    cacheStorage
  );

  const services: ApiServices = {
    // ... existing services
    newvendor,
  };

  return services;
};

Step 6: Create GraphQL API Layer

Create the GraphQL types and resolver in src/graphql-api/newvendor/.

a) Create Types

File: src/graphql-api/newvendor/types/newvendor-person.ts

@ObjectType("NewVendorPerson")
export class TypeNewVendorPerson { ... }

b) Create Input Types

File: src/graphql-api/newvendor/input/newvendor-person-lookup.ts

@InputType("InputNewVendorPersonLookup")
export class InputNewVendorPersonLookup { ... }

c) Create Resolver

File: src/graphql-api/newvendor/resolvers/newvendor-resolver.ts

@Resolver()
export class NewVendorResolver {
  @Query(() => TypeNewVendorPerson, { nullable: true })
  async newVendorPersonLookup(...) { ... }
}

d) Register Resolver

File: src/graphql-api/schema-resolvers.ts

import { NewVendorResolver } from "./newvendor/resolvers/newvendor-resolver";

export default [
    // ... existing resolvers
    NewVendorResolver,
];

Step 7: Create Data Repository Layer

If the vendor data needs to be persisted, create repositories in src/data/repos/newvendor/.

File: src/data/repos/newvendor/db-newvendor-person-repository.ts

export default class DbNewVendorPersonRepository
  extends DbRepository<NVPerson>
  implements NewVendorPersonRepository { ... }

File: src/data/repos/newvendor/cache-newvendor-person-repository.ts

export default class CacheNewVendorPersonRepository
  extends CacheDbRepository
  implements NewVendorPersonRepository { ... }

Model API Summary

Step Layer Files
1 Domain src/domain/newvendor/entity/, repository/, service/
2 Services src/services/newvendor/api-*.ts, default-*.ts
3 Provider Adapter src/services/provider-enrichment/newvendor-enrichment-adapter.ts
4 Factory provider-types.ts, provider-factory.ts
5 Container src/container/types.ts, services.ts
6 GraphQL src/graphql-api/newvendor/, schema-resolvers.ts
7 Data src/data/repos/newvendor/ (if persistence needed)

4. Commercial (Waterfall Integration)

Repository: cred-api-commercial

Prerequisite

The vendor must be integrated into the Model API first, exposing a providerEnrichment endpoint that returns standardized field values.

Step 1: Add Data Source Abbreviation Enum

File: src/domain/custom/entity/types.ts

Add the vendor abbreviation to CustomFieldRecordDataSourceEnum:

export enum CustomFieldRecordDataSourceEnum {
    // ... existing values
    /** Enrichment Providers */
    AP = "AP",
    LUSHA = "LUSHA",
    COG = "COG",
    ROCKETREACH = "ROCKETREACH",
    NV = "NV", // ← Add here (must match Model API abbreviation)
}

Step 2: Add Feature Constant

File: src/domain/feature/entity/feature.ts

Add a feature ID constant:

export const FEATURE_WATERFALL_DATASOURCE_NV = 99; // Use next available ID

Step 3: Add Feature Seed Entry

File: src/data/seeds/003-feature.ts

Add the feature configuration:

{
  id: 99,  // Must match the constant from Step 2
  name: "Waterfall datasource (NewVendor)",
  description: "NewVendor as data source for waterfall processing",
  transactionType: FeatureTransactionType.CREDIT,
  amount: 1,  // Credit cost per enrichment
  parentId: 24,  // FEATURE_CUSTOM_FIELD_WATERFALL
  dataSourceAbbreviation: "NV",  // Must match enum abbreviation
},

Step 4: Register in DataSource Enrichment Provider

File: src/domain/enrichment/provider/datasource-enrichment-provider.ts

Add the vendor abbreviation to the supported providers list:

const PROVIDER_ENRICHMENT_ABBREVIATIONS: CustomFieldRecordDataSourceEnum[] = [
    CustomFieldRecordDataSourceEnum.AP,
    CustomFieldRecordDataSourceEnum.LUSHA,
    CustomFieldRecordDataSourceEnum.COG,
    CustomFieldRecordDataSourceEnum.ROCKETREACH,
    CustomFieldRecordDataSourceEnum.NV, // ← Add here
];

Step 5: Add Validation

File: src/domain/custom/usecase/field/create-or-update-many-custom-field-data-source.ts

Add the new feature ID to the validation condition:

} else if (
  featureId === FEATURE_WATERFALL_DATASOURCE_CRED ||
  featureId === FEATURE_WATERFALL_DATASOURCE_AP ||
  featureId === FEATURE_WATERFALL_DATASOURCE_LUSHA ||
  featureId === FEATURE_WATERFALL_DATASOURCE_COG ||
  featureId === FEATURE_WATERFALL_DATASOURCE_ROCKETREACH ||
  featureId === FEATURE_WATERFALL_DATASOURCE_NV  // ← Add here
) {
  await this.validateCREDDataDescription(input);
}

Step 6: Add Default Configurations (Optional)

File: src/data/seeds/005-custom-field-waterfall-config.ts

If the vendor should be a default source for certain fields in new workspaces:

{
  templateName: "email",
  entityType: EntityTypeEnum.CONTACT,
  priority: 3,
  featureId: FEATURE_WATERFALL_DATASOURCE_NV,
  isEnabled: false,  // Disabled by default
  credDataDescriptionId: "xxx",  // Get from DBT DataDescription
  dataDescriptionEntityType: EntityTypeEnum.PERSON,
},

Commercial Summary

Step File Change
1 types.ts Add to CustomFieldRecordDataSourceEnum
2 feature.ts Add FEATURE_WATERFALL_DATASOURCE_* constant
3 003-feature.ts Add feature seed entry
4 datasource-enrichment-provider.ts Add to PROVIDER_ENRICHMENT_ABBREVIATIONS
5 create-or-update-many-custom-field-data-source.ts Add to validation
6 005-custom-field-waterfall-config.ts Add default configs (optional)

Cross-Project Verification

Before testing, verify naming consistency across all projects:

Check CRED Model (cred-model) DBT (cred-dbt) Model API (cred-model-api) Commercial (cred-api-commercial)
Abbreviation Table: NVPerson dataSourceAbbreviation: "NV" ProviderDataSourceAbbreviation.NV CustomFieldRecordDataSourceEnum.NV
Table Name NVPerson in migration tableName: "NVPerson" Entity: NVPerson N/A
Provider ID Column: providerUniqueId or nvId N/A Entity field: nvId N/A

!!! danger "Common Mistakes" - Using different abbreviations across projects (e.g., NV in Model API but NEW_VENDOR in Commercial) - Mismatched table names between CRED Model migration and DBT config - Forgetting to add the sync config entry in CRED Model - Not adding the provider to ProviderFactory switch statement


Checklist

Use this checklist when adding a new vendor:

  • CRED Model: Database migration and sync config created
  • DBT: Provider table added to DataDescription
  • Model API: Full integration (domain, services, adapter, factory, GraphQL)
  • Commercial: Waterfall integration (enum, feature, seed, validation)
  • Naming Consistency: Same abbreviation used in ALL projects
  • Testing: End-to-end tested in development
  • Documentation: Vendor added to relevant docs