UNPKG

@saturation-api/js

Version:
870 lines (710 loc) 22.6 kB
# @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. [![npm version](https://badge.fury.io/js/%40saturation-api%2Fjs.svg)](https://www.npmjs.com/package/@saturation-api/js) [![CI](https://github.com/saturation-api/saturation-js/actions/workflows/ci.yml/badge.svg)](https://github.com/saturation-api/saturation-js/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)