# Attio Fetch Implementation

Native Node.js fetch-based implementation for Attio CRM integration.

## Overview

This directory contains fetch-based implementations for Attio record operations, replacing the attio-js SDK with native Node.js fetch API for zero external dependencies and full control over HTTP communication.

**Supported Operations:**

- `create-record/` - Create new records (people, companies, deals, custom objects)
- `update-record/` - Update existing records
- `list-records/` - Query records with filtering, sorting, pagination
- `get-record/` - Retrieve single record by ID

**Deferred (Future Implementation):**

- Tasks and notes operations (to be added later if needed)

## Attio API Overview

### Base URL

```
https://api.attio.com/v2
```

### Authentication

All requests use Bearer token authentication:

```typescript
headers: {
  'Authorization': `Bearer ${apiKey}`,
  'Content-Type': 'application/json'
}
```

**Obtaining API Key:**

1. Log in to Attio workspace at https://app.attio.com
2. Navigate to Settings → Developers → API Keys
3. Click "Create API Key"
4. Copy the generated key

### Object Types

Attio uses a unified object model where different record types are specified via the `{object}` path parameter:

| Object Type | Slug | Example Use Case |
|------------|------|------------------|
| People (Contacts) | `people` | Individual contacts |
| Companies | `companies` | Organizations |
| Deals | `deals` | Sales opportunities |
| Custom Objects | `{custom-slug}` | User-defined types |

### Request Body Format

All write operations (POST, PATCH) use nested structure:

```json
{
  "data": {
    "values": {
      "attribute_slug": "value",
      "another_attribute": ["array", "values"]
    }
  }
}
```

### Response Format

All responses return data in nested structure:

```json
{
  "data": {
    "id": {
      "workspace_id": "...",
      "object_id": "...",
      "record_id": "..."
    },
    "created_at": "2022-11-21T13:22:49.061281000Z",
    "web_url": "https://app.attio.com/...",
    "values": {
      "attribute_slug": [...]
    }
  }
}
```

### Error Response Format

```json
{
  "status_code": 404,
  "type": "invalid_request_error",
  "code": "not_found",
  "message": "Record not found"
}
```

## Standard Fetch Pattern

All Attio fetch methods follow this standard pattern:

```typescript
/**
 * Standard Attio fetch method pattern
 *
 * Template for implementing new Attio operations.
 * Standard Attio fetch method template
 */

import { ToolingError } from '../../../../../../types'
import { withRetry } from '../../../../../../utils/retry'
import { DEFAULT_RETRY_POLICY } from '../../../../../../utils/types'
import type { ExecutionContext } from '../../../../../../../base/types'
import { mapAttioHttpError } from '../utils/error-mapping'
import type { AttioCredentials } from '../utils/types'

/**
 * Method parameters interface
 */
export interface MethodParams {
  // Define method-specific parameters
  object: string
  // ... other params
}

/**
 * Method result interface
 */
export interface MethodResult {
  // Define method-specific result shape
  success: boolean
  // ... other fields
}

/**
 * Execute Attio operation
 *
 * @param credentials - Attio API credentials (apiKey)
 * @param params - Method parameters
 * @param context - Execution context for logging
 * @returns Method result
 * @throws ToolingError on API errors
 */
export async function methodName(
  credentials: AttioCredentials,
  params: MethodParams,
  context?: ExecutionContext
): Promise<MethodResult> {
  const { apiKey } = credentials

  // Validate credentials
  if (!apiKey) {
    throw new ToolingError('credentials_invalid', 'Missing Attio API key', {
      integration: 'attio',
      method: 'methodName'
    })
  }

  // Validate required parameters
  if (!params.object) {
    throw new ToolingError('validation_error', 'Object type is required', {
      integration: 'attio',
      method: 'methodName'
    })
  }

  try {
    // Execute with retry logic
    const result = await withRetry(async () => {
      // Build request (GET example)
      const response = await fetch(
        `https://api.attio.com/v2/objects/${params.object}/records`,
        {
          method: 'GET',
          headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Accept': 'application/json'
          }
        }
      )

      // OR for POST/PATCH with body:
      // const response = await fetch(url, {
      //   method: 'POST',
      //   headers: {
      //     'Authorization': `Bearer ${apiKey}`,
      //     'Content-Type': 'application/json'
      //   },
      //   body: JSON.stringify({
      //     data: {
      //       values: params.values
      //     }
      //   })
      // })

      // Handle HTTP errors
      if (!response.ok) {
        throw await mapAttioHttpError(response, 'methodName', context)
      }

      // Parse response
      const data = await response.json()
      return data
    }, DEFAULT_RETRY_POLICY)

    // Log success
    if (context) {
      console.log('[AttioAdapter] Method executed:', {
        organizationId: context.organizationId,
        executionId: context.executionId,
        object: params.object
      })
    }

    // Transform to result format
    return {
      success: true
      // ... other fields
    }
  } catch (error) {
    // Re-throw ToolingError as-is
    if (error instanceof ToolingError) {
      throw error
    }

    // Wrap unknown errors
    throw new ToolingError(
      'api_error',
      `Failed to execute Attio method: ${(error as Error).message}`,
      {
        integration: 'attio',
        method: 'methodName',
        organizationId: context?.organizationId,
        params
      }
    )
  }
}
```

## Error Handling Conventions

### Error Mapping

All HTTP errors are mapped using `mapAttioHttpError()` utility:

```typescript
if (!response.ok) {
  throw await mapAttioHttpError(response, 'methodName', context)
}
```

**Error Type Mapping:**

| HTTP Status | ToolingError Type | Retryable | Description |
|------------|-------------------|-----------|-------------|
| 401 | `credentials_invalid` | No | Invalid API key |
| 404 | `validation_error` | No | Resource not found |
| 409 | `validation_error` | No | Conflict (duplicate) |
| 400 | `validation_error` | No | Bad request |
| 429 | `rate_limit_exceeded` | Yes | Rate limit exceeded |
| 500+ | `server_unavailable` | Yes | Server error |
| Other 4xx | `validation_error` | No | Client error |
| Other | `api_error` | Yes | Unknown error |

### Retry Logic

All fetch methods use `withRetry()` wrapper with `DEFAULT_RETRY_POLICY`:

```typescript
import { withRetry } from '../../../../../../utils/retry'
import { DEFAULT_RETRY_POLICY } from '../../../../../../utils/types'

