npm package: @land-catalyst/batch-data-sdk
Description: TypeScript SDK for BatchData.io Property API - Types, Builders, and Utilities
Latest version: 1.6.1

# @land-catalyst/batch-data-sdk

TypeScript SDK for BatchData.io Property API - Complete type definitions, fluent query builders, and utilities.

## Installation

This package is published to the public npm registry. To install it:

```bash
npm install @land-catalyst/batch-data-sdk
```

Or add to `package.json`:
```json
{
  "dependencies": {
    "@land-catalyst/batch-data-sdk": "^1.0.0"
  }
}
```

## Features

- ✅ **Complete TypeScript Types** - Full type definitions for all BatchData API endpoints and responses
- ✅ **Fluent Query Builders** - Type-safe, chainable API for constructing search criteria
- ✅ **Property Search Response Types** - Comprehensive types for property search results
- ✅ **HTTP Client** - Ready-to-use API client for BatchData endpoints
- ✅ **Custom Error Classes** - Specialized error handling for BatchData operations

## Examples

### Client Setup

```typescript
import { BatchDataClient } from "@land-catalyst/batch-data-sdk";
import { ConsoleLogger } from "@land-catalyst/batch-data-sdk";

// Create a client instance
const client = new BatchDataClient({
  apiKey: process.env.BATCHDATA_API_KEY!,
  // Optional: provide custom logger
  logger: new ConsoleLogger(),
});
```

### Basic Property Search

```typescript
import {
  SearchCriteriaBuilder,
  PropertySearchRequestBuilder,
  PropertyLookupOptionsBuilder,
} from "@land-catalyst/batch-data-sdk";

// Simple search by location
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) => c.query("Maricopa County, AZ"))
  .options((o) => o.take(10).skip(0))
  .build();

const response = await client.searchProperties(request);
```

### Address-Based Search

```typescript
import {
  SearchCriteriaBuilder,
  PropertySearchRequestBuilder,
} from "@land-catalyst/batch-data-sdk";

// Search by address with filters
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .address((a) =>
        a
          .city((city) => city.equals("Phoenix"))
          .state((state) => state.equals("AZ"))
          .zip((zip) => zip.inList(["85001", "85002", "85003"]))
          .street((street) => street.contains("Main"))
      )
  )
  .build();

const response = await client.searchProperties(request);
```

### String Filter Examples

```typescript
import { StringFilterBuilder } from "@land-catalyst/batch-data-sdk";

// All string filter methods can be chained
const filter = new StringFilterBuilder()
  .equals("Phoenix")
  .contains("Phx")
  .startsWith("Ph")
  .endsWith("ix")
  .matches(["Phoenix.*", ".*Arizona"])
  .inList(["Phoenix", "Tucson", "Flagstaff"])
  .build();

// Use in address search
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("AZ")
      .address((a) =>
        a
          .city((city) => city.equals("Phoenix").contains("Phx"))
          .state((state) => state.inList(["AZ", "CA", "NV"]))
      )
  )
  .build();
```

### Numeric Range Filter Examples

```typescript
import { NumericRangeFilterBuilder } from "@land-catalyst/batch-data-sdk";

// Numeric ranges with min/max
const filter = new NumericRangeFilterBuilder()
  .min(100000)
  .max(500000)
  .build();

// Use in assessment search
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .assessment((a) =>
        a
          .totalAssessedValue((v) => v.min(100000).max(500000))
          .assessmentYear((y) => y.min(2020).max(2023))
      )
  )
  .build();
```

### Date Range Filter Examples

```typescript
import { DateRangeFilterBuilder } from "@land-catalyst/batch-data-sdk";

// Date ranges
const filter = new DateRangeFilterBuilder()
  .minDate("2020-01-01")
  .maxDate("2023-12-31")
  .build();

// Use in sale search
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("AZ")
      .sale((s) =>
        s.lastSaleDate((d) =>
          d.minDate("2020-01-01").maxDate("2023-12-31")
        )
      )
  )
  .build();
```

### Building Criteria Search

