Skip to main content

Mappers System

Overview

The Mappers System provides pure, side-effect-free transformation functions that convert internal application data models into external CRM (Customer Relationship Management) payloads. Currently, it implements mappers for the Twenty CRM integration, converting ClientProfile and Company entities into Twenty-compatible Person and Company payloads with null-safe field mapping and required-field validation.

Architecture

The mappers module lives in lib/mappers/ and follows a strict separation-of-concerns pattern:

  • Mappers are pure functions: no I/O, no database calls, no HTTP requests.
  • Services (in lib/services/) consume mappers to prepare data before sending to external APIs.
  • Types are imported from the database schema (lib/db/schema) and CRM type definitions (lib/types/twenty-crm-entities.types).
lib/mappers/
|-- twenty-crm.mapper.ts
|-- ensureExternalId() (ID validation)
|-- extractCityFromLocation() (Location parsing)
|-- mapClientProfileToPerson() (ClientProfile -> TwentyPerson)
|-- mapCompanyToTwentyCompany() (Company -> TwentyCompany)

The data flow is:

Database Entity  -->  Mapper Function  -->  CRM Payload  -->  Service  -->  External API
(ClientProfile) (mapClientProfile (TwentyPerson) (CRM Service) (Twenty CRM)
ToPerson)

API Reference

Exports from lib/mappers/twenty-crm.mapper.ts

ensureExternalId(id: string | undefined | null, entityType: string): string

Validates that an entity ID is present and non-empty. This is a critical safety check ensuring that every CRM record has a valid external_id linking back to the local system.

Parameters:

  • id -- The local entity ID (may be undefined or null)
  • entityType -- Entity type name for error messages (e.g., 'ClientProfile')

Returns: Trimmed ID string

Throws: Error if the ID is missing, null, undefined, or an empty string.

extractCityFromLocation(location: string | undefined | null): string | null

Parses a free-form location string to extract the city name. Handles various formats by splitting on commas and taking the first part.

Supported formats:

  • "San Francisco" --> "San Francisco"
  • "San Francisco, CA" --> "San Francisco"
  • "San Francisco, CA, USA" --> "San Francisco"

Returns: The city name or null if the location is empty/undefined.

mapClientProfileToPerson(clientProfile: ClientProfile): TwentyPerson

Maps a local ClientProfile database entity to a Twenty CRM Person payload.

Field mapping:

ClientProfile FieldTwentyPerson FieldRequired
idexternal_idYes (throws if missing)
namenameYes
emailemailYes
phonephoneOptional
jobTitlejob_titleOptional
companycompany_nameOptional
websitewebsiteOptional
locationcity (extracted)Optional
accountTypeaccount_typeOptional
planplanOptional
totalSubmissionstotal_submissionsOptional

Returns: A TwentyPerson object with only populated fields.

Throws: Error if clientProfile.id is missing.

mapCompanyToTwentyCompany(company: Company): TwentyCompany

Maps a local Company entity to a Twenty CRM Company payload.

Field mapping:

Company FieldTwentyCompany FieldRequired
idexternal_idYes (throws if missing)
namenameYes
domaindomain_nameOptional
websitewebsiteOptional
statusstatusOptional

Returns: A TwentyCompany object with only populated fields.

Throws: Error if company.id is missing.

Implementation Details

Null-safe mapping: Optional fields use explicit if checks before assignment, ensuring that null, undefined, and empty values are never sent to the CRM. This keeps payloads clean and avoids overwriting existing CRM data with null values.

External ID enforcement: Every mapper calls ensureExternalId() as its first operation. This throws immediately on invalid IDs, following a fail-fast pattern that prevents orphaned records in the CRM.

No mutation: Mapper functions create new objects rather than modifying the input. The input ClientProfile or Company object is never altered.

Optional field pruning: Fields are only added to the output object when they have truthy values. This produces minimal payloads that only update non-null fields in the CRM.

City extraction heuristic: The extractCityFromLocation() function uses a simple comma-split approach. This handles the most common location formats (City, City + State, City + State + Country) but does not attempt to parse complex address formats.

Configuration

No configuration is required. The mappers are pure functions that depend only on their input types. The Twenty CRM connection configuration (API URL, tokens) is managed by the integration service layer.

Usage Examples

import {
mapClientProfileToPerson,
mapCompanyToTwentyCompany,
ensureExternalId,
extractCityFromLocation,
} from '@/lib/mappers/twenty-crm.mapper';

// Map a client profile to a CRM person
const clientProfile = await db.query.clientProfiles.findFirst({
where: eq(clientProfiles.id, userId),
});

const personPayload = mapClientProfileToPerson(clientProfile);
// {
// external_id: "usr_abc123",
// name: "Jane Doe",
// email: "jane@example.com",
// job_title: "CTO",
// company_name: "Acme Corp",
// city: "San Francisco",
// plan: "premium",
// }

// Map a company to a CRM company
const company = await db.query.companies.findFirst({
where: eq(companies.id, companyId),
});

const companyPayload = mapCompanyToTwentyCompany(company);
// {
// external_id: "comp_xyz789",
// name: "Acme Corp",
// domain_name: "acme.com",
// website: "https://acme.com",
// status: "active",
// }

// Use utility functions independently
const city = extractCityFromLocation("Berlin, Germany");
// "Berlin"

const validId = ensureExternalId(user.id, "User");
// "usr_abc123" or throws Error

Best Practices

  • Always use the mapper functions instead of manually constructing CRM payloads to ensure consistent field naming and null safety.
  • Handle the Error thrown by ensureExternalId() at the service layer; log it and skip the CRM sync for that record rather than crashing the entire batch.
  • When adding new fields to a mapper, follow the existing pattern: check for truthiness before assigning to the output object.
  • Write unit tests for mappers since they are pure functions with no dependencies, making them easy to test in isolation.
  • If a new CRM integration is needed, create a new mapper file (e.g., hubspot.mapper.ts) in the same directory following the same patterns.