const result = await withRetry(async () => {
  // Fetch operation here
}, DEFAULT_RETRY_POLICY)
```

**Retry Policy:**

- Max retries: 3
- Initial delay: 1s
- Max delay: 10s
- Jitter factor: 0.3 (30% randomness)
- Only retries on: `rate_limit_exceeded`, `server_unavailable`, `api_error`, `network_error`, `timeout_error`

### Logging

All operations log success with execution context:

```typescript
if (context) {
  console.log('[AttioAdapter] Record created:', {
    organizationId: context.organizationId,
    executionId: context.executionId,
    recordId: result.id,
    object: params.object
  })
}
```

## Testing Approach

### Unit Tests

Each fetch method should have unit tests covering:

1. **Success cases:**
   - Valid API response → correct result transformation
   - Optional parameters handled correctly

2. **Error cases:**
   - 401 → `credentials_invalid`
   - 404 → `validation_error`
   - 429 → `rate_limit_exceeded` (with retry)
   - 500+ → `server_unavailable` (with retry)

3. **Validation:**
   - Missing credentials → error
   - Missing required params → error

**Example:**

```typescript
import { describe, it, expect, vi } from 'vitest'
import { createRecord } from './index'
import { ToolingError } from '../../../../../../types'

describe('createRecord', () => {
  it('should create record successfully', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        data: {
          id: { record_id: 'test-id' },
          created_at: '2025-01-01T00:00:00Z',
          web_url: 'https://app.attio.com/...',
          values: {}
        }
      })
    })

    const result = await createRecord(
      { apiKey: 'test-key' },
      { object: 'people', values: { name: 'Test' } }
    )

    expect(result.recordId).toBe('test-id')
    expect(result.success).toBe(true)
  })

  it('should throw credentials_invalid on 401', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      statusText: 'Unauthorized',
      text: async () => JSON.stringify({ message: 'Invalid API key' })
    })

    await expect(
      createRecord({ apiKey: 'bad-key' }, { object: 'people', values: {} })
    ).rejects.toThrow(ToolingError)
  })
})
```

### Integration Tests

Test with real Attio API (test workspace):

1. Create record → verify success
2. Update record → verify changes
3. Query records → verify filtering
4. Get record → verify retrieval
5. Delete test data

### Manual Testing

Test tools via AI Studio or execution engine:

```typescript
// Test via IntegrationService
const service = IntegrationService.getInstance()
await service.execute(
  'attio',
  'createRecord',
  { object: 'people', values: { name: 'Test Contact' } },
  credentials,
  context
)
```

## Migration from SDK Notes

### What Changed

**Before (SDK):**

```typescript
import Attio from 'attio-js'

