@saturation-api/js
Version:
TypeScript SDK for the Saturation API
870 lines (710 loc) • 22.6 kB
Markdown
# @saturation-api/js
Official TypeScript SDK for the Saturation API. Build powerful integrations with your workspace data including projects, budgets, actuals, contacts, purchase orders, and attachments.
[](https://www.npmjs.com/package/@saturation-api/js)
[](https://github.com/saturation-api/saturation-js/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
## Features
- 🚀 **Full TypeScript Support** - Auto-generated types from OpenAPI specification
- 📦 **Universal JavaScript** - Works in Node.js, browsers, and React Native
- 🌐 **Browser Compatible** - CORS-friendly with native fetch API support
- 🔄 **Real-time Collaboration** - Changes appear instantly in the web app
- 🎯 **Type-Safe** - Complete type coverage for all API endpoints
- 🛠️ **Developer Friendly** - Intuitive API with comprehensive documentation
- ⚡ **Lightweight** - Minimal dependencies, optimized bundle size
## Installation
```bash
npm install @saturation-api/js
# or
yarn add @saturation-api/js
# or
pnpm add @saturation-api/js
```
## Quick Start
```typescript
import { Saturation, type Project, type Budget } from '@saturation-api/js';
// Initialize the client
const client = new Saturation({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.saturation.io/api/v1', // Optional, this is the default
});
// List all projects
const { projects } = await client.listProjects({ status: 'active' });
console.log('Active projects:', projects);
// Get a specific project with its budget
const budget: Budget = await client.getProjectBudget('nike-swoosh-commercial', {
expands: ['phases', 'fringes', 'lines.contact', 'lines.phaseData'],
});
console.log('Project budget:', budget);
```
## Authentication
Get your API key from the Saturation web app:
1. Go to Settings → API Keys
2. Create a new API key
3. Copy the key and use it in your application
```typescript
const client = new Saturation({
apiKey: process.env.SATURATION_API_KEY!,
});
```
## TypeScript Support
All types are auto-generated from the OpenAPI specification and available for import:
```typescript
import {
Saturation,
type Project,
type Budget,
type BudgetLine,
type Actual,
type PurchaseOrder,
type Contact,
type CreateProjectInput,
type UpdateProjectInput,
type ListProjectsData,
// ... and many more
} from '@saturation-api/js';
// Use types for better type safety
function processProject(project: Project): void {
console.log(`Processing ${project.name}`);
}
// Use query parameter types
const params: ListProjectsData['query'] = {
status: 'active',
labels: ['nike'],
};
const { projects } = await client.listProjects(params);
```
## Core Concepts
### Projects
Projects are the main organizational unit in Saturation. Each project contains budgets, actuals, purchase orders, and other related data.
```typescript
// Create a new project
const project = await client.createProject({
name: 'Nike Holiday Commercial',
icon: '🎬',
spaceId: 'commercial-productions',
status: 'active',
labels: ['nike', 'q4-2024', 'commercial'],
});
// Update project details
const updated = await client.updateProject(project.id, {
status: 'archived',
labels: ['completed', 'q4-2024'],
});
// List projects with filters
const { projects } = await client.listProjects({
status: 'active',
labels: ['nike', 'commercial'],
spaceId: 'commercial-productions',
});
```
### Budget Management
Work with multi-phase budgets, including line items, accounts, subtotals, and markups.
```typescript
// Get complete budget with all details
const budget = await client.getProjectBudget('my-project', {
expands: ['phases', 'fringes', 'globals', 'lines.contact'],
idMode: 'user', // Use human-readable IDs
});
// Add budget lines
const updatedBudget = await client.createBudgetLines('my-project', {
accountId: 'root',
lines: [
{
type: 'line',
accountId: '1100',
description: 'Director',
phaseData: {
estimate: { amount: 50000 },
actual: { amount: 48000 },
},
},
{
type: 'account',
accountId: '2000',
description: 'Camera Department',
},
],
});
// Update a specific budget line
const line = await client.updateBudgetLine('my-project', '1100-DIRECTOR', {
description: 'Director - Extended Cut',
tags: ['above-the-line', 'key-personnel'],
});
```
### Budget Phases
Manage different budget phases like estimate, revised, and actual.
```typescript
// Create a new phase
const phase = await client.createBudgetPhase('my-project', {
name: 'revised',
label: 'Revised Budget',
color: '#FFA500',
order: 2,
});
// List all phases
const { phases } = await client.listBudgetPhases('my-project');
// Update phase details
await client.updateBudgetPhase('my-project', phase.id, {
label: 'Revised Budget v2',
});
```
### Actuals Tracking
Track actual expenses and sync with your accounting system.
```typescript
// Create an actual
const actual = await client.createActual('my-project', {
lineItemId: '2150-CAMERA',
amount: 35000,
date: '2024-03-20',
description: 'RED Camera Package Rental',
contactId: 'vendor-123',
tags: ['equipment', 'week-2'],
});
// List actuals with filters
const { actuals } = await client.listProjectActuals('my-project', {
dateFrom: '2024-03-01',
dateTo: '2024-03-31',
lineItemId: ['2150-CAMERA', '2160-LENSES'],
expands: ['contact', 'attachments'],
});
// Upload attachment to actual
const attachment = await client.uploadActualAttachment(
'my-project',
actual.id,
fileBuffer
);
```
### Purchase Orders
Manage purchase orders with line-item level detail.
```typescript
// Create a purchase order
const po = await client.createPurchaseOrder('my-project', {
number: 'PO-001',
contactId: 'vendor-456',
date: '2024-03-15',
items: [
{
lineItemId: '2150-CAMERA',
amount: 35000,
description: 'Camera equipment rental - 5 days',
},
{
lineItemId: '2160-LENSES',
amount: 8000,
description: 'Lens kit rental',
},
],
tags: ['equipment', 'approved'],
});
// List purchase orders
const { purchaseOrders } = await client.listPurchaseOrders('my-project', {
contactId: 'vendor-456',
hasAttachments: true,
expands: ['items.lineItem', 'contact'],
});
```
### Contacts Management
Manage vendors, crew members, and other contacts.
```typescript
// Create a contact
const contact = await client.createContact({
name: 'John Smith',
email: 'john@example.com',
phone: '+1234567890',
type: 'individual',
companyName: 'Smith Productions',
address: '123 Main St, Los Angeles, CA 90001',
});
// Search contacts
const { contacts } = await client.listContacts({
name: 'Smith',
type: 'individual',
companyName: 'Productions',
});
// Upload tax documents
const taxDoc = await client.uploadContactTaxDocument(
contact.id,
w9Buffer
);
```
### Workspace Rates
Define custom rates for line items and contacts.
```typescript
// Create a rate
const rate = await client.createWorkspaceRate({
lineItemId: '1100-DIRECTOR',
contactId: 'contact-789',
rate: 1500,
unit: 'day',
currency: 'USD',
tags: ['union', 'tier-1'],
});
// List rates with filters
const { rates } = await client.listWorkspaceRates({
lineItemId: '1100-DIRECTOR',
tags: ['union'],
});
```
### File Uploads
Upload and manage file attachments.
```typescript
// Upload a file
const upload = await client.uploadFile(fileBuffer, 'document.pdf');
// Download a file
const fileContent = await client.downloadFile(upload.id);
// Delete a file
await client.deleteFile(upload.id);
```
## React Examples
The SDK works seamlessly in React applications. Here are some common patterns:
### Basic React Hook
```tsx
import { useState, useEffect } from 'react';
import { Saturation, Project } from '@saturation-api/js';
const client = new Saturation({
apiKey: process.env.REACT_APP_SATURATION_API_KEY!,
});
function useProjects() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
client.listProjects({ status: 'active' })
.then(({ projects }) => setProjects(projects))
.catch(setError)
.finally(() => setLoading(false));
}, []);
return { projects, loading, error };
}
// Usage in component
function ProjectList() {
const { projects, loading, error } = useProjects();
if (loading) return <div>Loading projects...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{projects.map(project => (
<li key={project.id}>
{project.icon} {project.name}
</li>
))}
</ul>
);
}
```
### Budget Dashboard Component
```tsx
import React, { useState, useEffect } from 'react';
import { Saturation, Budget } from '@saturation-api/js';
interface BudgetDashboardProps {
projectId: string;
apiKey: string;
}
export function BudgetDashboard({ projectId, apiKey }: BudgetDashboardProps) {
const [budget, setBudget] = useState<Budget | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const client = new Saturation({ apiKey });
client.getProjectBudget(projectId, {
expands: ['phases', 'fringes', 'lines.contact'],
})
.then(setBudget)
.catch(console.error)
.finally(() => setLoading(false));
}, [projectId, apiKey]);
if (loading) return <div>Loading budget...</div>;
if (!budget) return <div>No budget found</div>;
return (
<div className="budget-dashboard">
<h2>Budget Overview</h2>
<div className="budget-totals">
<div>Total: ${budget.account.totals.estimate || 0}</div>
<div>Actual: ${budget.account.totals.actual || 0}</div>
</div>
<h3>Budget Lines</h3>
<table>
<thead>
<tr>
<th>Account</th>
<th>Description</th>
<th>Estimate</th>
<th>Actual</th>
</tr>
</thead>
<tbody>
{budget.account.lines.map(line => (
<tr key={line.id}>
<td>{line.accountId}</td>
<td>{line.description}</td>
<td>${line.totals.estimate || 0}</td>
<td>${line.totals.actual || 0}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
```
### Create Actual Form
```tsx
import React, { useState } from 'react';
import { Saturation, CreateActualInput } from '@saturation-api/js';
interface ActualFormProps {
projectId: string;
client: Saturation;
onSuccess: () => void;
}
export function CreateActualForm({ projectId, client, onSuccess }: ActualFormProps) {
const [formData, setFormData] = useState<CreateActualInput>({
lineItemId: '',
amount: 0,
date: new Date().toISOString().split('T')[0],
description: '',
});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
try {
await client.createActual(projectId, formData);
onSuccess();
// Reset form
setFormData({
lineItemId: '',
amount: 0,
date: new Date().toISOString().split('T')[0],
description: '',
});
} catch (error) {
console.error('Failed to create actual:', error);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Line Item ID (e.g., 2150-CAMERA)"
value={formData.lineItemId}
onChange={(e) => setFormData({ ...formData, lineItemId: e.target.value })}
required
/>
<input
type="number"
placeholder="Amount"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: Number(e.target.value) })}
required
/>
<input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
required
/>
<textarea
placeholder="Description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Creating...' : 'Create Actual'}
</button>
</form>
);
}
```
### React Context Provider
```tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { Saturation } from '@saturation-api/js';
const SaturationContext = createContext<Saturation | null>(null);
interface SaturationProviderProps {
apiKey: string;
baseURL?: string;
children: ReactNode;
}
export function SaturationProvider({ apiKey, baseURL, children }: SaturationProviderProps) {
const client = React.useMemo(
() => new Saturation({ apiKey, baseURL }),
[apiKey, baseURL]
);
return (
<SaturationContext.Provider value={client}>
{children}
</SaturationContext.Provider>
);
}
export function useSaturation() {
const client = useContext(SaturationContext);
if (!client) {
throw new Error('useSaturation must be used within a SaturationProvider');
}
return client;
}
// Usage in your app
function App() {
return (
<SaturationProvider apiKey={process.env.REACT_APP_SATURATION_API_KEY!}>
<YourComponents />
</SaturationProvider>
);
}
```
### File Upload Component
```tsx
import React, { useState } from 'react';
import { useSaturation } from './SaturationProvider';
interface FileUploadProps {
projectId: string;
actualId: string;
onUploadComplete: () => void;
}
export function FileUpload({ projectId, actualId, onUploadComplete }: FileUploadProps) {
const client = useSaturation();
const [uploading, setUploading] = useState(false);
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
// Convert File to Blob for the SDK
const blob = new Blob([await file.arrayBuffer()], { type: file.type });
await client.uploadActualAttachment(
projectId,
actualId,
blob,
file.name
);
onUploadComplete();
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={handleFileSelect}
disabled={uploading}
accept=".pdf,.jpg,.jpeg,.png"
/>
{uploading && <span>Uploading...</span>}
</div>
);
}
```
### Next.js API Route
For server-side usage in Next.js:
```typescript
// pages/api/projects.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Saturation } from '@saturation-api/js';
const client = new Saturation({
apiKey: process.env.SATURATION_API_KEY!,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method === 'GET') {
const { projects } = await client.listProjects({ status: 'active' });
res.status(200).json(projects);
} else if (req.method === 'POST') {
const project = await client.createProject(req.body);
res.status(201).json(project);
} else {
res.status(405).json({ error: 'Method not allowed' });
}
} catch (error) {
console.error('API error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
```
## Advanced Usage
### Error Handling
The SDK provides detailed error information with proper TypeScript types.
```typescript
try {
const project = await client.getProject('non-existent');
} catch (error) {
console.error('API Error:', error);
// The generated client handles errors internally
// Check error.response for details if available
}
```
### Using Expands
Many endpoints support the `expands` parameter to include related data in a single request.
```typescript
// Get budget with all related data
const budget = await client.getProjectBudget('my-project', {
expands: [
'phases', // Include all budget phases
'fringes', // Include fringe calculations
'globals', // Include global calculations
'lines.contact', // Include contact info for each line
'lines.phaseData', // Include phase-specific data
],
});
// Get actuals with full details
const { actuals } = await client.listProjectActuals('my-project', {
expands: [
'contact', // Include contact information
'attachments', // Include file attachments
'lineItem', // Include budget line details
],
});
```
### Using Your Account Codes
The API supports using your existing account codes as identifiers.
```typescript
// Use your account codes directly
const line = await client.getBudgetLine('my-project', '1100-LABOR');
const updated = await client.updateBudgetLine('my-project', '2150-CAMERA', {
description: 'Camera Equipment - Updated',
});
// Create actuals with your codes
await client.createActual('my-project', {
lineItemId: '2150-CAMERA',
amount: 5000,
date: '2024-03-20',
});
```
### Public Rates
Access public rate libraries shared across workspaces.
```typescript
// List all public ratepacks
const { ratepacks } = await client.listPublicRatepacks();
// Search for specific ratepacks
const { ratepacks: iatseRatepacks } = await client.listPublicRatepacks({
search: 'IATSE',
});
// Get all rates from a specific ratepack
const { rates } = await client.getPublicRates('ratepack-123', {
search: 'grip',
includeArchived: false,
});
```
### Batch Operations
Create multiple items efficiently in a single request.
```typescript
// Create multiple budget lines at once
await client.createBudgetLines('my-project', {
accountId: 'root',
lines: [
{ type: 'account', accountId: '1000', description: 'Above The Line' },
{ type: 'line', accountId: '1100', description: 'Producer' },
{ type: 'line', accountId: '1200', description: 'Director' },
{ type: 'subtotal', description: 'Total ATL' },
{ type: 'account', accountId: '2000', description: 'Below The Line' },
{ type: 'line', accountId: '2100', description: 'Camera' },
{ type: 'line', accountId: '2200', description: 'Lighting' },
],
});
```
## TypeScript Support
The SDK is fully typed with TypeScript. All request and response types are auto-generated from the OpenAPI specification.
```typescript
import type {
Project,
Budget,
Actual,
PurchaseOrder,
Contact,
CreateProjectInput,
UpdateProjectInput,
ListProjectsParams,
} from '@saturation-api/js';
// Type-safe function
async function getProjectBudgetTotal(projectId: string): Promise<number> {
const budget = await client.getProjectBudget(projectId);
return budget.account.totals.estimate || 0;
}
// Type-safe error handling
function isNotFoundError(error: unknown): boolean {
return error instanceof SaturationError && error.statusCode === 404;
}
```
## API Reference
For complete API documentation, see the [API Reference](https://api.saturation.io/docs).
### Available Methods
#### Projects
- `listProjects(params?)` - List all projects
- `getProject(projectId)` - Get a specific project
- `createProject(data)` - Create a new project
- `updateProject(projectId, data)` - Update project details
- `deleteProject(projectId)` - Delete a project
#### Budget
- `getProjectBudget(projectId, params?)` - Get project budget
- `createBudgetLines(projectId, data)` - Add budget lines
- `getBudgetLine(projectId, lineId, params?)` - Get specific line
- `updateBudgetLine(projectId, lineId, data)` - Update budget line
- `deleteBudgetLine(projectId, lineId)` - Delete budget line
#### Phases
- `listBudgetPhases(projectId)` - List budget phases
- `createBudgetPhase(projectId, data)` - Create phase
- `getBudgetPhase(projectId, phaseId)` - Get phase details
- `updateBudgetPhase(projectId, phaseId, data)` - Update phase
- `deleteBudgetPhase(projectId, phaseId)` - Delete phase
#### Actuals
- `listProjectActuals(projectId, params?)` - List actuals
- `getActual(projectId, actualId, params?)` - Get actual details
- `createActual(projectId, data)` - Create actual
- `updateActual(projectId, actualId, data)` - Update actual
- `deleteActual(projectId, actualId)` - Delete actual
- `uploadActualAttachment(projectId, actualId, file, filename)` - Add attachment
#### Purchase Orders
- `listPurchaseOrders(projectId, params?)` - List purchase orders
- `getPurchaseOrder(projectId, poId, params?)` - Get PO details
- `createPurchaseOrder(projectId, data)` - Create purchase order
- `updatePurchaseOrder(projectId, poId, data)` - Update PO
- `deletePurchaseOrder(projectId, poId)` - Delete PO
- `uploadPurchaseOrderAttachment(projectId, poId, file, filename)` - Add attachment
#### Contacts
- `listContacts(params?)` - List contacts
- `createContact(data)` - Create contact
- `getContact(contactId)` - Get contact details
- `updateContact(contactId, data)` - Update contact
- `uploadContactTaxDocument(contactId, file, filename)` - Upload tax doc
- `uploadContactAttachment(contactId, file, filename)` - Add attachment
## Development
### Setup
```bash
# Clone the repository
git clone https://github.com/saturation-api/saturation-js.git
cd saturation-js
# Install dependencies
npm install
# Generate types from OpenAPI spec
npm run generate
# Run tests
npm test
# Build the package
npm run build
```
### Scripts
- `npm run build` - Build for production
- `npm run dev` - Watch mode for development
- `npm test` - Run tests
- `npm run lint` - Lint code
- `npm run format` - Format code with Prettier
- `npm run generate` - Generate types from OpenAPI spec
### Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
MIT - see [LICENSE](LICENSE) file for details.
## Support
- [Documentation](https://docs.saturation.io)
- [API Reference](https://api.saturation.io/docs)
- [GitHub Issues](https://github.com/saturation-api/saturation-js/issues)
- [Support Email](mailto:support@saturation.io)