```typescript
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .building((b) =>
        b
          .yearBuilt((y) => y.min(1990).max(2020))
          .bedroomCount((br) => br.min(3).max(5))
          .bathroomCount((ba) => ba.min(2).max(4))
          .totalBuildingAreaSquareFeet((area) => area.min(1500).max(3000))
          .buildingType((type) => type.equals("Single Family"))
          .pool((pool) => pool.equals("Yes"))
          .airConditioningSource((ac) => ac.contains("Central"))
      )
  )
  .build();
```

### Assessment Criteria Search

```typescript
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .assessment((a) =>
        a
          .totalAssessedValue((v) => v.min(100000).max(500000))
          .totalMarketValue((v) => v.min(200000).max(600000))
          .assessmentYear((y) => y.min(2020).max(2023))
      )
  )
  .build();
```

### Demographics Criteria Search

```typescript
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .demographics((d) =>
        d
          .age((a) => a.min(25).max(65))
          .income((i) => i.min(50000).max(150000))
          .netWorth((nw) => nw.min(100000).max(1000000))
          .householdSize((hs) => hs.min(2).max(5))
          .homeownerRenter((hr) => hr.equals("Homeowner"))
          .gender((g) => g.equals("M").inList(["M", "F"]))
          .hasChildren(true)
          .businessOwner((bo) => bo.equals("Yes"))
      )
  )
  .build();
```

### Complex Multi-Criteria Search

```typescript
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      // Address filters
      .address((a) =>
        a
          .city((city) => city.equals("Phoenix"))
          .state((state) => state.equals("AZ"))
          .zip((zip) => zip.startsWith("85"))
      )
      // Building filters
      .building((b) =>
        b
          .yearBuilt((y) => y.min(1990).max(2020))
          .bedroomCount((br) => br.min(3).max(5))
          .bathroomCount((ba) => ba.min(2).max(4))
      )
      // Assessment filters
      .assessment((a) =>
        a
          .totalAssessedValue((v) => v.min(100000).max(500000))
          .assessmentYear((y) => y.min(2020).max(2023))
      )
      // Demographics filters
      .demographics((d) =>
        d
          .income((i) => i.min(50000).max(150000))
          .age((a) => a.min(25).max(65))
      )
      // Quick list
      .quickList("vacant")
  )
  .options((o) =>
    o
      .take(50)
      .skip(0)
      .bedrooms(3, 5)
      .bathrooms(2, 4)
      .yearBuilt(1990, 2020)
      .skipTrace(true)
      .images(true)
      .quicklistCounts(true)
  )
  .build();
```

### Property Search with Options

```typescript
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) => c.query("Maricopa County, AZ"))
  .options((o) =>
    o
      // Pagination
      .pagination(0, 50)
      .skip(0)
      .take(50)
      // Distance filtering
      .distance(5) // 5 miles
      .distance(undefined, 8800, 26400) // yards and feet
      .distance(undefined, undefined, undefined, 8, 8047) // km and meters
      // Bounding box
      .boundingBox(
        { latitude: 33.5, longitude: -112.1 },
        { latitude: 33.4, longitude: -112.0 }
      )
      // Property features
      .bedrooms(3, 5)
      .bathrooms(2, 4)
      .stories(1, 2)
      .area(80, 120) // percentage
      .yearBuilt(1990, 2020)
      .lotSize(90, 110) // percentage
      // Flags
      .skipTrace(true)
      .aggregateLoanTypes(true)
      .images(true)
      .showRequests(true)
      .areaPolygon(true)
      .quicklistCounts(true)
      .useSubdivision(true)
      // Formatting
      .dateFormat("YYYY-MM-DD")
      // Sorting
      .sort("totalAssessedValue", "desc", "session-12345")
      .build()
  )
  .build();
```

### Property Subscription

```typescript
import {
  PropertySubscriptionBuilder,
  DeliveryConfigBuilder,
} from "@land-catalyst/batch-data-sdk";

// Webhook subscription
const subscription = new PropertySubscriptionBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .address((a) =>
        a
          .city((city) => city.equals("Phoenix"))
          .state((state) => state.equals("AZ"))
      )
      .quickList("vacant")
  )
  .deliveryConfig((dc) =>
    dc.webhook("https://example.com/webhook", {
      "X-API-Key": "secret",
    })
  )
  .build();

const response = await client.createPropertySubscription(subscription);
```

### Property Subscription with Kinesis