const client = new Attio({ apiKey })
const record = await client.objects.records.create({
  object: 'people',
  values: { name: 'John Doe' }
})
```

**After (Fetch):**

```typescript
import { createRecord } from './fetch/create-record/index.js'

const result = await createRecord(
  { apiKey },
  { object: 'people', values: { name: 'John Doe' } },
  context
)
```

### Why Fetch is Better

1. **Zero Dependencies:** No external SDK to maintain/update
2. **Full Control:** Direct HTTP communication, custom error handling
3. **Consistency:** Standardized fetch pattern across all adapters
4. **Observability:** Explicit logging with organization/execution context
5. **Type Safety:** Custom TypeScript interfaces match our domain

### Breaking Changes

**None for tool users** - Tool interfaces remain identical:

- `attio_create_record`
- `attio_update_record`
- `attio_list_records`
- `attio_get_record`

**Internal changes only:**

- `AttioAdapter` calls fetch functions instead of SDK methods
- Removed `createTask` and `createNote` (deferred to future)

## Common Attribute Types

### Text Fields

```typescript
{
  description: "This is a text field"
}
```

### Email Addresses

```typescript
{
  email_addresses: [
    { email_address: "user@example.com" }
  ]
}
```

### Phone Numbers

```typescript
{
  phone_numbers: [
    {
      original_phone_number: "+15558675309",
      country_code: "US"
    }
  ]
}
```

### Name (Person)

```typescript
{
  name: {
    first_name: "John",
    last_name: "Doe",
    full_name: "John Doe"
  }
}
```

### Domains (Company)

```typescript
{
  domains: [
    { domain: "example.com" }
  ]
}
```

### Record References (Relationships)

```typescript
{
  company: [
    {
      target_object: "companies",
      target_record_id: "company-uuid"
    }
  ]
}
```

### Currency/Money

```typescript
{
  value: {
    currency_code: "USD",
    value: 50000
  }
}
```

## Examples

### Create Person

```typescript
import { createRecord } from './fetch/create-record/index.js'

const person = await createRecord(
  { apiKey: 'your-api-key' },
  {
    object: 'people',
    values: {
      name: {
        first_name: 'Jane',
        last_name: 'Smith',
        full_name: 'Jane Smith'
      },
      email_addresses: [
        { email_address: 'jane@example.com' }
      ],
      company: [
        {
          target_object: 'companies',
          target_record_id: 'company-uuid'
        }
      ]
    }
  },
  context
)
```

### Query Companies by Domain

```typescript
import { listRecords } from './fetch/list-records/index.js'

const companies = await listRecords(
  { apiKey: 'your-api-key' },
  {
    object: 'companies',
    filter: {
      domains: {
        domain: {
          $eq: 'acme.com'
        }
      }
    },
    sorts: [
      {
        attribute: 'name',
        direction: 'asc'
      }
    ],
    limit: 50
  },
  context
)
```

### Update Deal Status

```typescript
import { updateRecord } from './fetch/update-record/index.js'

const updated = await updateRecord(
  { apiKey: 'your-api-key' },
  {
    object: 'deals',
    recordId: 'deal-uuid',
    values: {
      status: 'closed_won',
      value: {
        currency_code: 'USD',
        value: 100000
      }
    }
  },
  context
)
```

### Get Single Record

```typescript
import { getRecord } from './fetch/get-record/index.js'

const record = await getRecord(
  { apiKey: 'your-api-key' },
  {
    object: 'people',
    recordId: 'person-uuid'
  },
  context
)

console.log('Person name:', record.values.name)
```

## Related Documentation

- [Records Implementation Spec](../../../../../../../apps/docs/content/docs/in-progress/active-development/attio-fetch-migration/records-implementation.mdx) - Complete Attio Records API specification
- [Design Plan](../../../../../../../apps/docs/content/docs/in-progress/active-development/attio-fetch-migration/design-plan.mdx) - Overall migration plan
- [Attio API Docs](https://attio.mintlify.app/rest-api) - Official API documentation
- Current SDK Implementation: `../attio-adapter.ts` (to be replaced)