```typescript
const subscription = new PropertySubscriptionBuilder()
  .searchCriteria((c) => c.query("US").quickList("owner-occupied"))
  .deliveryConfig((dc) =>
    dc.kinesis(
      "my-stream",
      "us-east-1",
      "access-key-id",
      "secret-access-key"
    )
  )
  .build();
```

### Property Subscription with Event Hub

```typescript
const subscription = new PropertySubscriptionBuilder()
  .searchCriteria((c) => c.query("Phoenix, AZ").orQuickLists(["on-market"]))
  .deliveryConfig((dc) =>
    dc.eventHub({
      fullyQualifiedNamespace: "mynamespace.servicebus.windows.net",
      clientId: "12345678-1234-1234-1234-123456789abc",
      tenantId: "87654321-4321-4321-4321-cba987654321",
      clientSecret: "your-client-secret",
      eventHubName: "property-events",
    })
  )
  .build();
```

### Property Lookup by Address

```typescript
import {
  PropertyLookupRequestBuilder,
  PropertyLookupRequestItemBuilder,
} from "@land-catalyst/batch-data-sdk";

// Single property lookup
const request = new PropertyLookupRequestBuilder()
  .addItem(
    new PropertyLookupRequestItemBuilder()
      .address({
        street: "2800 N 24th St",
        city: "Phoenix",
        state: "AZ",
        zip: "85008",
      })
      .build()
  )
  .options((o) => o.take(1).skipTrace(true))
  .build();

const response = await client.lookupProperty(request);
```

### Property Lookup by Multiple Identifiers

```typescript
const request = new PropertyLookupRequestBuilder()
  .addItem(
    new PropertyLookupRequestItemBuilder()
      .propertyId("12345")
      .build()
  )
  .addItem(
    new PropertyLookupRequestItemBuilder()
      .apn("123-45-678")
      .countyFipsCode("04013")
      .build()
  )
  .addItem(
    new PropertyLookupRequestItemBuilder()
      .hash("abc123def456")
      .build()
  )
  .options((o) =>
    o
      .take(10)
      .images(true)
      .skipTrace(true)
      .showRequests(true)
  )
  .build();
```

### Async Property Lookup

```typescript
import {
  PropertyLookupAsyncRequestBuilder,
  AsyncPropertyLookupOptionsBuilder,
} from "@land-catalyst/batch-data-sdk";

const request = new PropertyLookupAsyncRequestBuilder()
  .addItem(
    new PropertyLookupRequestItemBuilder()
      .address({
        street: "2800 N 24th St",
        city: "Phoenix",
        state: "AZ",
        zip: "85008",
      })
      .build()
  )
  .options(
    new AsyncPropertyLookupOptionsBuilder()
      .webhook(
        "https://example.com/webhook",
        "https://example.com/error-webhook"
      )
      .build()
  )
  .build();

const response = await client.lookupPropertyAsync(request);
```

### Async Property Search

```typescript
import { PropertySearchAsyncRequestBuilder } from "@land-catalyst/batch-data-sdk";

const request = new PropertySearchAsyncRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      .address((a) =>
        a
          .city((city) => city.equals("Phoenix"))
          .state((state) => state.equals("AZ"))
      )
  )
  .options(
    new AsyncPropertyLookupOptionsBuilder()
      .webhook("https://example.com/webhook")
      .take(100)
      .build()
  )
  .build();

const response = await client.searchPropertiesAsync(request);
```

### Address Verification

```typescript
const request = {
  requests: [
    {
      street: "2800 N 24th St",
      city: "Phoenix",
      state: "Arizona",
      zip: "85008",
    },
  ],
};

const response = await client.verifyAddress(request);
```

### Address Autocomplete

```typescript
const request = {
  searchCriteria: {
    query: "2800 N 24th St Phoenix",
  },
  options: {
    skip: 0,
    take: 5,
  },
};

const response = await client.autocompleteAddress(request);
```

### Geocoding

```typescript
// Geocode address to coordinates
const geocodeRequest = {
  requests: [
    {
      address: "2800 N 24th St, Phoenix, AZ, 85008",
    },
  ],
};

const geocodeResponse = await client.geocodeAddress(geocodeRequest);

// Reverse geocode coordinates to address
const reverseGeocodeRequest = {
  request: {
    latitude: 33.47865,
    longitude: -112.03029,
  },
};

const reverseGeocodeResponse =
  await client.reverseGeocodeAddress(reverseGeocodeRequest);
```

### Builder Accumulation Pattern

```typescript
// Multiple calls to the same method accumulate changes
const criteria = new AddressSearchCriteriaBuilder()
  .street((s) => s.equals("Main"))
  .street((s) => s.contains("St"))
  .street((s) => s.startsWith("M"))
  .build();

// Result: { street: { equals: "Main", contains: "St", startsWith: "M" } }

// Direct values replace previous builder configuration
const criteria2 = new AddressSearchCriteriaBuilder()
  .street((s) => s.equals("Main St"))
  .street({ equals: "New Street" }) // Replaces previous
  .build();

// Result: { street: { equals: "New Street" } }
```

### Using Direct Values vs Lambda Configurators

```typescript
// Both patterns are supported - use whichever is more convenient

// Lambda pattern (recommended for complex configurations)
const request1 = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("AZ")
      .address((a) =>
        a
          .city((city) => city.equals("Phoenix").contains("Phx"))
          .state((state) => state.inList(["AZ", "CA"]))
      )
  )
  .build();

// Direct value pattern (useful for simple cases)
const request2 = new PropertySearchRequestBuilder()
  .searchCriteria({
    query: "AZ",
    address: {
      city: { equals: "Phoenix" },
      state: { equals: "AZ" },
    },
  })
  .options({
    take: 10,
    skip: 0,
  })
  .build();
```

### Complex Nested Search

```typescript
// Ultra-complex search with all criteria types
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Maricopa County, AZ")
      // Address with multiple filters
      .address((a) =>
        a
          .street((s) =>
            s
              .equals("Main")
              .contains("Street")
              .startsWith("M")
              .endsWith("St")
              .inList(["Main St", "Main Street", "Main Ave"])
          )
          .city((city) =>
            city.equals("Phoenix").contains("Phx").matches(["Phoenix.*"])
          )
          .state((state) => state.equals("AZ"))
          .zip((zip) => zip.startsWith("85").endsWith("01"))
      )
      // Assessment with accumulation
      .assessment((a) =>
        a
          .assessmentYear((y) => y.min(2020).max(2023))
          .assessmentYear((y) => y.min(2019)) // Accumulates
          .totalAssessedValue((v) => v.min(100000).max(500000))
          .totalAssessedValue((v) => v.min(150000)) // Accumulates
      )
      // Building criteria
      .building((b) =>
        b
          .buildingType((t) =>
            t.equals("Single Family").inList([
              "Single Family",
              "Townhouse",
              "Condo",
            ])
          )
          .yearBuilt((y) => y.min(1990).max(2020))
          .bedroomCount((br) => br.min(3).max(5))
          .bathroomCount((ba) => ba.min(2).max(4))
          .pool((pool) => pool.equals("Yes"))
      )
      // Demographics
      .demographics((d) =>
        d
          .age((a) => a.min(25).max(65))
          .income((i) => i.min(50000).max(150000))
          .income((i) => i.min(60000)) // Accumulates
          .gender((g) => g.equals("M").inList(["M", "F"]))
          .hasChildren(true)
      )
      // Quick lists
      .quickList("vacant")
  )
  .options((o) =>
    o
      .pagination(100, 50)
      .distance(5, 8800, 26400, 8, 8047)
      .bedrooms(3, 5)
      .bathrooms(2, 4)
      .yearBuilt(1990, 2020)
      .skipTrace(true)
      .images(true)
      .sort("totalAssessedValue", "desc", "session-12345")
      .build()
  )
  .build();
```

### Error Handling

```typescript
import { PropertyCountLimitExceededError } from "@land-catalyst/batch-data-sdk";

try {
  const response = await client.searchProperties(request);
} catch (error) {
  if (error instanceof PropertyCountLimitExceededError) {
    console.log(`Limit exceeded: ${error.count} > ${error.limit}`);
    console.log(`Limit type: ${error.limitType}`); // "sync" or "async"
  } else if (error instanceof Error) {
    console.error("API error:", error.message);
  }
}
```

### Using Builders with Existing Data

```typescript
// Create builder from existing criteria
const existingCriteria: SearchCriteria = {
  query: "AZ",
  address: {
    city: { equals: "Phoenix" },
  },
};

const builder = SearchCriteriaBuilder.from(existingCriteria);
builder.address((a) => a.state((s) => s.equals("AZ")));

const updatedCriteria = builder.build();
```

### GeoLocation Filters

```typescript
import { GeoLocationFactory } from "@land-catalyst/batch-data-sdk";

// Distance-based search
const request = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Phoenix, AZ")
      .address((a) =>
        a.geoLocationDistance(33.4484, -112.074, {
          miles: "5",
        })
      )
  )
  .build();

// Bounding box search
const request2 = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Phoenix, AZ")
      .address((a) =>
        a.geoLocationBoundingBox(
          33.5, // NW latitude
          -112.1, // NW longitude
          33.4, // SE latitude
          -112.0 // SE longitude
        )
      )
  )
  .build();

// Polygon search
const request3 = new PropertySearchRequestBuilder()
  .searchCriteria((c) =>
    c
      .query("Phoenix, AZ")
      .address((a) =>
        a.geoLocationPolygon([
          { latitude: 33.5, longitude: -112.1 },
          { latitude: 33.4, longitude: -112.1 },
          { latitude: 33.4, longitude: -112.0 },
          { latitude: 33.5, longitude: -112.0 },
        ])
      )
  )
  .build();
```

### All Search Criteria Types

```typescript
// Foreclosure search
.searchCriteria((c) =>
  c
    .query("AZ")
    .foreclosure((f) =>
      f
        .status((s) => s.equals("Active"))
        .recordingDate((d) => d.minDate("2020-01-01"))
    )
)

// Listing search
.listing((l) =>
  l
    .price((p) => p.min(200000).max(500000))
    .status((s) => s.equals("Active"))
    .daysOnMarket((d) => d.min(0).max(90))
)

// Sale search
.sale((s) =>
  s
    .lastSalePrice((p) => p.min(100000).max(400000))
    .lastSaleDate((d) => d.minDate("2020-01-01"))
)

// Owner search
.owner((o) =>
  o
    .firstName((f) => f.equals("John"))
    .lastName((l) => l.equals("Doe"))
    .ownerOccupied(true)
)

// Open lien search
.openLien((ol) =>
  ol
    .totalOpenLienCount((c) => c.min(1).max(5))
    .totalOpenLienBalance((b) => b.min(50000).max(500000))
)

// Permit search
.permit((p) =>
  p
    .permitCount((c) => c.min(1).max(10))
    .totalJobValue((v) => v.min(10000).max(100000))
)

// Tax search
.tax((t) => t.taxDelinquentYear((y) => y.min(2020).max(2023)))

// Valuation search
.valuation((v) =>
  v
    .estimatedValue((ev) => ev.min(200000).max(600000))
    .ltv((ltv) => ltv.min(0).max(80))
)

// Intel search
.intel((i) =>
  i
    .lastSoldPrice((p) => p.min(150000).max(450000))
    .lastSoldDate((d) => d.minDate("2020-01-01"))
    .salePropensity((sp) => sp.min(50).max(100))
)
```

### Quick Lists

```typescript
// Single quick list
.quickList("vacant")

// Multiple quick lists (AND)
.quickLists(["vacant", "owner-occupied", "high-equity"])

// Multiple quick lists (OR)
.orQuickLists(["vacant", "preforeclosure"])

// Exclude quick list
.quickList("not-vacant")
```

### Sorting and Pagination

```typescript
.options((o) =>
  o
    // Pagination
    .pagination(0, 50) // skip 0, take 50
    .skip(100) // Override skip
    .take(25) // Override take
    // Sorting
    .sort("totalAssessedValue", "desc")
    .sort("yearBuilt", "asc", "session-12345") // With session ID
    .build()
)
```

### Response Handling

```typescript
const response = await client.searchProperties(request);

// Check status
if (response.status?.code === 200) {
  console.log("Success!");
}

// Access properties
if (response.results?.properties) {
  response.results.properties.forEach((property) => {
    console.log(property.address?.street);
    console.log(property.valuation?.estimatedValue);
    console.log(property.demographics?.income);
    console.log(property.building?.yearBuilt);
  });
}

// Access metadata
if (response.results?.meta) {
  console.log(
    `Found ${response.results.meta.results?.resultsFound} properties`
  );
  console.log(
    `Request took ${response.results.meta.performance?.totalRequestTime}ms`
  );
}

// Quick list counts
if (response.results?.quicklistCounts) {
  response.results.quicklistCounts.forEach((count) => {
    console.log(`${count.name}: ${count.count}`);
  });
}
```

## API Documentation

### Types

- `SearchCriteria` - Complete search criteria structure
- `PropertySubscriptionRequest` - Subscription creation request
- `PropertySubscriptionResponse` - Subscription creation response
- `PropertySearchResponse` - Property search API response
- `Property` - Individual property object with all nested data
- `DeliveryConfig` - Webhook, Kinesis, or Event Hub delivery configuration
- `EventHubConfig` - Azure Event Hub configuration

### Builders

All builders follow a fluent, chainable pattern:

- `SearchCriteriaBuilder` - Main search criteria builder
- `AddressSearchCriteriaBuilder` - Address filters
- `BuildingSearchCriteriaBuilder` - Building/property filters
- `StringFilterBuilder` - String comparison filters
- `NumericRangeFilterBuilder` - Numeric range filters
- `DateRangeFilterBuilder` - Date range filters
- And many more...

### Errors

- `PropertyCountLimitExceededError` - Thrown when search results exceed configured limits

## Development

```bash
# Install dependencies
npm install

# Build the package
npm run build

# Run tests
npm test

# Run integration tests (requires BATCHDATA_API_KEY)
npm run test:integration

# The built files will be in ./dist
```

### Running Integration Tests

Integration tests make real API calls and require a valid API key:

```bash
export BATCHDATA_API_KEY=your_api_key_here
npm run test:integration
```

## Publishing

This package is published to npm using trusted publishing (OIDC) via GitHub Actions. Publishing happens automatically when a version tag is pushed.

### Manual Publishing (if needed)

If you need to publish manually, set up `.npmrc` to use your npm token:

1. **Create `.npmrc` file** (add to `.gitignore` - already configured):
   ```bash
   # Option 1: Use environment variable (recommended)
   echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
   
   # Option 2: Use token directly (less secure)
   echo "//registry.npmjs.org/:_authToken=your-token-here" > .npmrc
   ```

2. **Set your npm token as an environment variable**:
   ```bash
   export NPM_TOKEN=your-npm-token-here
   ```

3. **Publish**:
   ```bash
   npm run build
   npm publish --access public
   ```

**Note:** The `.npmrc` file is already in `.gitignore` so it won't be committed. See `.npmrc.example` for a template.

### Setting Up GitHub Actions Publishing (Trusted Publishing)

The workflow uses npm trusted publishing (OIDC) - no tokens needed! To set it up:

1. **Initial Manual Publish** (one-time, to create the package on npm):
   ```bash
   # Set up your .npmrc first (see Manual Publishing section above)
   npm run build
   npm publish --access public
   ```

2. **Enable Trusted Publishing on npm**:
   - Go to [npmjs.com](https://www.npmjs.com) and log in
   - Navigate to your package: `@land-catalyst/batch-data-sdk`
   - Go to **Package Settings** → **Automation** → **Trusted Publishing**
   - Click **Add GitHub Actions workflow**
   - Select repository: `land-catalyst/batch-data-sdk`
   - Select workflow file: `.github/workflows/cd.yml`
   - Click **Approve**

3. **Verify Setup**:
   - After enabling, the workflow will automatically publish to npm when you push a version tag
   - No authentication tokens needed - OIDC handles it automatically!
   - The package will show a verified checkmark on npm

For detailed setup instructions, see [SETUP_NPM_PUBLISHING.md](./SETUP_NPM_PUBLISHING.md).

### Versioning

To create a new version:

```bash
# Patch version (1.1.12 -> 1.1.13)
npm run v:patch

# Minor version (1.1.12 -> 1.2.0)
npm run v:minor

# Major version (1.1.12 -> 2.0.0)
npm run v:major
```

This will:
1. Update the version in `package.json`
2. Create a git tag
3. Push the tag to GitHub
4. Trigger the CD workflow to publish to npm

## License

MIT