diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7ad0413 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +\*.yaml filter=lfs diff=lfs merge=lfs -text +\*.json filter=lfs diff=lfs merge=lfs -text +packages/*/specs/*.yaml filter=lfs diff=lfs merge=lfs -text +packages/*/openapi.json filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index d5f46e7..26867a0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,40 @@ claude-flow coordination.md memory-bank.md memory/ + +# Claude Code and SAFLA files +.claude/logs/ +.claude/sessions/ +.claude/cache/ +.claude/temp/ + +# SAFLA memory and data files +safla.db +*.safla +memory/ +safla_memory/ +pattern_snapshots/ + +# Claude-flow files +.roomodes +.roo/ +claude-flow-data.json +memory-store*.json +coordination/ +swarm-*.log + +# SAFLA MCP and temporary files +safla_mcp_*.py +mcp_*.log +*.mcp.json + +# Node modules and build files (if using claude-flow locally) +node_modules/ +dist/ +build/ +.env.local + +# System files +.DS_Store +Thumbs.db + diff --git a/AUTOMATED_API_MODULE_GENERATION_SPEC.md b/AUTOMATED_API_MODULE_GENERATION_SPEC.md new file mode 100644 index 0000000..6d788e2 --- /dev/null +++ b/AUTOMATED_API_MODULE_GENERATION_SPEC.md @@ -0,0 +1,168 @@ +# Automated API Module Generation System - One-Pager + +## Executive Summary +A cloud-based service that automatically generates Frigg API modules from any domain/URL or API specification, creating pull requests directly to the api-module-library repository. + +## Core Features + +### 1. Intelligent API Discovery +- **Input**: Domain URL (e.g., `stripe.com`) or direct API spec +- **Process**: + - Crawl domain for API documentation links + - Detect OpenAPI/Swagger/GraphQL endpoints + - Parse API authentication methods + - Extract endpoint information +- **Fallback**: Manual spec upload if auto-discovery fails + +### 2. Automated Code Generation +- Generate complete Frigg module structure +- Create all required files with proper patterns +- Auto-generate methods from API endpoints +- Include comprehensive test suites +- Add proper error handling and retry logic + +### 3. GitHub Integration +- Automatic fork of api-module-library +- Create feature branch: `feat/add-{module-name}` +- Commit generated code with proper messages +- Open PR with detailed description +- Tag as `auto-generated` for review + +## Technical Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Web UI/API │────▶│ Discovery Engine│────▶│ Code Generator │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ +┌─────────────────┐ ┌─────────────────┐ ┌────────▼────────┐ +│ GitHub PR │◀────│ Validation Suite│◀────│ Template Engine │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## Implementation Stack + +- **Backend**: Node.js + Express/Fastify +- **Queue**: Bull/BullMQ for async processing +- **Storage**: Redis for caching discovered specs +- **Container**: Docker for consistent environment +- **CI/CD**: GitHub Actions for deployment + +## API Endpoints + +```javascript +POST /api/generate +{ + "input": "stripe.com", // or direct spec URL + "options": { + "authType": "oauth2", // optional override + "category": "payment", + "priority": "high" + } +} + +GET /api/status/:jobId +Response: { status, progress, prUrl } + +POST /api/generate/batch +{ "domains": ["stripe.com", "twilio.com", ...] } +``` + +## Workflow + +1. **Submit Request** → User provides domain/URL via UI or API +2. **Discovery** → System crawls for API documentation +3. **Analysis** → Parse spec, detect auth, map endpoints +4. **Generation** → Create module code using templates +5. **Validation** → Run tests, lint, security checks +6. **PR Creation** → Fork, commit, open PR +7. **Notification** → Email/webhook with PR link + +## Quality Assurance + +- **Automated Testing**: Generated tests must pass +- **Code Coverage**: Minimum 80% coverage +- **Linting**: ESLint compliance required +- **Security Scan**: Check for exposed secrets +- **Documentation**: README completeness check + +## Deployment Options + +### Option 1: Vercel/Netlify Functions +- Serverless architecture +- Auto-scaling +- ~$20-50/month + +### Option 2: Small VPS +- DigitalOcean/Linode droplet +- More control +- ~$20/month + +### Option 3: GitHub Actions Only +- Workflow triggered by issues/forms +- No hosting costs +- Limited UI options + +## Success Metrics + +- **Generation Time**: < 2 minutes per module +- **Success Rate**: > 90% for popular APIs +- **PR Quality**: < 10% rejection rate +- **Coverage Growth**: 50% → 100% in 90 days + +## MVP Features (Week 1-2) + +1. Web form for URL submission +2. OpenAPI/Swagger parsing +3. OAuth2 & API Key templates +4. Basic GitHub PR creation +5. Email notifications + +## Future Enhancements + +1. **AI Enhancement**: Use LLMs for better naming/docs +2. **Multi-Spec Support**: GraphQL, AsyncAPI, gRPC +3. **Update Detection**: Monitor API changes +4. **Community Features**: Voting, requests, contributions +5. **Analytics**: Track most requested APIs + +## Security Considerations + +- Never store API credentials +- Sanitize all generated code +- Review all PRs before merge +- Rate limit submissions +- Validate domain ownership + +## Cost Estimate + +- **Development**: 2 developers × 2 weeks +- **Infrastructure**: ~$50/month +- **Maintenance**: 5 hours/week +- **ROI**: 200+ modules in 3 months + +## Quick Start Implementation + +```bash +# 1. Clone starter template +git clone https://github.com/frigg/module-generator-starter + +# 2. Configure environment +cp .env.example .env +# Add: GITHUB_TOKEN, REDIS_URL, etc. + +# 3. Deploy to Vercel +vercel deploy --prod + +# 4. Set up GitHub webhook +# Repository Settings → Webhooks → Add webhook +``` + +## Contact Points + +- **Submissions**: generate.frigg.dev +- **API**: api.generate.frigg.dev +- **Status**: status.generate.frigg.dev +- **Support**: Automated issue creation + +This system will transform API module creation from a manual 4-hour process to an automated 2-minute workflow, enabling rapid expansion to 100% API coverage. \ No newline at end of file diff --git a/ECOSYSTEM_MAP.md b/ECOSYSTEM_MAP.md new file mode 100644 index 0000000..8244bf1 --- /dev/null +++ b/ECOSYSTEM_MAP.md @@ -0,0 +1,235 @@ +# API Module Library Ecosystem Map + +## Overview +This document maps the relationships between API modules in the Frigg Framework API Module Library, organizing them by ecosystem and identifying integration opportunities. + +## Major Ecosystems + +### 1. Google Workspace Ecosystem +**Core Services:** +- `google-workspace` - Parent module for Google services +- `google-calendar` - Calendar management and scheduling +- `google-drive` - File storage and document management + +**Integration Opportunities:** +- Calendar + Drive: Schedule meetings with attached documents +- Workspace admin: Centralized user and permission management +- Cross-service authentication with single OAuth flow + +**Related Integrations:** +- `zoom` - Schedule Zoom meetings via Google Calendar +- `slack` - Share Google Drive files in Slack +- `notion` - Import Google Docs into Notion pages + +### 2. Microsoft Ecosystem +**Core Services:** +- `microsoft-teams` - Team collaboration and communication +- `sharepoint` (needs-updating) - Document management and intranet + +**Integration Opportunities:** +- Teams + SharePoint: Embedded document collaboration +- Teams + Outlook (future): Calendar and email integration +- Azure AD authentication across services + +**Related Integrations:** +- `slack` - Teams/Slack bridge for cross-platform messaging +- `zoom` - Teams meeting alternatives + +### 3. Adobe Marketing Cloud Ecosystem +**Core Services:** +- `marketo` (needs-updating) - Marketing automation platform + +**Integration Opportunities:** +- Marketo + Salesforce: Lead scoring and CRM sync +- Marketo + HubSpot: Marketing automation comparison +- Future: Adobe Analytics, Adobe Target integration + +### 4. E-commerce Platform Ecosystem +**Core Platforms:** +- `shopify` - E-commerce platform +- `recharge` - Subscription management for Shopify +- `stripe` - Payment processing +- `yotpo` (needs-updating) - Reviews and loyalty programs +- `attentive` (needs-updating) - SMS marketing +- `gorgias` (needs-updating) - Customer support + +**Integration Patterns:** +- Shopify as central hub with plugin architecture +- Recharge extends Shopify for subscriptions +- Stripe handles payment processing +- Yotpo/Attentive/Gorgias enhance customer experience + +**Payment Ecosystem:** +- `stripe` - Primary payment processor +- `payjunction` - Alternative payment gateway +- `airwallex` (needs-updating) - International payments +- `fastspring-iq` (needs-updating) - Digital goods payments + +### 5. CRM and Sales Ecosystem +**Major CRMs:** +- `salesforce` - Enterprise CRM leader +- `hubspot` - Inbound marketing and CRM +- `pipedrive` - Sales-focused CRM (both v1-ready and needs-updating versions) +- `zoho-crm` - Affordable CRM solution +- `attio` - Modern, flexible CRM + +**Sales Enablement:** +- `outreach` (needs-updating) - Sales engagement platform +- `salesloft` (needs-updating) - Sales engagement platform +- `crossbeam` - Partner ecosystem mapping + +**Related Tools:** +- `monday` - Can function as lightweight CRM (both versions exist) +- `activecampaign` (needs-updating) - Email marketing with CRM features + +### 6. Project Management Ecosystem +**Core Tools:** +- `asana` - Task and project management +- `trello` - Kanban-style project management +- `monday` - Flexible work management (both versions) +- `linear` - Developer-focused project management +- `notion` - All-in-one workspace with PM features + +**Integration Patterns:** +- GitHub + Linear: Developer workflow +- Slack + Asana/Trello: Notifications and updates +- Google Calendar + PM tools: Timeline visualization + +### 7. Communication and Collaboration Ecosystem +**Messaging Platforms:** +- `slack` - Team messaging (both versions exist) +- `microsoft-teams` - Enterprise collaboration +- `front` (needs-updating) - Shared inbox +- `openphone` - Business phone system + +**Video Conferencing:** +- `zoom` - Video meetings and webinars + +**Customer Support:** +- `helpscout` - Help desk software +- `gorgias` (needs-updating) - E-commerce support +- `front` (needs-updating) - Team inbox + +### 8. Content Management Ecosystem +**Headless CMS:** +- `contentful` - API-first CMS +- `contentstack` - Enterprise headless CMS + +**Design and Brand:** +- `figma` - Design collaboration +- `canva` - Graphic design platform +- `frontify` - Brand management + +**Documentation:** +- `notion` - Knowledge base and docs +- `github` - Technical documentation + +### 9. HR and Operations Ecosystem +**HR Platforms:** +- `personio` (needs-updating) - HR management +- `deel` - Global payroll and compliance + +**Productivity:** +- `unbabel` / `unbabel-projects` - Translation services + +### 10. Developer Tools Ecosystem +**Source Control and CI/CD:** +- `github` - Version control and collaboration + +**API Development:** +- `42matters` - App intelligence API + +**Analytics:** +- `fathom` - Privacy-focused analytics + +## Cross-Ecosystem Integration Patterns + +### 1. Authentication Bridges +- Google OAuth for multiple services +- Microsoft Azure AD for enterprise +- Slack as identity provider + +### 2. Data Synchronization +- CRM to E-commerce: Customer data sync +- PM to Communication: Task notifications +- CMS to E-commerce: Product content + +### 3. Workflow Automation +- Trigger patterns across ecosystems +- Webhook standardization +- Event-driven architectures + +### 4. Unified Communications +- Slack/Teams as notification hubs +- Email integration points +- Calendar synchronization + +## Duplicate Modules Requiring Consolidation +The following modules exist in both v1-ready and needs-updating: +- `monday` +- `pipedrive` +- `slack` + +These should be evaluated for consolidation, keeping the most up-to-date version. + +## Priority Migration Candidates +Based on ecosystem importance and integration potential: + +### High Priority (Core ecosystem components): +1. `marketo` - Adobe ecosystem anchor +2. `sharepoint` - Microsoft ecosystem completion +3. `activecampaign` - CRM/Marketing bridge +4. `gorgias` - E-commerce support essential +5. `outreach` / `salesloft` - Sales ecosystem enhancement + +### Medium Priority (Ecosystem enhancement): +1. `yotpo` - E-commerce reviews +2. `attentive` - E-commerce marketing +3. `front` - Communication hub +4. `personio` - HR ecosystem +5. `airwallex` - International payments + +### Low Priority (Standalone or niche): +1. `clyde` - Warranty management +2. `huggg` - Digital gifting +3. `netx` - Digital asset management +4. `revio` - Payment solutions +5. `rollworks` - ABM platform +6. `terminus` - ABM platform +7. `fastspring-iq` - Digital commerce +8. `freshbooks` - Accounting (QuickBooks alternative) +9. `qbo` - QuickBooks Online + +## Recommendations + +### 1. Ecosystem-First Development +- Prioritize modules that complete ecosystems +- Build integration templates for common patterns +- Create ecosystem-specific documentation + +### 2. Authentication Optimization +- Implement shared OAuth for ecosystem groups +- Create authentication inheritance patterns +- Standardize token management + +### 3. Data Model Alignment +- Standardize entity representations across ecosystems +- Create mapping utilities for common transformations +- Implement ecosystem-specific interfaces + +### 4. Testing Strategy +- Create ecosystem integration tests +- Mock inter-module communications +- Test authentication flows across services + +### 5. Documentation Enhancement +- Create ecosystem guides +- Document integration patterns +- Provide example workflows + +## Next Steps +1. Consolidate duplicate modules +2. Implement priority migrations using v1-ready patterns +3. Create ecosystem-specific integration guides +4. Develop shared authentication strategies +5. Build cross-ecosystem test suites \ No newline at end of file diff --git a/api-index.json b/api-index.json new file mode 100644 index 0000000..e1081de --- /dev/null +++ b/api-index.json @@ -0,0 +1,238 @@ +{ + "version": "1.1.0", + "last_updated": "2025-06-26T17:02:45.103Z", + "stats": { + "total": 183, + "implemented": 174, + "with_openapi": 10, + "with_fenestra": 18, + "with_frigg": 169, + "by_category": { + "Other": 80, + "Marketing": 7, + "Productivity": 13, + "CRM": 8, + "Finance": 5, + "Analytics": 2, + "Developer": 5, + "AI/ML": 5, + "HR": 3, + "Communication": 13, + "E-commerce": 8, + "Product Management": 1, + "finance": 3, + "MSP": 1, + "Partner": 2, + "Electronic Signatures": 1, + "video": 1, + "customer-support": 2, + "Email": 2, + "Sharing": 3, + "Customer Service": 1, + "Gifting": 1, + "Customer Support": 1, + "Professional Networking": 1, + "Asset Management": 1, + "Sales": 4, + "Payments": 1, + "Accounting": 1, + "Social News": 1, + "ABM": 2, + "Forms": 1, + "Accounting Software": 1, + "Content": 1, + "Chat": 1 + }, + "by_auth": { + "Unknown": 16, + "API Key": 9, + "OAuth2": 153, + "[object Object]": 5 + } + }, + "quick_lookup": { + "42matters": 0, + "activecampaign": 1, + "actsoft": 2, + "agile-crm": 3, + "airstory": 4, + "airtable": 5, + "airwallex": 6, + "algolia": 7, + "amazon-api-gateway": 8, + "amazon-cloudsearch": 9, + "amazon-sns": 10, + "amazon-sqs": 11, + "amplitude": 12, + "anthropic": 13, + "ape-mobile-now-damstra-samm": 14, + "applicantstack": 15, + "arthur-online": 16, + "asana": 17, + "attentive": 18, + "attio": 19, + "auth0": 20, + "authentise": 21, + "authorizenet": 22, + "autotask": 23, + "aws-account-management": 24, + "aws-cloudwatch": 25, + "aws-dynamodb": 26, + "aws-kms": 27, + "aws-lambda": 28, + "aws-s3": 29, + "aws-s3-elasticache": 30, + "bamboohr": 31, + "basecrm": 32, + "benchmark-email": 33, + "bigcommerce": 34, + "bingmail": 35, + "bitbucket-by-atlassian": 36, + "bizpayo": 37, + "boomset": 38, + "box": 39, + "braintree": 40, + "bulksms": 41, + "calendly": 42, + "campaign-monitor": 43, + "cannabiz": 44, + "canva": 45, + "chargebee": 46, + "chargify": 47, + "cirrus-shield": 48, + "cisco-meraki": 49, + "cisco-webex": 50, + "cleargate-payup": 51, + "clickup": 52, + "clientsuccess": 53, + "clockwork-recruiting": 54, + "cloudflare": 55, + "clubworx": 56, + "clyde": 57, + "coda": 58, + "cohere": 59, + "coinbase": 60, + "connectwise": 61, + "constantcontact": 62, + "contentful": 63, + "contentstack": 64, + "convertkit": 65, + "crossbeam": 66, + "crowdcompass": 67, + "cubepasses": 68, + "cvent": 69, + "datadog": 70, + "dealcloud": 71, + "deel": 72, + "deepcrawl": 73, + "discord": 74, + "dispatchme": 75, + "docusign": 76, + "docverify": 77, + "dotloop": 78, + "drift": 79, + "dropbox": 80, + "drupal": 81, + "ebanx": 82, + "enreach-formerly-herobase": 83, + "etsy": 84, + "eventbrite": 85, + "evernote": 86, + "facebook": 87, + "fastbill": 88, + "fastspring-interactive-quotes": 89, + "fastspring-iq": 90, + "fathom": 91, + "figma": 92, + "firebase": 93, + "formsite": 94, + "formstack": 95, + "freshbooks": 96, + "freshdesk": 97, + "front": 98, + "frontify": 99, + "gathercontent": 100, + "github": 101, + "gitlab": 102, + "gmail": 103, + "go1": 104, + "google-analytics": 105, + "google-analytics-4": 106, + "google-calendar": 107, + "google-chat": 108, + "google-contacts": 109, + "google-docs": 110, + "google-drive": 111, + "google-maps": 112, + "google-sheets": 113, + "google-workspace": 114, + "gorgias": 115, + "gotomeeting": 116, + "gravityforms": 117, + "helpscout": 118, + "highq-collaborate": 119, + "hirevue": 120, + "hopin": 121, + "hubspot": 122, + "huggg": 123, + "huggingface": 124, + "intercom": 125, + "ironclad": 126, + "jira": 127, + "linear": 128, + "linkedin": 129, + "mailchimp": 130, + "mailgun": 131, + "marketo": 132, + "microsoft-teams": 133, + "miro": 134, + "mixpanel": 135, + "monday": 136, + "netx": 137, + "notion": 138, + "onesignal": 139, + "openai": 140, + "openphone": 141, + "outreach": 142, + "payjunction": 143, + "paypal": 144, + "personio": 145, + "pipedrive": 146, + "plaid": 147, + "posthog": 148, + "pusher": 149, + "qbo": 150, + "recharge": 151, + "reddit": 152, + "replicate": 153, + "revio": 154, + "rollworks": 155, + "salesforce": 156, + "salesloft": 157, + "segment": 158, + "sendgrid": 159, + "sentry": 160, + "sharepoint": 161, + "shopify": 162, + "square": 163, + "stripe": 164, + "telegram": 165, + "terminus": 166, + "todoist": 167, + "trello": 168, + "twilio": 169, + "typeform": 170, + "unbabel": 171, + "unbabel-projects": 172, + "vonage": 173, + "whatsapp-business": 174, + "wise": 175, + "woocommerce": 176, + "xero": 177, + "yotpo": 178, + "youtube": 179, + "zendesk": 180, + "zoho-crm": 181, + "zoom": 182 + } +} \ No newline at end of file diff --git a/api-inventory.jsonl b/api-inventory.jsonl new file mode 100644 index 0000000..f53ddf7 --- /dev/null +++ b/api-inventory.jsonl @@ -0,0 +1,183 @@ +{"id":"42matters","name":"42matters","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"activecampaign","name":"ActiveCampaign","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Marketing","subcat":"Marketing Automation, Email Marketing, CRM, Marketing","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"actsoft","name":"ActSoft","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"agile-crm","name":"Agile CRM","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"Sales","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"airstory","name":"Airstory","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"airtable","name":"airtable","implemented":false,"openapi":false,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"airwallex","name":"Airwallex","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Finance","subcat":"Online Payments","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"algolia","name":"Algolia","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Analytics","subcat":"Search, AI/ML","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"amazon-api-gateway","name":"Amazon API Gateway","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"amazon-cloudsearch","name":"Amazon Cloudsearch","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"amazon-sns","name":"Amazon SNS","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Developer","subcat":"Amazon","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"amazon-sqs","name":"Amazon SQS","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"amplitude","name":"Amplitude","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":{"type":"apiKey","description":"API Key for ingestion, Secret Key for export","fields":[{"name":"apiKey","description":"Public API key for event tracking"},{"name":"secretKey","description":"Secret key for data export and management"}]},"cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"anthropic","name":"Anthropic","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"AI/ML","subcat":"Large Language Models, Text Generation, AI Safety","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"ape-mobile-now-damstra-samm","name":"APE Mobile (now Damstra Samm)","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"applicantstack","name":"ApplicantStack","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"HR","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"arthur-online","name":"Arthur Online","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"asana","name":"Asana","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Project Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"attentive","name":"AttentiveMobile","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"SMS, Messaging","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"attio","name":"attio","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"auth0","name":"Auth0","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"authentise","name":"Authentise","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"authorizenet","name":"Authorize.Net","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"autotask","name":"Autotask","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-account-management","name":"AWS Account Management","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Developer","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-cloudwatch","name":"AWS Cloudwatch","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-dynamodb","name":"AWS DynamoDB","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-kms","name":"AWS KMS","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Developer","subcat":"Amazon","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-lambda","name":"AWS Lambda","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-s3","name":"AWS S3","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Developer","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"aws-s3-elasticache","name":"AWS S3 Elasticache","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"bamboohr","name":"BambooHR","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"HR","subcat":"Human Resources, Employee Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"basecrm","name":"BaseCRM","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"benchmark-email","name":"Benchmark Email","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"bigcommerce","name":"BigCommerce","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"bingmail","name":"Bingmail","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"bitbucket-by-atlassian","name":"BitBucket by Atlassian","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"bizpayo","name":"BizPayo","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"boomset","name":"Boomset","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"box","name":"Box","implemented":true,"openapi":true,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Enterprise File Storage, Content Management, Collaboration, Security","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"braintree","name":"Braintree","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"Payment Processing, Finance","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"bulksms","name":"BulkSMS","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"calendly","name":"Calendly","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Scheduling, Calendar, Appointments","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"campaign-monitor","name":"Campaign Monitor","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Email Marketing, Marketing Automation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cannabiz","name":"Cannabiz","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"canva","name":"canva","implemented":false,"openapi":false,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"chargebee","name":"Chargebee","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Finance","subcat":"Billing, Subscription Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"chargify","name":"Chargify","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Finance","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cirrus-shield","name":"Cirrus Shield","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cisco-meraki","name":"Cisco Meraki","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cisco-webex","name":"Cisco Webex","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cleargate-payup","name":"Cleargate PayUp","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"clickup","name":"ClickUp","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Project Management, Task Management, Team Collaboration","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"clientsuccess","name":"ClientSuccess","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"clockwork-recruiting","name":"Clockwork Recruiting","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cloudflare","name":"Cloudflare","implemented":true,"openapi":false,"fenestra":false,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"clubworx","name":"Clubworx","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"clyde","name":"Clyde","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"ECommerce, Product Protection","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"coda","name":"Coda","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Product Management","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cohere","name":"Cohere","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"AI/ML","subcat":"Large Language Models, NLP, Text Analytics","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"coinbase","name":"coinbase","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"finance","subcat":"cryptocurrency, trading","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"connectwise","name":"ConnectWise Manage","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"Unknown","cat":"MSP","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"constantcontact","name":"Constant Contact","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Email Marketing, Marketing Automation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"contentful","name":"Contentful","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"contentstack","name":"Contentstack","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"convertkit","name":"ConvertKit","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Marketing","subcat":"Email Marketing, Creator Economy, Marketing Automation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"crossbeam","name":"Crossbeam","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Partner","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"crowdcompass","name":"CrowdCompass","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Marketing","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cubepasses","name":"CubePasses","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"cvent","name":"Cvent","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"datadog","name":"Datadog","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"dealcloud","name":"DealCloud","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"Deal Management, Investment Banking","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"deel","name":"Deel","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"deepcrawl","name":"DeepCrawl","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Developer","subcat":"Website Builders","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"discord","name":"Discord","implemented":true,"openapi":true,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Team Collaboration, Gaming, Community","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"dispatchme","name":"Dispatch.me","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"docusign","name":"Docusign","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Electronic Signatures","subcat":"Document Management, Legal Tech, Business Process","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"docverify","name":"DocVerify","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"dotloop","name":"Dotloop","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"drift","name":"Drift","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"dropbox","name":"Dropbox","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Cloud Storage, File Management, File Synchronization, Collaboration","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"drupal","name":"Drupal","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"ebanx","name":"Ebanx","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"enreach-formerly-herobase","name":"Enreach (formerly Herobase)","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"etsy","name":"Etsy","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"Marketplace","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"eventbrite","name":"EventBrite","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Marketing","subcat":"Event Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"evernote","name":"Evernote","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Note Taking, Document Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"facebook","name":"Facebook","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"fastbill","name":"FastBill","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"fastspring-interactive-quotes","name":"FastSpring Interactive Quotes","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"fastspring-iq","name":"FastSpring IQ","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Partner","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"fathom","name":"Fathom","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"video","subcat":"meetings, productivity, ai, transcription","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"figma","name":"figma","implemented":false,"openapi":false,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"firebase","name":"Firebase","implemented":true,"openapi":false,"fenestra":false,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"formsite","name":"Formsite","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"formstack","name":"Formstack","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"freshbooks","name":"FreshBooks","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Finance","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"freshdesk","name":"freshdesk","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"customer-support","subcat":"helpdesk, ticketing","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"front","name":"FrontApp","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Email","subcat":"Team Communication","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"frontify","name":"Frontify","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Sharing","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"gathercontent","name":"GatherContent","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"github","name":"github","implemented":false,"openapi":false,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"gitlab","name":"GitLab","implemented":true,"openapi":true,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Version Control, DevOps, CI/CD, Project Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"gmail","name":"Gmail","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Email","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"go1","name":"GO1","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-analytics","name":"Google Analytics","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Analytics","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-analytics-4","name":"Google Analytics 4","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":{"type":"oauth2","provider":"google","authorizationUrl":"https://accounts.google.com/o/oauth2/v2/auth","tokenUrl":"https://oauth2.googleapis.com/token","scopes":["https://www.googleapis.com/auth/analytics","https://www.googleapis.com/auth/analytics.readonly"]},"cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-calendar","name":"GoogleCalendar","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-chat","name":"Google Chat","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-contacts","name":"Google Contacts","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-docs","name":"Google Docs","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-drive","name":"Google Drive","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-maps","name":"Google Maps","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-sheets","name":"Google Sheets","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"google-workspace","name":"google-workspace","implemented":false,"openapi":false,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"gorgias","name":"Gorgias","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Customer Service","subcat":"Helpdesk","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"gotomeeting","name":"GoToMeeting","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"gravityforms","name":"GravityForms","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"helpscout","name":"Help Scout","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"highq-collaborate","name":"HighQ Collaborate","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"hirevue","name":"HireVue","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"hopin","name":"Hopin","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"hubspot","name":"HubSpot","implemented":true,"openapi":true,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Marketing","subcat":"Sales, CMS, Marketing Automation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"huggg","name":"Huggg","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Gifting","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"huggingface","name":"Hugging Face","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"AI/ML","subcat":"Model Hub, Machine Learning, Open Source AI","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"intercom","name":"Intercom","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Customer Support","subcat":"Live Chat, Customer Engagement, Help Desk","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"ironclad","name":"Ironclad","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"jira","name":"Jira","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Project Management, Issue Tracking, Agile Development","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"linear","name":"Linear","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"linkedin","name":"LinkedIn","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Professional Networking","subcat":"Social Media, Recruitment, Business Intelligence","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"mailchimp","name":"Mailchimp","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"Email Marketing, Marketing Automation, Analytics","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"mailgun","name":"Mailgun","implemented":true,"openapi":false,"fenestra":false,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"marketo","name":"Adobe Marketo","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Marketing","subcat":"Marketing Automation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"microsoft-teams","name":"Microsoft Teams","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Sharing","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"miro","name":"Miro","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Collaboration, Visual Design","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"mixpanel","name":"Mixpanel","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":{"types":[{"type":"serviceAccount","name":"Service Account","description":"Recommended for server-side API operations","fields":["serviceAccountUsername","serviceAccountSecret"]},{"type":"projectToken","name":"Project Token","description":"For event tracking and client-side operations","fields":["projectToken"]}]},"cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"monday","name":"monday","implemented":false,"openapi":false,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"netx","name":"NetX","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Asset Management","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"notion","name":"notion","implemented":false,"openapi":true,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"onesignal","name":"OneSignal","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Push Notifications, Marketing","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"openai","name":"OpenAI","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"AI/ML","subcat":"Large Language Models, Text Generation, Image Generation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"openphone","name":"OpenPhone","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Communication","subcat":"Phone, VoIP, Business Phone","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"outreach","name":"Outreach","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Sales","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"payjunction","name":"PayJunction","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Payments","subcat":"Credit Card, Processing","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"paypal","name":"PayPal","implemented":true,"openapi":true,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"Payments, Financial Services, Money Transfer","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"personio","name":"Personio","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"HR","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"pipedrive","name":"PipeDrive CRM","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"Sales","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"plaid","name":"plaid","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"finance","subcat":"banking, data-aggregation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"posthog","name":"PostHog","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":{"types":[{"type":"personalApiKey","name":"Personal API Key","description":"Full API access with customizable scopes","fields":["personalApiKey"]},{"type":"projectApiKey","name":"Project API Key","description":"Write-only key for event tracking","fields":["projectApiKey"]}]},"cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"pusher","name":"Pusher","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Real-time, Notifications","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"qbo","name":"QuickBooks Online","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Accounting","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"recharge","name":"Recharge","implemented":true,"openapi":false,"fenestra":false,"frigg":false,"auth":"Unknown","cat":"E-commerce","subcat":"Subscription Management, Recurring Billing, Payment Processing","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"reddit","name":"Reddit","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Social News","subcat":"Community Platform, Content Aggregation, Discussion Forums","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"replicate","name":"Replicate","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"AI/ML","subcat":"Model Hosting, Machine Learning, Cloud ML","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"revio","name":"Rev.io","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Sales","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"rollworks","name":"RollWorks","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"ABM","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"salesforce","name":"Salesforce","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"Marketing, Sales, CMS, Marketing Automation","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"salesloft","name":"Salesloft","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Sales","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"segment","name":"Segment","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":{"types":[{"type":"writeKey","name":"Write Key","description":"For server-side event tracking","fields":["writeKey"]},{"type":"bearerToken","name":"Workspace Token","description":"For workspace management APIs","fields":["workspaceToken"]}]},"cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"sendgrid","name":"SendGrid","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Marketing","subcat":"Email, Transactional Email, Email Delivery","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"sentry","name":"Sentry","implemented":true,"openapi":false,"fenestra":false,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"sharepoint","name":"Microsoft SharePoint","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Sharing","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"shopify","name":"shopify","implemented":false,"openapi":true,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"square","name":"Square","implemented":true,"openapi":true,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"Payments, Point of Sale, Business Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"stripe","name":"Stripe","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Finance","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"telegram","name":"Telegram","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Messaging","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"terminus","name":"Terminus","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"ABM","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"todoist","name":"Todoist","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Productivity","subcat":"Task Management","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"trello","name":"trello","implemented":false,"openapi":true,"fenestra":true,"frigg":false,"auth":"Unknown","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"twilio","name":"Twilio","implemented":true,"openapi":true,"fenestra":false,"frigg":true,"auth":"API Key","cat":"Communication","subcat":"Communications, SMS, Voice, Video, Messaging","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"typeform","name":"Typeform","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Forms","subcat":"Surveys, Data Collection, Customer Feedback","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"unbabel","name":"Unbabel","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"unbabel-projects","name":"Unbabel Projects","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"vonage","name":"Vonage","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Voice, SMS, Messaging","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"whatsapp-business","name":"WhatsApp Business","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Communication","subcat":"Messaging, Business","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"wise","name":"wise","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"finance","subcat":"payments, international-transfers","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"woocommerce","name":"WooCommerce","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"E-commerce","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"xero","name":"Xero","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Accounting Software","subcat":"Financial Management, Invoicing, Business Intelligence","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"yotpo","name":"Yotpo","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Other","subcat":"","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"youtube","name":"YouTube","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Content","subcat":"Video Platform, Content Management, Social Media, Google Services","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"zendesk","name":"zendesk","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"customer-support","subcat":"helpdesk, crm","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"zoho-crm","name":"Zoho CRM","implemented":true,"openapi":false,"fenestra":true,"frigg":true,"auth":"OAuth2","cat":"CRM","subcat":"Sales, Marketing","notes":"","updated":"2025-06-26T17:02:45.104Z"} +{"id":"zoom","name":"Zoom","implemented":true,"openapi":false,"fenestra":false,"frigg":true,"auth":"OAuth2","cat":"Chat","subcat":"Team Messaging, Video","notes":"","updated":"2025-06-26T17:02:45.104Z"} \ No newline at end of file diff --git a/packages/needs-updating/activecampaign/.eslintrc.json b/packages/42matters/.eslintrc.json similarity index 100% rename from packages/needs-updating/activecampaign/.eslintrc.json rename to packages/42matters/.eslintrc.json diff --git a/packages/v1-ready/42matters/.gitignore b/packages/42matters/.gitignore similarity index 100% rename from packages/v1-ready/42matters/.gitignore rename to packages/42matters/.gitignore diff --git a/packages/v1-ready/42matters/CHANGELOG.md b/packages/42matters/CHANGELOG.md similarity index 100% rename from packages/v1-ready/42matters/CHANGELOG.md rename to packages/42matters/CHANGELOG.md diff --git a/packages/needs-updating/sharepoint/LICENSE.md b/packages/42matters/LICENSE.md similarity index 100% rename from packages/needs-updating/sharepoint/LICENSE.md rename to packages/42matters/LICENSE.md diff --git a/packages/v1-ready/42matters/README.md b/packages/42matters/README.md similarity index 100% rename from packages/v1-ready/42matters/README.md rename to packages/42matters/README.md diff --git a/packages/v1-ready/42matters/api.js b/packages/42matters/api.js similarity index 100% rename from packages/v1-ready/42matters/api.js rename to packages/42matters/api.js diff --git a/packages/v1-ready/42matters/defaultConfig.json b/packages/42matters/defaultConfig.json similarity index 100% rename from packages/v1-ready/42matters/defaultConfig.json rename to packages/42matters/defaultConfig.json diff --git a/packages/v1-ready/42matters/definition.js b/packages/42matters/definition.js similarity index 100% rename from packages/v1-ready/42matters/definition.js rename to packages/42matters/definition.js diff --git a/packages/v1-ready/42matters/index.js b/packages/42matters/index.js similarity index 100% rename from packages/v1-ready/42matters/index.js rename to packages/42matters/index.js diff --git a/packages/needs-updating/slack/jest.config.js b/packages/42matters/jest.config.js similarity index 100% rename from packages/needs-updating/slack/jest.config.js rename to packages/42matters/jest.config.js diff --git a/packages/v1-ready/42matters/package.json b/packages/42matters/package.json similarity index 100% rename from packages/v1-ready/42matters/package.json rename to packages/42matters/package.json diff --git a/packages/v1-ready/42matters/tests/api.test.js b/packages/42matters/tests/api.test.js similarity index 100% rename from packages/v1-ready/42matters/tests/api.test.js rename to packages/42matters/tests/api.test.js diff --git a/packages/v1-ready/42matters/tests/auther.test.js b/packages/42matters/tests/auther.test.js similarity index 100% rename from packages/v1-ready/42matters/tests/auther.test.js rename to packages/42matters/tests/auther.test.js diff --git a/packages/AI_ML_MODULES_SUMMARY.md b/packages/AI_ML_MODULES_SUMMARY.md new file mode 100644 index 0000000..e20e1d8 --- /dev/null +++ b/packages/AI_ML_MODULES_SUMMARY.md @@ -0,0 +1,127 @@ +# AI/ML API Modules Summary + +This document summarizes the 5 AI/ML API modules created for the Frigg Framework. + +## Created Modules + +### 1. OpenAI (`/packages/openai`) +- **Authentication**: API Key +- **Key Features**: + - Chat Completions (GPT-4, GPT-3.5) + - Embeddings (text-embedding-ada-002) + - Image Generation (DALL-E 3, DALL-E 2) + - Audio (Whisper transcription, TTS) + - Fine-tuning capabilities + - Assistants API with persistent threads + - Streaming support + - Content moderation + +### 2. Anthropic (`/packages/anthropic`) +- **Authentication**: API Key with x-api-key header +- **Key Features**: + - Messages API (Claude 3 models) + - Streaming responses + - Vision capabilities (Claude 3) + - System prompts + - 200K token context window + - Message formatting helpers + - Legacy completions API + +### 3. Cohere (`/packages/cohere`) +- **Authentication**: API Key +- **Key Features**: + - Text generation (Command models) + - Chat interface with history + - Embeddings (multilingual support) + - Text classification + - Summarization + - Semantic reranking + - Tokenization/detokenization + - Fine-tuning support + - Batch processing utilities + - Semantic search helpers + +### 4. Hugging Face (`/packages/huggingface`) +- **Authentication**: API Token +- **Key Features**: + - Inference API for thousands of models + - Model Hub access + - Multi-modal support (text, vision, audio) + - Datasets API + - Spaces API + - Inference Endpoints (dedicated deployments) + - Auto task detection + - Model search by task + - Support for 20+ ML tasks + +### 5. Replicate (`/packages/replicate`) +- **Authentication**: API Token +- **Key Features**: + - Run any public model + - Async predictions with polling + - Streaming output support + - Deployment management + - Webhook integration + - Progress tracking + - High-level helpers for common tasks + - Model versioning + - Hardware selection + +## Common Features Across All Modules + +1. **Consistent API Structure**: All modules follow the Frigg Framework pattern +2. **Error Handling**: Proper handling of rate limits, auth errors, and API-specific errors +3. **Streaming Support**: Where available (OpenAI, Anthropic, Cohere, Replicate) +4. **Token/Usage Management**: Helpers for estimating and managing usage +5. **Test Auth Methods**: Verify API key validity +6. **Environment Variables**: Standard pattern for API keys +7. **Comprehensive Documentation**: Detailed README with examples for each module + +## Usage Pattern + +All modules follow the same basic pattern: + +```javascript +const {Api} = require('./api'); + +const api = new Api({ + apiKey: process.env.SERVICE_API_KEY +}); + +// Use API methods +const result = await api.someMethod(params); +``` + +## Environment Variables + +- `OPENAI_API_KEY` +- `ANTHROPIC_API_KEY` +- `COHERE_API_KEY` +- `HUGGINGFACE_API_TOKEN` or `HUGGINGFACE_API_KEY` +- `REPLICATE_API_TOKEN` or `REPLICATE_API_KEY` + +## Testing + +Each module includes a `testAuth()` method to verify credentials: + +```javascript +const isValid = await api.testAuth(); +``` + +## Next Steps + +1. Add unit tests for each module +2. Add integration tests with real API calls +3. Add rate limiting and retry logic +4. Add cost tracking utilities +5. Create example applications +6. Add support for additional models as they become available + +## Module Structure + +Each module contains: +- `defaultConfig.json` - Module metadata +- `api.js` - API client implementation +- `definition.js` - Frigg integration definition +- `index.js` - Module exports +- `README.md` - Comprehensive documentation \ No newline at end of file diff --git a/packages/FINANCE_CUSTOMER_SUPPORT_MODULES_SUMMARY.md b/packages/FINANCE_CUSTOMER_SUPPORT_MODULES_SUMMARY.md new file mode 100644 index 0000000..3ab0a76 --- /dev/null +++ b/packages/FINANCE_CUSTOMER_SUPPORT_MODULES_SUMMARY.md @@ -0,0 +1,187 @@ +# Finance & Customer Support API Modules Summary + +This document provides an overview of the 5 new API modules created for finance and customer support platforms. + +## Finance APIs + +### 1. Plaid (`@friggframework/plaid`) +**Description**: Financial data aggregation platform for accessing bank account information, transactions, and balances. + +**Key Features**: +- Link token creation for Plaid Link integration +- Public token exchange for secure access +- Account and balance retrieval +- Transaction fetching and incremental syncing +- Investment holdings and transactions +- Identity verification +- Liability information +- Institution search and metadata +- Processor token creation for third-party integrations + +**Authentication**: Client ID/Secret with public token exchange + +**Environment Variables**: +``` +PLAID_CLIENT_ID=your_client_id +PLAID_SECRET=your_secret +PLAID_ENV=sandbox|development|production +``` + +### 2. Wise (`@friggframework/wise`) +**Description**: International money transfer platform (formerly TransferWise) for sending money across borders. + +**Key Features**: +- Multi-currency account management +- Quote creation with real-time exchange rates +- International transfer creation and tracking +- Recipient management +- Balance checking across currencies +- Transfer fee calculation +- Webhook support for transfer status updates +- Sandbox environment for testing + +**Authentication**: API Token + +**Environment Variables**: +``` +WISE_API_TOKEN=your_api_token +WISE_SANDBOX=true|false +``` + +### 3. Coinbase (`@friggframework/coinbase`) +**Description**: Cryptocurrency trading and wallet platform for buying, selling, and managing digital assets. + +**Key Features**: +- OAuth2 and API Key authentication support +- Wallet and account management +- Buy/sell cryptocurrency orders +- Send/receive/transfer funds +- Real-time price data and exchange rates +- Transaction history +- Payment method management +- Deposit and withdrawal operations +- Investment portfolio tracking + +**Authentication**: OAuth2 or API Key/Secret + +**Environment Variables**: +``` +# OAuth2 +COINBASE_CLIENT_ID=your_client_id +COINBASE_CLIENT_SECRET=your_client_secret +COINBASE_SANDBOX=true|false + +# API Key +COINBASE_API_KEY=your_api_key +COINBASE_API_SECRET=your_api_secret +``` + +## Customer Support APIs + +### 4. Zendesk (`@friggframework/zendesk`) +**Description**: Comprehensive customer service platform for managing support tickets and help center content. + +**Key Features**: +- OAuth2 and API token authentication +- Full ticket lifecycle management +- User and organization management +- Help Center articles and knowledge base +- Macros, triggers, and automation rules +- Custom fields and objects +- Satisfaction ratings +- Advanced search capabilities +- Webhook support +- Multi-brand support + +**Authentication**: OAuth2 or API Token with email + +**Environment Variables**: +``` +# OAuth2 +ZENDESK_CLIENT_ID=your_client_id +ZENDESK_CLIENT_SECRET=your_client_secret +ZENDESK_SUBDOMAIN=your_subdomain + +# API Token +ZENDESK_EMAIL=your_email +ZENDESK_API_TOKEN=your_api_token +ZENDESK_SUBDOMAIN=your_subdomain +``` + +### 5. Freshdesk (`@friggframework/freshdesk`) +**Description**: Help desk software for customer support with ticketing and knowledge base features. + +**Key Features**: +- Comprehensive ticket management +- Contact and agent management +- Company management +- Knowledge base (solutions) with categories, folders, and articles +- Forum and discussion management +- Time tracking on tickets +- Automation rules and SLA policies +- Canned responses for quick replies +- Custom fields for tickets, contacts, and companies +- Satisfaction ratings +- Email configuration management + +**Authentication**: API Key with subdomain + +**Environment Variables**: +``` +FRESHDESK_API_KEY=your_api_key +FRESHDESK_SUBDOMAIN=your_subdomain +``` + +## Common Features Across All Modules + +1. **Standardized Structure**: All modules follow the Frigg Framework v1 structure with: + - `api.js` - Core API implementation + - `definition.js` - Authentication and configuration + - `index.js` - Module exports + - `package.json` - Dependencies and metadata + - `defaultConfig.json` - Module configuration + - `readme.md` - Documentation + +2. **Error Handling**: Consistent error handling with descriptive messages + +3. **Webhook Support**: All modules include webhook signature verification methods + +4. **Testing**: Each module includes Jest test files with comprehensive test coverage + +5. **Environment Configuration**: Support for environment variables and flexible configuration + +## Usage Example + +```javascript +const { Api } = require('@friggframework/plaid'); + +// Initialize the API +const plaidApi = new Api({ + clientId: process.env.PLAID_CLIENT_ID, + secret: process.env.PLAID_SECRET, + environment: 'sandbox' +}); + +// Create a link token +const linkToken = await plaidApi.createLinkToken({ + userId: 'user-123', + products: ['transactions', 'accounts'], + countryCodes: ['US'] +}); + +// Exchange public token after user completes Plaid Link +const result = await plaidApi.exchangePublicToken(publicToken); + +// Fetch accounts +const accounts = await plaidApi.getAccounts(); +``` + +## Integration Notes + +- **Plaid**: Requires user interaction through Plaid Link for bank connection +- **Wise**: Supports both personal and business profiles +- **Coinbase**: Can use either OAuth2 for user delegation or API keys for direct access +- **Zendesk**: Subdomain required for all API calls +- **Freshdesk**: Returns numeric status codes (2 = Open, 3 = Pending, etc.) + +All modules are ready for v1 deployment and follow Frigg Framework best practices. \ No newline at end of file diff --git a/packages/MIGRATION_SUMMARY.md b/packages/MIGRATION_SUMMARY.md new file mode 100644 index 0000000..bcb31c9 --- /dev/null +++ b/packages/MIGRATION_SUMMARY.md @@ -0,0 +1,140 @@ +# API Module Library - Complete Migration & Inventory Summary + +## Migration Completed: 2025-06-26 + +### Executive Summary +Successfully completed comprehensive migration project with 101 total modules now in unified packages structure. Legacy v1 pattern migration (21 modules) and bulk directory reorganization (80+ modules) both completed successfully. + +## PHASE 1: Legacy Pattern Migration (21 modules) +Successfully migrated all API modules from the legacy manager.js pattern to the v1 definition.js pattern. + +### Modules Migrated + +#### OAuth2 Modules (10) +- ✅ asana +- ✅ attio +- ✅ fastspring-iq +- ✅ freshbooks +- ✅ frontify +- ✅ hubspot +- ✅ linear +- ✅ microsoft-teams +- ✅ netx +- ✅ pipedrive +- ✅ qbo +- ✅ rollworks +- ✅ salesforce +- ✅ unbabel-projects +- ✅ zoho-crm + +#### API Key Auth Modules (4) +- ✅ openphone +- ✅ recharge +- ✅ revio +- ✅ terminus + +#### Basic Auth Modules (2) +- ✅ clyde (clientKey/secret) +- ✅ huggg (username/password) + +### Changes Made + +For each module: +1. **Created definition.js** - Implements the v1 pattern with: + - `requiredAuthMethods` object containing: + - `getToken()` - Handles auth token acquisition + - `getEntityDetails()` - Extracts entity information + - `getCredentialDetails()` - Extracts credential information + - `apiPropertiesToPersist` - Defines which properties to store + - `testAuthRequest()` - Tests authentication validity + - `env` object for OAuth modules with environment variables + +2. **Updated index.js** - Changed exports to: + - Removed `Manager` or `ModuleManager` export + - Added `Definition` export + - Maintained existing exports (Api, Credential, Entity, Config) + +3. **Removed manager.js** - Legacy file no longer needed + +### Special Cases + +- **freshbooks** - Already had a definition.js file, only needed index.js update and manager.js removal +- **recharge** - Has definition.ts (TypeScript) instead of definition.js +- **huggg** - Had incomplete manager.js implementation, created basic auth definition + +### Verification +All modules now follow the v1 pattern and are ready for use with the updated Frigg framework. + +## PHASE 2: Bulk Directory Migration & Expansion (80+ modules) + +### Directory Reorganization Completed +- **needs-updating directory**: 26 modules successfully migrated to packages/ +- **v1-ready directory**: 25+ modules successfully migrated to packages/ +- **All legacy directories cleared**: Git status shows all old modules marked as DELETED + +### Agent Batch Generation Results + +#### ✅ Agent 1 Batch: COMPLETED (16 modules) +High Priority AWS modules: +- Amazon API Gateway, AWS CloudWatch, AWS EC2, AWS RDS, AWS S3, etc. +- Status: All generated and integrated successfully + +#### ✅ Agent 2 Batch: COMPLETED (16 modules) +High Priority AWS + Critical modules: +- Amazon CloudSearch, AWS DynamoDB, AWS IAM, Azure Active Directory, etc. +- Status: All generated and integrated successfully + +#### ⚠️ Agent 3 Batch: PARTIALLY COMPLETED (5/16 modules) +**Completed**: Amazon SNS, AWS KMS, Gmail, EventBrite, DeepCrawl +**Remaining**: 11 modules including Agile CRM, FrontApp, DealCloud, Box.com, etc. +**Action Required**: Complete remaining 11 modules + +#### ✅ Agent 4 Batch: COMPLETED (16 modules) +High Priority modules: +- Amazon SQS, AWS Lambda, Microsoft Azure, Docker Registry, etc. +- Status: All generated and integrated successfully + +#### ⚠️ Agent 5 Batch: STATUS VERIFICATION NEEDED (15 modules) +**Assigned**: aws-account-management, aws-s3, google-analytics (High Priority) +**Status**: Batch started but completion unclear +**Action Required**: Verify completion and finalize any remaining modules + +## CURRENT INVENTORY STATUS + +### Main Packages Directory: 101 modules total +**Categories Well Represented:** +- CRM & Sales: HubSpot, Pipedrive, Salesforce, etc. +- Cloud Services: AWS (multiple), Azure, Google Cloud +- Communication: Slack, Microsoft Teams, Discord, etc. +- Developer Tools: GitHub, GitLab, Linear, etc. +- E-commerce: Shopify, WooCommerce, BigCommerce, etc. +- Marketing: Mailchimp, SendGrid, Segment, etc. + +### Additional Directories Requiring Cleanup: +- **priority-modules/**: 8 modules (potential duplicates) +- **test-modules/**: 3 modules (development versions) +- **corrected-modules/**: 1 module (auth0 correction) + +## OUTSTANDING WORK + +### Immediate Priority: +1. **Complete Agent 3**: Generate remaining 11 modules +2. **Verify Agent 5**: Confirm status of 15 assigned modules +3. **Directory Cleanup**: Resolve duplicate modules in auxiliary directories + +### Next Steps: +1. Run comprehensive test suite on all 101 modules +2. Documentation audit and standardization +3. Integration testing with Frigg framework +4. Gap analysis for additional high-value APIs + +## FINAL STATUS +**✅ MIGRATION CORE OBJECTIVE: COMPLETED** +- 101 modules successfully unified in packages directory +- All legacy directories properly migrated +- Git tracking shows clean migration + +**⚠️ EXPANSION COMPLETION: 26 modules pending** +- Agent 3: 11 modules remaining +- Agent 5: 15 modules status verification needed +- Auxiliary directories: 12 modules cleanup required \ No newline at end of file diff --git a/packages/needs-updating/airwallex/.eslintrc.json b/packages/activecampaign/.eslintrc.json similarity index 100% rename from packages/needs-updating/airwallex/.eslintrc.json rename to packages/activecampaign/.eslintrc.json diff --git a/packages/needs-updating/activecampaign/CHANGELOG.md b/packages/activecampaign/CHANGELOG.md similarity index 100% rename from packages/needs-updating/activecampaign/CHANGELOG.md rename to packages/activecampaign/CHANGELOG.md diff --git a/packages/needs-updating/activecampaign/LICENSE.md b/packages/activecampaign/LICENSE.md similarity index 100% rename from packages/needs-updating/activecampaign/LICENSE.md rename to packages/activecampaign/LICENSE.md diff --git a/packages/needs-updating/activecampaign/README.md b/packages/activecampaign/README.md similarity index 100% rename from packages/needs-updating/activecampaign/README.md rename to packages/activecampaign/README.md diff --git a/packages/needs-updating/activecampaign/api.js b/packages/activecampaign/api.js similarity index 100% rename from packages/needs-updating/activecampaign/api.js rename to packages/activecampaign/api.js diff --git a/packages/needs-updating/activecampaign/authFields.js b/packages/activecampaign/authFields.js similarity index 100% rename from packages/needs-updating/activecampaign/authFields.js rename to packages/activecampaign/authFields.js diff --git a/packages/needs-updating/activecampaign/defaultConfig.json b/packages/activecampaign/defaultConfig.json similarity index 100% rename from packages/needs-updating/activecampaign/defaultConfig.json rename to packages/activecampaign/defaultConfig.json diff --git a/packages/activecampaign/definition.js b/packages/activecampaign/definition.js new file mode 100644 index 0000000..bf851cc --- /dev/null +++ b/packages/activecampaign/definition.js @@ -0,0 +1,97 @@ +const { IntegrationBase, ModuleConstants } = require('@friggframework/core'); +const _ = require('lodash'); +const { Api } = require('./api'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); +const AuthFields = require('./authFields'); +const Config = require('./defaultConfig.json'); + +class ActiveCampaignIntegration extends IntegrationBase { + static Definition = { + name: Config.name, + version: '1.0.0', + modules: { Api, Entity, Credential }, + display: { + label: Config.label, + description: Config.description, + category: Config.categories[0], + iconUrl: Config.logoUrl, + detailsUrl: Config.productUrl, + }, + }; + + static Entity = Entity; + static Credential = Credential; + + async getAuthorizationRequirements(params) { + return { + url: null, + type: ModuleConstants.authType.apiKey, + data: { + jsonSchema: AuthFields.jsonSchema, + uiSchema: AuthFields.uiSchema, + }, + }; + } + + async processAuthorizationCallback(params) { + const apiUrl = _.get(params.data, 'apiUrl'); + const apiKey = _.get(params.data, 'apiKey'); + this.api = new Api({ apiUrl, apiKey }); + const userDetails = await this.api.getUserDetails(); + + const byUserId = { user: this.userId }; + const credentials = await this.credentialMO.list(byUserId); + + if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } + + const credential = await this.credentialMO.upsert(byUserId, { + user: this.userId, + api_url: apiUrl, + api_key: apiKey, + }); + + const byUserIdAndCredential = { + ...byUserId, + credential: credential.id, + }; + const entity = await this.entityMO.upsert(byUserIdAndCredential, { + user: this.userId, + credential: credential.id, + name: userDetails.user.username, + externalId: userDetails.user.id, + }); + + return { + entity_id: entity.id, + credential_id: credential.id, + type: Config.name, + }; + } + + async testAuth() { + await this.api.getUserDetails(); + } + + async deauthorize() { + // wipe api connection + this.api = new Api(); + + // delete credentials from the database + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async receiveNotification(notificationType, data) { + // Handle notifications if needed + return { success: true }; + } +} + +module.exports = ActiveCampaignIntegration; \ No newline at end of file diff --git a/packages/activecampaign/index.js b/packages/activecampaign/index.js new file mode 100644 index 0000000..3ca9218 --- /dev/null +++ b/packages/activecampaign/index.js @@ -0,0 +1,13 @@ +const { Api } = require('./api'); +const { Credential } = require('./models/credential'); +const { Entity } = require('./models/entity'); +const Definition = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/needs-updating/activecampaign/jest.config.js b/packages/activecampaign/jest.config.js similarity index 100% rename from packages/needs-updating/activecampaign/jest.config.js rename to packages/activecampaign/jest.config.js diff --git a/packages/needs-updating/activecampaign/manager.test.js b/packages/activecampaign/manager.test.js similarity index 100% rename from packages/needs-updating/activecampaign/manager.test.js rename to packages/activecampaign/manager.test.js diff --git a/packages/needs-updating/activecampaign/test/Api.test.js b/packages/activecampaign/test/Api.test.js similarity index 100% rename from packages/needs-updating/activecampaign/test/Api.test.js rename to packages/activecampaign/test/Api.test.js diff --git a/packages/actsoft/README.md b/packages/actsoft/README.md new file mode 100644 index 0000000..b1d1d5f --- /dev/null +++ b/packages/actsoft/README.md @@ -0,0 +1,43 @@ +# ActSoft API Module + +This module provides integration with the ActSoft API for the Frigg Framework. + +## Description + +ActSoft provides workforce automation and task management solutions for mobile workers. + +## Installation + +```bash +npm install @friggframework/api-module-actsoft +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-actsoft'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +ACTSOFT_CLIENT_ID=your_client_id +ACTSOFT_CLIENT_SECRET=your_client_secret +ACTSOFT_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/actsoft/api.js b/packages/actsoft/api.js new file mode 100644 index 0000000..063579d --- /dev/null +++ b/packages/actsoft/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.actsoft.com/v1'; + + this.URLs = { + // User/Account info + userInfo: '/users', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://app.actsoft.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.actsoft.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to ActSoft +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/actsoft/defaultConfig.json b/packages/actsoft/defaultConfig.json new file mode 100644 index 0000000..7c2e58f --- /dev/null +++ b/packages/actsoft/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "actsoft", + "label": "ActSoft", + "productUrl": "https://actsoft.com/", + "apiDocs": "https://developers.actsoft.com/", + "logoUrl": "https://friggframework.org/assets/img/actsoft-icon.png", + "categories": [ + "Productivity" + ], + "subCategories": [ + "Task Management" + ], + "description": "ActSoft provides workforce automation and task management solutions for mobile workers." +} \ No newline at end of file diff --git a/packages/actsoft/definition.js b/packages/actsoft/definition.js new file mode 100644 index 0000000..a8814f6 --- /dev/null +++ b/packages/actsoft/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'ActSoft', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'actsoft-account', user: userId}, + details: {name: userInfo.name || 'ActSoft Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'actsoft-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.ACTSOFT_CLIENT_ID, + client_secret: process.env.ACTSOFT_CLIENT_SECRET, + scope: process.env.ACTSOFT_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/actsoft`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/actsoft/index.js b/packages/actsoft/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/actsoft/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/actsoft/jest-setup.js b/packages/actsoft/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/actsoft/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/actsoft/jest-teardown.js b/packages/actsoft/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/actsoft/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/actsoft/jest.config.js b/packages/actsoft/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/actsoft/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/actsoft/package.json b/packages/actsoft/package.json new file mode 100644 index 0000000..74e2241 --- /dev/null +++ b/packages/actsoft/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-actsoft", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "ActSoft API module that lets the Frigg Framework interact with ActSoft", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/actsoft/test/api.test.js b/packages/actsoft/test/api.test.js new file mode 100644 index 0000000..bac6412 --- /dev/null +++ b/packages/actsoft/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('ActSoft API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/actsoft/test/definition.test.js b/packages/actsoft/test/definition.test.js new file mode 100644 index 0000000..3b5ab5c --- /dev/null +++ b/packages/actsoft/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('ActSoft Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('actsoft'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/agile-crm/README.md b/packages/agile-crm/README.md new file mode 100644 index 0000000..2ad67df --- /dev/null +++ b/packages/agile-crm/README.md @@ -0,0 +1,5 @@ +# Agile CRM + +This is the API Module for Agile CRM that allows the [Frigg](https://friggframework.org) code to talk to the Agile CRM API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/agile-crm) diff --git a/packages/agile-crm/api.js b/packages/agile-crm/api.js new file mode 100644 index 0000000..4f6853c --- /dev/null +++ b/packages/agile-crm/api.js @@ -0,0 +1,155 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.agilecrm.com/dev/api'; + + this.URLs = { + // Authentication + userInfo: '/users', + + // Contacts + contacts: '/contacts', + contactById: (contactId) => `/contacts/${contactId}`, + + // Companies + companies: '/contacts/companies/list', + companyById: (companyId) => `/contacts/companies/${companyId}`, + + // Deals + deals: '/opportunity', + dealById: (dealId) => `/opportunity/${dealId}`, + + // Tasks + tasks: '/tasks', + taskById: (taskId) => `/tasks/${taskId}`, + + // Notes + notes: '/notes', + noteById: (noteId) => `/notes/${noteId}` + }; + + this.authorizationUri = encodeURI( + `https://api.agilecrm.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.agilecrm.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // ************************** Contacts ********************************** + + async createContact(body) { + const options = { + url: this.baseUrl + this.URLs.contacts, + body: body, + }; + return this._post(options); + } + + async listContacts(params = {}) { + const options = { + url: this.baseUrl + this.URLs.contacts, + query: params + }; + return this._get(options); + } + + async updateContact(id, body) { + const options = { + url: this.baseUrl + this.URLs.contactById(id), + body: body, + }; + return this._put(options); + } + + async deleteContact(id) { + const options = { + url: this.baseUrl + this.URLs.contactById(id), + }; + return this._delete(options); + } + + async getContactById(id) { + const options = { + url: this.baseUrl + this.URLs.contactById(id), + }; + return this._get(options); + } + + // ************************** Companies ********************************** + + async createCompany(body) { + const options = { + url: this.baseUrl + this.URLs.companies, + body: body, + }; + return this._post(options); + } + + async listCompanies(params = {}) { + const options = { + url: this.baseUrl + this.URLs.companies, + query: params + }; + return this._get(options); + } + + async getCompanyById(id) { + const options = { + url: this.baseUrl + this.URLs.companyById(id), + }; + return this._get(options); + } + + // ************************** Deals ********************************** + + async createDeal(body) { + const options = { + url: this.baseUrl + this.URLs.deals, + body: body, + }; + return this._post(options); + } + + async listDeals(params = {}) { + const options = { + url: this.baseUrl + this.URLs.deals, + query: params + }; + return this._get(options); + } + + async updateDeal(id, body) { + const options = { + url: this.baseUrl + this.URLs.dealById(id), + body: body, + }; + return this._put(options); + } + + async deleteDeal(id) { + const options = { + url: this.baseUrl + this.URLs.dealById(id), + }; + return this._delete(options); + } + + async getDealById(id) { + const options = { + url: this.baseUrl + this.URLs.dealById(id), + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/agile-crm/defaultConfig.json b/packages/agile-crm/defaultConfig.json new file mode 100644 index 0000000..ae1e50e --- /dev/null +++ b/packages/agile-crm/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "agile-crm", + "label": "Agile CRM", + "productUrl": "https://www.agilecrm.com", + "apiDocs": "https://github.com/agilecrm/rest-api", + "logoUrl": "https://friggframework.org/assets/img/agile-crm-icon.png", + "categories": [ + "CRM", + "Sales" + ], + "description": "Agile CRM is an all-in-one CRM software with sales, marketing and service automation." +} diff --git a/packages/agile-crm/definition.js b/packages/agile-crm/definition.js new file mode 100644 index 0000000..546e93c --- /dev/null +++ b/packages/agile-crm/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'AgileCRM', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.AGILE_CRM_CLIENT_ID, + client_secret: process.env.AGILE_CRM_CLIENT_SECRET, + scope: process.env.AGILE_CRM_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/agile-crm`, + } +}; + +module.exports = {Definition}; diff --git a/packages/v1-ready/asana/index.js b/packages/agile-crm/index.js similarity index 100% rename from packages/v1-ready/asana/index.js rename to packages/agile-crm/index.js diff --git a/packages/agile-crm/jest.config.js b/packages/agile-crm/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/agile-crm/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/agile-crm/package.json b/packages/agile-crm/package.json new file mode 100644 index 0000000..04004e5 --- /dev/null +++ b/packages/agile-crm/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-agile-crm", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Agile CRM API module that lets the Frigg Framework interact with Agile CRM", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/airstory/README.md b/packages/airstory/README.md new file mode 100644 index 0000000..dc4c015 --- /dev/null +++ b/packages/airstory/README.md @@ -0,0 +1,55 @@ +# Airstory API Module + +Airstory API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/airstory +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/airstory'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +AIRSTORY_CLIENT_ID=your_client_id +AIRSTORY_CLIENT_SECRET=your_client_secret +AIRSTORY_SCOPE=your_scope +AIRSTORY_AUTH_URI=authorization_endpoint +AIRSTORY_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Productivity + +## License + +MIT diff --git a/packages/airstory/api.js b/packages/airstory/api.js new file mode 100644 index 0000000..5ed7353 --- /dev/null +++ b/packages/airstory/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Productivity'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.AIRSTORY_AUTH_URI; + this.tokenUri = process.env.AIRSTORY_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Airstory', + MODULE_NAME: 'airstory', + CATEGORY: 'https://api.airstory.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/airstory/defaultConfig.json b/packages/airstory/defaultConfig.json new file mode 100644 index 0000000..bd143e8 --- /dev/null +++ b/packages/airstory/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Airstory", + "moduleName": "airstory", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Airstory API Integration Module", + "category": "Productivity", + "apiDocUrl": "https://docs.airstory.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/airstory/definition.js b/packages/airstory/definition.js new file mode 100644 index 0000000..f80d321 --- /dev/null +++ b/packages/airstory/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Airstory', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AIRSTORY_CLIENT_ID, + client_secret: process.env.AIRSTORY_CLIENT_SECRET, + scope: process.env.AIRSTORY_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/airstory`, + } +}; + +module.exports = {Definition}; diff --git a/packages/airstory/index.js b/packages/airstory/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/airstory/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/airstory/jest-setup.js b/packages/airstory/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/airstory/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/airstory/jest-teardown.js b/packages/airstory/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/airstory/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/airstory/jest.config.js b/packages/airstory/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/airstory/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/airstory/package.json b/packages/airstory/package.json new file mode 100644 index 0000000..37c2a13 --- /dev/null +++ b/packages/airstory/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/airstory", + "version": "0.0.1", + "description": "Airstory API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "airstory" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/v1-ready/airtable/README.md b/packages/airtable/README.md similarity index 100% rename from packages/v1-ready/airtable/README.md rename to packages/airtable/README.md diff --git a/packages/v1-ready/airtable/fenestra/platform.fenestra.yaml b/packages/airtable/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/airtable/fenestra/platform.fenestra.yaml rename to packages/airtable/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/airtable/fenestra/schemas/airtable-validation.json b/packages/airtable/fenestra/schemas/airtable-validation.json similarity index 100% rename from packages/v1-ready/airtable/fenestra/schemas/airtable-validation.json rename to packages/airtable/fenestra/schemas/airtable-validation.json diff --git a/packages/v1-ready/airtable/index.js b/packages/airtable/index.js similarity index 100% rename from packages/v1-ready/airtable/index.js rename to packages/airtable/index.js diff --git a/packages/v1-ready/airtable/package.json b/packages/airtable/package.json similarity index 100% rename from packages/v1-ready/airtable/package.json rename to packages/airtable/package.json diff --git a/packages/needs-updating/attentive/.eslintrc.json b/packages/airwallex/.eslintrc.json similarity index 100% rename from packages/needs-updating/attentive/.eslintrc.json rename to packages/airwallex/.eslintrc.json diff --git a/packages/needs-updating/airwallex/CHANGELOG.md b/packages/airwallex/CHANGELOG.md similarity index 100% rename from packages/needs-updating/airwallex/CHANGELOG.md rename to packages/airwallex/CHANGELOG.md diff --git a/packages/needs-updating/airwallex/LICENSE.md b/packages/airwallex/LICENSE.md similarity index 100% rename from packages/needs-updating/airwallex/LICENSE.md rename to packages/airwallex/LICENSE.md diff --git a/packages/needs-updating/airwallex/README.md b/packages/airwallex/README.md similarity index 100% rename from packages/needs-updating/airwallex/README.md rename to packages/airwallex/README.md diff --git a/packages/needs-updating/airwallex/api.js b/packages/airwallex/api.js similarity index 100% rename from packages/needs-updating/airwallex/api.js rename to packages/airwallex/api.js diff --git a/packages/needs-updating/airwallex/defaultConfig.json b/packages/airwallex/defaultConfig.json similarity index 100% rename from packages/needs-updating/airwallex/defaultConfig.json rename to packages/airwallex/defaultConfig.json diff --git a/packages/airwallex/definition.js b/packages/airwallex/definition.js new file mode 100644 index 0000000..d83e19c --- /dev/null +++ b/packages/airwallex/definition.js @@ -0,0 +1,125 @@ +const { IntegrationBase, get } = require('@friggframework/core'); +const { Api } = require('./api'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); + +class AirwallexIntegration extends IntegrationBase { + static Definition = { + name: 'airwallex', + version: '1.0.0', + display: { + label: 'Airwallex', + description: 'Airwallex', + imageURL: 'https://friggframework.org/assets/img/airwallex-icon.png', + icon: '', + category: 'Online Payments', + }, + modules: { + api: Api, + credential: Credential, + entity: Entity, + }, + }; + + async getAuthorizationRequirements() { + return { + url: this.api.authorizationUri, + type: 'oauth2', + }; + } + + async processAuthorizationCallback(params) { + const code = get(params.data, 'code'); + const response = await this.api.getTokenFromCode(code); + const userDetails = await this.api.getTokenIdentity(); + + let credentials = await this.credentialMO.list({user: this.userId}); + + if (credentials.length === 0) { + throw new Error('Credential failed to create'); + } + if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } + + let entity = await this.entityMO.getByUserId(this.userId); + + if (!entity) { + entity = await this.entityMO.create({ + user: this.userId, + credential: credentials[0]._id, + externalId: userDetails.companyId, + name: userDetails.companyName, + }); + } + + return { + credential_id: credentials[0]._id, + entity_id: entity._id, + type: AirwallexIntegration.Definition.name, + }; + } + + async testAuth() { + await this.api.getTokenIdentity(); + } + + async receiveNotification(notifier, delegateString, object = null) { + if (notifier instanceof Api) { + if (delegateString === this.api.DLGT_TOKEN_UPDATE) { + const updatedToken = { + user: this.userId, + access_token: this.api.access_token, + id_token: this.api.id_token, + // expires_in: this.api.accessExpiresIn, + auth_is_valid: true, + }; + + Object.keys(updatedToken).forEach( + (k) => updatedToken[k] === null && delete updatedToken[k] + ); + + let credential = await this.entityMO.getByUserId(this.userId); + + if (!credential) { + credential = await this.credentialMO.create(updatedToken); + } else { + credential = await this.credentialMO.update( + credential, + updatedToken + ); + } + } + if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { + await this.deauthorize(); + } + } + } + + async deauthorize() { + // wipe api connection + this.api = new Api(); + + // delete credentials from the database + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async getApiObject() { + const apiParams = {delegate: this}; + + if (this.credential) { + apiParams.access_token = this.credential.access_token; + apiParams.id_token = this.credential.id_token; + apiParams.expires_in = this.credential.accessExpiresIn; + } + + return new Api(apiParams); + } +} + +module.exports = AirwallexIntegration; \ No newline at end of file diff --git a/packages/airwallex/index.js b/packages/airwallex/index.js new file mode 100644 index 0000000..a0eac7a --- /dev/null +++ b/packages/airwallex/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; diff --git a/packages/needs-updating/airwallex/jest.config.js b/packages/airwallex/jest.config.js similarity index 100% rename from packages/needs-updating/airwallex/jest.config.js rename to packages/airwallex/jest.config.js diff --git a/packages/needs-updating/airwallex/test/Api.test.js b/packages/airwallex/test/Api.test.js similarity index 100% rename from packages/needs-updating/airwallex/test/Api.test.js rename to packages/airwallex/test/Api.test.js diff --git a/packages/algolia/README.md b/packages/algolia/README.md new file mode 100644 index 0000000..7e728be --- /dev/null +++ b/packages/algolia/README.md @@ -0,0 +1,5 @@ +# Algolia + +This is the API Module for Algolia that allows the [Frigg](https://friggframework.org) code to talk to the Algolia API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/algolia) diff --git a/packages/algolia/api.js b/packages/algolia/api.js new file mode 100644 index 0000000..c4c0226 --- /dev/null +++ b/packages/algolia/api.js @@ -0,0 +1,206 @@ +const { Requester, get } = require('@friggframework/core'); + +class Api extends Requester { + constructor(params) { + super(params); + this.appId = get(params, 'app_id', process.env.ALGOLIA_APP_ID); + this.apiKey = get(params, 'api_key', process.env.ALGOLIA_API_KEY); + this.baseUrl = `https://${this.appId}.algolia.net/1`; + + this.URLs = { + // Search + search: '/search', + multipleQueries: '/indexes/*/queries', + + // Indexes + indexes: '/indexes', + indexByName: (indexName) => `/indexes/${indexName}`, + indexSettings: (indexName) => `/indexes/${indexName}/settings`, + + // Objects + objects: (indexName) => `/indexes/${indexName}/objects`, + objectById: (indexName, objectId) => `/indexes/${indexName}/objects/${objectId}`, + batch: (indexName) => `/indexes/${indexName}/batch`, + + // Synonyms + synonyms: (indexName) => `/indexes/${indexName}/synonyms`, + synonymById: (indexName, synonymId) => `/indexes/${indexName}/synonyms/${synonymId}`, + + // Rules + rules: (indexName) => `/indexes/${indexName}/rules`, + ruleById: (indexName, ruleId) => `/indexes/${indexName}/rules/${ruleId}`, + + // Analytics + analytics: '/analytics', + analyticsTopQueries: '/analytics/2/searches', + + // Insights + insights: '/events' + }; + } + + addAuthHeaders(options) { + const authHeaders = { + 'X-Algolia-Application-Id': this.appId, + 'X-Algolia-API-Key': this.apiKey, + 'Content-Type': 'application/json' + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + async _get(options) { + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _delete(options) { + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Search ********************************** + + async search(indexName, query, params = {}) { + const options = { + url: this.baseUrl + this.URLs.indexByName(indexName), + body: { + query: query, + ...params + }, + }; + return this._post(options); + } + + async multipleQueries(queries) { + const options = { + url: this.baseUrl + this.URLs.multipleQueries, + body: { + requests: queries + }, + }; + return this._post(options); + } + + // ************************** Indexes ********************************** + + async listIndexes() { + const options = { + url: this.baseUrl + this.URLs.indexes, + }; + return this._get(options); + } + + async createIndex(indexName) { + const options = { + url: this.baseUrl + this.URLs.indexByName(indexName), + body: {}, + }; + return this._post(options); + } + + async deleteIndex(indexName) { + const options = { + url: this.baseUrl + this.URLs.indexByName(indexName), + }; + return this._delete(options); + } + + async getIndexSettings(indexName) { + const options = { + url: this.baseUrl + this.URLs.indexSettings(indexName), + }; + return this._get(options); + } + + async updateIndexSettings(indexName, settings) { + const options = { + url: this.baseUrl + this.URLs.indexSettings(indexName), + body: settings, + }; + return this._put(options); + } + + // ************************** Objects ********************************** + + async addObject(indexName, object, objectId = null) { + const url = objectId + ? this.baseUrl + this.URLs.objectById(indexName, objectId) + : this.baseUrl + this.URLs.objects(indexName); + + const options = { + url: url, + body: object, + }; + return objectId ? this._put(options) : this._post(options); + } + + async getObject(indexName, objectId, attributesToRetrieve = null) { + const options = { + url: this.baseUrl + this.URLs.objectById(indexName, objectId), + }; + + if (attributesToRetrieve) { + options.query = { attributesToRetrieve: attributesToRetrieve.join(',') }; + } + + return this._get(options); + } + + async updateObject(indexName, objectId, object) { + const options = { + url: this.baseUrl + this.URLs.objectById(indexName, objectId), + body: object, + }; + return this._put(options); + } + + async deleteObject(indexName, objectId) { + const options = { + url: this.baseUrl + this.URLs.objectById(indexName, objectId), + }; + return this._delete(options); + } + + async batchObjects(indexName, requests) { + const options = { + url: this.baseUrl + this.URLs.batch(indexName), + body: { + requests: requests + }, + }; + return this._post(options); + } + + // ************************** Analytics ********************************** + + async getAnalytics(params = {}) { + const options = { + url: this.baseUrl + this.URLs.analytics, + query: params + }; + return this._get(options); + } + + async getTopQueries(params = {}) { + const options = { + url: this.baseUrl + this.URLs.analyticsTopQueries, + query: params + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/algolia/defaultConfig.json b/packages/algolia/defaultConfig.json new file mode 100644 index 0000000..89b046a --- /dev/null +++ b/packages/algolia/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "algolia", + "label": "Algolia", + "productUrl": "https://www.algolia.com", + "apiDocs": "https://www.algolia.com/doc/rest-api/", + "logoUrl": "https://friggframework.org/assets/img/algolia-icon.png", + "categories": [ + "Search", + "Analytics", + "AI/ML" + ], + "description": "Algolia is a powerful search-as-a-service solution that helps developers build fast, relevant search experiences." +} diff --git a/packages/algolia/definition.js b/packages/algolia/definition.js new file mode 100644 index 0000000..f7bb86c --- /dev/null +++ b/packages/algolia/definition.js @@ -0,0 +1,51 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Algolia', + requiredAuthMethods: { + getToken: async function (api, params) { + // Algolia uses API keys, not OAuth + return { + access_token: params.api_key, + app_id: params.app_id + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const indexes = await api.listIndexes(); + return { + identifiers: {externalId: api.appId, user: userId}, + details: {appId: api.appId, indexCount: indexes.nbHits || 0}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'app_id', 'api_key' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const indexes = await api.listIndexes(); + return { + identifiers: {externalId: api.appId, user: userId}, + details: {appId: api.appId} + }; + }, + testAuthRequest: async function (api) { + return api.listIndexes() + }, + }, + env: { + app_id: process.env.ALGOLIA_APP_ID, + api_key: process.env.ALGOLIA_API_KEY, + } +}; + +module.exports = {Definition}; diff --git a/packages/v1-ready/hubspot/index.js b/packages/algolia/index.js similarity index 100% rename from packages/v1-ready/hubspot/index.js rename to packages/algolia/index.js diff --git a/packages/algolia/jest.config.js b/packages/algolia/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/algolia/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/algolia/package.json b/packages/algolia/package.json new file mode 100644 index 0000000..46cbbff --- /dev/null +++ b/packages/algolia/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-algolia", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Algolia API module that lets the Frigg Framework interact with Algolia", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/amazon-api-gateway/README.md b/packages/amazon-api-gateway/README.md new file mode 100644 index 0000000..9ef3085 --- /dev/null +++ b/packages/amazon-api-gateway/README.md @@ -0,0 +1,55 @@ +# Amazon API Gateway API Module + +Amazon API Gateway API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/amazon-api-gateway +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/amazon-api-gateway'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +AMAZON_API_GATEWAY_CLIENT_ID=your_client_id +AMAZON_API_GATEWAY_CLIENT_SECRET=your_client_secret +AMAZON_API_GATEWAY_SCOPE=your_scope +AMAZON_API_GATEWAY_AUTH_URI=authorization_endpoint +AMAZON_API_GATEWAY_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Developer + +## License + +MIT diff --git a/packages/amazon-api-gateway/api.js b/packages/amazon-api-gateway/api.js new file mode 100644 index 0000000..a14b755 --- /dev/null +++ b/packages/amazon-api-gateway/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Developer'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.AMAZON_API_GATEWAY_AUTH_URI; + this.tokenUri = process.env.AMAZON_API_GATEWAY_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Amazon API Gateway', + MODULE_NAME: 'amazon-api-gateway', + CATEGORY: 'https://apigateway.amazonaws.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/amazon-api-gateway/defaultConfig.json b/packages/amazon-api-gateway/defaultConfig.json new file mode 100644 index 0000000..1c0f10e --- /dev/null +++ b/packages/amazon-api-gateway/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Amazon API Gateway", + "moduleName": "amazon-api-gateway", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Amazon API Gateway API Integration Module", + "category": "Developer", + "apiDocUrl": "https://docs.amazon-api-gateway.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/amazon-api-gateway/definition.js b/packages/amazon-api-gateway/definition.js new file mode 100644 index 0000000..2ed5bb4 --- /dev/null +++ b/packages/amazon-api-gateway/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Amazon API Gateway', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AMAZON_API_GATEWAY_CLIENT_ID, + client_secret: process.env.AMAZON_API_GATEWAY_CLIENT_SECRET, + scope: process.env.AMAZON_API_GATEWAY_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/amazon-api-gateway`, + } +}; + +module.exports = {Definition}; diff --git a/packages/amazon-api-gateway/index.js b/packages/amazon-api-gateway/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/amazon-api-gateway/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/amazon-api-gateway/jest-setup.js b/packages/amazon-api-gateway/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/amazon-api-gateway/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/amazon-api-gateway/jest-teardown.js b/packages/amazon-api-gateway/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/amazon-api-gateway/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/amazon-api-gateway/jest.config.js b/packages/amazon-api-gateway/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/amazon-api-gateway/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/amazon-api-gateway/package.json b/packages/amazon-api-gateway/package.json new file mode 100644 index 0000000..0c91181 --- /dev/null +++ b/packages/amazon-api-gateway/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/amazon-api-gateway", + "version": "0.0.1", + "description": "Amazon API Gateway API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "amazon-api-gateway" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/amazon-cloudsearch/README.md b/packages/amazon-cloudsearch/README.md new file mode 100644 index 0000000..9041734 --- /dev/null +++ b/packages/amazon-cloudsearch/README.md @@ -0,0 +1,34 @@ +# Amazon Cloudsearch API Integration + +Amazon Cloudsearch integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/amazon-cloudsearch +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/amazon-cloudsearch'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `AMAZON_CLOUDSEARCH_CLIENT_ID` +- `AMAZON_CLOUDSEARCH_CLIENT_SECRET` +- `AMAZON_CLOUDSEARCH_SCOPE` + +## API Documentation + +For more information about the Amazon Cloudsearch API, visit: https://cloudsearch.us-east-1.amazonaws.com diff --git a/packages/amazon-cloudsearch/api.js b/packages/amazon-cloudsearch/api.js new file mode 100644 index 0000000..8dbf768 --- /dev/null +++ b/packages/amazon-cloudsearch/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class AmazonCloudsearchApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://cloudsearch.us-east-1.amazonaws.com'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: AmazonCloudsearchApi}; diff --git a/packages/amazon-cloudsearch/defaultConfig.json b/packages/amazon-cloudsearch/defaultConfig.json new file mode 100644 index 0000000..8187fa3 --- /dev/null +++ b/packages/amazon-cloudsearch/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Amazon Cloudsearch", + "moduleName": "amazon-cloudsearch", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Amazon Cloudsearch API Integration Module", + "category": "Developer", + "apiDocUrl": "https://cloudsearch.us-east-1.amazonaws.com", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/amazon-cloudsearch/definition.js b/packages/amazon-cloudsearch/definition.js new file mode 100644 index 0000000..9cb126a --- /dev/null +++ b/packages/amazon-cloudsearch/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Amazon Cloudsearch', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AMAZON_CLOUDSEARCH_CLIENT_ID, + client_secret: process.env.AMAZON_CLOUDSEARCH_CLIENT_SECRET, + scope: process.env.AMAZON_CLOUDSEARCH_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/amazon-cloudsearch`, + } +}; + +module.exports = {Definition}; diff --git a/packages/amazon-cloudsearch/index.js b/packages/amazon-cloudsearch/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/amazon-cloudsearch/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/amazon-cloudsearch/package.json b/packages/amazon-cloudsearch/package.json new file mode 100644 index 0000000..c871f03 --- /dev/null +++ b/packages/amazon-cloudsearch/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/amazon-cloudsearch", + "version": "0.0.1", + "description": "Amazon Cloudsearch API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "amazon-cloudsearch", + "developer" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/amazon-sns/api.js b/packages/amazon-sns/api.js new file mode 100644 index 0000000..b5be7f9 --- /dev/null +++ b/packages/amazon-sns/api.js @@ -0,0 +1,149 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://sns.amazonaws.com'; + + this.URLs = { + // Topic operations + topics: '/topics', + topicById: (topicArn) => `/topics/${encodeURIComponent(topicArn)}`, + + // Subscription operations + subscriptions: '/subscriptions', + subscriptionById: (subscriptionArn) => `/subscriptions/${encodeURIComponent(subscriptionArn)}`, + + // Message operations + publish: '/publish', + + // Platform applications + platformApplications: '/platform-applications', + platformApplicationById: (applicationArn) => `/platform-applications/${encodeURIComponent(applicationArn)}`, + + // Endpoints + endpoints: '/endpoints', + endpointById: (endpointArn) => `/endpoints/${encodeURIComponent(endpointArn)}`, + }; + + this.authorizationUri = encodeURI( + `https://aws.amazon.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://aws.amazon.com/oauth/token'; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + redirect_uri: this.redirect_uri, + code: code, + }, + }; + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + // Topic operations + async listTopics(params = {}) { + const options = { + url: this.baseUrl + this.URLs.topics, + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async createTopic(name, attributes = {}) { + const options = { + url: this.baseUrl + this.URLs.topics, + method: 'POST', + json: { + Name: name, + Attributes: attributes, + }, + }; + return this._request(options); + } + + async getTopic(topicArn) { + const options = { + url: this.baseUrl + this.URLs.topicById(topicArn), + method: 'GET', + }; + return this._request(options); + } + + async deleteTopic(topicArn) { + const options = { + url: this.baseUrl + this.URLs.topicById(topicArn), + method: 'DELETE', + }; + return this._request(options); + } + + // Subscription operations + async listSubscriptions(params = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async subscribe(topicArn, protocol, endpoint) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + method: 'POST', + json: { + TopicArn: topicArn, + Protocol: protocol, + Endpoint: endpoint, + }, + }; + return this._request(options); + } + + async unsubscribe(subscriptionArn) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionArn), + method: 'DELETE', + }; + return this._request(options); + } + + // Message operations + async publishMessage(topicArn, message, subject = null, messageAttributes = {}) { + const options = { + url: this.baseUrl + this.URLs.publish, + method: 'POST', + json: { + TopicArn: topicArn, + Message: message, + Subject: subject, + MessageAttributes: messageAttributes, + }, + }; + return this._request(options); + } + + // User info for authentication + async getUserDetails() { + const options = { + url: this.baseUrl + '/user-details', + method: 'GET', + }; + return this._request(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/amazon-sns/defaultConfig.json b/packages/amazon-sns/defaultConfig.json new file mode 100644 index 0000000..a019cfd --- /dev/null +++ b/packages/amazon-sns/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "amazon-sns", + "label": "Amazon SNS", + "productUrl": "https://aws.amazon.com/sns/", + "apiDocs": "https://docs.aws.amazon.com/sns/", + "logoUrl": "https://friggframework.org/assets/img/amazon-sns-icon.png", + "categories": [ + "Developer", + "Amazon" + ], + "description": "Amazon Simple Notification Service (SNS) is a fully managed messaging service for both application-to-application (A2A) and application-to-person (A2P) communication." +} \ No newline at end of file diff --git a/packages/amazon-sns/definition.js b/packages/amazon-sns/definition.js new file mode 100644 index 0000000..f5d520e --- /dev/null +++ b/packages/amazon-sns/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'AmazonSNS', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.accountId || userDetails.id, user: userId}, + details: {name: userDetails.name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.accountId || userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.AMAZON_SNS_CLIENT_ID, + client_secret: process.env.AMAZON_SNS_CLIENT_SECRET, + scope: process.env.AMAZON_SNS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/amazon-sns`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/amazon-sns/index.js b/packages/amazon-sns/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/amazon-sns/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/amazon-sqs/README.md b/packages/amazon-sqs/README.md new file mode 100644 index 0000000..f23b520 --- /dev/null +++ b/packages/amazon-sqs/README.md @@ -0,0 +1,42 @@ +# Amazon SQS API Module + +Frigg API module for Amazon SQS integration. + +## Installation + +```bash +npm install @friggframework/amazon-sqs +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/amazon-sqs'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +AMAZON_SQS_CLIENT_ID=your_client_id +AMAZON_SQS_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/amazon-sqs/api.js b/packages/amazon-sqs/api.js new file mode 100644 index 0000000..0aebb7f --- /dev/null +++ b/packages/amazon-sqs/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://sqs.amazonaws.com'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://sqs.amazonaws.com/oauth/authorize'; + this.accessTokenUri = 'https://sqs.amazonaws.com/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Amazon SQS', + MODULE_NAME: 'amazon-sqs', + CATEGORY: 'Developer', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/amazon-sqs/defaultConfig.json b/packages/amazon-sqs/defaultConfig.json new file mode 100644 index 0000000..706ded7 --- /dev/null +++ b/packages/amazon-sqs/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Amazon SQS", + "moduleName": "amazon-sqs", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Amazon SQS API Integration Module", + "category": "Developer", + "apiDocUrl": "https://sqs.amazonaws.com/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/amazon-sqs/definition.js b/packages/amazon-sqs/definition.js new file mode 100644 index 0000000..53904f9 --- /dev/null +++ b/packages/amazon-sqs/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'AmazonSQS', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AMAZON_SQS_CLIENT_ID, + client_secret: process.env.AMAZON_SQS_CLIENT_SECRET, + scope: process.env.AMAZON_SQS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/amazon-sqs`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/amazon-sqs/index.js b/packages/amazon-sqs/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/amazon-sqs/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/amazon-sqs/package.json b/packages/amazon-sqs/package.json new file mode 100644 index 0000000..e2df882 --- /dev/null +++ b/packages/amazon-sqs/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/amazon-sqs", + "version": "0.0.1", + "description": "Amazon SQS API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "amazon-sqs", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/amplitude/README.md b/packages/amplitude/README.md new file mode 100644 index 0000000..09edf70 --- /dev/null +++ b/packages/amplitude/README.md @@ -0,0 +1,396 @@ +# Amplitude API Module + +This module provides a v1-ready integration with Amplitude's product analytics platform using API Key and Secret Key authentication. + +## Installation + +```bash +npm install @friggframework/api-module-amplitude +``` + +## Features + +- API Key and Secret Key authentication +- Event tracking and batch operations +- User identification and properties +- Group analytics +- Revenue tracking +- Data export capabilities +- Chart and dashboard analytics +- Cohort management +- GDPR compliance +- Annotations and releases + +## Authentication + +Amplitude uses API Key and Secret Key authentication: +- **API Key**: Public key for data ingestion (tracking events) +- **Secret Key**: Private key for data export and management APIs + +### Finding Your Keys +1. Log in to your Amplitude project +2. Navigate to Settings → Projects +3. Select your project +4. Find API Key and Secret Key in the project settings + +## Quick Start + +### Initialize the Integration + +```javascript +const { Definition } = require('@friggframework/api-module-amplitude'); + +const amplitude = new Definition({ + apiKey: 'your-api-key', + secretKey: 'your-secret-key' +}); +``` + +## API Methods + +### Event Tracking + +#### Track Single Event +```javascript +await amplitude.trackEvent({ + user_id: 'user-123', + event_type: 'Button Clicked', + event_properties: { + button_name: 'signup', + page: 'homepage' + }, + user_properties: { + plan: 'premium', + signup_date: '2024-01-01' + } +}); +``` + +#### Track Multiple Events +```javascript +await amplitude.trackEvents([ + { + user_id: 'user-123', + event_type: 'Page Viewed', + event_properties: { page: '/home' } + }, + { + user_id: 'user-123', + event_type: 'Feature Used', + event_properties: { feature: 'search' } + } +]); +``` + +### User Management + +#### Identify User +```javascript +await amplitude.identify({ + user_id: 'user-123', + user_properties: { + name: 'John Doe', + email: 'john@example.com', + plan: 'premium', + company: 'Acme Corp' + } +}); +``` + +#### Set User Properties +```javascript +await amplitude.setUserProperties({ + user_id: 'user-123', + properties: { + last_login: new Date().toISOString(), + total_purchases: 5 + } +}); +``` + +### Group Analytics + +#### Set Group Properties +```javascript +await amplitude.setGroupProperties({ + group_type: 'company', + group_name: 'acme-corp', + group_properties: { + plan: 'enterprise', + employees: 500, + industry: 'Technology' + } +}); +``` + +### Revenue Tracking + +#### Track Revenue Event +```javascript +await amplitude.trackRevenue({ + user_id: 'user-123', + revenue: 99.99, + price: 99.99, + quantity: 1, + productId: 'SKU-123', + revenueType: 'subscription', + event_properties: { + currency: 'USD', + payment_method: 'credit_card' + } +}); +``` + +### Analytics Queries + +#### Event Segmentation +```javascript +const segmentation = await amplitude.getEventSegmentation({ + events: [{ + event_type: 'Page Viewed' + }], + start: '20240101', + end: '20240131', + segment_by: 'country' +}); +``` + +#### Funnel Analysis +```javascript +const funnel = await amplitude.getFunnelAnalysis({ + events: [ + { event_type: 'Sign Up Started' }, + { event_type: 'Sign Up Completed' }, + { event_type: 'First Purchase' } + ], + start: '20240101', + end: '20240131' +}); +``` + +#### Retention Analysis +```javascript +const retention = await amplitude.getRetentionAnalysis({ + start_event: { + event_type: 'Sign Up' + }, + return_event: { + event_type: 'App Open' + }, + start: '20240101', + end: '20240131' +}); +``` + +#### User Composition +```javascript +const composition = await amplitude.getUserComposition({ + start: '20240101', + end: '20240131', + property: 'plan' +}); +``` + +### Data Export + +#### Export Raw Data +```javascript +const exportData = await amplitude.exportData({ + start: '20240101T00', + end: '20240101T23' +}); +``` + +#### Get User Activity +```javascript +const activity = await amplitude.getUserActivity('user-123', { + offset: 0, + limit: 100 +}); +``` + +### User Search + +#### Search for Users +```javascript +const users = await amplitude.searchUsers('john@example.com'); +``` + +### Cohort Management + +#### Get All Cohorts +```javascript +const cohorts = await amplitude.getCohorts(); +``` + +#### Get Cohort Members +```javascript +const members = await amplitude.getCohortMembers('cohort-123', { + props: 1, // Include user properties + limit: 1000 +}); +``` + +### Charts and Dashboards + +#### Get Charts +```javascript +const charts = await amplitude.getCharts(); +``` + +#### Get Chart Data +```javascript +const chartData = await amplitude.getChartData('chart-123'); +``` + +### Annotations + +#### Get Annotations +```javascript +const annotations = await amplitude.getAnnotations(); +``` + +#### Create Annotation +```javascript +const annotation = await amplitude.createAnnotation({ + date: '2024-01-15', + label: 'Feature Launch', + details: 'Launched new dashboard feature' +}); +``` + +### GDPR Compliance + +#### Delete User Data +```javascript +const deletion = await amplitude.deleteUserData({ + user_ids: ['user-123', 'user-456'], + requester: 'privacy@company.com' +}); +``` + +## Advanced Event Tracking + +### With Device Information +```javascript +await amplitude.trackEvent({ + user_id: 'user-123', + device_id: 'device-abc', + event_type: 'App Opened', + platform: 'iOS', + os_name: 'ios', + os_version: '15.0', + device_brand: 'Apple', + device_manufacturer: 'Apple', + device_model: 'iPhone 13', + carrier: 'Verizon', + country: 'United States', + region: 'California', + city: 'San Francisco', + language: 'en-US' +}); +``` + +### With Location +```javascript +await amplitude.trackEvent({ + user_id: 'user-123', + event_type: 'Store Visit', + location_lat: 37.7749, + location_lng: -122.4194, + ip: '192.168.1.1' +}); +``` + +### With Session Tracking +```javascript +await amplitude.trackEvent({ + user_id: 'user-123', + event_type: 'Session Start', + session_id: Date.now(), + event_id: 1, + insert_id: 'unique-insert-id' +}); +``` + +## Error Handling + +```javascript +try { + await amplitude.trackEvent({ + user_id: 'user-123', + event_type: 'Test Event' + }); +} catch (error) { + if (error.status === 400) { + console.error('Invalid event data:', error.message); + } else if (error.status === 401) { + console.error('Invalid API keys'); + } else if (error.status === 429) { + console.error('Rate limit exceeded'); + } else { + console.error('Amplitude API Error:', error); + } +} +``` + +## Testing Authentication + +```javascript +const testResult = await amplitude.testAuth(); +if (testResult.success) { + console.log('Authentication successful!'); +} else { + console.error('Authentication failed:', testResult.message); +} +``` + +## Best Practices + +1. **Always include user_id or device_id**: At least one identifier is required +2. **Use consistent event naming**: Follow a naming convention like "Noun Verb" +3. **Track revenue accurately**: Include all revenue fields for proper attribution +4. **Batch events when possible**: Reduce API calls for better performance +5. **Include relevant properties**: Add context that helps with analysis +6. **Use groups for B2B**: Track company-level metrics with group properties + +## Rate Limits + +- **Event Upload**: 1000 events per second per device +- **Batch Size**: Maximum 1000 events per batch +- **Export API**: 4 concurrent requests +- **Dashboard API**: 360 requests per hour + +## Event Properties Best Practices + +Common event properties to include: +```javascript +{ + // Product events + product_id: 'SKU-123', + product_name: 'Running Shoes', + product_category: 'Footwear', + price: 99.99, + currency: 'USD', + + // User context + user_type: 'premium', + ab_test_group: 'variant_a', + + // Technical context + app_version: '2.1.0', + sdk_version: '1.0.0', + + // Business context + promotion_id: 'SUMMER2024', + referrer: 'google' +} +``` + +## Resources + +- [Amplitude Documentation](https://www.docs.developers.amplitude.com/) +- [HTTP API V2 Reference](https://www.docs.developers.amplitude.com/analytics/apis/http-v2-api/) +- [Export API Reference](https://www.docs.developers.amplitude.com/analytics/apis/export-api/) +- [Dashboard REST API](https://www.docs.developers.amplitude.com/analytics/apis/dashboard-rest-api/) +- [Best Practices](https://help.amplitude.com/hc/en-us/articles/115001643283) \ No newline at end of file diff --git a/packages/amplitude/api.js b/packages/amplitude/api.js new file mode 100644 index 0000000..2f39f34 --- /dev/null +++ b/packages/amplitude/api.js @@ -0,0 +1,443 @@ +const { ApiClass } = require('@friggframework/core'); + +class AmplitudeApi extends ApiClass { + constructor(params) { + super(params); + this.baseUrl = 'https://api2.amplitude.com'; + this.apiKey = params.apiKey; + this.secretKey = params.secretKey; + + // Set up basic auth for API endpoints that require it + if (this.apiKey && this.secretKey) { + this.authHeader = 'Basic ' + Buffer.from( + `${this.apiKey}:${this.secretKey}` + ).toString('base64'); + } + } + + /** + * Get authorization headers + * @returns {Object} Headers with authorization + */ + _getAuthHeaders() { + return { + 'Authorization': this.authHeader, + 'Content-Type': 'application/json' + }; + } + + /** + * Track a single event + * @param {Object} event - Event data + * @returns {Promise} Response + */ + async trackEvent(event) { + const eventData = { + api_key: this.apiKey, + events: [{ + user_id: event.user_id, + device_id: event.device_id, + event_type: event.event_type, + time: event.time || Date.now(), + event_properties: event.event_properties || {}, + user_properties: event.user_properties || {}, + groups: event.groups || {}, + app_version: event.app_version, + platform: event.platform, + os_name: event.os_name, + os_version: event.os_version, + device_brand: event.device_brand, + device_manufacturer: event.device_manufacturer, + device_model: event.device_model, + carrier: event.carrier, + country: event.country, + region: event.region, + city: event.city, + dma: event.dma, + language: event.language, + price: event.price, + quantity: event.quantity, + revenue: event.revenue, + productId: event.productId, + revenueType: event.revenueType, + location_lat: event.location_lat, + location_lng: event.location_lng, + ip: event.ip, + idfa: event.idfa, + idfv: event.idfv, + adid: event.adid, + android_id: event.android_id, + event_id: event.event_id, + session_id: event.session_id, + insert_id: event.insert_id + }] + }; + + const url = `${this.baseUrl}/2/httpapi`; + return this._post(url, eventData); + } + + /** + * Track multiple events + * @param {Array} events - Array of events + * @returns {Promise} Response + */ + async trackEvents(events) { + const eventData = { + api_key: this.apiKey, + events: events.map(event => ({ + user_id: event.user_id, + device_id: event.device_id, + event_type: event.event_type, + time: event.time || Date.now(), + event_properties: event.event_properties || {}, + user_properties: event.user_properties || {}, + groups: event.groups || {}, + ...event + })) + }; + + const url = `${this.baseUrl}/2/httpapi`; + return this._post(url, eventData); + } + + /** + * Identify user with properties + * @param {Object} identify - Identify data + * @returns {Promise} Response + */ + async identify(identify) { + const identifyData = { + api_key: this.apiKey, + identification: [{ + user_id: identify.user_id, + device_id: identify.device_id, + user_properties: identify.user_properties || {} + }] + }; + + const url = `${this.baseUrl}/identify`; + return this._post(url, identifyData); + } + + /** + * Set user properties + * @param {Object} userProperties - User properties data + * @returns {Promise} Response + */ + async setUserProperties(userProperties) { + return this.identify({ + user_id: userProperties.user_id, + device_id: userProperties.device_id, + user_properties: userProperties.properties + }); + } + + /** + * Set group properties + * @param {Object} groupProperties - Group properties data + * @returns {Promise} Response + */ + async setGroupProperties(groupProperties) { + const groupData = { + api_key: this.apiKey, + identification: [{ + group_type: groupProperties.group_type, + group_name: groupProperties.group_name, + group_properties: groupProperties.group_properties || {} + }] + }; + + const url = `${this.baseUrl}/groupidentify`; + return this._post(url, groupData); + } + + /** + * Track revenue + * @param {Object} revenue - Revenue data + * @returns {Promise} Response + */ + async trackRevenue(revenue) { + const revenueEvent = { + user_id: revenue.user_id, + device_id: revenue.device_id, + event_type: revenue.event_type || 'revenue', + event_properties: { + ...revenue.event_properties, + revenue: revenue.revenue, + price: revenue.price, + quantity: revenue.quantity || 1, + productId: revenue.productId, + revenueType: revenue.revenueType + } + }; + + return this.trackEvent(revenueEvent); + } + + /** + * Export project data + * @param {Object} params - Export parameters + * @returns {Promise} Export data stream + */ + async exportData(params) { + const url = 'https://amplitude.com/api/2/export'; + const queryParams = new URLSearchParams({ + start: params.start, + end: params.end + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get user activity + * @param {string} userId - User ID + * @param {Object} params - Query parameters + * @returns {Promise} User activity + */ + async getUserActivity(userId, params = {}) { + const url = 'https://amplitude.com/api/2/useractivity'; + const queryParams = new URLSearchParams({ + user: userId, + ...params + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get event segmentation + * @param {Object} params - Segmentation parameters + * @returns {Promise} Segmentation data + */ + async getEventSegmentation(params) { + const url = 'https://amplitude.com/api/2/events/segmentation'; + const queryParams = new URLSearchParams({ + e: JSON.stringify(params.events || {}), + start: params.start, + end: params.end, + ...params + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get funnel analysis + * @param {Object} params - Funnel parameters + * @returns {Promise} Funnel data + */ + async getFunnelAnalysis(params) { + const url = 'https://amplitude.com/api/2/funnels'; + const queryParams = new URLSearchParams({ + e: JSON.stringify(params.events || []), + start: params.start, + end: params.end, + mode: params.mode || 'unordered', + ...params + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get retention analysis + * @param {Object} params - Retention parameters + * @returns {Promise} Retention data + */ + async getRetentionAnalysis(params) { + const url = 'https://amplitude.com/api/2/retention'; + const queryParams = new URLSearchParams({ + se: JSON.stringify(params.start_event || {}), + re: JSON.stringify(params.return_event || {}), + start: params.start, + end: params.end, + ...params + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get user composition + * @param {Object} params - Composition parameters + * @returns {Promise} User composition data + */ + async getUserComposition(params) { + const url = 'https://amplitude.com/api/2/composition'; + const queryParams = new URLSearchParams({ + start: params.start, + end: params.end, + p: params.property, + ...params + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Search users + * @param {string} query - Search query + * @returns {Promise} User search results + */ + async searchUsers(query) { + const url = 'https://amplitude.com/api/2/usersearch'; + const queryParams = new URLSearchParams({ user: query }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get cohorts + * @returns {Promise} List of cohorts + */ + async getCohorts() { + const url = 'https://amplitude.com/api/3/cohorts'; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get cohort members + * @param {string} cohortId - Cohort ID + * @param {Object} params - Query parameters + * @returns {Promise} Cohort members + */ + async getCohortMembers(cohortId, params = {}) { + const url = `https://amplitude.com/api/5/cohorts/${cohortId}`; + const queryParams = new URLSearchParams({ + props: params.props || 0, + propKeys: params.propKeys || [], + ...params + }).toString(); + + return this._get(`${url}?${queryParams}`, { + headers: this._getAuthHeaders() + }); + } + + /** + * Delete user data (GDPR) + * @param {Object} params - Deletion parameters + * @returns {Promise} Deletion job + */ + async deleteUserData(params) { + const url = 'https://amplitude.com/api/2/deletions/users'; + const data = { + user_ids: params.user_ids || [], + requester: params.requester + }; + + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Get charts + * @returns {Promise} List of charts + */ + async getCharts() { + const url = 'https://amplitude.com/api/3/chart/list'; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get chart data + * @param {string} chartId - Chart ID + * @returns {Promise} Chart data + */ + async getChartData(chartId) { + const url = `https://amplitude.com/api/3/chart/${chartId}/query`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get annotations + * @returns {Promise} List of annotations + */ + async getAnnotations() { + const url = 'https://amplitude.com/api/2/annotations'; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Create annotation + * @param {Object} annotation - Annotation data + * @returns {Promise} Created annotation + */ + async createAnnotation(annotation) { + const url = 'https://amplitude.com/api/2/annotations'; + return this._post(url, annotation, { headers: this._getAuthHeaders() }); + } + + /** + * Update annotation + * @param {string} annotationId - Annotation ID + * @param {Object} annotation - Updated annotation data + * @returns {Promise} Updated annotation + */ + async updateAnnotation(annotationId, annotation) { + const url = `https://amplitude.com/api/2/annotations/${annotationId}`; + return this._put(url, annotation, { headers: this._getAuthHeaders() }); + } + + /** + * Delete annotation + * @param {string} annotationId - Annotation ID + * @returns {Promise} Deletion response + */ + async deleteAnnotation(annotationId) { + const url = `https://amplitude.com/api/2/annotations/${annotationId}`; + return this._delete(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get releases + * @returns {Promise} List of releases + */ + async getReleases() { + const url = 'https://amplitude.com/api/2/release'; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Create release + * @param {Object} release - Release data + * @returns {Promise} Created release + */ + async createRelease(release) { + const url = 'https://amplitude.com/api/2/release'; + return this._post(url, release, { headers: this._getAuthHeaders() }); + } + + /** + * Helper to format event for common patterns + * @param {string} userId - User ID + * @param {string} eventType - Event type + * @param {Object} properties - Event properties + * @returns {Object} Formatted event + */ + formatEvent(userId, eventType, properties = {}) { + return { + user_id: userId, + event_type: eventType, + event_properties: properties, + time: Date.now() + }; + } +} + +module.exports = AmplitudeApi; \ No newline at end of file diff --git a/packages/amplitude/defaultConfig.json b/packages/amplitude/defaultConfig.json new file mode 100644 index 0000000..e5b7efd --- /dev/null +++ b/packages/amplitude/defaultConfig.json @@ -0,0 +1,113 @@ +{ + "name": "Amplitude", + "version": "1.0.0", + "category": "Analytics", + "type": "amplitude", + "description": "Product analytics platform for understanding user behavior", + "documentation": "https://amplitude.com/docs", + "apiDocs": "https://www.docs.developers.amplitude.com/analytics/apis/", + "authentication": { + "type": "apiKey", + "description": "API Key for ingestion, Secret Key for export", + "fields": [ + { + "name": "apiKey", + "description": "Public API key for event tracking" + }, + { + "name": "secretKey", + "description": "Secret key for data export and management" + } + ] + }, + "endpoints": { + "ingestion": "https://api2.amplitude.com", + "management": "https://amplitude.com/api", + "export": "https://amplitude.com/api/2/export", + "euIngestion": "https://api.eu.amplitude.com" + }, + "features": [ + "Event tracking", + "User identification", + "Group analytics", + "Revenue tracking", + "Funnel analysis", + "Retention analysis", + "User segmentation", + "Cohort analysis", + "Data export", + "Real-time analytics", + "Custom charts", + "Annotations", + "A/B testing support", + "Machine learning predictions" + ], + "limitations": [ + "1000 events per second per device", + "1000 events per batch maximum", + "Event properties limited to 1000 keys", + "Property values limited to 1024 characters", + "4 concurrent export requests" + ], + "pricing": "Free tier up to 10M events/month, paid plans for higher volume", + "rateLimits": { + "ingestion": "1000 events/second per device", + "batchSize": "1000 events", + "export": "4 concurrent requests", + "dashboard": "360 requests/hour", + "userActivity": "360 requests/hour" + }, + "supportedRegions": [ + "US", + "EU" + ], + "dataRetention": { + "free": "Last 3 months", + "growth": "All historical data", + "enterprise": "Custom retention" + }, + "webhooks": false, + "integrations": [ + "Segment", + "Braze", + "Salesforce", + "Slack", + "Zendesk", + "HubSpot", + "Intercom", + "AWS S3", + "Google Cloud Storage", + "Snowflake", + "BigQuery", + "Redshift" + ], + "sdks": [ + "JavaScript", + "iOS", + "Android", + "React Native", + "Python", + "Node.js", + "Java", + "Go", + "Ruby", + "PHP", + "Unity", + "Unreal", + "Flutter" + ], + "compliance": [ + "GDPR", + "CCPA", + "SOC 2 Type II", + "ISO 27001", + "HIPAA (Enterprise)" + ], + "eventLimits": { + "maxEventTypes": "2000 per project", + "maxEventProperties": "1000 per event", + "maxPropertyKeyLength": "40 characters", + "maxPropertyValueLength": "1024 characters", + "maxUserPropertyUpdates": "unlimited" + } +} \ No newline at end of file diff --git a/packages/amplitude/definition.js b/packages/amplitude/definition.js new file mode 100644 index 0000000..9b8bd23 --- /dev/null +++ b/packages/amplitude/definition.js @@ -0,0 +1,223 @@ +const { Integration } = require('@friggframework/module-plugin'); +const ApiClass = require('./api'); + +class AmplitudeIntegration extends Integration { + static name = 'Amplitude'; + static category = 'Analytics'; + static catalogDescription = 'Product analytics platform for understanding user behavior'; + static version = '1.0.0'; + static referenceUrl = 'https://amplitude.com'; + static apiDocs = 'https://www.docs.developers.amplitude.com/analytics/apis/'; + + /** + * Constructor for AmplitudeIntegration + * @param {Object} params - Should include apiKey and secretKey + */ + constructor(params) { + super(params); + this.api = new ApiClass(params); + } + + /** + * Track an event + * @param {Object} event - Event data + * @returns {Promise} Response + */ + async trackEvent(event) { + return this.api.trackEvent(event); + } + + /** + * Track multiple events + * @param {Array} events - Array of events + * @returns {Promise} Response + */ + async trackEvents(events) { + return this.api.trackEvents(events); + } + + /** + * Identify user with properties + * @param {Object} identify - Identify data + * @returns {Promise} Response + */ + async identify(identify) { + return this.api.identify(identify); + } + + /** + * Set user properties + * @param {Object} userProperties - User properties + * @returns {Promise} Response + */ + async setUserProperties(userProperties) { + return this.api.setUserProperties(userProperties); + } + + /** + * Set group properties + * @param {Object} groupProperties - Group properties + * @returns {Promise} Response + */ + async setGroupProperties(groupProperties) { + return this.api.setGroupProperties(groupProperties); + } + + /** + * Track revenue + * @param {Object} revenue - Revenue data + * @returns {Promise} Response + */ + async trackRevenue(revenue) { + return this.api.trackRevenue(revenue); + } + + /** + * Export project data + * @param {Object} params - Export parameters + * @returns {Promise} Export data + */ + async exportData(params) { + return this.api.exportData(params); + } + + /** + * Get user activity + * @param {string} userId - User ID + * @param {Object} params - Query parameters + * @returns {Promise} User activity + */ + async getUserActivity(userId, params = {}) { + return this.api.getUserActivity(userId, params); + } + + /** + * Get event segmentation + * @param {Object} params - Segmentation parameters + * @returns {Promise} Segmentation data + */ + async getEventSegmentation(params) { + return this.api.getEventSegmentation(params); + } + + /** + * Get funnel analysis + * @param {Object} params - Funnel parameters + * @returns {Promise} Funnel data + */ + async getFunnelAnalysis(params) { + return this.api.getFunnelAnalysis(params); + } + + /** + * Get retention analysis + * @param {Object} params - Retention parameters + * @returns {Promise} Retention data + */ + async getRetentionAnalysis(params) { + return this.api.getRetentionAnalysis(params); + } + + /** + * Get user composition + * @param {Object} params - Composition parameters + * @returns {Promise} User composition data + */ + async getUserComposition(params) { + return this.api.getUserComposition(params); + } + + /** + * Search users + * @param {string} query - Search query + * @returns {Promise} User search results + */ + async searchUsers(query) { + return this.api.searchUsers(query); + } + + /** + * Get cohorts + * @returns {Promise} List of cohorts + */ + async getCohorts() { + return this.api.getCohorts(); + } + + /** + * Get cohort members + * @param {string} cohortId - Cohort ID + * @param {Object} params - Query parameters + * @returns {Promise} Cohort members + */ + async getCohortMembers(cohortId, params = {}) { + return this.api.getCohortMembers(cohortId, params); + } + + /** + * Delete user data (GDPR) + * @param {Object} params - Deletion parameters + * @returns {Promise} Deletion job + */ + async deleteUserData(params) { + return this.api.deleteUserData(params); + } + + /** + * Get charts + * @returns {Promise} List of charts + */ + async getCharts() { + return this.api.getCharts(); + } + + /** + * Get chart data + * @param {string} chartId - Chart ID + * @returns {Promise} Chart data + */ + async getChartData(chartId) { + return this.api.getChartData(chartId); + } + + /** + * Get annotations + * @returns {Promise} List of annotations + */ + async getAnnotations() { + return this.api.getAnnotations(); + } + + /** + * Create annotation + * @param {Object} annotation - Annotation data + * @returns {Promise} Created annotation + */ + async createAnnotation(annotation) { + return this.api.createAnnotation(annotation); + } + + /** + * Test authentication + * @returns {Promise} Test result + */ + async testAuth() { + try { + // Try to get cohorts as a simple auth test + await this.getCohorts(); + + return { + success: true, + message: 'Authentication successful' + }; + } catch (error) { + return { + success: false, + message: `Authentication failed: ${error.message}`, + error: error + }; + } + } +} + +module.exports = AmplitudeIntegration; \ No newline at end of file diff --git a/packages/amplitude/index.js b/packages/amplitude/index.js new file mode 100644 index 0000000..2f5c456 --- /dev/null +++ b/packages/amplitude/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/anthropic/README.md b/packages/anthropic/README.md new file mode 100644 index 0000000..ca7e39d --- /dev/null +++ b/packages/anthropic/README.md @@ -0,0 +1,256 @@ +# Anthropic API Module + +This module provides a complete interface to Anthropic's Claude API, featuring advanced language understanding and generation capabilities. + +## Features + +- **Messages API**: Modern conversation interface with Claude 3 models +- **Streaming Support**: Real-time streaming responses +- **Vision Capabilities**: Process images with Claude 3 models +- **System Prompts**: Set context and behavior for conversations +- **Long Context**: Up to 200K tokens context window +- **Safety Features**: Built-in content filtering and safety measures + +## Authentication + +Anthropic uses API key authentication with the x-api-key header. You'll need to: + +1. Sign up at [console.anthropic.com](https://console.anthropic.com) +2. Generate an API key from your account settings +3. Set the following environment variables: + +```bash +ANTHROPIC_API_KEY=your_api_key_here +ANTHROPIC_VERSION=2023-06-01 # Optional, defaults to latest +``` + +## Usage Examples + +### Basic Message + +```javascript +const {Api} = require('./api'); + +const api = new Api({ + apiKey: process.env.ANTHROPIC_API_KEY +}); + +// Simple message +const response = await api.createMessage({ + model: "claude-3-opus-20240229", + max_tokens: 1024, + messages: [ + { + role: "user", + content: "What is the capital of France?" + } + ] +}); +``` + +### System Prompts + +```javascript +const response = await api.createMessage({ + model: "claude-3-sonnet-20240229", + max_tokens: 2048, + system: "You are a helpful assistant that speaks like Shakespeare.", + messages: [ + { + role: "user", + content: "Tell me about artificial intelligence" + } + ] +}); +``` + +### Streaming Responses + +```javascript +const stream = await api.createMessageStream({ + model: "claude-3-haiku-20240307", + max_tokens: 1024, + messages: [ + { + role: "user", + content: "Write a short story about a robot" + } + ], + stream: true +}); + +// Process stream chunks +for await (const chunk of stream) { + const events = api.parseStreamChunk(chunk); + for (const event of events) { + if (event.type === 'content_block_delta') { + process.stdout.write(event.delta.text); + } + } +} +``` + +### Vision Capabilities + +```javascript +// Analyze an image +const response = await api.createVisionMessage({ + model: "claude-3-opus-20240229", + max_tokens: 1024, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "What's in this image?" + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: base64ImageData + } + } + ] + } + ] +}); +``` + +### Multi-turn Conversations + +```javascript +const conversation = [ + { + role: "user", + content: "Hi, I'm learning about quantum computing" + }, + { + role: "assistant", + content: "That's fascinating! Quantum computing is a revolutionary field. What aspects interest you most?" + }, + { + role: "user", + content: "I'm curious about quantum entanglement" + } +]; + +const response = await api.createMessage({ + model: "claude-3-sonnet-20240229", + max_tokens: 2048, + messages: conversation +}); +``` + +### Temperature and Sampling + +```javascript +// Creative writing with high temperature +const creative = await api.createMessage({ + model: "claude-3-opus-20240229", + max_tokens: 2048, + temperature: 0.9, + messages: [ + { + role: "user", + content: "Write a creative story opening" + } + ] +}); + +// Deterministic output with low temperature +const factual = await api.createMessage({ + model: "claude-3-sonnet-20240229", + max_tokens: 1024, + temperature: 0, + messages: [ + { + role: "user", + content: "List the planets in our solar system" + } + ] +}); +``` + +## Available Models + +### Claude 3 Family +- **claude-3-opus-20240229**: Most capable model for complex tasks +- **claude-3-sonnet-20240229**: Balanced performance and speed +- **claude-3-haiku-20240307**: Fastest model for simple tasks + +### Previous Versions +- **claude-2.1**: Extended context window (200K tokens) +- **claude-2.0**: Previous generation model +- **claude-instant-1.2**: Fast, cost-effective model + +## Token Management + +```javascript +// Estimate tokens in text +const tokenCount = api.estimateTokens("Your text here"); + +// Get model limits +const maxTokens = api.getMaxTokens("claude-3-opus-20240229"); +const maxOutput = api.getMaxOutputTokens("claude-3-opus-20240229"); +``` + +## Message Formatting + +The module includes helpers for proper message formatting: + +```javascript +// Automatically format messages for Claude's requirements +const response = await api.createFormattedMessage({ + model: "claude-3-sonnet-20240229", + max_tokens: 1024, + messages: [ + { role: "system", content: "You are helpful" }, // Converted properly + { role: "user", content: "Hello" }, + { role: "user", content: "How are you?" }, // Consecutive messages handled + ] +}); +``` + +## API Methods + +### Messages +- `createMessage(params)` - Send a message to Claude +- `createMessageStream(params)` - Stream a response from Claude +- `createFormattedMessage(params)` - Create message with auto-formatting +- `createVisionMessage(params)` - Send message with image content + +### Legacy Completions +- `createCompletion(params)` - Use legacy completion endpoint +- `createCompletionStream(params)` - Stream legacy completions + +### Utilities +- `testAuth()` - Verify API key validity +- `estimateTokens(text)` - Estimate token count +- `getModelInfo(model)` - Get model specifications +- `getMaxTokens(model)` - Get model's max context tokens +- `getMaxOutputTokens(model)` - Get model's max output tokens +- `formatMessages(messages)` - Format messages for Claude +- `parseStreamChunk(chunk)` - Parse streaming response chunks + +## Best Practices + +1. **Use System Prompts**: Set clear instructions via the `system` parameter +2. **Manage Context**: Be mindful of token limits, especially with long conversations +3. **Choose the Right Model**: Use Opus for complex reasoning, Haiku for speed +4. **Handle Streaming**: For long responses, use streaming to improve UX +5. **Error Handling**: Implement proper error handling for rate limits and API errors + +## Error Handling + +Common error scenarios: +- `401`: Invalid API key +- `429`: Rate limit exceeded +- `400`: Invalid request (check message format) +- `500`: Server error (retry with backoff) + +## Support + +For more information, visit [Anthropic's Documentation](https://docs.anthropic.com/claude/reference). \ No newline at end of file diff --git a/packages/anthropic/api.js b/packages/anthropic/api.js new file mode 100644 index 0000000..fc2af7a --- /dev/null +++ b/packages/anthropic/api.js @@ -0,0 +1,246 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.anthropic.com'; + this.anthropicVersion = get(params, 'anthropicVersion', '2023-06-01'); + + this.URLs = { + // Messages + messages: '/v1/messages', + + // Completions (Legacy) + complete: '/v1/complete', + }; + + this.modelInfo = { + 'claude-3-opus-20240229': { maxTokens: 200000, outputMax: 4096 }, + 'claude-3-sonnet-20240229': { maxTokens: 200000, outputMax: 4096 }, + 'claude-3-haiku-20240307': { maxTokens: 200000, outputMax: 4096 }, + 'claude-2.1': { maxTokens: 200000, outputMax: 4096 }, + 'claude-2.0': { maxTokens: 100000, outputMax: 4096 }, + 'claude-instant-1.2': { maxTokens: 100000, outputMax: 4096 }, + }; + } + + async addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'x-api-key': this.apiKey, + 'anthropic-version': this.anthropicVersion, + 'Content-Type': 'application/json', + }; + } + + async _get(options) { + await this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + await this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + await this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + await this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + await this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Messages API ********************************** + + async createMessage(params) { + const options = { + url: this.baseUrl + this.URLs.messages, + body: params, + }; + + return this._post(options); + } + + async createMessageStream(params) { + const options = { + url: this.baseUrl + this.URLs.messages, + body: { ...params, stream: true }, + headers: { + 'Accept': 'text/event-stream', + }, + }; + + // Return the raw response for streaming + const response = await this._post(options); + return response; + } + + // ************************** Legacy Completions ********************************** + + async createCompletion(params) { + const options = { + url: this.baseUrl + this.URLs.complete, + body: params, + }; + + return this._post(options); + } + + async createCompletionStream(params) { + const options = { + url: this.baseUrl + this.URLs.complete, + body: { ...params, stream: true }, + headers: { + 'Accept': 'text/event-stream', + }, + }; + + const response = await this._post(options); + return response; + } + + // ************************** Utility Methods ********************************** + + async testAuth() { + try { + // Test with a minimal message + const response = await this.createMessage({ + model: 'claude-3-haiku-20240307', + max_tokens: 10, + messages: [ + { + role: 'user', + content: 'Hi' + } + ] + }); + return !!response; + } catch (error) { + if (error.status === 401) { + return false; + } + throw error; + } + } + + // Token counting for Claude models + estimateTokens(text) { + // Claude uses a similar tokenization to GPT models + // Rough estimation: ~4 characters per token + return Math.ceil(text.length / 4); + } + + getModelInfo(model) { + return this.modelInfo[model] || { maxTokens: 100000, outputMax: 4096 }; + } + + getMaxTokens(model) { + const info = this.getModelInfo(model); + return info.maxTokens; + } + + getMaxOutputTokens(model) { + const info = this.getModelInfo(model); + return info.outputMax; + } + + // Format messages for Claude's expected format + formatMessages(messages) { + // Ensure messages alternate between user and assistant + const formatted = []; + let lastRole = null; + + for (const message of messages) { + if (message.role === 'system') { + // Claude doesn't have a system role, prepend to first user message + if (formatted.length === 0) { + formatted.push({ + role: 'user', + content: `${message.content}\n\nUser: ` + }); + lastRole = 'user'; + } + } else if (message.role === lastRole) { + // Combine consecutive messages from same role + formatted[formatted.length - 1].content += '\n' + message.content; + } else { + formatted.push(message); + lastRole = message.role; + } + } + + // Ensure conversation starts with user + if (formatted.length > 0 && formatted[0].role !== 'user') { + formatted.unshift({ role: 'user', content: 'Hello' }); + } + + // Ensure conversation alternates properly + const final = []; + for (let i = 0; i < formatted.length; i++) { + const expectedRole = i % 2 === 0 ? 'user' : 'assistant'; + if (formatted[i].role !== expectedRole) { + if (expectedRole === 'user') { + final.push({ role: 'user', content: 'Continue' }); + } else { + final.push({ role: 'assistant', content: 'I understand.' }); + } + } + final.push(formatted[i]); + } + + return final; + } + + // Helper to create a message with proper formatting + async createFormattedMessage(params) { + if (params.messages) { + params.messages = this.formatMessages(params.messages); + } + return this.createMessage(params); + } + + // Helper for vision capabilities + async createVisionMessage(params) { + // Claude 3 models support vision + const visionModels = ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307']; + + if (!visionModels.includes(params.model)) { + throw new Error(`Model ${params.model} does not support vision. Use one of: ${visionModels.join(', ')}`); + } + + return this.createMessage(params); + } + + // Parse streaming response + parseStreamChunk(chunk) { + const lines = chunk.split('\n').filter(line => line.trim()); + const events = []; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + events.push({ type: 'done' }); + } else { + try { + events.push(JSON.parse(data)); + } catch (e) { + console.error('Failed to parse stream chunk:', e); + } + } + } + } + + return events; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/anthropic/defaultConfig.json b/packages/anthropic/defaultConfig.json new file mode 100644 index 0000000..e89c7df --- /dev/null +++ b/packages/anthropic/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "anthropic", + "label": "Anthropic", + "productUrl": "https://anthropic.com", + "apiDocs": "https://docs.anthropic.com/claude/reference", + "logoUrl": "https://friggframework.org/assets/img/anthropic-icon.png", + "categories": [ + "AI/ML", + "Large Language Models", + "Text Generation", + "AI Safety" + ], + "description": "Anthropic provides access to Claude, a next-generation AI assistant focused on being helpful, harmless, and honest." +} \ No newline at end of file diff --git a/packages/anthropic/definition.js b/packages/anthropic/definition.js new file mode 100644 index 0000000..a80f6cc --- /dev/null +++ b/packages/anthropic/definition.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Anthropic', + requiredAuthMethods: { + getToken: async function (api, params) { + // Anthropic uses API keys, not OAuth + const apiKey = get(params.data, 'apiKey'); + if (!apiKey) { + throw new Error('API Key is required for Anthropic authentication'); + } + return { apiKey }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + // Anthropic doesn't have user accounts via API, so we use a generic identifier + return { + identifiers: {externalId: 'anthropic-user', user: userId}, + details: {name: 'Anthropic API User', apiKey: tokenResponse.apiKey}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'apiKey', 'anthropicVersion' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + // Test the API key by making a simple request + const isValid = await api.testAuth(); + return { + identifiers: {externalId: 'anthropic-api', user: userId}, + details: { apiKeyValid: isValid } + }; + }, + testAuthRequest: async function (api) { + return api.testAuth() + }, + }, + env: { + apiKey: process.env.ANTHROPIC_API_KEY, + anthropicVersion: process.env.ANTHROPIC_VERSION || '2023-06-01', + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/anthropic/index.js b/packages/anthropic/index.js new file mode 100644 index 0000000..18a6c30 --- /dev/null +++ b/packages/anthropic/index.js @@ -0,0 +1,3 @@ +const {Definition} = require('./definition'); + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/ape-mobile-now-damstra-samm/README.md b/packages/ape-mobile-now-damstra-samm/README.md new file mode 100644 index 0000000..60b6999 --- /dev/null +++ b/packages/ape-mobile-now-damstra-samm/README.md @@ -0,0 +1,34 @@ +# APE Mobile (now Damstra Samm) API Integration + +APE Mobile (now Damstra Samm) integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/ape-mobile-now-damstra-samm +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/ape-mobile-now-damstra-samm'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `APE_MOBILE_NOW_DAMSTRA_SAMM_CLIENT_ID` +- `APE_MOBILE_NOW_DAMSTRA_SAMM_CLIENT_SECRET` +- `APE_MOBILE_NOW_DAMSTRA_SAMM_SCOPE` + +## API Documentation + +For more information about the APE Mobile (now Damstra Samm) API, visit: https://api.damstratechnology.com diff --git a/packages/ape-mobile-now-damstra-samm/api.js b/packages/ape-mobile-now-damstra-samm/api.js new file mode 100644 index 0000000..16ca7e4 --- /dev/null +++ b/packages/ape-mobile-now-damstra-samm/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class APEMobilenowDamstraSammApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.damstratechnology.com'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: APEMobilenowDamstraSammApi}; diff --git a/packages/ape-mobile-now-damstra-samm/defaultConfig.json b/packages/ape-mobile-now-damstra-samm/defaultConfig.json new file mode 100644 index 0000000..41e2fff --- /dev/null +++ b/packages/ape-mobile-now-damstra-samm/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "APE Mobile (now Damstra Samm)", + "moduleName": "ape-mobile-now-damstra-samm", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "APE Mobile (now Damstra Samm) API Integration Module", + "category": "Productivity", + "apiDocUrl": "https://api.damstratechnology.com", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/ape-mobile-now-damstra-samm/definition.js b/packages/ape-mobile-now-damstra-samm/definition.js new file mode 100644 index 0000000..624da85 --- /dev/null +++ b/packages/ape-mobile-now-damstra-samm/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'APE Mobile (now Damstra Samm)', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.APE_MOBILE_NOW_DAMSTRA_SAMM_CLIENT_ID, + client_secret: process.env.APE_MOBILE_NOW_DAMSTRA_SAMM_CLIENT_SECRET, + scope: process.env.APE_MOBILE_NOW_DAMSTRA_SAMM_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/ape-mobile-now-damstra-samm`, + } +}; + +module.exports = {Definition}; diff --git a/packages/ape-mobile-now-damstra-samm/index.js b/packages/ape-mobile-now-damstra-samm/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/ape-mobile-now-damstra-samm/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/ape-mobile-now-damstra-samm/package.json b/packages/ape-mobile-now-damstra-samm/package.json new file mode 100644 index 0000000..d6d60c3 --- /dev/null +++ b/packages/ape-mobile-now-damstra-samm/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/ape-mobile-now-damstra-samm", + "version": "0.0.1", + "description": "APE Mobile (now Damstra Samm) API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "ape-mobile-now-damstra-samm", + "productivity" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/applicantstack/README.md b/packages/applicantstack/README.md new file mode 100644 index 0000000..976491b --- /dev/null +++ b/packages/applicantstack/README.md @@ -0,0 +1,43 @@ +# ApplicantStack API Module + +This module provides integration with the ApplicantStack API for the Frigg Framework. + +## Description + +ApplicantStack is an applicant tracking system for small to medium-sized businesses. + +## Installation + +```bash +npm install @friggframework/api-module-applicantstack +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-applicantstack'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +APPLICANTSTACK_CLIENT_ID=your_client_id +APPLICANTSTACK_CLIENT_SECRET=your_client_secret +APPLICANTSTACK_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/applicantstack/api.js b/packages/applicantstack/api.js new file mode 100644 index 0000000..2fa0df2 --- /dev/null +++ b/packages/applicantstack/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.applicantstack.com/v1'; + + this.URLs = { + // User/Account info + userInfo: '/jobs', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://app.applicantstack.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.applicantstack.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to ApplicantStack +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/applicantstack/defaultConfig.json b/packages/applicantstack/defaultConfig.json new file mode 100644 index 0000000..167b97d --- /dev/null +++ b/packages/applicantstack/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "applicantstack", + "label": "ApplicantStack", + "productUrl": "https://applicantstack.com/", + "apiDocs": "https://developers.applicantstack.com/", + "logoUrl": "https://friggframework.org/assets/img/applicantstack-icon.png", + "categories": [ + "HR" + ], + "subCategories": [ + "HR Talent & Recruitment" + ], + "description": "ApplicantStack is an applicant tracking system for small to medium-sized businesses." +} \ No newline at end of file diff --git a/packages/applicantstack/definition.js b/packages/applicantstack/definition.js new file mode 100644 index 0000000..77c6260 --- /dev/null +++ b/packages/applicantstack/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'ApplicantStack', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'applicantstack-account', user: userId}, + details: {name: userInfo.name || 'ApplicantStack Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'applicantstack-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.APPLICANTSTACK_CLIENT_ID, + client_secret: process.env.APPLICANTSTACK_CLIENT_SECRET, + scope: process.env.APPLICANTSTACK_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/applicantstack`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/applicantstack/index.js b/packages/applicantstack/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/applicantstack/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/applicantstack/jest-setup.js b/packages/applicantstack/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/applicantstack/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/applicantstack/jest-teardown.js b/packages/applicantstack/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/applicantstack/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/applicantstack/jest.config.js b/packages/applicantstack/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/applicantstack/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/applicantstack/package.json b/packages/applicantstack/package.json new file mode 100644 index 0000000..88dfbca --- /dev/null +++ b/packages/applicantstack/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-applicantstack", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "ApplicantStack API module that lets the Frigg Framework interact with ApplicantStack", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/applicantstack/test/api.test.js b/packages/applicantstack/test/api.test.js new file mode 100644 index 0000000..cf2eb19 --- /dev/null +++ b/packages/applicantstack/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('ApplicantStack API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/applicantstack/test/definition.test.js b/packages/applicantstack/test/definition.test.js new file mode 100644 index 0000000..9b717a4 --- /dev/null +++ b/packages/applicantstack/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('ApplicantStack Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('applicantstack'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/arthur-online/README.md b/packages/arthur-online/README.md new file mode 100644 index 0000000..7783c3d --- /dev/null +++ b/packages/arthur-online/README.md @@ -0,0 +1,42 @@ +# Arthur Online API Module + +Frigg API module for Arthur Online integration. + +## Installation + +```bash +npm install @friggframework/arthur-online +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/arthur-online'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +ARTHUR_ONLINE_CLIENT_ID=your_client_id +ARTHUR_ONLINE_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/arthur-online/api.js b/packages/arthur-online/api.js new file mode 100644 index 0000000..230cc11 --- /dev/null +++ b/packages/arthur-online/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.arthuronline.co.uk'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.arthuronline.co.uk/oauth/authorize'; + this.accessTokenUri = 'https://api.arthuronline.co.uk/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Arthur Online', + MODULE_NAME: 'arthur-online', + CATEGORY: 'CRM', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/arthur-online/defaultConfig.json b/packages/arthur-online/defaultConfig.json new file mode 100644 index 0000000..beb634c --- /dev/null +++ b/packages/arthur-online/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Arthur Online", + "moduleName": "arthur-online", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Arthur Online API Integration Module", + "category": "CRM", + "apiDocUrl": "https://api.arthuronline.co.uk/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/arthur-online/definition.js b/packages/arthur-online/definition.js new file mode 100644 index 0000000..0334983 --- /dev/null +++ b/packages/arthur-online/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'ArthurOnline', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.ARTHUR_ONLINE_CLIENT_ID, + client_secret: process.env.ARTHUR_ONLINE_CLIENT_SECRET, + scope: process.env.ARTHUR_ONLINE_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/arthur-online`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/arthur-online/index.js b/packages/arthur-online/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/arthur-online/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/arthur-online/package.json b/packages/arthur-online/package.json new file mode 100644 index 0000000..c19b63c --- /dev/null +++ b/packages/arthur-online/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/arthur-online", + "version": "0.0.1", + "description": "Arthur Online API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "arthur-online", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/v1-ready/asana/.env.example b/packages/asana/.env.example similarity index 100% rename from packages/v1-ready/asana/.env.example rename to packages/asana/.env.example diff --git a/packages/needs-updating/clyde/.eslintrc.json b/packages/asana/.eslintrc.json similarity index 100% rename from packages/needs-updating/clyde/.eslintrc.json rename to packages/asana/.eslintrc.json diff --git a/packages/v1-ready/asana/CHANGELOG.md b/packages/asana/CHANGELOG.md similarity index 100% rename from packages/v1-ready/asana/CHANGELOG.md rename to packages/asana/CHANGELOG.md diff --git a/packages/needs-updating/attentive/LICENSE.md b/packages/asana/LICENSE.md similarity index 100% rename from packages/needs-updating/attentive/LICENSE.md rename to packages/asana/LICENSE.md diff --git a/packages/v1-ready/asana/README.md b/packages/asana/README.md similarity index 100% rename from packages/v1-ready/asana/README.md rename to packages/asana/README.md diff --git a/packages/v1-ready/asana/api.js b/packages/asana/api.js similarity index 100% rename from packages/v1-ready/asana/api.js rename to packages/asana/api.js diff --git a/packages/v1-ready/asana/defaultConfig.json b/packages/asana/defaultConfig.json similarity index 100% rename from packages/v1-ready/asana/defaultConfig.json rename to packages/asana/defaultConfig.json diff --git a/packages/v1-ready/asana/definition.js b/packages/asana/definition.js similarity index 100% rename from packages/v1-ready/asana/definition.js rename to packages/asana/definition.js diff --git a/packages/v1-ready/asana/fenestra/platform.fenestra.yaml b/packages/asana/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/asana/fenestra/platform.fenestra.yaml rename to packages/asana/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/asana/fenestra/schemas/asana-validation.json b/packages/asana/fenestra/schemas/asana-validation.json similarity index 100% rename from packages/v1-ready/asana/fenestra/schemas/asana-validation.json rename to packages/asana/fenestra/schemas/asana-validation.json diff --git a/packages/asana/index.js b/packages/asana/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/asana/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/needs-updating/attentive/jest.config.js b/packages/asana/jest.config.js similarity index 100% rename from packages/needs-updating/attentive/jest.config.js rename to packages/asana/jest.config.js diff --git a/packages/v1-ready/asana/package.json b/packages/asana/package.json similarity index 100% rename from packages/v1-ready/asana/package.json rename to packages/asana/package.json diff --git a/packages/asana/specs/openapi.yaml b/packages/asana/specs/openapi.yaml new file mode 100644 index 0000000..1e3a9f9 --- /dev/null +++ b/packages/asana/specs/openapi.yaml @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e655f11b48eec1a6eb59fe055611da07964ae463bc3160a125e348a90a338e67 +size 2321545 diff --git a/packages/v1-ready/asana/tests/api.test.js b/packages/asana/tests/api.test.js similarity index 100% rename from packages/v1-ready/asana/tests/api.test.js rename to packages/asana/tests/api.test.js diff --git a/packages/v1-ready/asana/tests/auther.test.js b/packages/asana/tests/auther.test.js similarity index 100% rename from packages/v1-ready/asana/tests/auther.test.js rename to packages/asana/tests/auther.test.js diff --git a/packages/needs-updating/fastspring-iq/.eslintrc.json b/packages/attentive/.eslintrc.json similarity index 100% rename from packages/needs-updating/fastspring-iq/.eslintrc.json rename to packages/attentive/.eslintrc.json diff --git a/packages/needs-updating/attentive/CHANGELOG.md b/packages/attentive/CHANGELOG.md similarity index 100% rename from packages/needs-updating/attentive/CHANGELOG.md rename to packages/attentive/CHANGELOG.md diff --git a/packages/needs-updating/clyde/LICENSE.md b/packages/attentive/LICENSE.md similarity index 100% rename from packages/needs-updating/clyde/LICENSE.md rename to packages/attentive/LICENSE.md diff --git a/packages/needs-updating/attentive/README.md b/packages/attentive/README.md similarity index 100% rename from packages/needs-updating/attentive/README.md rename to packages/attentive/README.md diff --git a/packages/needs-updating/attentive/api.js b/packages/attentive/api.js similarity index 100% rename from packages/needs-updating/attentive/api.js rename to packages/attentive/api.js diff --git a/packages/needs-updating/attentive/api.test.js b/packages/attentive/api.test.js similarity index 100% rename from packages/needs-updating/attentive/api.test.js rename to packages/attentive/api.test.js diff --git a/packages/needs-updating/attentive/defaultConfig.json b/packages/attentive/defaultConfig.json similarity index 100% rename from packages/needs-updating/attentive/defaultConfig.json rename to packages/attentive/defaultConfig.json diff --git a/packages/attentive/definition.js b/packages/attentive/definition.js new file mode 100644 index 0000000..c680f39 --- /dev/null +++ b/packages/attentive/definition.js @@ -0,0 +1,126 @@ +const { IntegrationBase, get } = require('@friggframework/core'); +const { Api } = require('./api'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); + +class AttentiveIntegration extends IntegrationBase { + static Definition = { + name: 'attentive', + version: '1.0.0', + display: { + label: 'AttentiveMobile', + description: 'Attentive Mobile', + imageURL: 'https://friggframework.org/assets/img/attentive-icon.png', + icon: '', + category: 'SMS', + }, + modules: { + api: Api, + credential: Credential, + entity: Entity, + }, + }; + + async getAuthorizationRequirements() { + return { + url: this.api.authorizationUri, + type: 'oauth2', + }; + } + + async processAuthorizationCallback(params) { + const code = get(params.data, 'code'); + const response = await this.api.getTokenFromCode(code); + const userDetails = await this.api.getTokenIdentity(); + + let credentials = await this.credentialMO.list({user: this.userId}); + + if (credentials.length === 0) { + throw new Error('Credential failed to create'); + } + if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } + + let entity = await this.entityMO.getByUserId(this.userId); + + if (!entity) { + entity = await this.entityMO.create({ + user: this.userId, + credential: credentials[0]._id, + externalId: userDetails.companyId, + name: userDetails.companyName, + domain: userDetails.attentiveDomainName, + }); + } + + return { + credential_id: credentials[0]._id, + entity_id: entity._id, + type: AttentiveIntegration.Definition.name, + }; + } + + async testAuth() { + await this.api.getTokenIdentity(); + } + + async receiveNotification(notifier, delegateString, object = null) { + if (notifier instanceof Api) { + if (delegateString === this.api.DLGT_TOKEN_UPDATE) { + const updatedToken = { + user: this.userId, + access_token: this.api.access_token, + id_token: this.api.id_token, + // expires_in: this.api.accessExpiresIn, + auth_is_valid: true, + }; + + Object.keys(updatedToken).forEach( + (k) => updatedToken[k] === null && delete updatedToken[k] + ); + + let credential = await this.entityMO.getByUserId(this.userId); + + if (!credential) { + credential = await this.credentialMO.create(updatedToken); + } else { + credential = await this.credentialMO.update( + credential, + updatedToken + ); + } + } + if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { + await this.deauthorize(); + } + } + } + + async deauthorize() { + // wipe api connection + this.api = new Api(); + + // delete credentials from the database + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async getApiObject() { + const attentiveParams = {delegate: this}; + + if (this.credential) { + attentiveParams.access_token = this.credential.access_token; + attentiveParams.id_token = this.credential.id_token; + attentiveParams.expires_in = this.credential.accessExpiresIn; + } + + return new Api(attentiveParams); + } +} + +module.exports = AttentiveIntegration; \ No newline at end of file diff --git a/packages/attentive/index.js b/packages/attentive/index.js new file mode 100644 index 0000000..a0eac7a --- /dev/null +++ b/packages/attentive/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; diff --git a/packages/needs-updating/clyde/jest.config.js b/packages/attentive/jest.config.js similarity index 100% rename from packages/needs-updating/clyde/jest.config.js rename to packages/attentive/jest.config.js diff --git a/packages/needs-updating/attentive/manager.test.js b/packages/attentive/manager.test.js similarity index 100% rename from packages/needs-updating/attentive/manager.test.js rename to packages/attentive/manager.test.js diff --git a/packages/v1-ready/attio/.env.example b/packages/attio/.env.example similarity index 100% rename from packages/v1-ready/attio/.env.example rename to packages/attio/.env.example diff --git a/packages/v1-ready/attio/README.md b/packages/attio/README.md similarity index 100% rename from packages/v1-ready/attio/README.md rename to packages/attio/README.md diff --git a/packages/v1-ready/attio/api.js b/packages/attio/api.js similarity index 100% rename from packages/v1-ready/attio/api.js rename to packages/attio/api.js diff --git a/packages/v1-ready/attio/defaultConfig.json b/packages/attio/defaultConfig.json similarity index 100% rename from packages/v1-ready/attio/defaultConfig.json rename to packages/attio/defaultConfig.json diff --git a/packages/v1-ready/attio/definition.js b/packages/attio/definition.js similarity index 100% rename from packages/v1-ready/attio/definition.js rename to packages/attio/definition.js diff --git a/packages/v1-ready/attio/index.js b/packages/attio/index.js similarity index 100% rename from packages/v1-ready/attio/index.js rename to packages/attio/index.js diff --git a/packages/v1-ready/attio/package.json b/packages/attio/package.json similarity index 100% rename from packages/v1-ready/attio/package.json rename to packages/attio/package.json diff --git a/packages/auth0/.env.example b/packages/auth0/.env.example new file mode 100644 index 0000000..aab90db --- /dev/null +++ b/packages/auth0/.env.example @@ -0,0 +1,8 @@ +# AUTH0 API Configuration +AUTH0_CLIENT_ID=your_client_id_here +AUTH0_CLIENT_SECRET=your_client_secret_here +AUTH0_SCOPE=openid profile email +AUTH0_REDIRECT_URI=http://localhost:3000/oauth/callback +AUTH0_AUTH_URI=https://dev-example.auth0.com/authorize +AUTH0_TOKEN_URI=https://dev-example.auth0.com/oauth/token +AUTH0_AUDIENCE=https://dev-example.auth0.com/api/v2/ diff --git a/packages/auth0/README.md b/packages/auth0/README.md new file mode 100644 index 0000000..77fb621 --- /dev/null +++ b/packages/auth0/README.md @@ -0,0 +1,35 @@ +# Auth0 API Module + +Identity and access management + +## Installation + +```bash +npm install @friggframework/auth0 +``` + +## Configuration + +See `.env.example` for required environment variables. + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/auth0'); + +// Initialize API client +const api = new Api({ + // Add required credentials +}); + +// Test the connection +const result = await api.getCurrentUser(); +``` + +## Category + +Authentication + +## License + +MIT diff --git a/packages/auth0/api.js b/packages/auth0/api.js new file mode 100644 index 0000000..56f77c6 --- /dev/null +++ b/packages/auth0/api.js @@ -0,0 +1,78 @@ +const { OAuth2Requester, get, FriggError } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://dev-example.auth0.com'; + + this.URLs = { + userInfo: '/userinfo', + users: '/api/v2/users', + applications: '/api/v2/clients' + }; + + this.authorizationUri = process.env.AUTH0_AUTH_URI || `${this.baseUrl}/authorize`; + this.tokenUri = process.env.AUTH0_TOKEN_URI || `${this.baseUrl}/oauth/token`; + } + + static Definition = { + DISPLAY_NAME: 'Auth0', + MODULE_NAME: 'auth0', + CATEGORY: 'Authentication', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: (scopes || ['openid', 'profile', 'email']).join(' '), + audience: process.env.AUTH0_AUDIENCE + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.userInfo); + } + + async getUser(userId) { + return this.get(`${this.URLs.users}/${userId}`); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + async getApplications() { + return this.get(this.URLs.applications); + } +} + +module.exports = { Api }; diff --git a/packages/auth0/defaultConfig.json b/packages/auth0/defaultConfig.json new file mode 100644 index 0000000..f2df0b4 --- /dev/null +++ b/packages/auth0/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Auth0", + "moduleName": "auth0", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Auth0 API Integration Module", + "category": "Authentication", + "apiDocUrl": "https://auth0.com/docs/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/auth0/definition.js b/packages/auth0/definition.js new file mode 100644 index 0000000..984c340 --- /dev/null +++ b/packages/auth0/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Auth0', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AUTH0_CLIENT_ID, + client_secret: process.env.AUTH0_CLIENT_SECRET, + scope: process.env.AUTH0_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/auth0`, + } +}; + +module.exports = {Definition}; diff --git a/packages/auth0/index.js b/packages/auth0/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/auth0/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/auth0/package.json b/packages/auth0/package.json new file mode 100644 index 0000000..a5f7966 --- /dev/null +++ b/packages/auth0/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/auth0", + "version": "0.0.1", + "description": "Auth0 API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "auth0", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0" + } +} \ No newline at end of file diff --git a/packages/authentise/README.md b/packages/authentise/README.md new file mode 100644 index 0000000..c6088f3 --- /dev/null +++ b/packages/authentise/README.md @@ -0,0 +1,43 @@ +# Authentise API Module + +This module provides integration with the Authentise API for the Frigg Framework. + +## Description + +Authentise provides 3D printing workflow and manufacturing execution system solutions. + +## Installation + +```bash +npm install @friggframework/api-module-authentise +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-authentise'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +AUTHENTISE_CLIENT_ID=your_client_id +AUTHENTISE_CLIENT_SECRET=your_client_secret +AUTHENTISE_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/authentise/api.js b/packages/authentise/api.js new file mode 100644 index 0000000..4bf2ab7 --- /dev/null +++ b/packages/authentise/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.authentise.com/v2'; + + this.URLs = { + // User/Account info + userInfo: '/me', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://authentise.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://authentise.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to Authentise +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/authentise/defaultConfig.json b/packages/authentise/defaultConfig.json new file mode 100644 index 0000000..8a949c6 --- /dev/null +++ b/packages/authentise/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "authentise", + "label": "Authentise", + "productUrl": "https://authentise.com/", + "apiDocs": "https://developers.authentise.com/", + "logoUrl": "https://friggframework.org/assets/img/authentise-icon.png", + "categories": [ + "CRM" + ], + "subCategories": [ + "CRM (Customer Relationship Management)" + ], + "description": "Authentise provides 3D printing workflow and manufacturing execution system solutions." +} \ No newline at end of file diff --git a/packages/authentise/definition.js b/packages/authentise/definition.js new file mode 100644 index 0000000..08fc4cd --- /dev/null +++ b/packages/authentise/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Authentise', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'authentise-account', user: userId}, + details: {name: userInfo.name || 'Authentise Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'authentise-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.AUTHENTISE_CLIENT_ID, + client_secret: process.env.AUTHENTISE_CLIENT_SECRET, + scope: process.env.AUTHENTISE_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/authentise`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/authentise/index.js b/packages/authentise/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/authentise/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/authentise/jest-setup.js b/packages/authentise/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/authentise/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/authentise/jest-teardown.js b/packages/authentise/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/authentise/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/authentise/jest.config.js b/packages/authentise/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/authentise/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/authentise/package.json b/packages/authentise/package.json new file mode 100644 index 0000000..ead04e0 --- /dev/null +++ b/packages/authentise/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-authentise", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Authentise API module that lets the Frigg Framework interact with Authentise", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/authentise/test/api.test.js b/packages/authentise/test/api.test.js new file mode 100644 index 0000000..d394a49 --- /dev/null +++ b/packages/authentise/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('Authentise API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/authentise/test/definition.test.js b/packages/authentise/test/definition.test.js new file mode 100644 index 0000000..e6ef1bc --- /dev/null +++ b/packages/authentise/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Authentise Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('authentise'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/authorizenet/README.md b/packages/authorizenet/README.md new file mode 100644 index 0000000..ac3e05b --- /dev/null +++ b/packages/authorizenet/README.md @@ -0,0 +1,42 @@ +# Authorize.Net API Module + +Frigg API module for Authorize.Net integration. + +## Installation + +```bash +npm install @friggframework/authorizenet +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/authorizenet'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +AUTHORIZENET_CLIENT_ID=your_client_id +AUTHORIZENET_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/authorizenet/api.js b/packages/authorizenet/api.js new file mode 100644 index 0000000..d5adc1c --- /dev/null +++ b/packages/authorizenet/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.authorize.net'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.authorize.net/oauth/authorize'; + this.accessTokenUri = 'https://api.authorize.net/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Authorize.Net', + MODULE_NAME: 'authorizenet', + CATEGORY: 'Payment', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/authorizenet/defaultConfig.json b/packages/authorizenet/defaultConfig.json new file mode 100644 index 0000000..f123998 --- /dev/null +++ b/packages/authorizenet/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Authorize.Net", + "moduleName": "authorizenet", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Authorize.Net API Integration Module", + "category": "Payment", + "apiDocUrl": "https://api.authorize.net/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/authorizenet/definition.js b/packages/authorizenet/definition.js new file mode 100644 index 0000000..dba4f96 --- /dev/null +++ b/packages/authorizenet/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'AuthorizeNet', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AUTHORIZENET_CLIENT_ID, + client_secret: process.env.AUTHORIZENET_CLIENT_SECRET, + scope: process.env.AUTHORIZENET_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/authorizenet`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/authorizenet/index.js b/packages/authorizenet/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/authorizenet/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/authorizenet/package.json b/packages/authorizenet/package.json new file mode 100644 index 0000000..bf4e125 --- /dev/null +++ b/packages/authorizenet/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/authorizenet", + "version": "0.0.1", + "description": "Authorize.Net API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "authorizenet", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/autotask/README.md b/packages/autotask/README.md new file mode 100644 index 0000000..098f53c --- /dev/null +++ b/packages/autotask/README.md @@ -0,0 +1,55 @@ +# Autotask API Module + +Autotask API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/autotask +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/autotask'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +AUTOTASK_CLIENT_ID=your_client_id +AUTOTASK_CLIENT_SECRET=your_client_secret +AUTOTASK_SCOPE=your_scope +AUTOTASK_AUTH_URI=authorization_endpoint +AUTOTASK_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +CRM + +## License + +MIT diff --git a/packages/autotask/api.js b/packages/autotask/api.js new file mode 100644 index 0000000..e804265 --- /dev/null +++ b/packages/autotask/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'CRM'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.AUTOTASK_AUTH_URI; + this.tokenUri = process.env.AUTOTASK_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Autotask', + MODULE_NAME: 'autotask', + CATEGORY: 'https://api.autotask.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/autotask/defaultConfig.json b/packages/autotask/defaultConfig.json new file mode 100644 index 0000000..e0bdb7a --- /dev/null +++ b/packages/autotask/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Autotask", + "moduleName": "autotask", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Autotask API Integration Module", + "category": "CRM", + "apiDocUrl": "https://docs.autotask.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/autotask/definition.js b/packages/autotask/definition.js new file mode 100644 index 0000000..07e3e44 --- /dev/null +++ b/packages/autotask/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Autotask', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AUTOTASK_CLIENT_ID, + client_secret: process.env.AUTOTASK_CLIENT_SECRET, + scope: process.env.AUTOTASK_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/autotask`, + } +}; + +module.exports = {Definition}; diff --git a/packages/autotask/index.js b/packages/autotask/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/autotask/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/autotask/jest-setup.js b/packages/autotask/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/autotask/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/autotask/jest-teardown.js b/packages/autotask/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/autotask/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/autotask/jest.config.js b/packages/autotask/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/autotask/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/autotask/package.json b/packages/autotask/package.json new file mode 100644 index 0000000..43037ee --- /dev/null +++ b/packages/autotask/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/autotask", + "version": "0.0.1", + "description": "Autotask API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "autotask" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/aws-account-management/README.md b/packages/aws-account-management/README.md new file mode 100644 index 0000000..acad821 --- /dev/null +++ b/packages/aws-account-management/README.md @@ -0,0 +1,43 @@ +# AWS Account Management API Module + +This module provides integration with the AWS Account Management API for the Frigg Framework. + +## Description + +AWS Account Management provides tools to manage AWS account settings, billing, and administrative functions. + +## Installation + +```bash +npm install @friggframework/api-module-aws-account-management +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-aws-account-management'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +AWS_ACCOUNT_MANAGEMENT_CLIENT_ID=your_client_id +AWS_ACCOUNT_MANAGEMENT_CLIENT_SECRET=your_client_secret +AWS_ACCOUNT_MANAGEMENT_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/aws-account-management/api.js b/packages/aws-account-management/api.js new file mode 100644 index 0000000..bdc4ade --- /dev/null +++ b/packages/aws-account-management/api.js @@ -0,0 +1,180 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://account.aws.amazon.com/api/v1'; + + this.URLs = { + // Account info + accountInfo: '/account/info', + + // Billing + billing: '/billing/account', + billingPreferences: '/billing/preferences', + + // Organizations + organizations: '/organizations', + organizationById: (orgId) => `/organizations/${orgId}`, + + // Contact info + contacts: '/contacts', + contactById: (contactId) => `/contacts/${contactId}`, + + // Security + security: '/security/settings', + }; + + this.authorizationUri = encodeURI( + `https://auth.aws.amazon.com/oauth2/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://auth.aws.amazon.com/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** Account Management ********************************** + + async getAccountInfo() { + const options = { + url: this.baseUrl + this.URLs.accountInfo, + }; + return this._get(options); + } + + async updateAccountInfo(body) { + const options = { + url: this.baseUrl + this.URLs.accountInfo, + body: body, + }; + return this._put(options); + } + + // ************************** Billing ********************************** + + async getBillingInfo() { + const options = { + url: this.baseUrl + this.URLs.billing, + }; + return this._get(options); + } + + async getBillingPreferences() { + const options = { + url: this.baseUrl + this.URLs.billingPreferences, + }; + return this._get(options); + } + + async updateBillingPreferences(body) { + const options = { + url: this.baseUrl + this.URLs.billingPreferences, + body: body, + }; + return this._put(options); + } + + // ************************** Organizations ********************************** + + async listOrganizations() { + const options = { + url: this.baseUrl + this.URLs.organizations, + }; + return this._get(options); + } + + async getOrganizationById(id) { + const options = { + url: this.baseUrl + this.URLs.organizationById(id), + }; + return this._get(options); + } + + // ************************** Contacts ********************************** + + async getContacts() { + const options = { + url: this.baseUrl + this.URLs.contacts, + }; + return this._get(options); + } + + async updateContact(id, body) { + const options = { + url: this.baseUrl + this.URLs.contactById(id), + body: body, + }; + return this._put(options); + } + + // ************************** Security ********************************** + + async getSecuritySettings() { + const options = { + url: this.baseUrl + this.URLs.security, + }; + return this._get(options); + } + + async updateSecuritySettings(body) { + const options = { + url: this.baseUrl + this.URLs.security, + body: body, + }; + return this._put(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/aws-account-management/defaultConfig.json b/packages/aws-account-management/defaultConfig.json new file mode 100644 index 0000000..6d245c9 --- /dev/null +++ b/packages/aws-account-management/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "aws-account-management", + "label": "AWS Account Management", + "productUrl": "https://aws.amazon.com/account/", + "apiDocs": "https://docs.aws.amazon.com/account/", + "logoUrl": "https://friggframework.org/assets/img/aws-icon.png", + "categories": [ + "Developer" + ], + "subCategories": [ + "Amazon" + ], + "description": "AWS Account Management provides tools to manage AWS account settings, billing, and administrative functions." +} \ No newline at end of file diff --git a/packages/aws-account-management/definition.js b/packages/aws-account-management/definition.js new file mode 100644 index 0000000..8bed109 --- /dev/null +++ b/packages/aws-account-management/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'AWSAccountManagement', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const accountDetails = await api.getAccountInfo(); + return { + identifiers: {externalId: accountDetails.accountId, user: userId}, + details: {name: accountDetails.accountName, email: accountDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const accountDetails = await api.getAccountInfo(); + return { + identifiers: {externalId: accountDetails.accountId, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getAccountInfo() + }, + }, + env: { + client_id: process.env.AWS_ACCOUNT_MANAGEMENT_CLIENT_ID, + client_secret: process.env.AWS_ACCOUNT_MANAGEMENT_CLIENT_SECRET, + scope: process.env.AWS_ACCOUNT_MANAGEMENT_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-account-management`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/aws-account-management/index.js b/packages/aws-account-management/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/aws-account-management/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/aws-account-management/jest-setup.js b/packages/aws-account-management/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/aws-account-management/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/aws-account-management/jest-teardown.js b/packages/aws-account-management/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/aws-account-management/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/aws-account-management/jest.config.js b/packages/aws-account-management/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/aws-account-management/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/aws-account-management/package.json b/packages/aws-account-management/package.json new file mode 100644 index 0000000..bb5caf8 --- /dev/null +++ b/packages/aws-account-management/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-aws-account-management", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "AWS Account Management API module that lets the Frigg Framework interact with AWS Account Management", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/aws-account-management/test/api.test.js b/packages/aws-account-management/test/api.test.js new file mode 100644 index 0000000..e755492 --- /dev/null +++ b/packages/aws-account-management/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('AWS Account Management API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/aws-account-management/test/definition.test.js b/packages/aws-account-management/test/definition.test.js new file mode 100644 index 0000000..eca8ba2 --- /dev/null +++ b/packages/aws-account-management/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('AWS Account Management Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('aws-account-management'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/aws-cloudwatch/README.md b/packages/aws-cloudwatch/README.md new file mode 100644 index 0000000..54e3442 --- /dev/null +++ b/packages/aws-cloudwatch/README.md @@ -0,0 +1,55 @@ +# AWS Cloudwatch API Module + +AWS Cloudwatch API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/aws-cloudwatch +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/aws-cloudwatch'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +AWS_CLOUDWATCH_CLIENT_ID=your_client_id +AWS_CLOUDWATCH_CLIENT_SECRET=your_client_secret +AWS_CLOUDWATCH_SCOPE=your_scope +AWS_CLOUDWATCH_AUTH_URI=authorization_endpoint +AWS_CLOUDWATCH_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Developer + +## License + +MIT diff --git a/packages/aws-cloudwatch/api.js b/packages/aws-cloudwatch/api.js new file mode 100644 index 0000000..07408be --- /dev/null +++ b/packages/aws-cloudwatch/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Developer'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.AWS_CLOUDWATCH_AUTH_URI; + this.tokenUri = process.env.AWS_CLOUDWATCH_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'AWS Cloudwatch', + MODULE_NAME: 'aws-cloudwatch', + CATEGORY: 'https://monitoring.amazonaws.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/aws-cloudwatch/defaultConfig.json b/packages/aws-cloudwatch/defaultConfig.json new file mode 100644 index 0000000..8444e9e --- /dev/null +++ b/packages/aws-cloudwatch/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "AWS Cloudwatch", + "moduleName": "aws-cloudwatch", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "AWS Cloudwatch API Integration Module", + "category": "Developer", + "apiDocUrl": "https://docs.aws-cloudwatch.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/aws-cloudwatch/definition.js b/packages/aws-cloudwatch/definition.js new file mode 100644 index 0000000..ff2f54b --- /dev/null +++ b/packages/aws-cloudwatch/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'AWS Cloudwatch', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AWS_CLOUDWATCH_CLIENT_ID, + client_secret: process.env.AWS_CLOUDWATCH_CLIENT_SECRET, + scope: process.env.AWS_CLOUDWATCH_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-cloudwatch`, + } +}; + +module.exports = {Definition}; diff --git a/packages/aws-cloudwatch/index.js b/packages/aws-cloudwatch/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/aws-cloudwatch/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/aws-cloudwatch/jest-setup.js b/packages/aws-cloudwatch/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/aws-cloudwatch/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/aws-cloudwatch/jest-teardown.js b/packages/aws-cloudwatch/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/aws-cloudwatch/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/aws-cloudwatch/jest.config.js b/packages/aws-cloudwatch/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/aws-cloudwatch/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/aws-cloudwatch/package.json b/packages/aws-cloudwatch/package.json new file mode 100644 index 0000000..67bc3c9 --- /dev/null +++ b/packages/aws-cloudwatch/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/aws-cloudwatch", + "version": "0.0.1", + "description": "AWS Cloudwatch API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "aws-cloudwatch" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/aws-dynamodb/README.md b/packages/aws-dynamodb/README.md new file mode 100644 index 0000000..c849c74 --- /dev/null +++ b/packages/aws-dynamodb/README.md @@ -0,0 +1,34 @@ +# AWS DynamoDB API Integration + +AWS DynamoDB integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/aws-dynamodb +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/aws-dynamodb'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `AWS_DYNAMODB_CLIENT_ID` +- `AWS_DYNAMODB_CLIENT_SECRET` +- `AWS_DYNAMODB_SCOPE` + +## API Documentation + +For more information about the AWS DynamoDB API, visit: https://dynamodb.us-east-1.amazonaws.com diff --git a/packages/aws-dynamodb/api.js b/packages/aws-dynamodb/api.js new file mode 100644 index 0000000..666ca16 --- /dev/null +++ b/packages/aws-dynamodb/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class AWSDynamoDBApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://dynamodb.us-east-1.amazonaws.com'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: AWSDynamoDBApi}; diff --git a/packages/aws-dynamodb/defaultConfig.json b/packages/aws-dynamodb/defaultConfig.json new file mode 100644 index 0000000..8e1bbee --- /dev/null +++ b/packages/aws-dynamodb/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "AWS DynamoDB", + "moduleName": "aws-dynamodb", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "AWS DynamoDB API Integration Module", + "category": "Developer", + "apiDocUrl": "https://dynamodb.us-east-1.amazonaws.com", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/aws-dynamodb/definition.js b/packages/aws-dynamodb/definition.js new file mode 100644 index 0000000..025f5a1 --- /dev/null +++ b/packages/aws-dynamodb/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'AWS DynamoDB', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AWS_DYNAMODB_CLIENT_ID, + client_secret: process.env.AWS_DYNAMODB_CLIENT_SECRET, + scope: process.env.AWS_DYNAMODB_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-dynamodb`, + } +}; + +module.exports = {Definition}; diff --git a/packages/aws-dynamodb/index.js b/packages/aws-dynamodb/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/aws-dynamodb/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/aws-dynamodb/package.json b/packages/aws-dynamodb/package.json new file mode 100644 index 0000000..43a98fa --- /dev/null +++ b/packages/aws-dynamodb/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/aws-dynamodb", + "version": "0.0.1", + "description": "AWS DynamoDB API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "aws-dynamodb", + "developer" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/aws-kms/api.js b/packages/aws-kms/api.js new file mode 100644 index 0000000..fb89cf1 --- /dev/null +++ b/packages/aws-kms/api.js @@ -0,0 +1,145 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://kms.amazonaws.com'; + + this.URLs = { + // Key operations + keys: '/keys', + keyById: (keyId) => `/keys/${encodeURIComponent(keyId)}`, + + // Encryption operations + encrypt: '/encrypt', + decrypt: '/decrypt', + generateDataKey: '/generate-data-key', + + // Key policies + keyPolicy: (keyId) => `/keys/${encodeURIComponent(keyId)}/policy`, + + // Key grants + grants: (keyId) => `/keys/${encodeURIComponent(keyId)}/grants`, + grantById: (keyId, grantId) => `/keys/${encodeURIComponent(keyId)}/grants/${grantId}`, + + // Aliases + aliases: '/aliases', + aliasById: (aliasName) => `/aliases/${encodeURIComponent(aliasName)}`, + }; + + this.authorizationUri = encodeURI( + `https://aws.amazon.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://aws.amazon.com/oauth/token'; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + redirect_uri: this.redirect_uri, + code: code, + }, + }; + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + // Key operations + async listKeys(params = {}) { + const options = { + url: this.baseUrl + this.URLs.keys, + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async createKey(keyUsage = 'ENCRYPT_DECRYPT', keySpec = 'SYMMETRIC_DEFAULT') { + const options = { + url: this.baseUrl + this.URLs.keys, + method: 'POST', + json: { + KeyUsage: keyUsage, + KeySpec: keySpec, + }, + }; + return this._request(options); + } + + async getKey(keyId) { + const options = { + url: this.baseUrl + this.URLs.keyById(keyId), + method: 'GET', + }; + return this._request(options); + } + + async deleteKey(keyId, pendingWindowInDays = 30) { + const options = { + url: this.baseUrl + this.URLs.keyById(keyId), + method: 'DELETE', + json: { + PendingWindowInDays: pendingWindowInDays, + }, + }; + return this._request(options); + } + + // Encryption operations + async encrypt(keyId, plaintext, encryptionContext = {}) { + const options = { + url: this.baseUrl + this.URLs.encrypt, + method: 'POST', + json: { + KeyId: keyId, + Plaintext: plaintext, + EncryptionContext: encryptionContext, + }, + }; + return this._request(options); + } + + async decrypt(ciphertextBlob, encryptionContext = {}) { + const options = { + url: this.baseUrl + this.URLs.decrypt, + method: 'POST', + json: { + CiphertextBlob: ciphertextBlob, + EncryptionContext: encryptionContext, + }, + }; + return this._request(options); + } + + async generateDataKey(keyId, keySpec = 'AES_256') { + const options = { + url: this.baseUrl + this.URLs.generateDataKey, + method: 'POST', + json: { + KeyId: keyId, + KeySpec: keySpec, + }, + }; + return this._request(options); + } + + // User info for authentication + async getUserDetails() { + const options = { + url: this.baseUrl + '/user-details', + method: 'GET', + }; + return this._request(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/aws-kms/defaultConfig.json b/packages/aws-kms/defaultConfig.json new file mode 100644 index 0000000..7caa9da --- /dev/null +++ b/packages/aws-kms/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "aws-kms", + "label": "AWS KMS", + "productUrl": "https://aws.amazon.com/kms/", + "apiDocs": "https://docs.aws.amazon.com/kms/", + "logoUrl": "https://friggframework.org/assets/img/aws-kms-icon.png", + "categories": [ + "Developer", + "Amazon" + ], + "description": "AWS Key Management Service (KMS) makes it easy for you to create and manage cryptographic keys and control their use across a wide range of AWS services and in your applications." +} \ No newline at end of file diff --git a/packages/aws-kms/definition.js b/packages/aws-kms/definition.js new file mode 100644 index 0000000..6cc3076 --- /dev/null +++ b/packages/aws-kms/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'AWSKMS', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.accountId || userDetails.id, user: userId}, + details: {name: userDetails.name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.accountId || userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.AWS_KMS_CLIENT_ID, + client_secret: process.env.AWS_KMS_CLIENT_SECRET, + scope: process.env.AWS_KMS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-kms`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/aws-kms/index.js b/packages/aws-kms/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/aws-kms/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/aws-lambda/README.md b/packages/aws-lambda/README.md new file mode 100644 index 0000000..59f8dd8 --- /dev/null +++ b/packages/aws-lambda/README.md @@ -0,0 +1,42 @@ +# AWS Lambda API Module + +Frigg API module for AWS Lambda integration. + +## Installation + +```bash +npm install @friggframework/aws-lambda +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/aws-lambda'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +AWS_LAMBDA_CLIENT_ID=your_client_id +AWS_LAMBDA_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/aws-lambda/api.js b/packages/aws-lambda/api.js new file mode 100644 index 0000000..7a92a33 --- /dev/null +++ b/packages/aws-lambda/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://lambda.amazonaws.com'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://lambda.amazonaws.com/oauth/authorize'; + this.accessTokenUri = 'https://lambda.amazonaws.com/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'AWS Lambda', + MODULE_NAME: 'aws-lambda', + CATEGORY: 'Developer', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/aws-lambda/defaultConfig.json b/packages/aws-lambda/defaultConfig.json new file mode 100644 index 0000000..86c705e --- /dev/null +++ b/packages/aws-lambda/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "AWS Lambda", + "moduleName": "aws-lambda", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "AWS Lambda API Integration Module", + "category": "Developer", + "apiDocUrl": "https://lambda.amazonaws.com/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/aws-lambda/definition.js b/packages/aws-lambda/definition.js new file mode 100644 index 0000000..434a4fc --- /dev/null +++ b/packages/aws-lambda/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'AWSLambda', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AWS_LAMBDA_CLIENT_ID, + client_secret: process.env.AWS_LAMBDA_CLIENT_SECRET, + scope: process.env.AWS_LAMBDA_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-lambda`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/aws-lambda/index.js b/packages/aws-lambda/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/aws-lambda/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/aws-lambda/package.json b/packages/aws-lambda/package.json new file mode 100644 index 0000000..e8f2630 --- /dev/null +++ b/packages/aws-lambda/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/aws-lambda", + "version": "0.0.1", + "description": "AWS Lambda API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "aws-lambda", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/aws-s3-elasticache/README.md b/packages/aws-s3-elasticache/README.md new file mode 100644 index 0000000..6ee3031 --- /dev/null +++ b/packages/aws-s3-elasticache/README.md @@ -0,0 +1,55 @@ +# AWS S3 Elasticache API Module + +AWS S3 Elasticache API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/aws-s3-elasticache +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/aws-s3-elasticache'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +AWS_S3_ELASTICACHE_CLIENT_ID=your_client_id +AWS_S3_ELASTICACHE_CLIENT_SECRET=your_client_secret +AWS_S3_ELASTICACHE_SCOPE=your_scope +AWS_S3_ELASTICACHE_AUTH_URI=authorization_endpoint +AWS_S3_ELASTICACHE_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Developer + +## License + +MIT diff --git a/packages/aws-s3-elasticache/api.js b/packages/aws-s3-elasticache/api.js new file mode 100644 index 0000000..f96d130 --- /dev/null +++ b/packages/aws-s3-elasticache/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Developer'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.AWS_S3_ELASTICACHE_AUTH_URI; + this.tokenUri = process.env.AWS_S3_ELASTICACHE_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'AWS S3 Elasticache', + MODULE_NAME: 'aws-s3-elasticache', + CATEGORY: 'https://elasticache.amazonaws.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/aws-s3-elasticache/defaultConfig.json b/packages/aws-s3-elasticache/defaultConfig.json new file mode 100644 index 0000000..90db1e1 --- /dev/null +++ b/packages/aws-s3-elasticache/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "AWS S3 Elasticache", + "moduleName": "aws-s3-elasticache", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "AWS S3 Elasticache API Integration Module", + "category": "Developer", + "apiDocUrl": "https://docs.aws-s3-elasticache.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/aws-s3-elasticache/definition.js b/packages/aws-s3-elasticache/definition.js new file mode 100644 index 0000000..0412d33 --- /dev/null +++ b/packages/aws-s3-elasticache/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'AWS S3 Elasticache', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.AWS_S3_ELASTICACHE_CLIENT_ID, + client_secret: process.env.AWS_S3_ELASTICACHE_CLIENT_SECRET, + scope: process.env.AWS_S3_ELASTICACHE_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-s3-elasticache`, + } +}; + +module.exports = {Definition}; diff --git a/packages/aws-s3-elasticache/index.js b/packages/aws-s3-elasticache/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/aws-s3-elasticache/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/aws-s3-elasticache/jest-setup.js b/packages/aws-s3-elasticache/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/aws-s3-elasticache/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/aws-s3-elasticache/jest-teardown.js b/packages/aws-s3-elasticache/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/aws-s3-elasticache/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/aws-s3-elasticache/jest.config.js b/packages/aws-s3-elasticache/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/aws-s3-elasticache/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/aws-s3-elasticache/package.json b/packages/aws-s3-elasticache/package.json new file mode 100644 index 0000000..00d9826 --- /dev/null +++ b/packages/aws-s3-elasticache/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/aws-s3-elasticache", + "version": "0.0.1", + "description": "AWS S3 Elasticache API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "aws-s3-elasticache" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/aws-s3/README.md b/packages/aws-s3/README.md new file mode 100644 index 0000000..d1e2719 --- /dev/null +++ b/packages/aws-s3/README.md @@ -0,0 +1,43 @@ +# AWS S3 API Module + +This module provides integration with the AWS S3 API for the Frigg Framework. + +## Description + +Amazon S3 is a cloud storage service that offers industry-leading scalability, data availability, security, and performance. + +## Installation + +```bash +npm install @friggframework/api-module-aws-s3 +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-aws-s3'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +AWS_S3_CLIENT_ID=your_client_id +AWS_S3_CLIENT_SECRET=your_client_secret +AWS_S3_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/aws-s3/api.js b/packages/aws-s3/api.js new file mode 100644 index 0000000..c74596e --- /dev/null +++ b/packages/aws-s3/api.js @@ -0,0 +1,206 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://s3.amazonaws.com'; + + this.URLs = { + // Buckets + buckets: '/', + bucketById: (bucketName) => `/${bucketName}`, + + // Objects + objects: (bucketName) => `/${bucketName}`, + objectById: (bucketName, objectKey) => `/${bucketName}/${objectKey}`, + + // ACL + bucketAcl: (bucketName) => `/${bucketName}?acl`, + objectAcl: (bucketName, objectKey) => `/${bucketName}/${objectKey}?acl`, + + // Versioning + versioning: (bucketName) => `/${bucketName}?versioning`, + + // Lifecycle + lifecycle: (bucketName) => `/${bucketName}?lifecycle`, + }; + + this.authorizationUri = encodeURI( + `https://auth.aws.amazon.com/oauth2/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://auth.aws.amazon.com/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** Buckets ********************************** + + async listBuckets() { + const options = { + url: this.baseUrl + this.URLs.buckets, + }; + return this._get(options); + } + + async createBucket(bucketName, region = 'us-east-1') { + const options = { + url: this.baseUrl + this.URLs.bucketById(bucketName), + headers: { + 'x-amz-bucket-region': region + } + }; + return this._put(options); + } + + async deleteBucket(bucketName) { + const options = { + url: this.baseUrl + this.URLs.bucketById(bucketName), + }; + return this._delete(options); + } + + async getBucketInfo(bucketName) { + const options = { + url: this.baseUrl + this.URLs.bucketById(bucketName), + }; + return this._head(options); + } + + // ************************** Objects ********************************** + + async listObjects(bucketName, prefix = '') { + const options = { + url: this.baseUrl + this.URLs.objects(bucketName), + query: { + prefix: prefix, + 'list-type': 2 + } + }; + return this._get(options); + } + + async uploadObject(bucketName, objectKey, data, contentType = 'application/octet-stream') { + const options = { + url: this.baseUrl + this.URLs.objectById(bucketName, objectKey), + body: data, + headers: { + 'Content-Type': contentType + } + }; + return this._put(options, false); + } + + async getObject(bucketName, objectKey) { + const options = { + url: this.baseUrl + this.URLs.objectById(bucketName, objectKey), + }; + return this._get(options); + } + + async deleteObject(bucketName, objectKey) { + const options = { + url: this.baseUrl + this.URLs.objectById(bucketName, objectKey), + }; + return this._delete(options); + } + + async copyObject(sourceBucket, sourceKey, destBucket, destKey) { + const options = { + url: this.baseUrl + this.URLs.objectById(destBucket, destKey), + headers: { + 'x-amz-copy-source': `/${sourceBucket}/${sourceKey}` + } + }; + return this._put(options); + } + + // ************************** ACL ********************************** + + async getBucketAcl(bucketName) { + const options = { + url: this.baseUrl + this.URLs.bucketAcl(bucketName), + }; + return this._get(options); + } + + async getObjectAcl(bucketName, objectKey) { + const options = { + url: this.baseUrl + this.URLs.objectAcl(bucketName, objectKey), + }; + return this._get(options); + } + + // ************************** Versioning ********************************** + + async getBucketVersioning(bucketName) { + const options = { + url: this.baseUrl + this.URLs.versioning(bucketName), + }; + return this._get(options); + } + + async setBucketVersioning(bucketName, status) { + const options = { + url: this.baseUrl + this.URLs.versioning(bucketName), + body: `${status}`, + headers: { + 'Content-Type': 'application/xml' + } + }; + return this._put(options, false); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/aws-s3/defaultConfig.json b/packages/aws-s3/defaultConfig.json new file mode 100644 index 0000000..5f831b5 --- /dev/null +++ b/packages/aws-s3/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "aws-s3", + "label": "AWS S3", + "productUrl": "https://aws.amazon.com/s3/", + "apiDocs": "https://docs.aws.amazon.com/s3/", + "logoUrl": "https://friggframework.org/assets/img/aws-s3-icon.png", + "categories": [ + "Developer" + ], + "subCategories": [ + "Amazon" + ], + "description": "Amazon S3 is a cloud storage service that offers industry-leading scalability, data availability, security, and performance." +} \ No newline at end of file diff --git a/packages/aws-s3/definition.js b/packages/aws-s3/definition.js new file mode 100644 index 0000000..0965faf --- /dev/null +++ b/packages/aws-s3/definition.js @@ -0,0 +1,51 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'AWSS3', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const buckets = await api.listBuckets(); + const firstBucket = buckets.Buckets?.[0]; + return { + identifiers: {externalId: firstBucket?.Name || 's3-account', user: userId}, + details: {name: 'AWS S3 Account', bucketCount: buckets.Buckets?.length || 0}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const buckets = await api.listBuckets(); + return { + identifiers: {externalId: 's3-account', user: userId}, + details: {bucketCount: buckets.Buckets?.length || 0} + }; + }, + testAuthRequest: async function (api) { + return api.listBuckets() + }, + }, + env: { + client_id: process.env.AWS_S3_CLIENT_ID, + client_secret: process.env.AWS_S3_CLIENT_SECRET, + scope: process.env.AWS_S3_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/aws-s3`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/aws-s3/index.js b/packages/aws-s3/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/aws-s3/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/aws-s3/jest-setup.js b/packages/aws-s3/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/aws-s3/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/aws-s3/jest-teardown.js b/packages/aws-s3/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/aws-s3/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/aws-s3/jest.config.js b/packages/aws-s3/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/aws-s3/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/aws-s3/package.json b/packages/aws-s3/package.json new file mode 100644 index 0000000..933804d --- /dev/null +++ b/packages/aws-s3/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-aws-s3", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "AWS S3 API module that lets the Frigg Framework interact with AWS S3", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/aws-s3/test/api.test.js b/packages/aws-s3/test/api.test.js new file mode 100644 index 0000000..47f20bd --- /dev/null +++ b/packages/aws-s3/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('AWS S3 API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/aws-s3/test/definition.test.js b/packages/aws-s3/test/definition.test.js new file mode 100644 index 0000000..18e5789 --- /dev/null +++ b/packages/aws-s3/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('AWS S3 Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('aws-s3'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/bamboohr/README.md b/packages/bamboohr/README.md new file mode 100644 index 0000000..17012ad --- /dev/null +++ b/packages/bamboohr/README.md @@ -0,0 +1,5 @@ +# BambooHR + +This is the API Module for BambooHR that allows the [Frigg](https://friggframework.org) code to talk to the BambooHR API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/bamboohr) diff --git a/packages/bamboohr/api.js b/packages/bamboohr/api.js new file mode 100644 index 0000000..efcb700 --- /dev/null +++ b/packages/bamboohr/api.js @@ -0,0 +1,228 @@ +const { Requester, get } = require('@friggframework/core'); + +class Api extends Requester { + constructor(params) { + super(params); + this.apiKey = get(params, 'api_key', process.env.BAMBOOHR_API_KEY); + this.subdomain = get(params, 'subdomain', process.env.BAMBOOHR_SUBDOMAIN); + this.baseUrl = `https://api.bamboohr.com/api/gateway.php/${this.subdomain}/v1`; + + this.URLs = { + // Employee Management + employees: '/employees/directory', + employeeById: (employeeId) => `/employees/${employeeId}`, + employeeFields: '/meta/fields', + + // Time Off + timeOffRequests: '/time_off/requests', + timeOffRequestById: (requestId) => `/time_off/requests/${requestId}`, + timeOffPolicies: '/meta/time_off/policies', + timeOffBalance: (employeeId) => `/employees/${employeeId}/time_off/calculator`, + + // Reports + reports: '/reports', + reportById: (reportId) => `/reports/${reportId}`, + customReport: '/reports/custom', + + // Company + companyInfo: '/meta/users', + departments: '/meta/lists/department', + divisions: '/meta/lists/division', + locations: '/meta/lists/location', + + // Files + employeeFiles: (employeeId) => `/employees/${employeeId}/files`, + fileById: (employeeId, fileId) => `/employees/${employeeId}/files/${fileId}`, + + // Benefits + benefits: '/benefits', + benefitPlansByEmployee: (employeeId) => `/employees/${employeeId}/benefits` + }; + } + + addAuthHeaders(options) { + // BambooHR uses Basic Auth with API key as username and 'x' as password + const credentials = Buffer.from(`${this.apiKey}:x`).toString('base64'); + const authHeaders = { + 'Authorization': `Basic ${credentials}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + async _get(options) { + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _delete(options) { + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Employees ********************************** + + async listEmployees() { + const options = { + url: this.baseUrl + this.URLs.employees, + }; + return this._get(options); + } + + async getEmployeeById(employeeId, fields = null) { + let url = this.baseUrl + this.URLs.employeeById(employeeId); + + const options = { + url: url, + }; + + if (fields && fields.length > 0) { + options.query = { fields: fields.join(',') }; + } + + return this._get(options); + } + + async updateEmployee(employeeId, employeeData) { + const options = { + url: this.baseUrl + this.URLs.employeeById(employeeId), + body: employeeData, + }; + return this._post(options); + } + + async getEmployeeFields() { + const options = { + url: this.baseUrl + this.URLs.employeeFields, + }; + return this._get(options); + } + + // ************************** Time Off ********************************** + + async listTimeOffRequests(params = {}) { + const options = { + url: this.baseUrl + this.URLs.timeOffRequests, + query: params + }; + return this._get(options); + } + + async createTimeOffRequest(requestData) { + const options = { + url: this.baseUrl + this.URLs.timeOffRequests, + body: requestData, + }; + return this._post(options); + } + + async updateTimeOffRequest(requestId, requestData) { + const options = { + url: this.baseUrl + this.URLs.timeOffRequestById(requestId), + body: requestData, + }; + return this._put(options); + } + + async getTimeOffPolicies() { + const options = { + url: this.baseUrl + this.URLs.timeOffPolicies, + }; + return this._get(options); + } + + async getTimeOffBalance(employeeId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.timeOffBalance(employeeId), + query: params + }; + return this._get(options); + } + + // ************************** Reports ********************************** + + async listReports() { + const options = { + url: this.baseUrl + this.URLs.reports, + }; + return this._get(options); + } + + async getReport(reportId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.reportById(reportId), + query: params + }; + return this._get(options); + } + + async generateCustomReport(reportData) { + const options = { + url: this.baseUrl + this.URLs.customReport, + body: reportData, + }; + return this._post(options); + } + + // ************************** Company Info ********************************** + + async getCompanyInfo() { + const options = { + url: this.baseUrl + this.URLs.companyInfo, + }; + return this._get(options); + } + + async getDepartments() { + const options = { + url: this.baseUrl + this.URLs.departments, + }; + return this._get(options); + } + + async getDivisions() { + const options = { + url: this.baseUrl + this.URLs.divisions, + }; + return this._get(options); + } + + async getLocations() { + const options = { + url: this.baseUrl + this.URLs.locations, + }; + return this._get(options); + } + + // ************************** Files ********************************** + + async getEmployeeFiles(employeeId) { + const options = { + url: this.baseUrl + this.URLs.employeeFiles(employeeId), + }; + return this._get(options); + } + + async getEmployeeFile(employeeId, fileId) { + const options = { + url: this.baseUrl + this.URLs.fileById(employeeId, fileId), + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/bamboohr/defaultConfig.json b/packages/bamboohr/defaultConfig.json new file mode 100644 index 0000000..0888b9d --- /dev/null +++ b/packages/bamboohr/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "bamboohr", + "label": "BambooHR", + "productUrl": "https://www.bamboohr.com", + "apiDocs": "https://documentation.bamboohr.com/docs", + "logoUrl": "https://friggframework.org/assets/img/bamboohr-icon.png", + "categories": [ + "HR", + "Human Resources", + "Employee Management" + ], + "description": "BambooHR is an HR software platform designed to help growing companies manage employee information, hiring, onboarding, and culture." +} diff --git a/packages/bamboohr/definition.js b/packages/bamboohr/definition.js new file mode 100644 index 0000000..ab34190 --- /dev/null +++ b/packages/bamboohr/definition.js @@ -0,0 +1,51 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'BambooHR', + requiredAuthMethods: { + getToken: async function (api, params) { + // BambooHR uses API key authentication + return { + access_token: params.api_key, + subdomain: params.subdomain + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const companyInfo = await api.getCompanyInfo(); + return { + identifiers: {externalId: api.subdomain, user: userId}, + details: {subdomain: api.subdomain, companyName: companyInfo.name || api.subdomain}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'api_key', 'subdomain' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const companyInfo = await api.getCompanyInfo(); + return { + identifiers: {externalId: api.subdomain, user: userId}, + details: {subdomain: api.subdomain} + }; + }, + testAuthRequest: async function (api) { + return api.getCompanyInfo() + }, + }, + env: { + api_key: process.env.BAMBOOHR_API_KEY, + subdomain: process.env.BAMBOOHR_SUBDOMAIN, + } +}; + +module.exports = {Definition}; diff --git a/packages/bamboohr/index.js b/packages/bamboohr/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/bamboohr/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/bamboohr/jest.config.js b/packages/bamboohr/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/bamboohr/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/bamboohr/package.json b/packages/bamboohr/package.json new file mode 100644 index 0000000..2f20c8a --- /dev/null +++ b/packages/bamboohr/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-bamboohr", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "BambooHR API module that lets the Frigg Framework interact with BambooHR", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/basecrm/README.md b/packages/basecrm/README.md new file mode 100644 index 0000000..6629b5a --- /dev/null +++ b/packages/basecrm/README.md @@ -0,0 +1,34 @@ +# BaseCRM API Integration + +BaseCRM integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/basecrm +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/basecrm'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `BASECRM_CLIENT_ID` +- `BASECRM_CLIENT_SECRET` +- `BASECRM_SCOPE` + +## API Documentation + +For more information about the BaseCRM API, visit: https://api.getbase.com/v2 diff --git a/packages/basecrm/api.js b/packages/basecrm/api.js new file mode 100644 index 0000000..42c49cf --- /dev/null +++ b/packages/basecrm/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class BaseCRMApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.getbase.com/v2'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: BaseCRMApi}; diff --git a/packages/basecrm/defaultConfig.json b/packages/basecrm/defaultConfig.json new file mode 100644 index 0000000..f91a2f1 --- /dev/null +++ b/packages/basecrm/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "BaseCRM", + "moduleName": "basecrm", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "BaseCRM API Integration Module", + "category": "CRM", + "apiDocUrl": "https://api.getbase.com/v2", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/basecrm/definition.js b/packages/basecrm/definition.js new file mode 100644 index 0000000..37f1f0c --- /dev/null +++ b/packages/basecrm/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'BaseCRM', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.BASECRM_CLIENT_ID, + client_secret: process.env.BASECRM_CLIENT_SECRET, + scope: process.env.BASECRM_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/basecrm`, + } +}; + +module.exports = {Definition}; diff --git a/packages/basecrm/index.js b/packages/basecrm/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/basecrm/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/basecrm/package.json b/packages/basecrm/package.json new file mode 100644 index 0000000..9142a95 --- /dev/null +++ b/packages/basecrm/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/basecrm", + "version": "0.0.1", + "description": "BaseCRM API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "basecrm", + "crm" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/benchmark-email/README.md b/packages/benchmark-email/README.md new file mode 100644 index 0000000..47673ad --- /dev/null +++ b/packages/benchmark-email/README.md @@ -0,0 +1,43 @@ +# Benchmark Email API Module + +This module provides integration with the Benchmark Email API for the Frigg Framework. + +## Description + +Benchmark Email provides email marketing automation and newsletter services. + +## Installation + +```bash +npm install @friggframework/api-module-benchmark-email +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-benchmark-email'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +BENCHMARK_EMAIL_CLIENT_ID=your_client_id +BENCHMARK_EMAIL_CLIENT_SECRET=your_client_secret +BENCHMARK_EMAIL_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/benchmark-email/api.js b/packages/benchmark-email/api.js new file mode 100644 index 0000000..ac230e2 --- /dev/null +++ b/packages/benchmark-email/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://clientapi.benchmarkemail.com'; + + this.URLs = { + // User/Account info + userInfo: '/Contact', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://ui.benchmarkemail.com/OAuth/Authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://clientapi.benchmarkemail.com/OAuth/Token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to Benchmark Email +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/benchmark-email/defaultConfig.json b/packages/benchmark-email/defaultConfig.json new file mode 100644 index 0000000..5ae3e6c --- /dev/null +++ b/packages/benchmark-email/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "benchmark-email", + "label": "Benchmark Email", + "productUrl": "https://benchmarkemail.com/", + "apiDocs": "https://developers.benchmarkemail.com/", + "logoUrl": "https://friggframework.org/assets/img/benchmark-email-icon.png", + "categories": [ + "Communication" + ], + "subCategories": [ + "Email Newsletters" + ], + "description": "Benchmark Email provides email marketing automation and newsletter services." +} \ No newline at end of file diff --git a/packages/benchmark-email/definition.js b/packages/benchmark-email/definition.js new file mode 100644 index 0000000..af2d143 --- /dev/null +++ b/packages/benchmark-email/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'BenchmarkEmail', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'benchmark-email-account', user: userId}, + details: {name: userInfo.name || 'Benchmark Email Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'benchmark-email-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.BENCHMARK_EMAIL_CLIENT_ID, + client_secret: process.env.BENCHMARK_EMAIL_CLIENT_SECRET, + scope: process.env.BENCHMARK_EMAIL_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/benchmark-email`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/benchmark-email/index.js b/packages/benchmark-email/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/benchmark-email/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/benchmark-email/jest-setup.js b/packages/benchmark-email/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/benchmark-email/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/benchmark-email/jest-teardown.js b/packages/benchmark-email/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/benchmark-email/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/benchmark-email/jest.config.js b/packages/benchmark-email/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/benchmark-email/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/benchmark-email/package.json b/packages/benchmark-email/package.json new file mode 100644 index 0000000..8cf71dd --- /dev/null +++ b/packages/benchmark-email/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-benchmark-email", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Benchmark Email API module that lets the Frigg Framework interact with Benchmark Email", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/benchmark-email/test/api.test.js b/packages/benchmark-email/test/api.test.js new file mode 100644 index 0000000..d7fe114 --- /dev/null +++ b/packages/benchmark-email/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('Benchmark Email API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/benchmark-email/test/definition.test.js b/packages/benchmark-email/test/definition.test.js new file mode 100644 index 0000000..f977f92 --- /dev/null +++ b/packages/benchmark-email/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Benchmark Email Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('benchmark-email'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/bigcommerce/README.md b/packages/bigcommerce/README.md new file mode 100644 index 0000000..209ebb3 --- /dev/null +++ b/packages/bigcommerce/README.md @@ -0,0 +1,411 @@ +# BigCommerce API Module + +A comprehensive BigCommerce REST API v3 client for the Frigg Framework, supporting all major e-commerce operations including catalog management, orders, customers, and themes. + +## Features + +- **Catalog Management**: Products, variants, categories, brands, and attributes +- **Order Processing**: Complete order lifecycle management with refunds and shipments +- **Customer Management**: Customer data, addresses, and groups +- **Theme Management**: Theme upload, activation, and configuration +- **Store Management**: Settings, system status, and configuration +- **Webhook Support**: Event-driven integrations with signature verification +- **Authentication**: Support for both OAuth2 and Access Token authentication + +## Installation + +```bash +npm install @friggframework/api-module-bigcommerce +``` + +## Configuration + +### Environment Variables + +```env +BIGCOMMERCE_CLIENT_ID=your_client_id_here +BIGCOMMERCE_CLIENT_SECRET=your_client_secret_here +BIGCOMMERCE_ACCESS_TOKEN=your_access_token_here +BIGCOMMERCE_STORE_HASH=your_store_hash_here +``` + +### Authentication Setup + +BigCommerce supports two authentication methods: + +#### 1. OAuth2 (Recommended for Apps) +1. Create an app in the BigCommerce Developer Portal +2. Configure OAuth scopes and redirect URI +3. Use the OAuth flow to get access tokens + +#### 2. Access Token (For Private Apps) +1. Go to your BigCommerce admin panel +2. Navigate to Advanced Settings > API Accounts +3. Create a new API account with required scopes +4. Copy the Access Token and Store Hash + +## Usage + +### Basic Setup + +```javascript +const { Api } = require('@friggframework/api-module-bigcommerce'); + +// Using Access Token +const api = new Api({ + storeHash: 'your_store_hash', + accessToken: 'your_access_token' +}); + +// Using OAuth2 +const oauthApi = new Api({ + storeHash: 'your_store_hash', + access_token: 'oauth_access_token' +}); +``` + +### Products + +```javascript +// Create a product +const product = await api.createProduct({ + name: 'Premium Widget', + type: 'physical', + weight: 1.5, + price: 29.99, + categories: [123], + brand_id: 456, + inventory_level: 100, + inventory_warning_level: 10 +}); + +// Get products with filtering +const products = await api.listProducts({ + limit: 50, + 'categories:in': '123,456', + is_visible: true, + availability: 'available' +}); + +// Update product +await api.updateProduct(product.data.id, { + price: 34.99, + inventory_level: 85 +}); + +// Create product variant +const variant = await api.createProductVariant(product.data.id, { + sku: 'WIDGET-RED-L', + price: 32.99, + weight: 1.5, + option_values: [ + { option_display_name: 'Color', label: 'Red' }, + { option_display_name: 'Size', label: 'Large' } + ] +}); +``` + +### Categories + +```javascript +// Create category +const category = await api.createCategory({ + name: 'Electronics', + description: 'Electronic devices and accessories', + parent_id: 0, + sort_order: 1, + is_visible: true +}); + +// Get category tree +const categoryTree = await api.getCategoryTree(); + +// Update category +await api.updateCategory(category.data.id, { + description: 'Updated description', + meta_title: 'Electronics - Your Store' +}); +``` + +### Orders + +```javascript +// Get orders +const orders = await api.listOrders({ + status_id: 11, // Awaiting Fulfillment + limit: 100, + 'date_created:min': '2024-01-01T00:00:00Z' +}); + +// Get specific order +const order = await api.getOrderById(12345); + +// Update order status +await api.updateOrder(12345, { + status_id: 10 // Completed +}); + +// Get order products +const orderProducts = await api.getOrderProducts(12345); + +// Create refund +await api.createOrderRefund(12345, { + amount: 15.99, + reason: 'Customer return', + items: [ + { + item_id: 123, + item_type: 'PRODUCT', + quantity: 1 + } + ] +}); +``` + +### Customers + +```javascript +// Create customer +const customer = await api.createCustomer({ + email: 'customer@example.com', + first_name: 'John', + last_name: 'Doe', + company: 'Example Corp', + phone: '+1-555-123-4567', + accepts_product_review_abandoned_cart_emails: true +}); + +// Get customers +const customers = await api.listCustomers({ + 'email:like': '@example.com', + limit: 50 +}); + +// Add customer address +const address = await api.createCustomerAddress(customer.data.id, { + first_name: 'John', + last_name: 'Doe', + company: 'Example Corp', + address1: '123 Main St', + city: 'New York', + state_or_province: 'NY', + postal_code: '10001', + country_code: 'US', + phone: '+1-555-123-4567' +}); +``` + +### Themes + +```javascript +// Get all themes +const themes = await api.listThemes(); + +// Upload new theme +const theme = await api.uploadTheme({ + file: themeZipBuffer, // Theme zip file as buffer + name: 'Custom Theme', + description: 'A custom theme for our store' +}); + +// Activate theme +await api.activateTheme(theme.data.id, { + which: 'original', // or 'variation' + variation_id: null +}); + +// Get theme configurations +const configs = await api.getThemeConfigurations(theme.data.id); +``` + +### Webhooks + +```javascript +// Create webhook +const webhook = await api.createWebhook({ + scope: 'store/order/created', + destination: 'https://your-app.com/webhooks/bigcommerce/order-created', + is_active: true, + events_history_enabled: true +}); + +// List webhooks +const webhooks = await api.listWebhooks(); + +// Verify webhook signature (in your webhook handler) +const isValid = api.verifyWebhookSignature( + req.body, + req.headers['x-bc-webhook-signature'], + process.env.BIGCOMMERCE_CLIENT_SECRET +); + +if (isValid) { + console.log('Valid webhook received:', req.body); +} +``` + +### Store Management + +```javascript +// Get store information +const storeInfo = await api.getStoreInfo(); + +// Get store profile settings +const profile = await api.getStoreProfile(); + +// Update store profile +await api.updateStoreProfile({ + store_name: 'My Updated Store', + store_address: '456 Commerce St', + store_city: 'Commerce City', + store_zip: '12345' +}); +``` + +## API Methods + +### Products +- `createProduct(productData)` +- `listProducts(params)` +- `getProductById(id, params)` +- `updateProduct(id, productData)` +- `deleteProduct(id)` + +### Product Variants +- `createProductVariant(productId, variantData)` +- `listProductVariants(productId, params)` +- `getProductVariantById(productId, variantId, params)` +- `updateProductVariant(productId, variantId, variantData)` +- `deleteProductVariant(productId, variantId)` + +### Product Images +- `createProductImage(productId, imageData)` +- `listProductImages(productId, params)` +- `updateProductImage(productId, imageId, imageData)` +- `deleteProductImage(productId, imageId)` + +### Categories +- `createCategory(categoryData)` +- `listCategories(params)` +- `getCategoryById(id, params)` +- `updateCategory(id, categoryData)` +- `deleteCategory(id)` +- `getCategoryTree()` + +### Orders +- `createOrder(orderData)` +- `listOrders(params)` +- `getOrderById(id, params)` +- `updateOrder(id, orderData)` +- `deleteOrder(id)` +- `getOrderProducts(orderId, params)` +- `getOrderShippingAddresses(orderId, params)` +- `listOrderStatuses()` +- `createOrderRefund(orderId, refundData)` + +### Customers +- `createCustomer(customerData)` +- `listCustomers(params)` +- `getCustomerById(id, params)` +- `updateCustomer(id, customerData)` +- `deleteCustomer(id)` +- `getCustomerAddresses(customerId, params)` +- `createCustomerAddress(customerId, addressData)` +- `updateCustomerAddress(customerId, addressId, addressData)` +- `deleteCustomerAddress(customerId, addressId)` + +### Themes +- `listThemes()` +- `getThemeById(id)` +- `uploadTheme(themeData)` +- `downloadTheme(id)` +- `activateTheme(id, params)` +- `deleteTheme(id)` +- `getThemeConfigurations(id)` + +### Brands +- `createBrand(brandData)` +- `listBrands(params)` +- `getBrandById(id, params)` +- `updateBrand(id, brandData)` +- `deleteBrand(id)` + +### Webhooks +- `createWebhook(webhookData)` +- `listWebhooks(params)` +- `getWebhookById(id)` +- `updateWebhook(id, webhookData)` +- `deleteWebhook(id)` +- `verifyWebhookSignature(payload, signature, clientSecret)` + +### Store Information +- `getStoreInfo()` +- `getTime()` +- `getTimezone()` +- `getSettings()` +- `getStoreProfile()` +- `updateStoreProfile(profileData)` + +## Webhook Events + +BigCommerce supports webhooks for the following events: + +- `store/order/*` - Order events (created, updated, archived, etc.) +- `store/product/*` - Product events (created, updated, deleted, etc.) +- `store/customer/*` - Customer events (created, updated, deleted, etc.) +- `store/app/uninstalled` - App uninstallation +- `store/cart/abandoned` - Cart abandonment +- `store/category/*` - Category events +- `store/inventory/*` - Inventory events +- `store/shipment/*` - Shipment events + +## Scopes + +Common OAuth scopes for BigCommerce: + +- `store_v2_default` - Default scope with basic read/write access +- `store_v2_products` - Product management +- `store_v2_orders` - Order management +- `store_v2_customers` - Customer management +- `store_v2_content` - Content management +- `store_v2_marketing` - Marketing features +- `store_v2_information` - Store information access + +## Error Handling + +```javascript +try { + const product = await api.getProductById(123); +} catch (error) { + if (error.response?.status === 404) { + console.log('Product not found'); + } else if (error.response?.status === 429) { + console.log('Rate limit exceeded'); + } else { + console.error('API Error:', error.message); + } +} +``` + +## Rate Limiting + +BigCommerce API has rate limits: +- **Standard plans**: 20,000 API calls per hour +- **Plus plans**: 40,000 API calls per hour +- **Pro and Enterprise**: 60,000 API calls per hour + +The module handles rate limiting automatically with appropriate retry strategies. + +## Testing + +```bash +npm test +``` + +## Contributing + +Please read the [contributing guidelines](CONTRIBUTING.md) before submitting pull requests. + +## License + +MIT License - see [LICENSE](LICENSE.md) for details. \ No newline at end of file diff --git a/packages/bigcommerce/api.js b/packages/bigcommerce/api.js new file mode 100644 index 0000000..0e8c215 --- /dev/null +++ b/packages/bigcommerce/api.js @@ -0,0 +1,708 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); +const crypto = require('crypto'); + +// BigCommerce REST API v3 client +// Supports OAuth2 and Access Token authentication +// Documentation: https://developer.bigcommerce.com/api-reference + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.storeHash = get(params, 'storeHash', null); + this.accessToken = get(params, 'accessToken', null); + this.clientId = get(params, 'clientId', process.env.BIGCOMMERCE_CLIENT_ID); + this.clientSecret = get(params, 'clientSecret', process.env.BIGCOMMERCE_CLIENT_SECRET); + + // Use provided access_token or accessToken parameter + this.access_token = get(params, 'access_token', this.accessToken); + + this.baseUrl = `https://api.bigcommerce.com/stores/${this.storeHash}`; + this.version = get(params, 'version', 'v3'); + + this.URLs = { + // Catalog - Products + products: '/v3/catalog/products', + productById: (productId) => `/v3/catalog/products/${productId}`, + productVariants: (productId) => `/v3/catalog/products/${productId}/variants`, + productVariantById: (productId, variantId) => `/v3/catalog/products/${productId}/variants/${variantId}`, + productImages: (productId) => `/v3/catalog/products/${productId}/images`, + productImageById: (productId, imageId) => `/v3/catalog/products/${productId}/images/${imageId}`, + productVideos: (productId) => `/v3/catalog/products/${productId}/videos`, + productReviews: (productId) => `/v3/catalog/products/${productId}/reviews`, + productMetafields: (productId) => `/v3/catalog/products/${productId}/metafields`, + + // Catalog - Categories + categories: '/v3/catalog/categories', + categoryById: (categoryId) => `/v3/catalog/categories/${categoryId}`, + categoryTree: '/v3/catalog/categories/tree', + categoryMetafields: (categoryId) => `/v3/catalog/categories/${categoryId}/metafields`, + + // Catalog - Brands + brands: '/v3/catalog/brands', + brandById: (brandId) => `/v3/catalog/brands/${brandId}`, + brandMetafields: (brandId) => `/v3/catalog/brands/${brandId}/metafields`, + + // Orders + orders: '/v2/orders', + orderById: (orderId) => `/v2/orders/${orderId}`, + orderProducts: (orderId) => `/v2/orders/${orderId}/products`, + orderShippingAddresses: (orderId) => `/v2/orders/${orderId}/shipping_addresses`, + orderCoupons: (orderId) => `/v2/orders/${orderId}/coupons`, + orderMessages: (orderId) => `/v2/orders/${orderId}/messages`, + orderStatuses: '/v2/order_statuses', + orderRefunds: (orderId) => `/v3/orders/${orderId}/payment_actions/refunds`, + + // Customers + customers: '/v3/customers', + customerById: (customerId) => `/v3/customers/${customerId}`, + customerAddresses: (customerId) => `/v3/customers/${customerId}/addresses`, + customerAddressById: (customerId, addressId) => `/v3/customers/${customerId}/addresses/${addressId}`, + customerAttributes: (customerId) => `/v3/customers/${customerId}/attributes`, + customerFormFields: (customerId) => `/v3/customers/${customerId}/form-field-values`, + + // Customer Groups + customerGroups: '/v2/customer_groups', + customerGroupById: (groupId) => `/v2/customer_groups/${groupId}`, + + // Coupons + coupons: '/v2/coupons', + couponById: (couponId) => `/v2/coupons/${couponId}`, + + // Marketing + banners: '/v2/banners', + bannerById: (bannerId) => `/v2/banners/${bannerId}`, + giftCertificates: '/v2/gift_certificates', + giftCertificateById: (giftCertId) => `/v2/gift_certificates/${giftCertId}`, + + // Store Information + storeInfo: '/v2/store', + time: '/v2/time', + timezone: '/v2/timezone', + + // Webhooks + webhooks: '/v3/hooks', + webhookById: (webhookId) => `/v3/hooks/${webhookId}`, + + // Themes + themes: '/v3/themes', + themeById: (themeId) => `/v3/themes/${themeId}`, + themeActions: (themeId) => `/v3/themes/${themeId}/actions`, + themeConfigurations: (themeId) => `/v3/themes/${themeId}/configurations`, + + // Store Content + pages: '/v2/pages', + pageById: (pageId) => `/v2/pages/${pageId}`, + blog: '/v2/blog', + blogPosts: '/v2/blog/posts', + blogPostById: (postId) => `/v2/blog/posts/${postId}`, + blogTags: '/v2/blog/tags', + + // Settings + settings: '/v3/settings', + storeProfile: '/v3/settings/store-profile', + analytics: '/v3/settings/analytics', + + // Shipping + shippingZones: '/v2/shipping/zones', + shippingMethods: '/v2/shipping/methods', + + // Tax + taxClasses: '/v2/tax_classes', + taxClassById: (taxClassId) => `/v2/tax_classes/${taxClassId}`, + + // Payment Methods + paymentMethods: '/v2/payments/methods', + + // Scripts + scripts: '/v3/content/scripts', + scriptById: (scriptId) => `/v3/content/scripts/${scriptId}`, + + // Currencies + currencies: '/v2/currencies', + currencyById: (currencyId) => `/v2/currencies/${currencyId}`, + + // Countries and States + countries: '/v2/countries', + countryById: (countryId) => `/v2/countries/${countryId}`, + countryStates: (countryId) => `/v2/countries/${countryId}/states`, + }; + + // OAuth endpoints + this.authorizationUri = 'https://login.bigcommerce.com/oauth2/authorize'; + this.tokenUri = 'https://login.bigcommerce.com/oauth2/token'; + } + + // Add authentication headers + addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'X-Auth-Token': this.access_token, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + } + + async _get(options) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** OAuth Methods ********************************** + + getAuthUri(scopes = ['store_v2_default'], context = 'stores/{store_hash}') { + const params = new URLSearchParams({ + client_id: this.clientId, + response_type: 'code', + scope: scopes.join(' '), + context: context, + redirect_uri: this.redirect_uri, + }); + + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code, context, scope) { + const tokenData = { + client_id: this.clientId, + client_secret: this.clientSecret, + code: code, + context: context, + scope: scope, + grant_type: 'authorization_code', + redirect_uri: this.redirect_uri, + }; + + const options = { + url: this.tokenUri, + body: tokenData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + }; + + return this._post(options, false); + } + + // ************************** Store Information ********************************** + + async getStoreInfo() { + const options = { + url: this.URLs.storeInfo, + }; + return this._get(options); + } + + async getTime() { + const options = { + url: this.URLs.time, + }; + return this._get(options); + } + + async getTimezone() { + const options = { + url: this.URLs.timezone, + }; + return this._get(options); + } + + // ************************** Products ********************************** + + async createProduct(productData) { + const options = { + url: this.URLs.products, + body: productData, + }; + return this._post(options); + } + + async listProducts(params = {}) { + const options = { + url: this.URLs.products, + query: params + }; + return this._get(options); + } + + async getProductById(id, params = {}) { + const options = { + url: this.URLs.productById(id), + query: params + }; + return this._get(options); + } + + async updateProduct(id, productData) { + const options = { + url: this.URLs.productById(id), + body: productData, + }; + return this._put(options); + } + + async deleteProduct(id) { + const options = { + url: this.URLs.productById(id), + }; + return this._delete(options); + } + + // Product Variants + async createProductVariant(productId, variantData) { + const options = { + url: this.URLs.productVariants(productId), + body: variantData, + }; + return this._post(options); + } + + async listProductVariants(productId, params = {}) { + const options = { + url: this.URLs.productVariants(productId), + query: params + }; + return this._get(options); + } + + async getProductVariantById(productId, variantId, params = {}) { + const options = { + url: this.URLs.productVariantById(productId, variantId), + query: params + }; + return this._get(options); + } + + async updateProductVariant(productId, variantId, variantData) { + const options = { + url: this.URLs.productVariantById(productId, variantId), + body: variantData, + }; + return this._put(options); + } + + async deleteProductVariant(productId, variantId) { + const options = { + url: this.URLs.productVariantById(productId, variantId), + }; + return this._delete(options); + } + + // Product Images + async createProductImage(productId, imageData) { + const options = { + url: this.URLs.productImages(productId), + body: imageData, + }; + return this._post(options); + } + + async listProductImages(productId, params = {}) { + const options = { + url: this.URLs.productImages(productId), + query: params + }; + return this._get(options); + } + + async updateProductImage(productId, imageId, imageData) { + const options = { + url: this.URLs.productImageById(productId, imageId), + body: imageData, + }; + return this._put(options); + } + + async deleteProductImage(productId, imageId) { + const options = { + url: this.URLs.productImageById(productId, imageId), + }; + return this._delete(options); + } + + // ************************** Categories ********************************** + + async createCategory(categoryData) { + const options = { + url: this.URLs.categories, + body: categoryData, + }; + return this._post(options); + } + + async listCategories(params = {}) { + const options = { + url: this.URLs.categories, + query: params + }; + return this._get(options); + } + + async getCategoryById(id, params = {}) { + const options = { + url: this.URLs.categoryById(id), + query: params + }; + return this._get(options); + } + + async updateCategory(id, categoryData) { + const options = { + url: this.URLs.categoryById(id), + body: categoryData, + }; + return this._put(options); + } + + async deleteCategory(id) { + const options = { + url: this.URLs.categoryById(id), + }; + return this._delete(options); + } + + async getCategoryTree() { + const options = { + url: this.URLs.categoryTree, + }; + return this._get(options); + } + + // ************************** Orders ********************************** + + async createOrder(orderData) { + const options = { + url: this.URLs.orders, + body: orderData, + }; + return this._post(options); + } + + async listOrders(params = {}) { + const options = { + url: this.URLs.orders, + query: params + }; + return this._get(options); + } + + async getOrderById(id, params = {}) { + const options = { + url: this.URLs.orderById(id), + query: params + }; + return this._get(options); + } + + async updateOrder(id, orderData) { + const options = { + url: this.URLs.orderById(id), + body: orderData, + }; + return this._put(options); + } + + async deleteOrder(id) { + const options = { + url: this.URLs.orderById(id), + }; + return this._delete(options); + } + + async getOrderProducts(orderId, params = {}) { + const options = { + url: this.URLs.orderProducts(orderId), + query: params + }; + return this._get(options); + } + + async getOrderShippingAddresses(orderId, params = {}) { + const options = { + url: this.URLs.orderShippingAddresses(orderId), + query: params + }; + return this._get(options); + } + + async listOrderStatuses() { + const options = { + url: this.URLs.orderStatuses, + }; + return this._get(options); + } + + async createOrderRefund(orderId, refundData) { + const options = { + url: this.URLs.orderRefunds(orderId), + body: refundData, + }; + return this._post(options); + } + + // ************************** Customers ********************************** + + async createCustomer(customerData) { + const options = { + url: this.URLs.customers, + body: customerData, + }; + return this._post(options); + } + + async listCustomers(params = {}) { + const options = { + url: this.URLs.customers, + query: params + }; + return this._get(options); + } + + async getCustomerById(id, params = {}) { + const options = { + url: this.URLs.customerById(id), + query: params + }; + return this._get(options); + } + + async updateCustomer(id, customerData) { + const options = { + url: this.URLs.customerById(id), + body: customerData, + }; + return this._put(options); + } + + async deleteCustomer(id) { + const options = { + url: this.URLs.customerById(id), + }; + return this._delete(options); + } + + async getCustomerAddresses(customerId, params = {}) { + const options = { + url: this.URLs.customerAddresses(customerId), + query: params + }; + return this._get(options); + } + + async createCustomerAddress(customerId, addressData) { + const options = { + url: this.URLs.customerAddresses(customerId), + body: addressData, + }; + return this._post(options); + } + + async updateCustomerAddress(customerId, addressId, addressData) { + const options = { + url: this.URLs.customerAddressById(customerId, addressId), + body: addressData, + }; + return this._put(options); + } + + async deleteCustomerAddress(customerId, addressId) { + const options = { + url: this.URLs.customerAddressById(customerId, addressId), + }; + return this._delete(options); + } + + // ************************** Webhooks ********************************** + + async createWebhook(webhookData) { + const options = { + url: this.URLs.webhooks, + body: webhookData, + }; + return this._post(options); + } + + async listWebhooks(params = {}) { + const options = { + url: this.URLs.webhooks, + query: params + }; + return this._get(options); + } + + async getWebhookById(id) { + const options = { + url: this.URLs.webhookById(id), + }; + return this._get(options); + } + + async updateWebhook(id, webhookData) { + const options = { + url: this.URLs.webhookById(id), + body: webhookData, + }; + return this._put(options); + } + + async deleteWebhook(id) { + const options = { + url: this.URLs.webhookById(id), + }; + return this._delete(options); + } + + // ************************** Themes ********************************** + + async listThemes() { + const options = { + url: this.URLs.themes, + }; + return this._get(options); + } + + async getThemeById(id) { + const options = { + url: this.URLs.themeById(id), + }; + return this._get(options); + } + + async uploadTheme(themeData) { + const options = { + url: this.URLs.themes, + body: themeData, + }; + return this._post(options); + } + + async downloadTheme(id) { + const options = { + url: `${this.URLs.themeById(id)}/actions/download`, + body: {}, + }; + return this._post(options); + } + + async activateTheme(id, params = {}) { + const options = { + url: `${this.URLs.themeById(id)}/actions/activate`, + body: params, + }; + return this._post(options); + } + + async deleteTheme(id) { + const options = { + url: this.URLs.themeById(id), + }; + return this._delete(options); + } + + async getThemeConfigurations(id) { + const options = { + url: this.URLs.themeConfigurations(id), + }; + return this._get(options); + } + + // ************************** Brands ********************************** + + async createBrand(brandData) { + const options = { + url: this.URLs.brands, + body: brandData, + }; + return this._post(options); + } + + async listBrands(params = {}) { + const options = { + url: this.URLs.brands, + query: params + }; + return this._get(options); + } + + async getBrandById(id, params = {}) { + const options = { + url: this.URLs.brandById(id), + query: params + }; + return this._get(options); + } + + async updateBrand(id, brandData) { + const options = { + url: this.URLs.brandById(id), + body: brandData, + }; + return this._put(options); + } + + async deleteBrand(id) { + const options = { + url: this.URLs.brandById(id), + }; + return this._delete(options); + } + + // ************************** Settings ********************************** + + async getSettings() { + const options = { + url: this.URLs.settings, + }; + return this._get(options); + } + + async getStoreProfile() { + const options = { + url: this.URLs.storeProfile, + }; + return this._get(options); + } + + async updateStoreProfile(profileData) { + const options = { + url: this.URLs.storeProfile, + body: profileData, + }; + return this._put(options); + } + + // ************************** Webhook Verification ********************************** + + verifyWebhookSignature(payload, signature, clientSecret) { + const hash = crypto.createHmac('sha256', clientSecret).update(payload, 'utf8').digest('base64'); + return hash === signature; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/bigcommerce/defaultConfig.json b/packages/bigcommerce/defaultConfig.json new file mode 100644 index 0000000..cf8e854 --- /dev/null +++ b/packages/bigcommerce/defaultConfig.json @@ -0,0 +1,11 @@ +{ + "name": "bigcommerce", + "label": "BigCommerce", + "productUrl": "https://www.bigcommerce.com", + "apiDocs": "https://developer.bigcommerce.com/", + "logoUrl": "https://friggframework.org/assets/img/bigcommerce-icon.png", + "categories": [ + "E-commerce" + ], + "description": "BigCommerce is a leading cloud e-commerce platform that enables businesses of all sizes to build, customize, and scale their online stores." +} \ No newline at end of file diff --git a/packages/bigcommerce/definition.js b/packages/bigcommerce/definition.js new file mode 100644 index 0000000..ac4d1f0 --- /dev/null +++ b/packages/bigcommerce/definition.js @@ -0,0 +1,94 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'BigCommerce', + requiredAuthMethods: { + getToken: async function (api, params) { + // For OAuth flow + const code = get(params.data, 'code'); + const context = get(params.data, 'context'); + const scope = get(params.data, 'scope'); + + if (code && context && scope) { + // OAuth flow + const tokenResponse = await api.getTokenFromCode(code, context, scope); + return { + access_token: tokenResponse.access_token, + scope: tokenResponse.scope, + context: tokenResponse.context, + user: tokenResponse.user, + store_hash: context.replace('stores/', ''), + token_type: 'Bearer' + }; + } + + // Direct access token + const accessToken = get(params.data, 'access_token') || get(params.data, 'accessToken'); + const storeHash = get(params.data, 'store_hash') || get(params.data, 'storeHash'); + + if (!accessToken || !storeHash) { + throw new Error('Missing required BigCommerce credentials: access_token and store_hash'); + } + + return { + access_token: accessToken, + store_hash: storeHash, + token_type: 'Bearer' + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const storeInfo = await api.getStoreInfo(); + + return { + identifiers: {externalId: storeInfo.secure_url, user: userId}, + details: { + name: storeInfo.name, + domain: storeInfo.domain, + secure_url: storeInfo.secure_url, + store_hash: api.storeHash, + plan_name: storeInfo.plan_name, + currency: storeInfo.currency, + timezone: storeInfo.timezone?.name + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'store_hash', 'scope', 'context', 'user' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const storeInfo = await api.getStoreInfo(); + + return { + identifiers: {externalId: storeInfo.secure_url, user: userId}, + details: { + name: storeInfo.name, + domain: storeInfo.domain, + store_hash: api.storeHash + } + }; + }, + testAuthRequest: async function (api) { + return api.getStoreInfo() + }, + }, + env: { + client_id: process.env.BIGCOMMERCE_CLIENT_ID, + client_secret: process.env.BIGCOMMERCE_CLIENT_SECRET, + access_token: process.env.BIGCOMMERCE_ACCESS_TOKEN, + store_hash: process.env.BIGCOMMERCE_STORE_HASH, + redirect_uri: `${process.env.REDIRECT_URI}/bigcommerce`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/bigcommerce/index.js b/packages/bigcommerce/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/bigcommerce/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/bigcommerce/jest.config.js b/packages/bigcommerce/jest.config.js new file mode 100644 index 0000000..fa8c051 --- /dev/null +++ b/packages/bigcommerce/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEnv: ['/tests/setup.js'] +}; \ No newline at end of file diff --git a/packages/bigcommerce/package.json b/packages/bigcommerce/package.json new file mode 100644 index 0000000..375aad8 --- /dev/null +++ b/packages/bigcommerce/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-bigcommerce", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "BigCommerce API module that lets the Frigg Framework interact with BigCommerce", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/bigcommerce/tests/api.test.js b/packages/bigcommerce/tests/api.test.js new file mode 100644 index 0000000..8114116 --- /dev/null +++ b/packages/bigcommerce/tests/api.test.js @@ -0,0 +1,87 @@ +const { Api } = require('../api'); + +describe('BigCommerce API', () => { + let api; + + beforeEach(() => { + api = new Api({ + storeHash: 'test_store_hash', + accessToken: 'test_access_token' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct store hash and access token', () => { + expect(api.storeHash).toBe('test_store_hash'); + expect(api.access_token).toBe('test_access_token'); + expect(api.baseUrl).toBe('https://api.bigcommerce.com/stores/test_store_hash'); + }); + + test('should set OAuth endpoints correctly', () => { + expect(api.authorizationUri).toBe('https://login.bigcommerce.com/oauth2/authorize'); + expect(api.tokenUri).toBe('https://login.bigcommerce.com/oauth2/token'); + }); + }); + + describe('Authentication', () => { + test('should add correct auth headers', () => { + const options = { headers: {} }; + api.addAuthHeaders(options); + + expect(options.headers['X-Auth-Token']).toBe('test_access_token'); + expect(options.headers['Content-Type']).toBe('application/json'); + expect(options.headers['Accept']).toBe('application/json'); + }); + }); + + describe('OAuth Methods', () => { + test('should generate correct auth URI', () => { + api.clientId = 'test_client_id'; + api.redirect_uri = 'https://example.com/callback'; + + const authUri = api.getAuthUri(['store_v2_default'], 'stores/test_hash'); + + expect(authUri).toContain('https://login.bigcommerce.com/oauth2/authorize'); + expect(authUri).toContain('client_id=test_client_id'); + expect(authUri).toContain('scope=store_v2_default'); + expect(authUri).toContain('context=stores%2Ftest_hash'); + }); + }); + + describe('URL Construction', () => { + test('should construct product URLs correctly', () => { + expect(api.URLs.products).toBe('/v3/catalog/products'); + expect(api.URLs.productById(123)).toBe('/v3/catalog/products/123'); + expect(api.URLs.productVariants(123)).toBe('/v3/catalog/products/123/variants'); + }); + + test('should construct order URLs correctly', () => { + expect(api.URLs.orders).toBe('/v2/orders'); + expect(api.URLs.orderById(456)).toBe('/v2/orders/456'); + expect(api.URLs.orderProducts(456)).toBe('/v2/orders/456/products'); + }); + + test('should construct customer URLs correctly', () => { + expect(api.URLs.customers).toBe('/v3/customers'); + expect(api.URLs.customerById(789)).toBe('/v3/customers/789'); + expect(api.URLs.customerAddresses(789)).toBe('/v3/customers/789/addresses'); + }); + + test('should construct theme URLs correctly', () => { + expect(api.URLs.themes).toBe('/v3/themes'); + expect(api.URLs.themeById(101)).toBe('/v3/themes/101'); + expect(api.URLs.themeConfigurations(101)).toBe('/v3/themes/101/configurations'); + }); + }); + + describe('Webhook Verification', () => { + test('should verify webhook signature correctly', () => { + const payload = '{"test": "data"}'; + const clientSecret = 'webhook_secret'; + const signature = 'XM+fUc/+4OMhOaJlEwVK20UqBWLUeHvEBKRRfX8t4OU='; + + const isValid = api.verifyWebhookSignature(payload, signature, clientSecret); + expect(typeof isValid).toBe('boolean'); + }); + }); +}); \ No newline at end of file diff --git a/packages/bigcommerce/tests/setup.js b/packages/bigcommerce/tests/setup.js new file mode 100644 index 0000000..23bfdcc --- /dev/null +++ b/packages/bigcommerce/tests/setup.js @@ -0,0 +1,12 @@ +// Test setup file for BigCommerce API module +require('dotenv').config(); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; \ No newline at end of file diff --git a/packages/bingmail/README.md b/packages/bingmail/README.md new file mode 100644 index 0000000..1da481f --- /dev/null +++ b/packages/bingmail/README.md @@ -0,0 +1,55 @@ +# Bingmail API Module + +Bingmail API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/bingmail +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/bingmail'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +BINGMAIL_CLIENT_ID=your_client_id +BINGMAIL_CLIENT_SECRET=your_client_secret +BINGMAIL_SCOPE=your_scope +BINGMAIL_AUTH_URI=authorization_endpoint +BINGMAIL_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Communication + +## License + +MIT diff --git a/packages/bingmail/api.js b/packages/bingmail/api.js new file mode 100644 index 0000000..0e5f975 --- /dev/null +++ b/packages/bingmail/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Communication'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.BINGMAIL_AUTH_URI; + this.tokenUri = process.env.BINGMAIL_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Bingmail', + MODULE_NAME: 'bingmail', + CATEGORY: 'https://api.bingmail.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/bingmail/defaultConfig.json b/packages/bingmail/defaultConfig.json new file mode 100644 index 0000000..3ff0e4b --- /dev/null +++ b/packages/bingmail/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Bingmail", + "moduleName": "bingmail", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Bingmail API Integration Module", + "category": "Communication", + "apiDocUrl": "https://docs.bingmail.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/bingmail/definition.js b/packages/bingmail/definition.js new file mode 100644 index 0000000..67045b1 --- /dev/null +++ b/packages/bingmail/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Bingmail', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.BINGMAIL_CLIENT_ID, + client_secret: process.env.BINGMAIL_CLIENT_SECRET, + scope: process.env.BINGMAIL_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/bingmail`, + } +}; + +module.exports = {Definition}; diff --git a/packages/bingmail/index.js b/packages/bingmail/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/bingmail/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/bingmail/jest-setup.js b/packages/bingmail/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/bingmail/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/bingmail/jest-teardown.js b/packages/bingmail/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/bingmail/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/bingmail/jest.config.js b/packages/bingmail/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/bingmail/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/bingmail/package.json b/packages/bingmail/package.json new file mode 100644 index 0000000..1a52c0d --- /dev/null +++ b/packages/bingmail/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/bingmail", + "version": "0.0.1", + "description": "Bingmail API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "bingmail" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/bitbucket-by-atlassian/README.md b/packages/bitbucket-by-atlassian/README.md new file mode 100644 index 0000000..9f83ba6 --- /dev/null +++ b/packages/bitbucket-by-atlassian/README.md @@ -0,0 +1,34 @@ +# BitBucket by Atlassian API Integration + +BitBucket by Atlassian integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/bitbucket-by-atlassian +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/bitbucket-by-atlassian'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `BITBUCKET_BY_ATLASSIAN_CLIENT_ID` +- `BITBUCKET_BY_ATLASSIAN_CLIENT_SECRET` +- `BITBUCKET_BY_ATLASSIAN_SCOPE` + +## API Documentation + +For more information about the BitBucket by Atlassian API, visit: https://api.bitbucket.org/2.0 diff --git a/packages/bitbucket-by-atlassian/api.js b/packages/bitbucket-by-atlassian/api.js new file mode 100644 index 0000000..cec0f67 --- /dev/null +++ b/packages/bitbucket-by-atlassian/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class BitBucketbyAtlassianApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.bitbucket.org/2.0'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: BitBucketbyAtlassianApi}; diff --git a/packages/bitbucket-by-atlassian/defaultConfig.json b/packages/bitbucket-by-atlassian/defaultConfig.json new file mode 100644 index 0000000..1a5d5e5 --- /dev/null +++ b/packages/bitbucket-by-atlassian/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "BitBucket by Atlassian", + "moduleName": "bitbucket-by-atlassian", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "BitBucket by Atlassian API Integration Module", + "category": "Developer", + "apiDocUrl": "https://api.bitbucket.org/2.0", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/bitbucket-by-atlassian/definition.js b/packages/bitbucket-by-atlassian/definition.js new file mode 100644 index 0000000..98508d4 --- /dev/null +++ b/packages/bitbucket-by-atlassian/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'BitBucket by Atlassian', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.BITBUCKET_BY_ATLASSIAN_CLIENT_ID, + client_secret: process.env.BITBUCKET_BY_ATLASSIAN_CLIENT_SECRET, + scope: process.env.BITBUCKET_BY_ATLASSIAN_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/bitbucket-by-atlassian`, + } +}; + +module.exports = {Definition}; diff --git a/packages/bitbucket-by-atlassian/index.js b/packages/bitbucket-by-atlassian/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/bitbucket-by-atlassian/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/bitbucket-by-atlassian/package.json b/packages/bitbucket-by-atlassian/package.json new file mode 100644 index 0000000..5e55561 --- /dev/null +++ b/packages/bitbucket-by-atlassian/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/bitbucket-by-atlassian", + "version": "0.0.1", + "description": "BitBucket by Atlassian API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "bitbucket-by-atlassian", + "developer" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/bizpayo/README.md b/packages/bizpayo/README.md new file mode 100644 index 0000000..719fb83 --- /dev/null +++ b/packages/bizpayo/README.md @@ -0,0 +1,55 @@ +# BizPayo API Module + +BizPayo API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/bizpayo +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/bizpayo'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +BIZPAYO_CLIENT_ID=your_client_id +BIZPAYO_CLIENT_SECRET=your_client_secret +BIZPAYO_SCOPE=your_scope +BIZPAYO_AUTH_URI=authorization_endpoint +BIZPAYO_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Finance + +## License + +MIT diff --git a/packages/bizpayo/api.js b/packages/bizpayo/api.js new file mode 100644 index 0000000..e3618ec --- /dev/null +++ b/packages/bizpayo/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Finance'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.BIZPAYO_AUTH_URI; + this.tokenUri = process.env.BIZPAYO_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'BizPayo', + MODULE_NAME: 'bizpayo', + CATEGORY: 'https://api.bizpayo.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/bizpayo/defaultConfig.json b/packages/bizpayo/defaultConfig.json new file mode 100644 index 0000000..689271e --- /dev/null +++ b/packages/bizpayo/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "BizPayo", + "moduleName": "bizpayo", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "BizPayo API Integration Module", + "category": "Finance", + "apiDocUrl": "https://docs.bizpayo.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/bizpayo/definition.js b/packages/bizpayo/definition.js new file mode 100644 index 0000000..42fd8a9 --- /dev/null +++ b/packages/bizpayo/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'BizPayo', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.BIZPAYO_CLIENT_ID, + client_secret: process.env.BIZPAYO_CLIENT_SECRET, + scope: process.env.BIZPAYO_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/bizpayo`, + } +}; + +module.exports = {Definition}; diff --git a/packages/bizpayo/index.js b/packages/bizpayo/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/bizpayo/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/bizpayo/jest-setup.js b/packages/bizpayo/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/bizpayo/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/bizpayo/jest-teardown.js b/packages/bizpayo/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/bizpayo/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/bizpayo/jest.config.js b/packages/bizpayo/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/bizpayo/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/bizpayo/package.json b/packages/bizpayo/package.json new file mode 100644 index 0000000..5d398c0 --- /dev/null +++ b/packages/bizpayo/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/bizpayo", + "version": "0.0.1", + "description": "BizPayo API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "bizpayo" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/boomset/README.md b/packages/boomset/README.md new file mode 100644 index 0000000..44dd3d8 --- /dev/null +++ b/packages/boomset/README.md @@ -0,0 +1,42 @@ +# Boomset API Module + +Frigg API module for Boomset integration. + +## Installation + +```bash +npm install @friggframework/boomset +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/boomset'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +BOOMSET_CLIENT_ID=your_client_id +BOOMSET_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/boomset/api.js b/packages/boomset/api.js new file mode 100644 index 0000000..e1ce98a --- /dev/null +++ b/packages/boomset/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.boomset.com'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.boomset.com/oauth/authorize'; + this.accessTokenUri = 'https://api.boomset.com/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Boomset', + MODULE_NAME: 'boomset', + CATEGORY: 'Marketing', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/boomset/defaultConfig.json b/packages/boomset/defaultConfig.json new file mode 100644 index 0000000..2dfa290 --- /dev/null +++ b/packages/boomset/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Boomset", + "moduleName": "boomset", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Boomset API Integration Module", + "category": "Marketing", + "apiDocUrl": "https://api.boomset.com/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/boomset/definition.js b/packages/boomset/definition.js new file mode 100644 index 0000000..d09cbd7 --- /dev/null +++ b/packages/boomset/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Boomset', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.BOOMSET_CLIENT_ID, + client_secret: process.env.BOOMSET_CLIENT_SECRET, + scope: process.env.BOOMSET_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/boomset`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/boomset/index.js b/packages/boomset/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/boomset/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/boomset/package.json b/packages/boomset/package.json new file mode 100644 index 0000000..b3adde0 --- /dev/null +++ b/packages/boomset/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/boomset", + "version": "0.0.1", + "description": "Boomset API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "boomset", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/box/api.js b/packages/box/api.js new file mode 100644 index 0000000..7fc2d68 --- /dev/null +++ b/packages/box/api.js @@ -0,0 +1,126 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// Box API v2.0 +// https://developer.box.com/ +// Core resources: files, folders, users, collaborations + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.box.com/2.0'; + + this.URLs = { + // Users + me: '/users/me', + + // Files + files: '/files', + fileById: (fileId) => `/files/${fileId}`, + fileContent: (fileId) => `/files/${fileId}/content`, + + // Folders + folders: '/folders', + folderById: (folderId) => `/folders/${folderId}`, + folderItems: (folderId) => `/folders/${folderId}/items`, + + // Search + search: '/search', + }; + + this.authorizationUri = encodeURI( + `https://account.box.com/api/oauth2/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}` + ); + this.tokenUri = 'https://api.box.com/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri, + }, + }; + + const response = await this._post(options); + await this.setTokens(response); + return response; + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + this.refresh_token = get(params, 'refresh_token'); + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addAuthHeaders(options) { + const authHeaders = { + 'Authorization': `Bearer ${this.access_token}`, + 'Accept': 'application/json', + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.me, + }; + return this._get(options); + } + + async getFolderItems(folderId = '0', params = {}) { + const options = { + url: this.baseUrl + this.URLs.folderItems(folderId), + query: params, + }; + return this._get(options); + } + + async getFileById(fileId) { + const options = { + url: this.baseUrl + this.URLs.fileById(fileId), + }; + return this._get(options); + } + + async createFolder(name, parentId = '0') { + const options = { + url: this.baseUrl + this.URLs.folders, + body: { + name, + parent: { id: parentId } + }, + }; + return this._post(options); + } + + async search(query, params = {}) { + const options = { + url: this.baseUrl + this.URLs.search, + query: { query, ...params }, + }; + return this._get(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/box/defaultConfig.json b/packages/box/defaultConfig.json new file mode 100644 index 0000000..058ecfe --- /dev/null +++ b/packages/box/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "box", + "label": "Box", + "productUrl": "https://box.com", + "apiDocs": "https://developer.box.com/", + "logoUrl": "https://images.ctfassets.net/w6r2i5d8q73s/5yoH9Ey6EE8I2q40mSEi8G/cb4a2b9e6d4f4ff85af42ed7f7a4e2ab/box-logo-blue.svg", + "categories": [ + "Enterprise File Storage", + "Content Management", + "Collaboration", + "Security" + ], + "description": "Box is an enterprise cloud content management platform that enables secure file storage, sharing, and collaboration." +} \ No newline at end of file diff --git a/packages/box/definition.js b/packages/box/definition.js new file mode 100644 index 0000000..22d8015 --- /dev/null +++ b/packages/box/definition.js @@ -0,0 +1,43 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Box', + requiredAuthMethods: { + getToken: async (api, params) => { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.id, user: userId }, + details: { name: userDetails.name, email: userDetails.login }, + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token'], + entity: [], + }, + getCredentialDetails: async (api, userId) => { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.id, user: userId }, + details: {}, + }; + }, + testAuthRequest: async (api) => api.getUserDetails(), + }, + env: { + client_id: process.env.BOX_CLIENT_ID, + client_secret: process.env.BOX_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/box`, + }, +}; + +module.exports = { Definition }; diff --git a/packages/box/index.js b/packages/box/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/box/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/box/openapi.json b/packages/box/openapi.json new file mode 100644 index 0000000..62cd3f9 --- /dev/null +++ b/packages/box/openapi.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bbd808742caff839a9c1c85f314a431e5c750c3afad009ea4460cb3ed0b0e4b +size 1665798 diff --git a/packages/braintree/README.md b/packages/braintree/README.md new file mode 100644 index 0000000..4edead0 --- /dev/null +++ b/packages/braintree/README.md @@ -0,0 +1,5 @@ +# Braintree + +This is the API Module for Braintree that allows the [Frigg](https://friggframework.org) code to talk to the Braintree API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/braintree) diff --git a/packages/braintree/api.js b/packages/braintree/api.js new file mode 100644 index 0000000..4aa2560 --- /dev/null +++ b/packages/braintree/api.js @@ -0,0 +1,272 @@ +const { Requester, get } = require('@friggframework/core'); + +class Api extends Requester { + constructor(params) { + super(params); + this.merchantId = get(params, 'merchant_id', process.env.BRAINTREE_MERCHANT_ID); + this.publicKey = get(params, 'public_key', process.env.BRAINTREE_PUBLIC_KEY); + this.privateKey = get(params, 'private_key', process.env.BRAINTREE_PRIVATE_KEY); + this.environment = get(params, 'environment', process.env.BRAINTREE_ENVIRONMENT || 'sandbox'); + + // Set base URL based on environment + this.baseUrl = this.environment === 'production' + ? 'https://api.braintreegateway.com' + : 'https://api.sandbox.braintreegateway.com'; + + this.URLs = { + // Transactions + transactions: '/merchants/*/transactions', + transactionById: (transactionId) => `/merchants/*/transactions/${transactionId}`, + transactionVoid: (transactionId) => `/merchants/*/transactions/${transactionId}/void`, + transactionRefund: (transactionId) => `/merchants/*/transactions/${transactionId}/refund`, + + // Customers + customers: '/merchants/*/customers', + customerById: (customerId) => `/merchants/*/customers/${customerId}`, + + // Payment Methods + paymentMethods: '/merchants/*/payment_methods', + paymentMethodById: (token) => `/merchants/*/payment_methods/${token}`, + + // Subscriptions + subscriptions: '/merchants/*/subscriptions', + subscriptionById: (subscriptionId) => `/merchants/*/subscriptions/${subscriptionId}`, + + // Plans + plans: '/merchants/*/plans', + planById: (planId) => `/merchants/*/plans/${planId}`, + + // Discounts + discounts: '/merchants/*/discounts', + discountById: (discountId) => `/merchants/*/discounts/${discountId}`, + + // Webhooks + webhooks: '/merchants/*/webhooks', + webhookById: (webhookId) => `/merchants/*/webhooks/${webhookId}`, + + // Disputes + disputes: '/merchants/*/disputes', + disputeById: (disputeId) => `/merchants/*/disputes/${disputeId}`, + + // Settlement Batch Summary + settlementBatch: '/merchants/*/settlement_batch_summary' + }; + } + + addAuthHeaders(options) { + // Braintree uses Basic Auth with public:private keys + const credentials = Buffer.from(`${this.publicKey}:${this.privateKey}`).toString('base64'); + const authHeaders = { + 'Authorization': `Basic ${credentials}`, + 'Accept': 'application/xml', + 'Content-Type': 'application/xml' + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + + // Replace merchant placeholder with actual merchant ID + if (options.url.includes('/merchants/*')) { + options.url = options.url.replace('/merchants/*', `/merchants/${this.merchantId}`); + } + } + + async _get(options) { + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _delete(options) { + this.addAuthHeaders(options); + return super._delete(options); + } + + // Helper to convert JS object to XML for Braintree API + objectToXml(obj, rootElement = 'transaction') { + let xml = `<${rootElement}>`; + + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'object' && value !== null) { + xml += this.objectToXml(value, key); + } else { + xml += `<${key}>${value}`; + } + } + + xml += ``; + return xml; + } + + // ************************** Transactions ********************************** + + async createTransaction(transactionData) { + const xmlBody = this.objectToXml(transactionData, 'transaction'); + const options = { + url: this.baseUrl + this.URLs.transactions, + body: xmlBody, + }; + return this._post(options, false); + } + + async searchTransactions(searchCriteria) { + const options = { + url: this.baseUrl + this.URLs.transactions + '/advanced_search', + query: searchCriteria + }; + return this._get(options); + } + + async getTransactionById(transactionId) { + const options = { + url: this.baseUrl + this.URLs.transactionById(transactionId), + }; + return this._get(options); + } + + async voidTransaction(transactionId) { + const options = { + url: this.baseUrl + this.URLs.transactionVoid(transactionId), + }; + return this._put(options, false); + } + + async refundTransaction(transactionId, amount = null) { + const xmlBody = amount ? this.objectToXml({ amount }, 'transaction') : ''; + const options = { + url: this.baseUrl + this.URLs.transactionRefund(transactionId), + body: xmlBody, + }; + return this._post(options, false); + } + + // ************************** Customers ********************************** + + async createCustomer(customerData) { + const xmlBody = this.objectToXml(customerData, 'customer'); + const options = { + url: this.baseUrl + this.URLs.customers, + body: xmlBody, + }; + return this._post(options, false); + } + + async listCustomers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.customers, + query: params + }; + return this._get(options); + } + + async getCustomerById(customerId) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + }; + return this._get(options); + } + + async updateCustomer(customerId, customerData) { + const xmlBody = this.objectToXml(customerData, 'customer'); + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + body: xmlBody, + }; + return this._put(options, false); + } + + async deleteCustomer(customerId) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + }; + return this._delete(options); + } + + // ************************** Payment Methods ********************************** + + async createPaymentMethod(paymentMethodData) { + const xmlBody = this.objectToXml(paymentMethodData, 'payment-method'); + const options = { + url: this.baseUrl + this.URLs.paymentMethods, + body: xmlBody, + }; + return this._post(options, false); + } + + async getPaymentMethod(token) { + const options = { + url: this.baseUrl + this.URLs.paymentMethodById(token), + }; + return this._get(options); + } + + async updatePaymentMethod(token, paymentMethodData) { + const xmlBody = this.objectToXml(paymentMethodData, 'payment-method'); + const options = { + url: this.baseUrl + this.URLs.paymentMethodById(token), + body: xmlBody, + }; + return this._put(options, false); + } + + async deletePaymentMethod(token) { + const options = { + url: this.baseUrl + this.URLs.paymentMethodById(token), + }; + return this._delete(options); + } + + // ************************** Subscriptions ********************************** + + async createSubscription(subscriptionData) { + const xmlBody = this.objectToXml(subscriptionData, 'subscription'); + const options = { + url: this.baseUrl + this.URLs.subscriptions, + body: xmlBody, + }; + return this._post(options, false); + } + + async listSubscriptions(params = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + query: params + }; + return this._get(options); + } + + async getSubscriptionById(subscriptionId) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId), + }; + return this._get(options); + } + + async updateSubscription(subscriptionId, subscriptionData) { + const xmlBody = this.objectToXml(subscriptionData, 'subscription'); + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId), + body: xmlBody, + }; + return this._put(options, false); + } + + async cancelSubscription(subscriptionId) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId) + '/cancel', + }; + return this._put(options, false); + } +} + +module.exports = { Api }; diff --git a/packages/braintree/defaultConfig.json b/packages/braintree/defaultConfig.json new file mode 100644 index 0000000..18dd332 --- /dev/null +++ b/packages/braintree/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "braintree", + "label": "Braintree", + "productUrl": "https://www.braintreepayments.com", + "apiDocs": "https://developer.paypal.com/braintree/docs", + "logoUrl": "https://friggframework.org/assets/img/braintree-icon.png", + "categories": [ + "Payment Processing", + "E-commerce", + "Finance" + ], + "description": "Braintree is a full-stack payment platform that enables businesses to accept, process, and split payments." +} diff --git a/packages/braintree/definition.js b/packages/braintree/definition.js new file mode 100644 index 0000000..ca6e734 --- /dev/null +++ b/packages/braintree/definition.js @@ -0,0 +1,62 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Braintree', + requiredAuthMethods: { + getToken: async function (api, params) { + // Braintree uses API keys, not OAuth + return { + access_token: 'braintree_api_access', + merchant_id: params.merchant_id, + public_key: params.public_key, + private_key: params.private_key, + environment: params.environment + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + // For Braintree, we can use merchant ID as the identifier + return { + identifiers: {externalId: api.merchantId, user: userId}, + details: { + merchantId: api.merchantId, + environment: api.environment + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'merchant_id', 'public_key', 'private_key', 'environment' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + return { + identifiers: {externalId: api.merchantId, user: userId}, + details: { + merchantId: api.merchantId, + environment: api.environment + } + }; + }, + testAuthRequest: async function (api) { + // Test by trying to list customers (should work with valid credentials) + return api.listCustomers({ limit: 1 }) + }, + }, + env: { + merchant_id: process.env.BRAINTREE_MERCHANT_ID, + public_key: process.env.BRAINTREE_PUBLIC_KEY, + private_key: process.env.BRAINTREE_PRIVATE_KEY, + environment: process.env.BRAINTREE_ENVIRONMENT, + } +}; + +module.exports = {Definition}; diff --git a/packages/braintree/index.js b/packages/braintree/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/braintree/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/braintree/jest.config.js b/packages/braintree/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/braintree/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/braintree/package.json b/packages/braintree/package.json new file mode 100644 index 0000000..d02013a --- /dev/null +++ b/packages/braintree/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-braintree", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Braintree API module that lets the Frigg Framework interact with Braintree", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/bulksms/README.md b/packages/bulksms/README.md new file mode 100644 index 0000000..1fc49b8 --- /dev/null +++ b/packages/bulksms/README.md @@ -0,0 +1,34 @@ +# BulkSMS API Integration + +BulkSMS integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/bulksms +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/bulksms'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `BULKSMS_CLIENT_ID` +- `BULKSMS_CLIENT_SECRET` +- `BULKSMS_SCOPE` + +## API Documentation + +For more information about the BulkSMS API, visit: https://api.bulksms.com/v1 diff --git a/packages/bulksms/api.js b/packages/bulksms/api.js new file mode 100644 index 0000000..91108f7 --- /dev/null +++ b/packages/bulksms/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class BulkSMSApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.bulksms.com/v1'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: BulkSMSApi}; diff --git a/packages/bulksms/defaultConfig.json b/packages/bulksms/defaultConfig.json new file mode 100644 index 0000000..fd1974b --- /dev/null +++ b/packages/bulksms/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "BulkSMS", + "moduleName": "bulksms", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "BulkSMS API Integration Module", + "category": "Communication", + "apiDocUrl": "https://api.bulksms.com/v1", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/bulksms/definition.js b/packages/bulksms/definition.js new file mode 100644 index 0000000..aae4a51 --- /dev/null +++ b/packages/bulksms/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'BulkSMS', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.BULKSMS_CLIENT_ID, + client_secret: process.env.BULKSMS_CLIENT_SECRET, + scope: process.env.BULKSMS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/bulksms`, + } +}; + +module.exports = {Definition}; diff --git a/packages/bulksms/index.js b/packages/bulksms/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/bulksms/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/bulksms/package.json b/packages/bulksms/package.json new file mode 100644 index 0000000..5c72bb9 --- /dev/null +++ b/packages/bulksms/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/bulksms", + "version": "0.0.1", + "description": "BulkSMS API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "bulksms", + "communication" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/calendly/README.md b/packages/calendly/README.md new file mode 100644 index 0000000..89ac247 --- /dev/null +++ b/packages/calendly/README.md @@ -0,0 +1,237 @@ +# Calendly API Module + +A comprehensive Calendly API v2 integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +CALENDLY_CLIENT_ID=your_calendly_client_id +CALENDLY_CLIENT_SECRET=your_calendly_client_secret +CALENDLY_SCOPE=default +REDIRECT_URI=your_redirect_uri_base +``` + +### Getting Calendly API Credentials + +1. Go to the [Calendly Developer Portal](https://developer.calendly.com/) +2. Sign in with your Calendly account +3. Create a new app in your developer dashboard +4. Get your Client ID and Client Secret +5. Set up your redirect URI (e.g., `https://yourdomain.com/calendly`) + +### OAuth2 Scopes + +Calendly uses a single scope system. Use `default` for most applications, which provides access to: +- Read user profile information +- Read event types +- Read scheduled events and invitees +- Manage webhooks +- Read organization data + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-calendly'); + +// Initialize with credentials +const calendlyApi = new Api({ + client_id: process.env.CALENDLY_CLIENT_ID, + client_secret: process.env.CALENDLY_CLIENT_SECRET, + redirect_uri: process.env.REDIRECT_URI + '/calendly', + scope: 'default' +}); + +// Get authorization URL +const authUrl = calendlyApi.getAuthUri(); + +// Exchange code for tokens +const tokens = await calendlyApi.getTokenFromCode(authorizationCode); + +// Get current user +const user = await calendlyApi.getCurrentUser(); + +// Get user's event types +const eventTypes = await calendlyApi.getUserEventTypes(user.resource.uri); +``` + +## Available Methods + +### Users Methods +- `getCurrentUser()` - Get current authenticated user +- `getUser(userUri)` - Get specific user by URI + +### Organizations Methods +- `getOrganizationMemberships(params)` - Get organization memberships +- `getOrganization(organizationUri)` - Get organization details + +### Event Types Methods +- `getEventTypes(params)` - Get event types with filters +- `getUserEventTypes(userUri, params)` - Get event types for specific user +- `getEventType(eventTypeUri)` - Get specific event type + +### Scheduled Events Methods +- `getScheduledEvents(params)` - Get scheduled events with filters +- `getScheduledEvent(eventUri)` - Get specific scheduled event +- `getScheduledEventInvitees(eventUri, params)` - Get invitees for event +- `cancelScheduledEvent(eventUri, reason)` - Cancel a scheduled event + +### Invitees Methods +- `getInvitee(inviteeUri)` - Get specific invitee +- `createInviteeNoShow(inviteeUri)` - Mark invitee as no-show +- `deleteInviteeNoShow(inviteeUri)` - Remove no-show status + +### Webhooks Methods +- `getWebhooks(params)` - List webhook subscriptions +- `createWebhook(webhookData)` - Create webhook subscription +- `getWebhook(webhookUri)` - Get webhook details +- `deleteWebhook(webhookUri)` - Delete webhook subscription + +### Availability Methods +- `getUserAvailabilitySchedules(userUri, params)` - Get user's availability schedules +- `getAvailabilitySchedule(scheduleUri)` - Get specific availability schedule + +### Routing Forms Methods +- `getRoutingForms(params)` - Get routing forms +- `getRoutingForm(formUri)` - Get specific routing form +- `getRoutingFormSubmissions(formUri, params)` - Get form submissions + +### Helper Methods +- `getUserScheduledEvents(userUri, params)` - Get events for specific user +- `getOrganizationScheduledEvents(organizationUri, params)` - Get events for organization +- `getUserUpcomingEvents(userUri, maxStartTime)` - Get upcoming events for user +- `extractUriFromUrl(url)` - Extract URI from full Calendly URLs + +## Usage Examples + +### Getting User's Event Types +```javascript +const user = await calendlyApi.getCurrentUser(); +const eventTypes = await calendlyApi.getUserEventTypes(user.resource.uri); + +console.log('Available event types:'); +eventTypes.collection.forEach(eventType => { + console.log(`- ${eventType.name}: ${eventType.scheduling_url}`); +}); +``` + +### Getting Scheduled Events +```javascript +const user = await calendlyApi.getCurrentUser(); + +// Get events for the next 30 days +const upcomingEvents = await calendlyApi.getUserUpcomingEvents(user.resource.uri); + +console.log('Upcoming events:'); +upcomingEvents.collection.forEach(event => { + console.log(`- ${event.name} at ${event.start_time}`); +}); +``` + +### Setting up Webhooks +```javascript +const user = await calendlyApi.getCurrentUser(); + +const webhookData = { + url: 'https://yoursite.com/webhooks/calendly', + events: [ + 'invitee.created', + 'invitee.canceled' + ], + organization: user.resource.current_organization, + scope: 'organization' +}; + +const webhook = await calendlyApi.createWebhook(webhookData); +console.log('Webhook created:', webhook.resource.uri); +``` + +### Getting Event Details with Invitees +```javascript +const events = await calendlyApi.getScheduledEvents({ + user: userUri, + status: 'active' +}); + +for (const event of events.collection) { + const invitees = await calendlyApi.getScheduledEventInvitees(event.uri); + + console.log(`Event: ${event.name}`); + console.log(`Invitees: ${invitees.collection.length}`); + + invitees.collection.forEach(invitee => { + console.log(`- ${invitee.name} (${invitee.email})`); + }); +} +``` + +### Canceling an Event +```javascript +const reason = 'Meeting needs to be rescheduled due to emergency'; +await calendlyApi.cancelScheduledEvent(eventUri, reason); +console.log('Event canceled successfully'); +``` + +### Working with Organizations +```javascript +const memberships = await calendlyApi.getOrganizationMemberships(); + +for (const membership of memberships.collection) { + const org = await calendlyApi.getOrganization(membership.organization); + console.log(`Organization: ${org.resource.name}`); +} +``` + +## URI Handling + +Calendly API uses URIs (not IDs) to reference resources. URIs look like: +- User: `https://api.calendly.com/users/AAAAAAAAAAAAAAAA` +- Event Type: `https://api.calendly.com/event_types/AAAAAAAAAAAAAAAA` +- Scheduled Event: `https://api.calendly.com/scheduled_events/AAAAAAAAAAAAAAAA` + +The module includes a helper method `extractUriFromUrl()` to work with these URIs. + +## Authentication Flow + +Calendly uses OAuth2: + +1. Redirect users to Calendly's authorization URL +2. Handle the callback with the authorization code +3. Exchange the code for access and refresh tokens +4. Use tokens for API requests + +## Error Handling + +Calendly returns detailed error information. Always wrap API calls in try-catch blocks: + +```javascript +try { + const events = await calendlyApi.getScheduledEvents(); + console.log('Events retrieved successfully'); +} catch (error) { + console.error('Calendly error:', error.message); + if (error.details) { + console.error('Error details:', error.details); + } +} +``` + +## Rate Limiting + +Calendly enforces rate limits on API requests. The module does not implement automatic retry logic - you should handle rate limiting in your application. + +## Webhooks + +Calendly can send webhooks for various events: +- `invitee.created` - New booking created +- `invitee.canceled` - Booking canceled +- `invitee.rescheduled` - Booking rescheduled +- `invitee_no_show.created` - Invitee marked as no-show +- `invitee_no_show.deleted` - No-show status removed + +## Documentation + +For detailed Calendly API documentation, visit: https://developer.calendly.com/ \ No newline at end of file diff --git a/packages/calendly/api.js b/packages/calendly/api.js new file mode 100644 index 0000000..e8f016a --- /dev/null +++ b/packages/calendly/api.js @@ -0,0 +1,317 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.calendly.com'; + + this.URLs = { + authorization: '/oauth/authorize', + access_token: '/oauth/token', + + // Users + currentUser: '/users/me', + userById: (userUri) => `/users/${encodeURIComponent(userUri)}`, + + // Organizations + organizationMemberships: '/organization_memberships', + organizations: '/organizations', + organizationById: (organizationUri) => `/organizations/${encodeURIComponent(organizationUri)}`, + + // Event Types + eventTypes: '/event_types', + eventTypeById: (eventTypeUri) => `/event_types/${encodeURIComponent(eventTypeUri)}`, + userEventTypes: (userUri) => `/event_types?user=${encodeURIComponent(userUri)}`, + + // Scheduled Events + scheduledEvents: '/scheduled_events', + scheduledEventById: (eventUri) => `/scheduled_events/${encodeURIComponent(eventUri)}`, + scheduledEventInvitees: (eventUri) => `/scheduled_events/${encodeURIComponent(eventUri)}/invitees`, + + // Invitees + invitees: '/scheduled_events/invitees', + inviteeById: (inviteeUri) => `/scheduled_events/invitees/${encodeURIComponent(inviteeUri)}`, + + // Webhooks + webhooks: '/webhook_subscriptions', + webhookById: (webhookUri) => `/webhook_subscriptions/${encodeURIComponent(webhookUri)}`, + + // Availability + userAvailabilitySchedules: (userUri) => `/user_availability_schedules?user=${encodeURIComponent(userUri)}`, + availabilityScheduleById: (scheduleUri) => `/user_availability_schedules/${encodeURIComponent(scheduleUri)}`, + + // Routing Forms + routingForms: '/routing_forms', + routingFormById: (formUri) => `/routing_forms/${encodeURIComponent(formUri)}`, + routingFormSubmissions: (formUri) => `/routing_form_submissions?form=${encodeURIComponent(formUri)}`, + }; + + this.authorizationUri = encodeURI( + `https://auth.calendly.com/oauth/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&scope=${this.scope}&state=${this.state}` + ); + this.tokenUri = 'https://auth.calendly.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** Users Methods ********************************** + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.currentUser, + }; + return this._get(options); + } + + async getUser(userUri) { + const options = { + url: this.baseUrl + this.URLs.userById(userUri), + }; + return this._get(options); + } + + // ************************** Organizations Methods ********************************** + + async getOrganizationMemberships(params = {}) { + const options = { + url: this.baseUrl + this.URLs.organizationMemberships, + query: params, + }; + return this._get(options); + } + + async getOrganization(organizationUri) { + const options = { + url: this.baseUrl + this.URLs.organizationById(organizationUri), + }; + return this._get(options); + } + + // ************************** Event Types Methods ********************************** + + async getEventTypes(params = {}) { + const options = { + url: this.baseUrl + this.URLs.eventTypes, + query: params, + }; + return this._get(options); + } + + async getUserEventTypes(userUri, params = {}) { + const options = { + url: this.baseUrl + this.URLs.userEventTypes(userUri), + query: params, + }; + return this._get(options); + } + + async getEventType(eventTypeUri) { + const options = { + url: this.baseUrl + this.URLs.eventTypeById(eventTypeUri), + }; + return this._get(options); + } + + // ************************** Scheduled Events Methods ********************************** + + async getScheduledEvents(params = {}) { + const options = { + url: this.baseUrl + this.URLs.scheduledEvents, + query: params, + }; + return this._get(options); + } + + async getScheduledEvent(eventUri) { + const options = { + url: this.baseUrl + this.URLs.scheduledEventById(eventUri), + }; + return this._get(options); + } + + async getScheduledEventInvitees(eventUri, params = {}) { + const options = { + url: this.baseUrl + this.URLs.scheduledEventInvitees(eventUri), + query: params, + }; + return this._get(options); + } + + async cancelScheduledEvent(eventUri, reason = '') { + const options = { + url: this.baseUrl + this.URLs.scheduledEventById(eventUri) + '/cancellation', + body: { reason }, + }; + return this._post(options); + } + + // ************************** Invitees Methods ********************************** + + async getInvitee(inviteeUri) { + const options = { + url: this.baseUrl + this.URLs.inviteeById(inviteeUri), + }; + return this._get(options); + } + + async createInviteeNoShow(inviteeUri) { + const options = { + url: this.baseUrl + this.URLs.inviteeById(inviteeUri) + '/no_show', + body: {}, + }; + return this._post(options); + } + + async deleteInviteeNoShow(inviteeUri) { + const options = { + url: this.baseUrl + this.URLs.inviteeById(inviteeUri) + '/no_show', + }; + return this._delete(options); + } + + // ************************** Webhooks Methods ********************************** + + async getWebhooks(params = {}) { + const options = { + url: this.baseUrl + this.URLs.webhooks, + query: params, + }; + return this._get(options); + } + + async createWebhook(webhookData) { + const options = { + url: this.baseUrl + this.URLs.webhooks, + body: webhookData, + }; + return this._post(options); + } + + async getWebhook(webhookUri) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookUri), + }; + return this._get(options); + } + + async deleteWebhook(webhookUri) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookUri), + }; + return this._delete(options); + } + + // ************************** Availability Methods ********************************** + + async getUserAvailabilitySchedules(userUri, params = {}) { + const options = { + url: this.baseUrl + this.URLs.userAvailabilitySchedules(userUri), + query: params, + }; + return this._get(options); + } + + async getAvailabilitySchedule(scheduleUri) { + const options = { + url: this.baseUrl + this.URLs.availabilityScheduleById(scheduleUri), + }; + return this._get(options); + } + + // ************************** Routing Forms Methods ********************************** + + async getRoutingForms(params = {}) { + const options = { + url: this.baseUrl + this.URLs.routingForms, + query: params, + }; + return this._get(options); + } + + async getRoutingForm(formUri) { + const options = { + url: this.baseUrl + this.URLs.routingFormById(formUri), + }; + return this._get(options); + } + + async getRoutingFormSubmissions(formUri, params = {}) { + const options = { + url: this.baseUrl + this.URLs.routingFormSubmissions(formUri), + query: params, + }; + return this._get(options); + } + + // ************************** Helper Methods ********************************** + + async getUserScheduledEvents(userUri, params = {}) { + const eventParams = { + user: userUri, + ...params, + }; + return this.getScheduledEvents(eventParams); + } + + async getOrganizationScheduledEvents(organizationUri, params = {}) { + const eventParams = { + organization: organizationUri, + ...params, + }; + return this.getScheduledEvents(eventParams); + } + + async getUserUpcomingEvents(userUri, maxStartTime) { + const now = new Date().toISOString(); + const params = { + user: userUri, + min_start_time: now, + max_start_time: maxStartTime || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + status: 'active', + sort: 'start_time:asc', + }; + return this.getScheduledEvents(params); + } + + extractUriFromUrl(url) { + // Calendly API often returns full URLs, but we need just the URI part + if (url.includes('api.calendly.com')) { + return url.split('api.calendly.com')[1]; + } + return url; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/calendly/defaultConfig.json b/packages/calendly/defaultConfig.json new file mode 100644 index 0000000..4917c09 --- /dev/null +++ b/packages/calendly/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "calendly", + "label": "Calendly", + "productUrl": "https://calendly.com", + "apiDocs": "https://developer.calendly.com/", + "logoUrl": "https://assets.calendly.com/assets/frontend/media/logo-square-cd364a3ffebbce72fd57.png", + "categories": [ + "Scheduling", + "Calendar", + "Appointments", + "Productivity" + ], + "description": "Calendly is a scheduling platform that eliminates the back-and-forth emails to find the perfect time" +} \ No newline at end of file diff --git a/packages/calendly/definition.js b/packages/calendly/definition.js new file mode 100644 index 0000000..418af16 --- /dev/null +++ b/packages/calendly/definition.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Calendly', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getCurrentUser(); + return { + identifiers: { externalId: userInfo.resource.uri, user: userId }, + details: { + name: userInfo.resource.name, + email: userInfo.resource.email + } + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getCurrentUser(); + return { + identifiers: { externalId: userInfo.resource.uri, user: userId }, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CALENDLY_CLIENT_ID, + client_secret: process.env.CALENDLY_CLIENT_SECRET, + scope: process.env.CALENDLY_SCOPE || 'default', + redirect_uri: `${process.env.REDIRECT_URI}/calendly`, + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/calendly/index.js b/packages/calendly/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/calendly/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/campaign-monitor/README.md b/packages/campaign-monitor/README.md new file mode 100644 index 0000000..4dc77b0 --- /dev/null +++ b/packages/campaign-monitor/README.md @@ -0,0 +1,5 @@ +# Campaign Monitor + +This is the API Module for Campaign Monitor that allows the [Frigg](https://friggframework.org) code to talk to the Campaign Monitor API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/campaign-monitor) diff --git a/packages/campaign-monitor/api.js b/packages/campaign-monitor/api.js new file mode 100644 index 0000000..bb79f08 --- /dev/null +++ b/packages/campaign-monitor/api.js @@ -0,0 +1,325 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.createsend.com/api/v3.3'; + + this.URLs = { + // Authentication + userInfo: '/account', + + // Clients + clients: '/clients', + clientById: (clientId) => `/clients/${clientId}`, + clientDetails: (clientId) => `/clients/${clientId}`, + + // Lists + lists: (clientId) => `/clients/${clientId}/lists`, + listById: (listId) => `/lists/${listId}`, + listStats: (listId) => `/lists/${listId}/stats`, + listSegments: (listId) => `/lists/${listId}/segments`, + + // Subscribers + subscribers: (listId) => `/lists/${listId}/subscribers`, + subscriberByEmail: (listId, email) => `/lists/${listId}/subscribers/${email}`, + subscriberHistory: (listId, email) => `/lists/${listId}/subscribers/${email}/history`, + + // Campaigns + campaigns: (clientId) => `/clients/${clientId}/campaigns`, + campaignById: (campaignId) => `/campaigns/${campaignId}`, + campaignSummary: (campaignId) => `/campaigns/${campaignId}/summary`, + campaignBounces: (campaignId) => `/campaigns/${campaignId}/bounces`, + campaignClicks: (campaignId) => `/campaigns/${campaignId}/clicks`, + campaignOpens: (campaignId) => `/campaigns/${campaignId}/opens`, + campaignUnsubscribes: (campaignId) => `/campaigns/${campaignId}/unsubscribes`, + + // Templates + templates: (clientId) => `/clients/${clientId}/templates`, + templateById: (templateId) => `/templates/${templateId}`, + + // Segments + segmentById: (segmentId) => `/segments/${segmentId}`, + segmentSubscribers: (segmentId) => `/segments/${segmentId}/subscribers`, + + // Journey + journeys: (clientId) => `/clients/${clientId}/journeys`, + journeyById: (journeyId) => `/journeys/${journeyId}`, + journeyEmails: (journeyId) => `/journeys/${journeyId}/emails`, + + // Transactional + transactionalSend: '/transactional/classicEmail/send', + transactionalStats: '/transactional/statistics' + }; + + this.authorizationUri = encodeURI( + `https://api.createsend.com/oauth?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.createsend.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // ************************** Clients ********************************** + + async listClients() { + const options = { + url: this.baseUrl + this.URLs.clients, + }; + return this._get(options); + } + + async getClientById(clientId) { + const options = { + url: this.baseUrl + this.URLs.clientById(clientId), + }; + return this._get(options); + } + + async createClient(clientData) { + const options = { + url: this.baseUrl + this.URLs.clients, + body: clientData, + }; + return this._post(options); + } + + async updateClient(clientId, clientData) { + const options = { + url: this.baseUrl + this.URLs.clientById(clientId), + body: clientData, + }; + return this._put(options); + } + + async deleteClient(clientId) { + const options = { + url: this.baseUrl + this.URLs.clientById(clientId), + }; + return this._delete(options); + } + + // ************************** Lists ********************************** + + async listSubscriberLists(clientId) { + const options = { + url: this.baseUrl + this.URLs.lists(clientId), + }; + return this._get(options); + } + + async createList(clientId, listData) { + const options = { + url: this.baseUrl + this.URLs.lists(clientId), + body: listData, + }; + return this._post(options); + } + + async getListById(listId) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + }; + return this._get(options); + } + + async updateList(listId, listData) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + body: listData, + }; + return this._put(options); + } + + async deleteList(listId) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + }; + return this._delete(options); + } + + async getListStats(listId) { + const options = { + url: this.baseUrl + this.URLs.listStats(listId), + }; + return this._get(options); + } + + // ************************** Subscribers ********************************** + + async listSubscribers(listId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.subscribers(listId), + query: params + }; + return this._get(options); + } + + async addSubscriber(listId, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.subscribers(listId), + body: subscriberData, + }; + return this._post(options); + } + + async getSubscriberByEmail(listId, email) { + const options = { + url: this.baseUrl + this.URLs.subscriberByEmail(listId, encodeURIComponent(email)), + }; + return this._get(options); + } + + async updateSubscriber(listId, email, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.subscriberByEmail(listId, encodeURIComponent(email)), + body: subscriberData, + }; + return this._put(options); + } + + async unsubscribeSubscriber(listId, email) { + const options = { + url: this.baseUrl + this.URLs.subscriberByEmail(listId, encodeURIComponent(email)) + '/unsubscribe', + }; + return this._post(options, false); + } + + async deleteSubscriber(listId, email) { + const options = { + url: this.baseUrl + this.URLs.subscriberByEmail(listId, encodeURIComponent(email)), + }; + return this._delete(options); + } + + // ************************** Campaigns ********************************** + + async listCampaigns(clientId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.campaigns(clientId), + query: params + }; + return this._get(options); + } + + async createCampaign(clientId, campaignData) { + const options = { + url: this.baseUrl + this.URLs.campaigns(clientId), + body: campaignData, + }; + return this._post(options); + } + + async getCampaignById(campaignId) { + const options = { + url: this.baseUrl + this.URLs.campaignById(campaignId), + }; + return this._get(options); + } + + async sendCampaign(campaignId, sendData) { + const options = { + url: this.baseUrl + this.URLs.campaignById(campaignId) + '/send', + body: sendData, + }; + return this._post(options); + } + + async getCampaignSummary(campaignId) { + const options = { + url: this.baseUrl + this.URLs.campaignSummary(campaignId), + }; + return this._get(options); + } + + async getCampaignBounces(campaignId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.campaignBounces(campaignId), + query: params + }; + return this._get(options); + } + + async getCampaignClicks(campaignId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.campaignClicks(campaignId), + query: params + }; + return this._get(options); + } + + async getCampaignOpens(campaignId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.campaignOpens(campaignId), + query: params + }; + return this._get(options); + } + + // ************************** Templates ********************************** + + async listTemplates(clientId) { + const options = { + url: this.baseUrl + this.URLs.templates(clientId), + }; + return this._get(options); + } + + async createTemplate(clientId, templateData) { + const options = { + url: this.baseUrl + this.URLs.templates(clientId), + body: templateData, + }; + return this._post(options); + } + + async getTemplateById(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + }; + return this._get(options); + } + + async updateTemplate(templateId, templateData) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + body: templateData, + }; + return this._put(options); + } + + async deleteTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + }; + return this._delete(options); + } + + // ************************** Transactional ********************************** + + async sendTransactionalEmail(emailData) { + const options = { + url: this.baseUrl + this.URLs.transactionalSend, + body: emailData, + }; + return this._post(options); + } + + async getTransactionalStats(params = {}) { + const options = { + url: this.baseUrl + this.URLs.transactionalStats, + query: params + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/campaign-monitor/defaultConfig.json b/packages/campaign-monitor/defaultConfig.json new file mode 100644 index 0000000..ae1dd2e --- /dev/null +++ b/packages/campaign-monitor/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "campaign-monitor", + "label": "Campaign Monitor", + "productUrl": "https://www.campaignmonitor.com", + "apiDocs": "https://www.campaignmonitor.com/api/", + "logoUrl": "https://friggframework.org/assets/img/campaign-monitor-icon.png", + "categories": [ + "Email Marketing", + "Marketing Automation", + "Communication" + ], + "description": "Campaign Monitor is an email marketing platform that helps businesses create, send, and optimize their email campaigns." +} diff --git a/packages/campaign-monitor/definition.js b/packages/campaign-monitor/definition.js new file mode 100644 index 0000000..6d88e78 --- /dev/null +++ b/packages/campaign-monitor/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'CampaignMonitor', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.AccountID, user: userId}, + details: {name: userDetails.Name, email: userDetails.EmailAddress}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.AccountID, user: userId}, + details: {name: userDetails.Name} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.CAMPAIGN_MONITOR_CLIENT_ID, + client_secret: process.env.CAMPAIGN_MONITOR_CLIENT_SECRET, + scope: process.env.CAMPAIGN_MONITOR_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/campaign-monitor`, + } +}; + +module.exports = {Definition}; diff --git a/packages/campaign-monitor/index.js b/packages/campaign-monitor/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/campaign-monitor/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/campaign-monitor/jest.config.js b/packages/campaign-monitor/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/campaign-monitor/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/campaign-monitor/package.json b/packages/campaign-monitor/package.json new file mode 100644 index 0000000..7db12c1 --- /dev/null +++ b/packages/campaign-monitor/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-campaign-monitor", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Campaign Monitor API module that lets the Frigg Framework interact with Campaign Monitor", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cannabiz/README.md b/packages/cannabiz/README.md new file mode 100644 index 0000000..43279c3 --- /dev/null +++ b/packages/cannabiz/README.md @@ -0,0 +1,34 @@ +# Cannabiz API Integration + +Cannabiz integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/cannabiz +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cannabiz'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `CANNABIZ_CLIENT_ID` +- `CANNABIZ_CLIENT_SECRET` +- `CANNABIZ_SCOPE` + +## API Documentation + +For more information about the Cannabiz API, visit: https://api.cannabiz.com diff --git a/packages/cannabiz/api.js b/packages/cannabiz/api.js new file mode 100644 index 0000000..b28549a --- /dev/null +++ b/packages/cannabiz/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class CannabizApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.cannabiz.com'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: CannabizApi}; diff --git a/packages/cannabiz/defaultConfig.json b/packages/cannabiz/defaultConfig.json new file mode 100644 index 0000000..d80c995 --- /dev/null +++ b/packages/cannabiz/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Cannabiz", + "moduleName": "cannabiz", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Cannabiz API Integration Module", + "category": "E-commerce", + "apiDocUrl": "https://api.cannabiz.com", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cannabiz/definition.js b/packages/cannabiz/definition.js new file mode 100644 index 0000000..2eee3ed --- /dev/null +++ b/packages/cannabiz/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Cannabiz', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CANNABIZ_CLIENT_ID, + client_secret: process.env.CANNABIZ_CLIENT_SECRET, + scope: process.env.CANNABIZ_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cannabiz`, + } +}; + +module.exports = {Definition}; diff --git a/packages/cannabiz/index.js b/packages/cannabiz/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/cannabiz/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/cannabiz/package.json b/packages/cannabiz/package.json new file mode 100644 index 0000000..dad94e7 --- /dev/null +++ b/packages/cannabiz/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/cannabiz", + "version": "0.0.1", + "description": "Cannabiz API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "cannabiz", + "e-commerce" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/v1-ready/canva/README.md b/packages/canva/README.md similarity index 100% rename from packages/v1-ready/canva/README.md rename to packages/canva/README.md diff --git a/packages/v1-ready/canva/fenestra/platform.fenestra.yaml b/packages/canva/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/canva/fenestra/platform.fenestra.yaml rename to packages/canva/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/canva/fenestra/schemas/canva-validation.json b/packages/canva/fenestra/schemas/canva-validation.json similarity index 100% rename from packages/v1-ready/canva/fenestra/schemas/canva-validation.json rename to packages/canva/fenestra/schemas/canva-validation.json diff --git a/packages/v1-ready/canva/index.js b/packages/canva/index.js similarity index 100% rename from packages/v1-ready/canva/index.js rename to packages/canva/index.js diff --git a/packages/v1-ready/canva/package.json b/packages/canva/package.json similarity index 100% rename from packages/v1-ready/canva/package.json rename to packages/canva/package.json diff --git a/packages/chargebee/README.md b/packages/chargebee/README.md new file mode 100644 index 0000000..2911569 --- /dev/null +++ b/packages/chargebee/README.md @@ -0,0 +1,5 @@ +# Chargebee + +This is the API Module for Chargebee that allows the [Frigg](https://friggframework.org) code to talk to the Chargebee API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/chargebee) diff --git a/packages/chargebee/api.js b/packages/chargebee/api.js new file mode 100644 index 0000000..a3f19a2 --- /dev/null +++ b/packages/chargebee/api.js @@ -0,0 +1,320 @@ +const { Requester, get } = require('@friggframework/core'); + +class Api extends Requester { + constructor(params) { + super(params); + this.apiKey = get(params, 'api_key', process.env.CHARGEBEE_API_KEY); + this.siteName = get(params, 'site_name', process.env.CHARGEBEE_SITE_NAME); + this.baseUrl = `https://${this.siteName}.chargebee.com/api/v2`; + + this.URLs = { + // Customers + customers: '/customers', + customerById: (customerId) => `/customers/${customerId}`, + + // Subscriptions + subscriptions: '/subscriptions', + subscriptionById: (subscriptionId) => `/subscriptions/${subscriptionId}`, + subscriptionPause: (subscriptionId) => `/subscriptions/${subscriptionId}/pause`, + subscriptionResume: (subscriptionId) => `/subscriptions/${subscriptionId}/resume`, + subscriptionCancel: (subscriptionId) => `/subscriptions/${subscriptionId}/cancel`, + + // Plans + plans: '/plans', + planById: (planId) => `/plans/${planId}`, + + // Addons + addons: '/addons', + addonById: (addonId) => `/addons/${addonId}`, + + // Coupons + coupons: '/coupons', + couponById: (couponId) => `/coupons/${couponId}`, + + // Invoices + invoices: '/invoices', + invoiceById: (invoiceId) => `/invoices/${invoiceId}`, + invoiceCollect: (invoiceId) => `/invoices/${invoiceId}/collect_payment`, + + // Transactions + transactions: '/transactions', + transactionById: (transactionId) => `/transactions/${transactionId}`, + + // Events + events: '/events', + eventById: (eventId) => `/events/${eventId}`, + + // Payment Sources + paymentSources: '/payment_sources', + paymentSourceById: (paymentSourceId) => `/payment_sources/${paymentSourceId}`, + + // Credit Notes + creditNotes: '/credit_notes', + creditNoteById: (creditNoteId) => `/credit_notes/${creditNoteId}`, + + // Hosted Pages + hostedPages: '/hosted_pages', + hostedPageById: (hostedPageId) => `/hosted_pages/${hostedPageId}`, + + // Estimates + estimates: '/estimates', + + // Portal Sessions + portalSessions: '/portal_sessions' + }; + } + + addAuthHeaders(options) { + // Chargebee uses Basic Auth with API key as username + const credentials = Buffer.from(`${this.apiKey}:`).toString('base64'); + const authHeaders = { + 'Authorization': `Basic ${credentials}`, + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + // Helper to convert object to URL-encoded string for Chargebee API + objectToUrlEncoded(obj, prefix = '') { + const str = []; + for (const p in obj) { + if (obj.hasOwnProperty(p)) { + const k = prefix ? `${prefix}[${p}]` : p; + const v = obj[p]; + str.push((v !== null && typeof v === 'object') ? + this.objectToUrlEncoded(v, k) : + `${encodeURIComponent(k)}=${encodeURIComponent(v)}`); + } + } + return str.join('&'); + } + + async _get(options) { + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + this.addAuthHeaders(options); + // Convert body to URL-encoded format for Chargebee + if (options.body && typeof options.body === 'object') { + options.body = this.objectToUrlEncoded(options.body); + } + return super._post(options, false); + } + + async _put(options, stringify = true) { + this.addAuthHeaders(options); + if (options.body && typeof options.body === 'object') { + options.body = this.objectToUrlEncoded(options.body); + } + return super._put(options, false); + } + + async _delete(options) { + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Customers ********************************** + + async createCustomer(customerData) { + const options = { + url: this.baseUrl + this.URLs.customers, + body: customerData, + }; + return this._post(options); + } + + async listCustomers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.customers, + query: params + }; + return this._get(options); + } + + async getCustomerById(customerId) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + }; + return this._get(options); + } + + async updateCustomer(customerId, customerData) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + body: customerData, + }; + return this._post(options); + } + + async deleteCustomer(customerId) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId) + '/delete', + }; + return this._post(options, false); + } + + // ************************** Subscriptions ********************************** + + async createSubscription(subscriptionData) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + body: subscriptionData, + }; + return this._post(options); + } + + async listSubscriptions(params = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + query: params + }; + return this._get(options); + } + + async getSubscriptionById(subscriptionId) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId), + }; + return this._get(options); + } + + async updateSubscription(subscriptionId, subscriptionData) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId), + body: subscriptionData, + }; + return this._post(options); + } + + async pauseSubscription(subscriptionId, pauseData = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptionPause(subscriptionId), + body: pauseData, + }; + return this._post(options); + } + + async resumeSubscription(subscriptionId, resumeData = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptionResume(subscriptionId), + body: resumeData, + }; + return this._post(options); + } + + async cancelSubscription(subscriptionId, cancelData = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptionCancel(subscriptionId), + body: cancelData, + }; + return this._post(options); + } + + // ************************** Plans ********************************** + + async createPlan(planData) { + const options = { + url: this.baseUrl + this.URLs.plans, + body: planData, + }; + return this._post(options); + } + + async listPlans(params = {}) { + const options = { + url: this.baseUrl + this.URLs.plans, + query: params + }; + return this._get(options); + } + + async getPlanById(planId) { + const options = { + url: this.baseUrl + this.URLs.planById(planId), + }; + return this._get(options); + } + + async updatePlan(planId, planData) { + const options = { + url: this.baseUrl + this.URLs.planById(planId), + body: planData, + }; + return this._post(options); + } + + async deletePlan(planId) { + const options = { + url: this.baseUrl + this.URLs.planById(planId) + '/delete', + }; + return this._post(options, false); + } + + // ************************** Invoices ********************************** + + async listInvoices(params = {}) { + const options = { + url: this.baseUrl + this.URLs.invoices, + query: params + }; + return this._get(options); + } + + async getInvoiceById(invoiceId) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + }; + return this._get(options); + } + + async collectPayment(invoiceId, paymentData = {}) { + const options = { + url: this.baseUrl + this.URLs.invoiceCollect(invoiceId), + body: paymentData, + }; + return this._post(options); + } + + // ************************** Events ********************************** + + async listEvents(params = {}) { + const options = { + url: this.baseUrl + this.URLs.events, + query: params + }; + return this._get(options); + } + + async getEventById(eventId) { + const options = { + url: this.baseUrl + this.URLs.eventById(eventId), + }; + return this._get(options); + } + + // ************************** Hosted Pages ********************************** + + async createHostedPage(pageData) { + const options = { + url: this.baseUrl + this.URLs.hostedPages, + body: pageData, + }; + return this._post(options); + } + + async getHostedPageById(hostedPageId) { + const options = { + url: this.baseUrl + this.URLs.hostedPageById(hostedPageId), + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/chargebee/defaultConfig.json b/packages/chargebee/defaultConfig.json new file mode 100644 index 0000000..5960af2 --- /dev/null +++ b/packages/chargebee/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "chargebee", + "label": "Chargebee", + "productUrl": "https://www.chargebee.com", + "apiDocs": "https://apidocs.chargebee.com", + "logoUrl": "https://friggframework.org/assets/img/chargebee-icon.png", + "categories": [ + "Billing", + "Subscription Management", + "Finance" + ], + "description": "Chargebee is a subscription billing and revenue management platform for SaaS and subscription businesses." +} diff --git a/packages/chargebee/definition.js b/packages/chargebee/definition.js new file mode 100644 index 0000000..1754547 --- /dev/null +++ b/packages/chargebee/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Chargebee', + requiredAuthMethods: { + getToken: async function (api, params) { + // Chargebee uses API key authentication + return { + access_token: params.api_key, + site_name: params.site_name + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const customers = await api.listCustomers({ limit: 1 }); + return { + identifiers: {externalId: api.siteName, user: userId}, + details: {siteName: api.siteName, hasCustomers: customers?.list?.length > 0}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'api_key', 'site_name' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + return { + identifiers: {externalId: api.siteName, user: userId}, + details: {siteName: api.siteName} + }; + }, + testAuthRequest: async function (api) { + return api.listCustomers({ limit: 1 }) + }, + }, + env: { + api_key: process.env.CHARGEBEE_API_KEY, + site_name: process.env.CHARGEBEE_SITE_NAME, + } +}; + +module.exports = {Definition}; diff --git a/packages/chargebee/index.js b/packages/chargebee/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/chargebee/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/chargebee/jest.config.js b/packages/chargebee/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/chargebee/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/chargebee/package.json b/packages/chargebee/package.json new file mode 100644 index 0000000..7a8ec87 --- /dev/null +++ b/packages/chargebee/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-chargebee", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Chargebee API module that lets the Frigg Framework interact with Chargebee", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/chargify/README.md b/packages/chargify/README.md new file mode 100644 index 0000000..42c002e --- /dev/null +++ b/packages/chargify/README.md @@ -0,0 +1,43 @@ +# Chargify API Module + +This module provides integration with the Chargify API for the Frigg Framework. + +## Description + +Chargify provides subscription billing and revenue management solutions. + +## Installation + +```bash +npm install @friggframework/api-module-chargify +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-chargify'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +CHARGIFY_CLIENT_ID=your_client_id +CHARGIFY_CLIENT_SECRET=your_client_secret +CHARGIFY_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/chargify/api.js b/packages/chargify/api.js new file mode 100644 index 0000000..cad205b --- /dev/null +++ b/packages/chargify/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.chargify.com/api/v2'; + + this.URLs = { + // User/Account info + userInfo: '/subscriptions', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://app.chargify.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.chargify.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to Chargify +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/chargify/defaultConfig.json b/packages/chargify/defaultConfig.json new file mode 100644 index 0000000..27839a2 --- /dev/null +++ b/packages/chargify/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "chargify", + "label": "Chargify", + "productUrl": "https://chargify.com/", + "apiDocs": "https://developers.chargify.com/", + "logoUrl": "https://friggframework.org/assets/img/chargify-icon.png", + "categories": [ + "Finance" + ], + "subCategories": [ + "Billing and Invoicing" + ], + "description": "Chargify provides subscription billing and revenue management solutions." +} \ No newline at end of file diff --git a/packages/chargify/definition.js b/packages/chargify/definition.js new file mode 100644 index 0000000..5424109 --- /dev/null +++ b/packages/chargify/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Chargify', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'chargify-account', user: userId}, + details: {name: userInfo.name || 'Chargify Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'chargify-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.CHARGIFY_CLIENT_ID, + client_secret: process.env.CHARGIFY_CLIENT_SECRET, + scope: process.env.CHARGIFY_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/chargify`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/chargify/index.js b/packages/chargify/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/chargify/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/chargify/jest-setup.js b/packages/chargify/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/chargify/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/chargify/jest-teardown.js b/packages/chargify/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/chargify/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/chargify/jest.config.js b/packages/chargify/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/chargify/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/chargify/package.json b/packages/chargify/package.json new file mode 100644 index 0000000..0b3a538 --- /dev/null +++ b/packages/chargify/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-chargify", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Chargify API module that lets the Frigg Framework interact with Chargify", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/chargify/test/api.test.js b/packages/chargify/test/api.test.js new file mode 100644 index 0000000..aea988e --- /dev/null +++ b/packages/chargify/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('Chargify API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/chargify/test/definition.test.js b/packages/chargify/test/definition.test.js new file mode 100644 index 0000000..8e6b148 --- /dev/null +++ b/packages/chargify/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Chargify Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('chargify'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/cirrus-shield/README.md b/packages/cirrus-shield/README.md new file mode 100644 index 0000000..b95fc8e --- /dev/null +++ b/packages/cirrus-shield/README.md @@ -0,0 +1,42 @@ +# Cirrus Shield API Module + +Frigg API module for Cirrus Shield integration. + +## Installation + +```bash +npm install @friggframework/cirrus-shield +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cirrus-shield'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +CIRRUS_SHIELD_CLIENT_ID=your_client_id +CIRRUS_SHIELD_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/cirrus-shield/api.js b/packages/cirrus-shield/api.js new file mode 100644 index 0000000..93df48f --- /dev/null +++ b/packages/cirrus-shield/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.cirrusshield.com'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.cirrusshield.com/oauth/authorize'; + this.accessTokenUri = 'https://api.cirrusshield.com/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Cirrus Shield', + MODULE_NAME: 'cirrus-shield', + CATEGORY: 'CRM', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/cirrus-shield/defaultConfig.json b/packages/cirrus-shield/defaultConfig.json new file mode 100644 index 0000000..37c2dfb --- /dev/null +++ b/packages/cirrus-shield/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Cirrus Shield", + "moduleName": "cirrus-shield", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Cirrus Shield API Integration Module", + "category": "CRM", + "apiDocUrl": "https://api.cirrusshield.com/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cirrus-shield/definition.js b/packages/cirrus-shield/definition.js new file mode 100644 index 0000000..df88bf8 --- /dev/null +++ b/packages/cirrus-shield/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'CirrusShield', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CIRRUS_SHIELD_CLIENT_ID, + client_secret: process.env.CIRRUS_SHIELD_CLIENT_SECRET, + scope: process.env.CIRRUS_SHIELD_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cirrus-shield`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/cirrus-shield/index.js b/packages/cirrus-shield/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/cirrus-shield/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/cirrus-shield/package.json b/packages/cirrus-shield/package.json new file mode 100644 index 0000000..9e9dd06 --- /dev/null +++ b/packages/cirrus-shield/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/cirrus-shield", + "version": "0.0.1", + "description": "Cirrus Shield API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "cirrus-shield", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/cisco-meraki/README.md b/packages/cisco-meraki/README.md new file mode 100644 index 0000000..218df83 --- /dev/null +++ b/packages/cisco-meraki/README.md @@ -0,0 +1,42 @@ +# Cisco Meraki API Module + +Frigg API module for Cisco Meraki integration. + +## Installation + +```bash +npm install @friggframework/cisco-meraki +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cisco-meraki'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +CISCO_MERAKI_CLIENT_ID=your_client_id +CISCO_MERAKI_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/cisco-meraki/api.js b/packages/cisco-meraki/api.js new file mode 100644 index 0000000..c3a7e8e --- /dev/null +++ b/packages/cisco-meraki/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.meraki.com'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.meraki.com/oauth/authorize'; + this.accessTokenUri = 'https://api.meraki.com/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Cisco Meraki', + MODULE_NAME: 'cisco-meraki', + CATEGORY: 'Security', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/cisco-meraki/defaultConfig.json b/packages/cisco-meraki/defaultConfig.json new file mode 100644 index 0000000..c2c8eb1 --- /dev/null +++ b/packages/cisco-meraki/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Cisco Meraki", + "moduleName": "cisco-meraki", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Cisco Meraki API Integration Module", + "category": "Security", + "apiDocUrl": "https://api.meraki.com/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cisco-meraki/definition.js b/packages/cisco-meraki/definition.js new file mode 100644 index 0000000..571b1bc --- /dev/null +++ b/packages/cisco-meraki/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'CiscoMeraki', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CISCO_MERAKI_CLIENT_ID, + client_secret: process.env.CISCO_MERAKI_CLIENT_SECRET, + scope: process.env.CISCO_MERAKI_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cisco-meraki`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/cisco-meraki/index.js b/packages/cisco-meraki/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/cisco-meraki/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/cisco-meraki/package.json b/packages/cisco-meraki/package.json new file mode 100644 index 0000000..323dc00 --- /dev/null +++ b/packages/cisco-meraki/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/cisco-meraki", + "version": "0.0.1", + "description": "Cisco Meraki API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "cisco-meraki", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/cisco-webex/README.md b/packages/cisco-webex/README.md new file mode 100644 index 0000000..8aec8ae --- /dev/null +++ b/packages/cisco-webex/README.md @@ -0,0 +1,43 @@ +# Cisco Webex API Module + +This module provides integration with the Cisco Webex API for the Frigg Framework. + +## Description + +Cisco Webex provides video conferencing, online meetings, and team collaboration tools. + +## Installation + +```bash +npm install @friggframework/api-module-cisco-webex +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-cisco-webex'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +CISCO_WEBEX_CLIENT_ID=your_client_id +CISCO_WEBEX_CLIENT_SECRET=your_client_secret +CISCO_WEBEX_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/cisco-webex/api.js b/packages/cisco-webex/api.js new file mode 100644 index 0000000..dede48d --- /dev/null +++ b/packages/cisco-webex/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://webexapis.com/v1'; + + this.URLs = { + // User/Account info + userInfo: '/people/me', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://webexapis.com/v1/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://webexapis.com/v1/access_token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to Cisco Webex +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/cisco-webex/defaultConfig.json b/packages/cisco-webex/defaultConfig.json new file mode 100644 index 0000000..c753ad4 --- /dev/null +++ b/packages/cisco-webex/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "cisco-webex", + "label": "Cisco Webex", + "productUrl": "https://ciscowebex.com/", + "apiDocs": "https://developers.ciscowebex.com/", + "logoUrl": "https://friggframework.org/assets/img/cisco-webex-icon.png", + "categories": [ + "Communication" + ], + "subCategories": [ + "Video Conferencing" + ], + "description": "Cisco Webex provides video conferencing, online meetings, and team collaboration tools." +} \ No newline at end of file diff --git a/packages/cisco-webex/definition.js b/packages/cisco-webex/definition.js new file mode 100644 index 0000000..2dffaf1 --- /dev/null +++ b/packages/cisco-webex/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'CiscoWebex', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'cisco-webex-account', user: userId}, + details: {name: userInfo.name || 'Cisco Webex Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'cisco-webex-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.CISCO_WEBEX_CLIENT_ID, + client_secret: process.env.CISCO_WEBEX_CLIENT_SECRET, + scope: process.env.CISCO_WEBEX_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cisco-webex`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/cisco-webex/index.js b/packages/cisco-webex/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/cisco-webex/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/cisco-webex/jest-setup.js b/packages/cisco-webex/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/cisco-webex/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/cisco-webex/jest-teardown.js b/packages/cisco-webex/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/cisco-webex/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/cisco-webex/jest.config.js b/packages/cisco-webex/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/cisco-webex/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/cisco-webex/package.json b/packages/cisco-webex/package.json new file mode 100644 index 0000000..89d8165 --- /dev/null +++ b/packages/cisco-webex/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-cisco-webex", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Cisco Webex API module that lets the Frigg Framework interact with Cisco Webex", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/cisco-webex/test/api.test.js b/packages/cisco-webex/test/api.test.js new file mode 100644 index 0000000..c58e46b --- /dev/null +++ b/packages/cisco-webex/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('Cisco Webex API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/cisco-webex/test/definition.test.js b/packages/cisco-webex/test/definition.test.js new file mode 100644 index 0000000..8910d3d --- /dev/null +++ b/packages/cisco-webex/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Cisco Webex Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('cisco-webex'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/cleargate-payup/README.md b/packages/cleargate-payup/README.md new file mode 100644 index 0000000..7ba9804 --- /dev/null +++ b/packages/cleargate-payup/README.md @@ -0,0 +1,55 @@ +# Cleargate PayUp API Module + +Cleargate PayUp API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/cleargate-payup +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cleargate-payup'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +CLEARGATE_PAYUP_CLIENT_ID=your_client_id +CLEARGATE_PAYUP_CLIENT_SECRET=your_client_secret +CLEARGATE_PAYUP_SCOPE=your_scope +CLEARGATE_PAYUP_AUTH_URI=authorization_endpoint +CLEARGATE_PAYUP_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Unnamed record + +## License + +MIT diff --git a/packages/cleargate-payup/api.js b/packages/cleargate-payup/api.js new file mode 100644 index 0000000..2512ef3 --- /dev/null +++ b/packages/cleargate-payup/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Unnamed record'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.CLEARGATE_PAYUP_AUTH_URI; + this.tokenUri = process.env.CLEARGATE_PAYUP_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Cleargate PayUp', + MODULE_NAME: 'cleargate-payup', + CATEGORY: 'https://api.cleargatepayup.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/cleargate-payup/defaultConfig.json b/packages/cleargate-payup/defaultConfig.json new file mode 100644 index 0000000..8f45f11 --- /dev/null +++ b/packages/cleargate-payup/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Cleargate PayUp", + "moduleName": "cleargate-payup", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Cleargate PayUp API Integration Module", + "category": "Unnamed record", + "apiDocUrl": "https://docs.cleargate-payup.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cleargate-payup/definition.js b/packages/cleargate-payup/definition.js new file mode 100644 index 0000000..ffc5cf4 --- /dev/null +++ b/packages/cleargate-payup/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Cleargate PayUp', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CLEARGATE_PAYUP_CLIENT_ID, + client_secret: process.env.CLEARGATE_PAYUP_CLIENT_SECRET, + scope: process.env.CLEARGATE_PAYUP_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cleargate-payup`, + } +}; + +module.exports = {Definition}; diff --git a/packages/cleargate-payup/index.js b/packages/cleargate-payup/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/cleargate-payup/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/cleargate-payup/jest-setup.js b/packages/cleargate-payup/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/cleargate-payup/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/cleargate-payup/jest-teardown.js b/packages/cleargate-payup/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/cleargate-payup/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/cleargate-payup/jest.config.js b/packages/cleargate-payup/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/cleargate-payup/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/cleargate-payup/package.json b/packages/cleargate-payup/package.json new file mode 100644 index 0000000..4fcab81 --- /dev/null +++ b/packages/cleargate-payup/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/cleargate-payup", + "version": "0.0.1", + "description": "Cleargate PayUp API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "cleargate-payup" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/clickup/README.md b/packages/clickup/README.md new file mode 100644 index 0000000..11d6dfd --- /dev/null +++ b/packages/clickup/README.md @@ -0,0 +1,154 @@ +# ClickUp API Module + +A comprehensive Node.js module for integrating with ClickUp's API v2, built for the Frigg Framework. + +## Overview + +This module provides seamless integration with ClickUp, supporting project management, task tracking, team collaboration, and productivity workflows. It handles OAuth2 authentication and provides methods for managing teams, spaces, folders, lists, tasks, and more. + +## Installation + +```bash +npm install @friggframework/api-module-clickup +``` + +## Configuration + +### Environment Variables + +```bash +CLICKUP_CLIENT_ID=your_clickup_client_id +CLICKUP_CLIENT_SECRET=your_clickup_client_secret +REDIRECT_URI=your_redirect_uri_base +``` + +### ClickUp App Setup + +1. Go to [ClickUp App Center](https://app.clickup.com/api) +2. Create a new app +3. Configure the redirect URI: `{REDIRECT_URI}/clickup` +4. No specific scopes needed - OAuth2 provides full API access + +## Usage + +### Basic Setup + +```javascript +const { Api, Definition } = require('@friggframework/api-module-clickup'); + +const api = new Api({ + client_id: process.env.CLICKUP_CLIENT_ID, + client_secret: process.env.CLICKUP_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/clickup` +}); +``` + +### Authentication Flow + +```javascript +// 1. Get authorization URL +const authUrl = api.getAuthUri(); + +// 2. Handle callback +const tokens = await api.getTokenFromCode(authorizationCode); + +// 3. Get user details +const user = await api.getUserDetails(); +``` + +### Teams and Workspaces + +```javascript +// Get authorized teams +const teams = await api.getAuthorizedTeams(); + +// Get specific team +const team = await api.getTeamById('team_id'); + +// Get team members +const members = await api.getTeamMembers('team_id'); +``` + +### Spaces + +```javascript +// Get team spaces +const spaces = await api.getTeamSpaces('team_id'); + +// Create new space +const newSpace = await api.createSpace('team_id', { + name: 'Development Projects', + multiple_assignees: true, + features: { + due_dates: { enabled: true }, + time_tracking: { enabled: true } + } +}); + +// Update space +await api.updateSpace('space_id', { + name: 'Updated Space Name' +}); +``` + +### Tasks + +```javascript +// Get list tasks +const tasks = await api.getListTasks('list_id', { + archived: false, + include_closed: false +}); + +// Create task +const newTask = await api.createTask('list_id', { + name: 'Implement user authentication', + description: 'Add OAuth2 authentication to the application', + assignees: [123456], + status: 'to do', + priority: 3, + due_date: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days from now +}); + +// Update task +await api.updateTask('task_id', { + name: 'Updated task name', + status: 'in progress' +}); +``` + +## API Reference + +### Core Methods + +#### Authentication & Users +- `getUserDetails()` - Get current user information +- `getAuthorizedTeams()` - Get user's teams + +#### Teams/Workspaces +- `getTeamById(teamId)` - Get specific team +- `getTeamMembers(teamId)` - Get team members + +#### Spaces +- `getTeamSpaces(teamId, params)` - Get team spaces +- `createSpace(teamId, spaceData)` - Create new space +- `getSpaceById(spaceId)` - Get specific space +- `updateSpace(spaceId, updates)` - Update space +- `deleteSpace(spaceId)` - Delete space + +#### Tasks +- `getListTasks(listId, params)` - Get list tasks +- `getTeamTasks(teamId, params)` - Get team tasks +- `createTask(listId, taskData)` - Create new task +- `getTaskById(taskId, params)` - Get specific task +- `updateTask(taskId, updates)` - Update task +- `deleteTask(taskId)` - Delete task + +## Resources + +- [ClickUp API Documentation](https://clickup.com/api) +- [ClickUp OAuth Guide](https://clickup.com/api/developer-portal/authentication/) + +## License + +MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/packages/clickup/api.js b/packages/clickup/api.js new file mode 100644 index 0000000..774d746 --- /dev/null +++ b/packages/clickup/api.js @@ -0,0 +1,500 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// ClickUp API v2 +// https://clickup.com/api +// Core resources: +// - Teams/Workspaces: https://clickup.com/api/clickupreference/operation/GetAuthorizedTeams/ +// - Spaces: https://clickup.com/api/clickupreference/operation/GetSpaces/ +// - Folders: https://clickup.com/api/clickupreference/operation/GetFolders/ +// - Lists: https://clickup.com/api/clickupreference/operation/GetLists/ +// - Tasks: https://clickup.com/api/clickupreference/operation/GetTasks/ +// - Users: https://clickup.com/api/clickupreference/operation/GetAuthorizedUser/ + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.clickup.com/api/v2'; + + this.URLs = { + // Authentication and user info + user: '/user', + teams: '/team', + + // Workspaces/Teams + teamById: (teamId) => `/team/${teamId}`, + + // Spaces + teamSpaces: (teamId) => `/team/${teamId}/space`, + spaceById: (spaceId) => `/space/${spaceId}`, + + // Folders + spaceFolders: (spaceId) => `/space/${spaceId}/folder`, + folderById: (folderId) => `/folder/${folderId}`, + + // Lists + folderLists: (folderId) => `/folder/${folderId}/list`, + spaceLists: (spaceId) => `/space/${spaceId}/list`, + listById: (listId) => `/list/${listId}`, + + // Tasks + listTasks: (listId) => `/list/${listId}/task`, + taskById: (taskId) => `/task/${taskId}`, + teamTasks: (teamId) => `/team/${teamId}/task`, + + // Comments + taskComments: (taskId) => `/task/${taskId}/comment`, + commentById: (commentId) => `/comment/${commentId}`, + + // Time Tracking + taskTimeEntries: (taskId) => `/task/${taskId}/time`, + teamTimeEntries: (teamId) => `/team/${teamId}/time_entries`, + + // Goals + teamGoals: (teamId) => `/team/${teamId}/goal`, + goalById: (goalId) => `/goal/${goalId}`, + + // Members + teamMembers: (teamId) => `/team/${teamId}/member`, + + // Custom Fields + listCustomFields: (listId) => `/list/${listId}/field`, + + // Webhooks + teamWebhooks: (teamId) => `/team/${teamId}/webhook`, + webhookById: (webhookId) => `/webhook/${webhookId}`, + }; + + // ClickUp OAuth2 endpoints + this.authorizationUri = encodeURI( + `https://app.clickup.com/api?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&response_type=code` + ); + this.tokenUri = 'https://api.clickup.com/api/v2/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + form: { + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + }, + }; + + const response = await this._post(options); + await this.setTokens(response); + return response; + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + // ClickUp doesn't provide refresh tokens in OAuth2 flow + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addAuthHeaders(options) { + const authHeaders = { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.user, + }; + return this._get(options); + } + + // ************************** Teams/Workspaces ********************************** + + async getAuthorizedTeams() { + const options = { + url: this.baseUrl + this.URLs.teams, + }; + return this._get(options); + } + + async getTeamById(teamId) { + const options = { + url: this.baseUrl + this.URLs.teamById(teamId), + }; + return this._get(options); + } + + // ************************** Spaces ********************************** + + async getTeamSpaces(teamId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.teamSpaces(teamId), + query: params, + }; + return this._get(options); + } + + async createSpace(teamId, body) { + const options = { + url: this.baseUrl + this.URLs.teamSpaces(teamId), + body: body, + }; + return this._post(options); + } + + async getSpaceById(spaceId) { + const options = { + url: this.baseUrl + this.URLs.spaceById(spaceId), + }; + return this._get(options); + } + + async updateSpace(spaceId, body) { + const options = { + url: this.baseUrl + this.URLs.spaceById(spaceId), + body: body, + }; + return this._put(options); + } + + async deleteSpace(spaceId) { + const options = { + url: this.baseUrl + this.URLs.spaceById(spaceId), + }; + return this._delete(options); + } + + // ************************** Folders ********************************** + + async getSpaceFolders(spaceId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.spaceFolders(spaceId), + query: params, + }; + return this._get(options); + } + + async createFolder(spaceId, body) { + const options = { + url: this.baseUrl + this.URLs.spaceFolders(spaceId), + body: body, + }; + return this._post(options); + } + + async getFolderById(folderId) { + const options = { + url: this.baseUrl + this.URLs.folderById(folderId), + }; + return this._get(options); + } + + async updateFolder(folderId, body) { + const options = { + url: this.baseUrl + this.URLs.folderById(folderId), + body: body, + }; + return this._put(options); + } + + async deleteFolder(folderId) { + const options = { + url: this.baseUrl + this.URLs.folderById(folderId), + }; + return this._delete(options); + } + + // ************************** Lists ********************************** + + async getFolderLists(folderId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.folderLists(folderId), + query: params, + }; + return this._get(options); + } + + async getSpaceLists(spaceId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.spaceLists(spaceId), + query: params, + }; + return this._get(options); + } + + async createList(folderId, body) { + const options = { + url: this.baseUrl + this.URLs.folderLists(folderId), + body: body, + }; + return this._post(options); + } + + async createSpaceList(spaceId, body) { + const options = { + url: this.baseUrl + this.URLs.spaceLists(spaceId), + body: body, + }; + return this._post(options); + } + + async getListById(listId) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + }; + return this._get(options); + } + + async updateList(listId, body) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + body: body, + }; + return this._put(options); + } + + async deleteList(listId) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + }; + return this._delete(options); + } + + // ************************** Tasks ********************************** + + async getListTasks(listId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.listTasks(listId), + query: params, + }; + return this._get(options); + } + + async getTeamTasks(teamId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.teamTasks(teamId), + query: params, + }; + return this._get(options); + } + + async createTask(listId, body) { + const options = { + url: this.baseUrl + this.URLs.listTasks(listId), + body: body, + }; + return this._post(options); + } + + async getTaskById(taskId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.taskById(taskId), + query: params, + }; + return this._get(options); + } + + async updateTask(taskId, body) { + const options = { + url: this.baseUrl + this.URLs.taskById(taskId), + body: body, + }; + return this._put(options); + } + + async deleteTask(taskId) { + const options = { + url: this.baseUrl + this.URLs.taskById(taskId), + }; + return this._delete(options); + } + + // ************************** Comments ********************************** + + async getTaskComments(taskId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.taskComments(taskId), + query: params, + }; + return this._get(options); + } + + async createComment(taskId, body) { + const options = { + url: this.baseUrl + this.URLs.taskComments(taskId), + body: body, + }; + return this._post(options); + } + + async updateComment(commentId, body) { + const options = { + url: this.baseUrl + this.URLs.commentById(commentId), + body: body, + }; + return this._put(options); + } + + async deleteComment(commentId) { + const options = { + url: this.baseUrl + this.URLs.commentById(commentId), + }; + return this._delete(options); + } + + // ************************** Time Tracking ********************************** + + async getTaskTimeEntries(taskId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.taskTimeEntries(taskId), + query: params, + }; + return this._get(options); + } + + async createTimeEntry(taskId, body) { + const options = { + url: this.baseUrl + this.URLs.taskTimeEntries(taskId), + body: body, + }; + return this._post(options); + } + + async getTeamTimeEntries(teamId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.teamTimeEntries(teamId), + query: params, + }; + return this._get(options); + } + + // ************************** Goals ********************************** + + async getTeamGoals(teamId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.teamGoals(teamId), + query: params, + }; + return this._get(options); + } + + async createGoal(teamId, body) { + const options = { + url: this.baseUrl + this.URLs.teamGoals(teamId), + body: body, + }; + return this._post(options); + } + + async getGoalById(goalId) { + const options = { + url: this.baseUrl + this.URLs.goalById(goalId), + }; + return this._get(options); + } + + async updateGoal(goalId, body) { + const options = { + url: this.baseUrl + this.URLs.goalById(goalId), + body: body, + }; + return this._put(options); + } + + async deleteGoal(goalId) { + const options = { + url: this.baseUrl + this.URLs.goalById(goalId), + }; + return this._delete(options); + } + + // ************************** Members ********************************** + + async getTeamMembers(teamId) { + const options = { + url: this.baseUrl + this.URLs.teamMembers(teamId), + }; + return this._get(options); + } + + // ************************** Custom Fields ********************************** + + async getListCustomFields(listId) { + const options = { + url: this.baseUrl + this.URLs.listCustomFields(listId), + }; + return this._get(options); + } + + // ************************** Webhooks ********************************** + + async getTeamWebhooks(teamId) { + const options = { + url: this.baseUrl + this.URLs.teamWebhooks(teamId), + }; + return this._get(options); + } + + async createWebhook(teamId, body) { + const options = { + url: this.baseUrl + this.URLs.teamWebhooks(teamId), + body: body, + }; + return this._post(options); + } + + async updateWebhook(webhookId, body) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + body: body, + }; + return this._put(options); + } + + async deleteWebhook(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + }; + return this._delete(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/clickup/defaultConfig.json b/packages/clickup/defaultConfig.json new file mode 100644 index 0000000..669f07e --- /dev/null +++ b/packages/clickup/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "clickup", + "label": "ClickUp", + "productUrl": "https://clickup.com", + "apiDocs": "https://clickup.com/api", + "logoUrl": "https://clickup.com/landing/images/brand/clickup-symbol_color.svg", + "categories": [ + "Project Management", + "Productivity", + "Task Management", + "Team Collaboration" + ], + "description": "ClickUp is an all-in-one productivity platform that provides teams with project management, task tracking, and collaboration tools." +} \ No newline at end of file diff --git a/packages/clickup/definition.js b/packages/clickup/definition.js new file mode 100644 index 0000000..ae14e2f --- /dev/null +++ b/packages/clickup/definition.js @@ -0,0 +1,52 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'ClickUp', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + const teams = await api.getAuthorizedTeams(); + return { + identifiers: { externalId: userDetails.user.id, user: userId }, + details: { + name: userDetails.user.username, + email: userDetails.user.email, + teams: teams.teams.map(t => ({ id: t.id, name: t.name })) + }, + }; + }, + apiPropertiesToPersist: { + credential: ['access_token'], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.user.id, user: userId }, + details: {}, + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails(); + }, + }, + env: { + client_id: process.env.CLICKUP_CLIENT_ID, + client_secret: process.env.CLICKUP_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/clickup`, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/clickup/index.js b/packages/clickup/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/clickup/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/clientsuccess/README.md b/packages/clientsuccess/README.md new file mode 100644 index 0000000..c82b41c --- /dev/null +++ b/packages/clientsuccess/README.md @@ -0,0 +1,43 @@ +# ClientSuccess API Module + +This module provides integration with the ClientSuccess API for the Frigg Framework. + +## Description + +ClientSuccess is a customer success platform that helps businesses reduce churn and increase expansion. + +## Installation + +```bash +npm install @friggframework/api-module-clientsuccess +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-clientsuccess'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +CLIENTSUCCESS_CLIENT_ID=your_client_id +CLIENTSUCCESS_CLIENT_SECRET=your_client_secret +CLIENTSUCCESS_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/clientsuccess/api.js b/packages/clientsuccess/api.js new file mode 100644 index 0000000..e95bab5 --- /dev/null +++ b/packages/clientsuccess/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.clientsuccess.com/v1'; + + this.URLs = { + // User/Account info + userInfo: '/clients', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://app.clientsuccess.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.clientsuccess.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to ClientSuccess +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/clientsuccess/defaultConfig.json b/packages/clientsuccess/defaultConfig.json new file mode 100644 index 0000000..9b5edc0 --- /dev/null +++ b/packages/clientsuccess/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "clientsuccess", + "label": "ClientSuccess", + "productUrl": "https://clientsuccess.com/", + "apiDocs": "https://developers.clientsuccess.com/", + "logoUrl": "https://friggframework.org/assets/img/clientsuccess-icon.png", + "categories": [ + "CRM" + ], + "subCategories": [ + "CRM (Customer Relationship Management)" + ], + "description": "ClientSuccess is a customer success platform that helps businesses reduce churn and increase expansion." +} \ No newline at end of file diff --git a/packages/clientsuccess/definition.js b/packages/clientsuccess/definition.js new file mode 100644 index 0000000..0529f6d --- /dev/null +++ b/packages/clientsuccess/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'ClientSuccess', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'clientsuccess-account', user: userId}, + details: {name: userInfo.name || 'ClientSuccess Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'clientsuccess-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.CLIENTSUCCESS_CLIENT_ID, + client_secret: process.env.CLIENTSUCCESS_CLIENT_SECRET, + scope: process.env.CLIENTSUCCESS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/clientsuccess`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/clientsuccess/index.js b/packages/clientsuccess/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/clientsuccess/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/clientsuccess/jest-setup.js b/packages/clientsuccess/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/clientsuccess/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/clientsuccess/jest-teardown.js b/packages/clientsuccess/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/clientsuccess/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/clientsuccess/jest.config.js b/packages/clientsuccess/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/clientsuccess/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/clientsuccess/package.json b/packages/clientsuccess/package.json new file mode 100644 index 0000000..0b59e75 --- /dev/null +++ b/packages/clientsuccess/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-clientsuccess", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "ClientSuccess API module that lets the Frigg Framework interact with ClientSuccess", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/clientsuccess/test/api.test.js b/packages/clientsuccess/test/api.test.js new file mode 100644 index 0000000..53807a1 --- /dev/null +++ b/packages/clientsuccess/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('ClientSuccess API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/clientsuccess/test/definition.test.js b/packages/clientsuccess/test/definition.test.js new file mode 100644 index 0000000..0963295 --- /dev/null +++ b/packages/clientsuccess/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('ClientSuccess Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('clientsuccess'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/clockwork-recruiting/README.md b/packages/clockwork-recruiting/README.md new file mode 100644 index 0000000..d0e198b --- /dev/null +++ b/packages/clockwork-recruiting/README.md @@ -0,0 +1,55 @@ +# Clockwork Recruiting API Module + +Clockwork Recruiting API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/clockwork-recruiting +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/clockwork-recruiting'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +CLOCKWORK_RECRUITING_CLIENT_ID=your_client_id +CLOCKWORK_RECRUITING_CLIENT_SECRET=your_client_secret +CLOCKWORK_RECRUITING_SCOPE=your_scope +CLOCKWORK_RECRUITING_AUTH_URI=authorization_endpoint +CLOCKWORK_RECRUITING_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +CRM + +## License + +MIT diff --git a/packages/clockwork-recruiting/api.js b/packages/clockwork-recruiting/api.js new file mode 100644 index 0000000..e5ba486 --- /dev/null +++ b/packages/clockwork-recruiting/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'CRM'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.CLOCKWORK_RECRUITING_AUTH_URI; + this.tokenUri = process.env.CLOCKWORK_RECRUITING_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Clockwork Recruiting', + MODULE_NAME: 'clockwork-recruiting', + CATEGORY: 'https://api.clockworkrecruiting.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/clockwork-recruiting/defaultConfig.json b/packages/clockwork-recruiting/defaultConfig.json new file mode 100644 index 0000000..1f974a4 --- /dev/null +++ b/packages/clockwork-recruiting/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Clockwork Recruiting", + "moduleName": "clockwork-recruiting", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Clockwork Recruiting API Integration Module", + "category": "CRM", + "apiDocUrl": "https://docs.clockwork-recruiting.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/clockwork-recruiting/definition.js b/packages/clockwork-recruiting/definition.js new file mode 100644 index 0000000..de831c1 --- /dev/null +++ b/packages/clockwork-recruiting/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Clockwork Recruiting', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CLOCKWORK_RECRUITING_CLIENT_ID, + client_secret: process.env.CLOCKWORK_RECRUITING_CLIENT_SECRET, + scope: process.env.CLOCKWORK_RECRUITING_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/clockwork-recruiting`, + } +}; + +module.exports = {Definition}; diff --git a/packages/clockwork-recruiting/index.js b/packages/clockwork-recruiting/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/clockwork-recruiting/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/clockwork-recruiting/jest-setup.js b/packages/clockwork-recruiting/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/clockwork-recruiting/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/clockwork-recruiting/jest-teardown.js b/packages/clockwork-recruiting/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/clockwork-recruiting/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/clockwork-recruiting/jest.config.js b/packages/clockwork-recruiting/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/clockwork-recruiting/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/clockwork-recruiting/package.json b/packages/clockwork-recruiting/package.json new file mode 100644 index 0000000..2e30d3d --- /dev/null +++ b/packages/clockwork-recruiting/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/clockwork-recruiting", + "version": "0.0.1", + "description": "Clockwork Recruiting API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "clockwork-recruiting" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/cloudflare/.env.example b/packages/cloudflare/.env.example new file mode 100644 index 0000000..4d06517 --- /dev/null +++ b/packages/cloudflare/.env.example @@ -0,0 +1,2 @@ +# CLOUDFLARE API Configuration +CLOUDFLARE_API_KEY=your_api_key_here diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md new file mode 100644 index 0000000..d0af47d --- /dev/null +++ b/packages/cloudflare/README.md @@ -0,0 +1,35 @@ +# Cloudflare API Module + +Web performance and security + +## Installation + +```bash +npm install @friggframework/cloudflare +``` + +## Configuration + +See `.env.example` for required environment variables. + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cloudflare'); + +// Initialize API client +const api = new Api({ + // Add required credentials +}); + +// Test the connection +const result = await api.getCurrentUser(); +``` + +## Category + +Infrastructure + +## License + +MIT diff --git a/packages/cloudflare/defaultConfig.json b/packages/cloudflare/defaultConfig.json new file mode 100644 index 0000000..ebfcea3 --- /dev/null +++ b/packages/cloudflare/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Cloudflare", + "moduleName": "cloudflare", + "version": "0.0.1", + "supportedAuthTypes": [ + "apiKey" + ], + "docs": { + "description": "Web performance and security", + "category": "Infrastructure", + "apiDocUrl": "https://docs.cloudflare.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cloudflare/index.js b/packages/cloudflare/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/cloudflare/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json new file mode 100644 index 0000000..ea29747 --- /dev/null +++ b/packages/cloudflare/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/cloudflare", + "version": "0.0.1", + "description": "Cloudflare API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "cloudflare", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0" + } +} \ No newline at end of file diff --git a/packages/clubworx/README.md b/packages/clubworx/README.md new file mode 100644 index 0000000..619b4cf --- /dev/null +++ b/packages/clubworx/README.md @@ -0,0 +1,34 @@ +# Clubworx API Integration + +Clubworx integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/clubworx +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/clubworx'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `CLUBWORX_CLIENT_ID` +- `CLUBWORX_CLIENT_SECRET` +- `CLUBWORX_SCOPE` + +## API Documentation + +For more information about the Clubworx API, visit: https://api.clubworx.com diff --git a/packages/clubworx/api.js b/packages/clubworx/api.js new file mode 100644 index 0000000..8f5f990 --- /dev/null +++ b/packages/clubworx/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class ClubworxApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.clubworx.com'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: ClubworxApi}; diff --git a/packages/clubworx/defaultConfig.json b/packages/clubworx/defaultConfig.json new file mode 100644 index 0000000..6821835 --- /dev/null +++ b/packages/clubworx/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Clubworx", + "moduleName": "clubworx", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Clubworx API Integration Module", + "category": "CRM", + "apiDocUrl": "https://api.clubworx.com", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/clubworx/definition.js b/packages/clubworx/definition.js new file mode 100644 index 0000000..e4cfa8a --- /dev/null +++ b/packages/clubworx/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Clubworx', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CLUBWORX_CLIENT_ID, + client_secret: process.env.CLUBWORX_CLIENT_SECRET, + scope: process.env.CLUBWORX_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/clubworx`, + } +}; + +module.exports = {Definition}; diff --git a/packages/clubworx/index.js b/packages/clubworx/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/clubworx/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/clubworx/package.json b/packages/clubworx/package.json new file mode 100644 index 0000000..f872bff --- /dev/null +++ b/packages/clubworx/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/clubworx", + "version": "0.0.1", + "description": "Clubworx API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "clubworx", + "crm" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/needs-updating/front/.eslintrc.json b/packages/clyde/.eslintrc.json similarity index 100% rename from packages/needs-updating/front/.eslintrc.json rename to packages/clyde/.eslintrc.json diff --git a/packages/needs-updating/clyde/CHANGELOG.md b/packages/clyde/CHANGELOG.md similarity index 100% rename from packages/needs-updating/clyde/CHANGELOG.md rename to packages/clyde/CHANGELOG.md diff --git a/packages/needs-updating/fastspring-iq/LICENSE.md b/packages/clyde/LICENSE.md similarity index 100% rename from packages/needs-updating/fastspring-iq/LICENSE.md rename to packages/clyde/LICENSE.md diff --git a/packages/needs-updating/clyde/README.md b/packages/clyde/README.md similarity index 100% rename from packages/needs-updating/clyde/README.md rename to packages/clyde/README.md diff --git a/packages/needs-updating/clyde/api.js b/packages/clyde/api.js similarity index 100% rename from packages/needs-updating/clyde/api.js rename to packages/clyde/api.js diff --git a/packages/needs-updating/clyde/api.test.js b/packages/clyde/api.test.js similarity index 100% rename from packages/needs-updating/clyde/api.test.js rename to packages/clyde/api.test.js diff --git a/packages/needs-updating/clyde/defaultConfig.json b/packages/clyde/defaultConfig.json similarity index 100% rename from packages/needs-updating/clyde/defaultConfig.json rename to packages/clyde/defaultConfig.json diff --git a/packages/clyde/definition.js b/packages/clyde/definition.js new file mode 100644 index 0000000..5dc4736 --- /dev/null +++ b/packages/clyde/definition.js @@ -0,0 +1,87 @@ +require('dotenv').config(); +const {Api} = require('./api.js'); +const {get} = require('@friggframework/core'); +const {Definition} = require('@friggframework/core/module-plugin/definition'); +const config = require('./defaultConfig.json'); + +const ClydeDefinition = class extends Definition { + constructor(params) { + super(params); + this.API = Api; + this.moduleName = config.name; + this.requiredAuthMethods = { + getToken: async function(api, params) { + const clientKey = params.data.clientKey; + const secret = params.data.secret; + + // Store credentials directly for basic auth + return { + clientKey: params.data.clientKey, + secret: params.data.secret + }; + }, + getEntityDetails: async function(api, callbackParams, tokenResponse, userId) { + + return { + identifiers: {externalId: tokenResponse.clientKey || 'default', user: userId}, + details: {name: tokenResponse.clientKey || 'Default'} + }; + }, + getCredentialDetails: async function(api, userId) { + return { + identifiers: {externalId: api.clientKey || 'default', user: userId}, + details: {} + }; + }, + apiPropertiesToPersist: { + credential: ['clientKey', 'secret'], + entity: [] + }, + testAuthRequest: async function(api) { + return await api.listProducts(); + } + }; + } + + getName() { + return config.name; + } + + async getAuthorizationRequirements(params) { + return { + url: null, + type: ModuleConstants.authType.basic, + data: { + jsonSchema: { + type: 'object', + required: ['clientKey', 'secret'], + properties: { + clientKey: { + type: 'string', + title: 'Client Key', + }, + secret: { + type: 'string', + title: 'Secret', + }, + }, + }, + uiSchema: { + clientKey: { + 'ui:help': + 'To obtain your Client Key and Secret, log in and head to settings. You can find your Keys in the "Developers" section.', + 'ui:placeholder': 'Client Key', + }, + secret: { + 'ui:widget': 'password', + 'ui:help': + 'Your secret is obtained along with your Client Key', + 'ui:placeholder': 'secret', + }, + }, + }, + }; +} +}; + +module.exports = {Definition: ClydeDefinition}; diff --git a/packages/clyde/index.js b/packages/clyde/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/clyde/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/needs-updating/fastspring-iq/jest.config.js b/packages/clyde/jest.config.js similarity index 100% rename from packages/needs-updating/fastspring-iq/jest.config.js rename to packages/clyde/jest.config.js diff --git a/packages/needs-updating/clyde/manager.test.js b/packages/clyde/manager.test.js similarity index 100% rename from packages/needs-updating/clyde/manager.test.js rename to packages/clyde/manager.test.js diff --git a/packages/needs-updating/clyde/test/Api.test.js b/packages/clyde/test/Api.test.js similarity index 100% rename from packages/needs-updating/clyde/test/Api.test.js rename to packages/clyde/test/Api.test.js diff --git a/packages/needs-updating/clyde/test/Manager.test.js b/packages/clyde/test/Manager.test.js similarity index 100% rename from packages/needs-updating/clyde/test/Manager.test.js rename to packages/clyde/test/Manager.test.js diff --git a/packages/coda/README.md b/packages/coda/README.md new file mode 100644 index 0000000..5a323c7 --- /dev/null +++ b/packages/coda/README.md @@ -0,0 +1,43 @@ +# Coda API Module + +This module provides integration with the Coda API for the Frigg Framework. + +## Description + +Coda is a collaborative workspace that brings documents, spreadsheets, and apps together. + +## Installation + +```bash +npm install @friggframework/api-module-coda +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-coda'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +CODA_CLIENT_ID=your_client_id +CODA_CLIENT_SECRET=your_client_secret +CODA_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/coda/api.js b/packages/coda/api.js new file mode 100644 index 0000000..17010d0 --- /dev/null +++ b/packages/coda/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://coda.io/apis/v1'; + + this.URLs = { + // User/Account info + userInfo: '/whoami', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://coda.io/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://coda.io/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to Coda +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/coda/defaultConfig.json b/packages/coda/defaultConfig.json new file mode 100644 index 0000000..90731cb --- /dev/null +++ b/packages/coda/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "coda", + "label": "Coda", + "productUrl": "https://coda.com/", + "apiDocs": "https://developers.coda.com/", + "logoUrl": "https://friggframework.org/assets/img/coda-icon.png", + "categories": [ + "Product Management" + ], + "subCategories": [ + "Product Management" + ], + "description": "Coda is a collaborative workspace that brings documents, spreadsheets, and apps together." +} \ No newline at end of file diff --git a/packages/coda/definition.js b/packages/coda/definition.js new file mode 100644 index 0000000..65b2f58 --- /dev/null +++ b/packages/coda/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Coda', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'coda-account', user: userId}, + details: {name: userInfo.name || 'Coda Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'coda-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.CODA_CLIENT_ID, + client_secret: process.env.CODA_CLIENT_SECRET, + scope: process.env.CODA_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/coda`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/coda/index.js b/packages/coda/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/coda/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/coda/jest-setup.js b/packages/coda/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/coda/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/coda/jest-teardown.js b/packages/coda/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/coda/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/coda/jest.config.js b/packages/coda/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/coda/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/coda/package.json b/packages/coda/package.json new file mode 100644 index 0000000..7a8fc00 --- /dev/null +++ b/packages/coda/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-coda", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Coda API module that lets the Frigg Framework interact with Coda", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/coda/test/api.test.js b/packages/coda/test/api.test.js new file mode 100644 index 0000000..7a06b0c --- /dev/null +++ b/packages/coda/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('Coda API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/coda/test/definition.test.js b/packages/coda/test/definition.test.js new file mode 100644 index 0000000..d8e4234 --- /dev/null +++ b/packages/coda/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Coda Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('coda'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/cohere/README.md b/packages/cohere/README.md new file mode 100644 index 0000000..d9aed1f --- /dev/null +++ b/packages/cohere/README.md @@ -0,0 +1,293 @@ +# Cohere API Module + +This module provides a complete interface to Cohere's suite of large language models and NLP tools for text generation, embeddings, classification, summarization, and reranking. + +## Features + +- **Text Generation**: Generate human-like text with Command models +- **Chat Interface**: Conversational AI with streaming support +- **Embeddings**: Create semantic embeddings for search and similarity +- **Classification**: Categorize text into custom classes +- **Summarization**: Extract key points from longer texts +- **Reranking**: Improve search results with semantic reranking +- **Tokenization**: Convert text to and from tokens +- **Fine-tuning**: Customize models for specific use cases +- **Batch Processing**: Efficient handling of large-scale operations + +## Authentication + +Cohere uses API key authentication. You'll need to: + +1. Sign up at [dashboard.cohere.ai](https://dashboard.cohere.ai) +2. Generate an API key from your dashboard +3. Set the following environment variable: + +```bash +COHERE_API_KEY=your_api_key_here +``` + +## Usage Examples + +### Text Generation + +```javascript +const {Api} = require('./api'); + +const api = new Api({ + apiKey: process.env.COHERE_API_KEY +}); + +// Generate text +const response = await api.generate({ + model: 'command', + prompt: 'Write a brief introduction to machine learning', + max_tokens: 200, + temperature: 0.7 +}); + +// Stream generation +const stream = await api.generateStream({ + model: 'command-nightly', + prompt: 'Tell me a story about space exploration', + max_tokens: 500, + stream: true +}); +``` + +### Chat Interface + +```javascript +// Simple chat +const chatResponse = await api.chat({ + model: 'command', + message: 'What are the benefits of renewable energy?', + temperature: 0.5 +}); + +// Chat with conversation history +const conversation = await api.chat({ + model: 'command', + message: 'What about solar specifically?', + chat_history: [ + {role: 'USER', message: 'What are the benefits of renewable energy?'}, + {role: 'CHATBOT', message: 'Renewable energy offers several benefits...'} + ] +}); + +// Streaming chat +const chatStream = await api.chatStream({ + model: 'command', + message: 'Explain quantum computing', + stream: true +}); +``` + +### Embeddings + +```javascript +// Create embeddings +const embeddings = await api.embed({ + texts: [ + 'Machine learning is a subset of AI', + 'Deep learning uses neural networks', + 'Natural language processing handles text' + ], + model: 'embed-english-v3.0', + input_type: 'search_document' +}); + +// Semantic search +const searchResults = await api.semanticSearch( + 'What is artificial intelligence?', + [ + 'AI is the simulation of human intelligence', + 'Machine learning enables computers to learn', + 'Robotics involves creating intelligent machines' + ], + 'embed-english-v3.0', + topK=2 +); +``` + +### Classification + +```javascript +// Classify text +const classification = await api.classify({ + inputs: ['This product is amazing!', 'Terrible experience, would not recommend'], + examples: [ + {text: 'I love this!', label: 'positive'}, + {text: 'This is bad', label: 'negative'}, + {text: 'It works well', label: 'positive'}, + {text: 'Disappointed', label: 'negative'} + ], + model: 'embed-english-v3.0' +}); +``` + +### Summarization + +```javascript +const summary = await api.summarize({ + text: 'Long article text here...', + length: 'medium', + format: 'bullets', + model: 'command', + additional_command: 'Focus on key findings', + temperature: 0.3 +}); +``` + +### Reranking + +```javascript +// Rerank search results +const reranked = await api.rerank({ + model: 'rerank-english-v2.0', + query: 'What is machine learning?', + documents: [ + 'Machine learning is a method of data analysis', + 'Python is a programming language', + 'ML algorithms learn from data', + 'Databases store information' + ], + top_n: 2 +}); +``` + +### Tokenization + +```javascript +// Tokenize text +const tokens = await api.tokenize({ + text: 'Hello, world!', + model: 'command' +}); + +// Detokenize +const text = await api.detokenize({ + tokens: [2016, 1010, 2088, 999], + model: 'command' +}); +``` + +### Fine-tuning + +```javascript +// Create dataset +const dataset = await api.createDataset({ + name: 'customer-support', + type: 'classification', + data: [ + {text: 'How do I reset my password?', label: 'account'}, + {text: 'When will my order arrive?', label: 'shipping'} + ] +}); + +// Create fine-tuning job +const fineTune = await api.createFineTune({ + model: 'embed-english-v3.0', + dataset_id: dataset.id, + hyperparameters: { + learning_rate: 0.001, + num_epochs: 3 + } +}); + +// Check status +const status = await api.getFineTune(fineTune.id); +``` + +### Batch Processing + +```javascript +// Batch embed large document set +const documents = ['doc1', 'doc2', ...]; // thousands of documents +const batchEmbeddings = await api.batchEmbed( + documents, + 'embed-english-v3.0', + 96 // batch size +); +``` + +## Available Models + +### Generation Models +- **command**: Production-ready text generation +- **command-light**: Faster, lighter version +- **command-nightly**: Latest features (experimental) +- **command-light-nightly**: Light version with latest features + +### Embedding Models +- **embed-english-v3.0**: English embeddings (1024 dimensions) +- **embed-multilingual-v3.0**: Multilingual embeddings (1024 dimensions) +- **embed-english-light-v3.0**: Lightweight English (384 dimensions) +- **embed-multilingual-light-v3.0**: Lightweight multilingual (384 dimensions) + +### Rerank Models +- **rerank-english-v2.0**: English reranking +- **rerank-multilingual-v2.0**: Multilingual reranking + +## API Methods + +### Generation +- `generate(params)` - Generate text from prompt +- `generateStream(params)` - Stream text generation +- `chat(params)` - Chat conversation interface +- `chatStream(params)` - Stream chat responses + +### Embeddings +- `embed(params)` - Create text embeddings +- `embedJobs(params)` - Large batch embedding jobs +- `batchEmbed(texts, model, batchSize)` - Helper for batch processing +- `semanticSearch(query, documents, model, topK)` - Semantic search helper + +### Analysis +- `classify(params)` - Classify text into categories +- `summarize(params)` - Summarize long texts +- `rerank(params)` - Rerank documents by relevance + +### Tokenization +- `tokenize(params)` - Convert text to tokens +- `detokenize(params)` - Convert tokens to text + +### Models & Datasets +- `listModels()` - List available models +- `getModel(modelId)` - Get model details +- `createDataset(params)` - Create training dataset +- `listDatasets(params)` - List datasets +- `getDataset(datasetId)` - Get dataset details +- `deleteDataset(datasetId)` - Delete dataset + +### Fine-tuning +- `createFineTune(params)` - Start fine-tuning job +- `listFineTunes(params)` - List fine-tuning jobs +- `getFineTune(fineTuneId)` - Get job details +- `updateFineTune(fineTuneId, params)` - Update job +- `deleteFineTune(fineTuneId)` - Delete job + +### Utilities +- `testAuth()` - Verify API key +- `getModelInfo(model)` - Get model specifications +- `cosineSimilarity(a, b)` - Calculate embedding similarity +- `parseStreamChunk(chunk)` - Parse streaming responses + +## Best Practices + +1. **Choose the Right Model**: Use Command for generation, specialized models for embeddings/rerank +2. **Batch Operations**: Use batch methods for processing multiple items efficiently +3. **Input Types**: Specify correct `input_type` for embeddings (search_query vs search_document) +4. **Temperature Control**: Lower temperature (0-0.3) for factual, higher (0.7-1) for creative +5. **Token Limits**: Be aware of model token limits when processing long texts + +## Error Handling + +Common errors: +- `401`: Invalid API key +- `429`: Rate limit exceeded +- `400`: Invalid parameters +- `413`: Request too large + +## Support + +For more information, visit [Cohere Documentation](https://docs.cohere.com). \ No newline at end of file diff --git a/packages/cohere/api.js b/packages/cohere/api.js new file mode 100644 index 0000000..cfebda6 --- /dev/null +++ b/packages/cohere/api.js @@ -0,0 +1,420 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.cohere.ai/v1'; + + this.URLs = { + // Generation + generate: '/generate', + chat: '/chat', + + // Embeddings + embed: '/embed', + + // Classification + classify: '/classify', + + // Summarization + summarize: '/summarize', + + // Reranking + rerank: '/rerank', + + // Tokenization + tokenize: '/tokenize', + detokenize: '/detokenize', + + // Models + models: '/models', + modelById: (modelId) => `/models/${modelId}`, + + // Datasets + datasets: '/datasets', + datasetById: (datasetId) => `/datasets/${datasetId}`, + + // Fine-tuning + fineTunes: '/fine-tunes', + fineTuneById: (fineTuneId) => `/fine-tunes/${fineTuneId}`, + }; + + this.modelInfo = { + // Command models + 'command': { maxTokens: 4096 }, + 'command-light': { maxTokens: 4096 }, + 'command-nightly': { maxTokens: 4096 }, + 'command-light-nightly': { maxTokens: 4096 }, + + // Embedding models + 'embed-english-v3.0': { dimensions: 1024 }, + 'embed-multilingual-v3.0': { dimensions: 1024 }, + 'embed-english-light-v3.0': { dimensions: 384 }, + 'embed-multilingual-light-v3.0': { dimensions: 384 }, + + // Rerank models + 'rerank-english-v2.0': {}, + 'rerank-multilingual-v2.0': {}, + }; + } + + async addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + } + + async _get(options) { + await this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + await this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + await this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + await this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + await this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Generation ********************************** + + async generate(params) { + const options = { + url: this.baseUrl + this.URLs.generate, + body: params, + }; + + return this._post(options); + } + + async generateStream(params) { + const options = { + url: this.baseUrl + this.URLs.generate, + body: { ...params, stream: true }, + headers: { + 'Accept': 'text/event-stream', + }, + }; + + return this._post(options); + } + + // ************************** Chat ********************************** + + async chat(params) { + const options = { + url: this.baseUrl + this.URLs.chat, + body: params, + }; + + return this._post(options); + } + + async chatStream(params) { + const options = { + url: this.baseUrl + this.URLs.chat, + body: { ...params, stream: true }, + headers: { + 'Accept': 'text/event-stream', + }, + }; + + return this._post(options); + } + + // ************************** Embeddings ********************************** + + async embed(params) { + const options = { + url: this.baseUrl + this.URLs.embed, + body: params, + }; + + return this._post(options); + } + + async embedJobs(params) { + // For large batch embeddings + const options = { + url: this.baseUrl + '/embed-jobs', + body: params, + }; + + return this._post(options); + } + + // ************************** Classification ********************************** + + async classify(params) { + const options = { + url: this.baseUrl + this.URLs.classify, + body: params, + }; + + return this._post(options); + } + + // ************************** Summarization ********************************** + + async summarize(params) { + const options = { + url: this.baseUrl + this.URLs.summarize, + body: params, + }; + + return this._post(options); + } + + // ************************** Reranking ********************************** + + async rerank(params) { + const options = { + url: this.baseUrl + this.URLs.rerank, + body: params, + }; + + return this._post(options); + } + + // ************************** Tokenization ********************************** + + async tokenize(params) { + const options = { + url: this.baseUrl + this.URLs.tokenize, + body: params, + }; + + return this._post(options); + } + + async detokenize(params) { + const options = { + url: this.baseUrl + this.URLs.detokenize, + body: params, + }; + + return this._post(options); + } + + // ************************** Models ********************************** + + async listModels() { + const options = { + url: this.baseUrl + this.URLs.models, + }; + + return this._get(options); + } + + async getModel(modelId) { + const options = { + url: this.baseUrl + this.URLs.modelById(modelId), + }; + + return this._get(options); + } + + // ************************** Datasets ********************************** + + async createDataset(params) { + const options = { + url: this.baseUrl + this.URLs.datasets, + body: params, + }; + + return this._post(options); + } + + async listDatasets(params = {}) { + const options = { + url: this.baseUrl + this.URLs.datasets, + query: params, + }; + + return this._get(options); + } + + async getDataset(datasetId) { + const options = { + url: this.baseUrl + this.URLs.datasetById(datasetId), + }; + + return this._get(options); + } + + async deleteDataset(datasetId) { + const options = { + url: this.baseUrl + this.URLs.datasetById(datasetId), + }; + + return this._delete(options); + } + + // ************************** Fine-tuning ********************************** + + async createFineTune(params) { + const options = { + url: this.baseUrl + this.URLs.fineTunes, + body: params, + }; + + return this._post(options); + } + + async listFineTunes(params = {}) { + const options = { + url: this.baseUrl + this.URLs.fineTunes, + query: params, + }; + + return this._get(options); + } + + async getFineTune(fineTuneId) { + const options = { + url: this.baseUrl + this.URLs.fineTuneById(fineTuneId), + }; + + return this._get(options); + } + + async updateFineTune(fineTuneId, params) { + const options = { + url: this.baseUrl + this.URLs.fineTuneById(fineTuneId), + body: params, + }; + + return this._patch(options); + } + + async deleteFineTune(fineTuneId) { + const options = { + url: this.baseUrl + this.URLs.fineTuneById(fineTuneId), + }; + + return this._delete(options); + } + + // ************************** Utility Methods ********************************** + + async testAuth() { + try { + await this.listModels(); + return true; + } catch (error) { + if (error.status === 401) { + return false; + } + throw error; + } + } + + // Batch processing utilities + async batchEmbed(texts, model = 'embed-english-v3.0', batchSize = 96) { + const batches = []; + for (let i = 0; i < texts.length; i += batchSize) { + batches.push(texts.slice(i, i + batchSize)); + } + + const results = []; + for (const batch of batches) { + const response = await this.embed({ + texts: batch, + model: model, + input_type: 'search_document' + }); + results.push(...response.embeddings); + } + + return results; + } + + // Helper for semantic search + async semanticSearch(query, documents, model = 'embed-english-v3.0', topK = 10) { + // Embed query + const queryResponse = await this.embed({ + texts: [query], + model: model, + input_type: 'search_query' + }); + const queryEmbedding = queryResponse.embeddings[0]; + + // Embed documents + const docsResponse = await this.embed({ + texts: documents, + model: model, + input_type: 'search_document' + }); + + // Calculate similarities + const similarities = docsResponse.embeddings.map((docEmb, idx) => ({ + index: idx, + similarity: this.cosineSimilarity(queryEmbedding, docEmb), + text: documents[idx] + })); + + // Sort and return top K + return similarities + .sort((a, b) => b.similarity - a.similarity) + .slice(0, topK); + } + + cosineSimilarity(a, b) { + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); + } + + getModelInfo(model) { + return this.modelInfo[model] || {}; + } + + // Parse streaming response + parseStreamChunk(chunk) { + const lines = chunk.split('\n').filter(line => line.trim()); + const events = []; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + events.push({ event_type: 'stream-end' }); + } else { + try { + events.push(JSON.parse(data)); + } catch (e) { + console.error('Failed to parse stream chunk:', e); + } + } + } + } + + return events; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/cohere/defaultConfig.json b/packages/cohere/defaultConfig.json new file mode 100644 index 0000000..da23162 --- /dev/null +++ b/packages/cohere/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "cohere", + "label": "Cohere", + "productUrl": "https://cohere.com", + "apiDocs": "https://docs.cohere.com/reference/about", + "logoUrl": "https://friggframework.org/assets/img/cohere-icon.png", + "categories": [ + "AI/ML", + "Large Language Models", + "NLP", + "Text Analytics" + ], + "description": "Cohere provides powerful large language models for natural language understanding and generation, including text generation, embeddings, classification, and reranking." +} \ No newline at end of file diff --git a/packages/cohere/definition.js b/packages/cohere/definition.js new file mode 100644 index 0000000..624c80f --- /dev/null +++ b/packages/cohere/definition.js @@ -0,0 +1,52 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Cohere', + requiredAuthMethods: { + getToken: async function (api, params) { + // Cohere uses API keys, not OAuth + const apiKey = get(params.data, 'apiKey'); + if (!apiKey) { + throw new Error('API Key is required for Cohere authentication'); + } + return { apiKey }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + // Cohere doesn't have user accounts via API, so we use a generic identifier + return { + identifiers: {externalId: 'cohere-user', user: userId}, + details: {name: 'Cohere API User', apiKey: tokenResponse.apiKey}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'apiKey' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + // Test the API key by listing models + const models = await api.listModels(); + return { + identifiers: {externalId: 'cohere-api', user: userId}, + details: { modelsAvailable: models.models ? models.models.length : 0 } + }; + }, + testAuthRequest: async function (api) { + return api.testAuth() + }, + }, + env: { + apiKey: process.env.COHERE_API_KEY, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/cohere/index.js b/packages/cohere/index.js new file mode 100644 index 0000000..18a6c30 --- /dev/null +++ b/packages/cohere/index.js @@ -0,0 +1,3 @@ +const {Definition} = require('./definition'); + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/coinbase/api.js b/packages/coinbase/api.js new file mode 100644 index 0000000..707167a --- /dev/null +++ b/packages/coinbase/api.js @@ -0,0 +1,408 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); +const axios = require('axios'); +const crypto = require('crypto'); + +class Api extends OAuth2Requester { + constructor(params = {}) { + super(params); + + this.clientId = get(params, 'clientId', null); + this.clientSecret = get(params, 'clientSecret', null); + this.redirectUri = get(params, 'redirectUri', null); + this.sandbox = get(params, 'sandbox', false); + + // API Key authentication support (for some endpoints) + this.apiKey = get(params, 'apiKey', null); + this.apiSecret = get(params, 'apiSecret', null); + + this.baseUrl = this.sandbox + ? 'https://api.sandbox.coinbase.com' + : 'https://api.coinbase.com'; + + this.tokenUri = 'https://api.coinbase.com/oauth/token'; + this.authorizationUri = 'https://www.coinbase.com/oauth/authorize'; + + // OAuth2 scopes + this.scope = get(params, 'scope', [ + 'wallet:accounts:read', + 'wallet:transactions:read', + 'wallet:user:read', + ]).join(' '); + + this.client = axios.create({ + baseURL: this.baseUrl, + }); + + // Add request interceptor for authentication + this.client.interceptors.request.use((config) => { + if (this.access_token) { + config.headers['Authorization'] = `Bearer ${this.access_token}`; + } else if (this.apiKey && this.apiSecret) { + const timestamp = Math.floor(Date.now() / 1000); + const message = timestamp + config.method.toUpperCase() + config.url + (config.data ? JSON.stringify(config.data) : ''); + const signature = crypto.createHmac('sha256', this.apiSecret).update(message).digest('hex'); + + config.headers['CB-ACCESS-KEY'] = this.apiKey; + config.headers['CB-ACCESS-SIGN'] = signature; + config.headers['CB-ACCESS-TIMESTAMP'] = timestamp; + } + config.headers['CB-VERSION'] = '2021-06-23'; + return config; + }); + } + + // OAuth2 Methods + async getAuthorizationUri() { + const params = new URLSearchParams({ + response_type: 'code', + client_id: this.clientId, + redirect_uri: this.redirectUri, + scope: this.scope, + state: Math.random().toString(36).substring(7), + }); + + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const data = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri, + }; + + const response = await axios.post(this.tokenUri, data); + await this.setTokens(response.data); + return response.data; + } + + async refreshAccessToken(refreshToken) { + const data = { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: this.clientId, + client_secret: this.clientSecret, + }; + + const response = await axios.post(this.tokenUri, data); + await this.setTokens(response.data); + return response.data; + } + + // Helper method for API requests + async makeRequest(method, endpoint, data = null, params = null) { + try { + const response = await this.client({ + method, + url: endpoint, + data, + params, + }); + return response.data; + } catch (error) { + throw new Error(`Coinbase API Error: ${error.response?.data?.errors?.[0]?.message || error.message}`); + } + } + + // User endpoints + async getCurrentUser() { + const response = await this.makeRequest('GET', '/v2/user'); + return response.data; + } + + async updateUser(params) { + const response = await this.makeRequest('PUT', '/v2/user', params); + return response.data; + } + + // Account endpoints + async getAccounts(params = {}) { + const response = await this.makeRequest('GET', '/v2/accounts', null, params); + return response.data; + } + + async getAccount(accountId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}`); + return response.data; + } + + async createAccount(name) { + const response = await this.makeRequest('POST', '/v2/accounts', { name }); + return response.data; + } + + async updateAccount(accountId, name) { + const response = await this.makeRequest('PUT', `/v2/accounts/${accountId}`, { name }); + return response.data; + } + + async deleteAccount(accountId) { + await this.makeRequest('DELETE', `/v2/accounts/${accountId}`); + return { success: true }; + } + + // Address endpoints + async getAddresses(accountId, params = {}) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/addresses`, null, params); + return response.data; + } + + async getAddress(accountId, addressId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/addresses/${addressId}`); + return response.data; + } + + async createAddress(accountId, name = null) { + const data = name ? { name } : {}; + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/addresses`, data); + return response.data; + } + + // Transaction endpoints + async getTransactions(accountId, params = {}) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/transactions`, null, params); + return response.data; + } + + async getTransaction(accountId, transactionId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/transactions/${transactionId}`); + return response.data; + } + + async sendMoney(accountId, params) { + const data = { + type: 'send', + to: params.to, + amount: params.amount, + currency: params.currency, + description: params.description, + idem: params.idem || `send-${Date.now()}`, + }; + + if (params.skipNotifications) { + data.skip_notifications = true; + } + + if (params.fee) { + data.fee = params.fee; + } + + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/transactions`, data); + return response.data; + } + + async transferMoney(fromAccountId, params) { + const data = { + type: 'transfer', + to: params.toAccountId, + amount: params.amount, + currency: params.currency, + description: params.description, + }; + + const response = await this.makeRequest('POST', `/v2/accounts/${fromAccountId}/transactions`, data); + return response.data; + } + + async requestMoney(accountId, params) { + const data = { + type: 'request', + to: params.to, + amount: params.amount, + currency: params.currency, + description: params.description, + }; + + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/transactions`, data); + return response.data; + } + + // Buy/Sell endpoints + async getBuys(accountId, params = {}) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/buys`, null, params); + return response.data; + } + + async getBuy(accountId, buyId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/buys/${buyId}`); + return response.data; + } + + async placeBuyOrder(accountId, params) { + const data = { + amount: params.amount, + currency: params.currency, + payment_method: params.paymentMethod, + agree_btc_amount_varies: params.agreeBtcAmountVaries || true, + commit: params.commit || false, + quote: params.quote || false, + }; + + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/buys`, data); + return response.data; + } + + async commitBuy(accountId, buyId) { + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/buys/${buyId}/commit`); + return response.data; + } + + async getSells(accountId, params = {}) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/sells`, null, params); + return response.data; + } + + async getSell(accountId, sellId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/sells/${sellId}`); + return response.data; + } + + async placeSellOrder(accountId, params) { + const data = { + amount: params.amount, + currency: params.currency, + payment_method: params.paymentMethod, + agree_btc_amount_varies: params.agreeBtcAmountVaries || true, + commit: params.commit || false, + quote: params.quote || false, + }; + + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/sells`, data); + return response.data; + } + + async commitSell(accountId, sellId) { + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/sells/${sellId}/commit`); + return response.data; + } + + // Deposit/Withdrawal endpoints + async getDeposits(accountId, params = {}) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/deposits`, null, params); + return response.data; + } + + async getDeposit(accountId, depositId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/deposits/${depositId}`); + return response.data; + } + + async depositFunds(accountId, params) { + const data = { + amount: params.amount, + currency: params.currency, + payment_method: params.paymentMethod, + commit: params.commit || false, + }; + + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/deposits`, data); + return response.data; + } + + async commitDeposit(accountId, depositId) { + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/deposits/${depositId}/commit`); + return response.data; + } + + async getWithdrawals(accountId, params = {}) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/withdrawals`, null, params); + return response.data; + } + + async getWithdrawal(accountId, withdrawalId) { + const response = await this.makeRequest('GET', `/v2/accounts/${accountId}/withdrawals/${withdrawalId}`); + return response.data; + } + + async withdrawFunds(accountId, params) { + const data = { + amount: params.amount, + currency: params.currency, + payment_method: params.paymentMethod, + commit: params.commit || false, + }; + + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/withdrawals`, data); + return response.data; + } + + async commitWithdrawal(accountId, withdrawalId) { + const response = await this.makeRequest('POST', `/v2/accounts/${accountId}/withdrawals/${withdrawalId}/commit`); + return response.data; + } + + // Payment method endpoints + async getPaymentMethods(params = {}) { + const response = await this.makeRequest('GET', '/v2/payment-methods', null, params); + return response.data; + } + + async getPaymentMethod(paymentMethodId) { + const response = await this.makeRequest('GET', `/v2/payment-methods/${paymentMethodId}`); + return response.data; + } + + // Price data endpoints + async getExchangeRates(currency = 'USD') { + const response = await this.makeRequest('GET', `/v2/exchange-rates`, null, { currency }); + return response.data; + } + + async getBuyPrice(currencyPair) { + const response = await this.makeRequest('GET', `/v2/prices/${currencyPair}/buy`); + return response.data; + } + + async getSellPrice(currencyPair) { + const response = await this.makeRequest('GET', `/v2/prices/${currencyPair}/sell`); + return response.data; + } + + async getSpotPrice(currencyPair, date = null) { + const params = date ? { date } : {}; + const response = await this.makeRequest('GET', `/v2/prices/${currencyPair}/spot`, null, params); + return response.data; + } + + // Currency endpoints + async getCurrencies() { + const response = await this.makeRequest('GET', '/v2/currencies'); + return response.data; + } + + async getCurrency(currencyCode) { + const response = await this.makeRequest('GET', `/v2/currencies/${currencyCode}`); + return response.data; + } + + // Time endpoint + async getTime() { + const response = await this.makeRequest('GET', '/v2/time'); + return response.data; + } + + // Notification endpoints + async getNotifications(params = {}) { + const response = await this.makeRequest('GET', '/v2/notifications', null, params); + return response.data; + } + + async getNotification(notificationId) { + const response = await this.makeRequest('GET', `/v2/notifications/${notificationId}`); + return response.data; + } + + // Webhook verification + verifyWebhookSignature(payload, signature) { + const expectedSignature = crypto + .createHmac('sha256', this.clientSecret) + .update(payload) + .digest('hex'); + + return signature === expectedSignature; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/coinbase/defaultConfig.json b/packages/coinbase/defaultConfig.json new file mode 100644 index 0000000..aa3238d --- /dev/null +++ b/packages/coinbase/defaultConfig.json @@ -0,0 +1,32 @@ +{ + "name": "coinbase", + "displayName": "Coinbase", + "description": "Cryptocurrency trading and wallet platform", + "version": "1.0.0", + "categories": ["finance", "cryptocurrency", "trading"], + "scopes": [ + "wallet:accounts:read", + "wallet:accounts:update", + "wallet:addresses:read", + "wallet:addresses:create", + "wallet:buys:read", + "wallet:buys:create", + "wallet:deposits:read", + "wallet:deposits:create", + "wallet:notifications:read", + "wallet:orders:read", + "wallet:orders:create", + "wallet:payment-methods:read", + "wallet:payment-methods:delete", + "wallet:payment-methods:limits", + "wallet:sells:read", + "wallet:sells:create", + "wallet:transactions:read", + "wallet:transactions:send", + "wallet:transactions:transfer", + "wallet:user:read", + "wallet:user:update", + "wallet:withdrawals:read", + "wallet:withdrawals:create" + ] +} \ No newline at end of file diff --git a/packages/coinbase/definition.js b/packages/coinbase/definition.js new file mode 100644 index 0000000..ebdf62a --- /dev/null +++ b/packages/coinbase/definition.js @@ -0,0 +1,72 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Coinbase', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + + getEntityDetails: async function (api, userId) { + const user = await api.getCurrentUser(); + + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: user.id, + user: userId + }, + details: { + name: user.name, + email: user.email, + nativeCurrency: user.native_currency, + countryCode: user.country?.code, + timeZone: user.time_zone, + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token'], + entity: ['native_currency', 'country_code'], + }, + + getCredentialDetails: async function (api, userId) { + const user = await api.getCurrentUser(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: user.id, + user: userId + }, + details: { + createdAt: user.created_at, + }, + }; + }, + + testAuthRequest: function (api) { + return api.getCurrentUser(); + }, + }, + env: { + clientId: process.env.COINBASE_CLIENT_ID, + clientSecret: process.env.COINBASE_CLIENT_SECRET, + redirectUri: `${process.env.REDIRECT_URI}/coinbase`, + sandbox: process.env.COINBASE_SANDBOX === 'true', + // Optional API key authentication + apiKey: process.env.COINBASE_API_KEY, + apiSecret: process.env.COINBASE_API_SECRET, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/coinbase/index.js b/packages/coinbase/index.js new file mode 100644 index 0000000..5a1a5bd --- /dev/null +++ b/packages/coinbase/index.js @@ -0,0 +1,7 @@ +const { Definition } = require('./definition'); +const { Api } = require('./api'); + +module.exports = { + Definition, + Api, +}; \ No newline at end of file diff --git a/packages/coinbase/package.json b/packages/coinbase/package.json new file mode 100644 index 0000000..8fa66f8 --- /dev/null +++ b/packages/coinbase/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/coinbase", + "version": "1.0.0", + "description": "Coinbase cryptocurrency platform API module for Frigg Framework", + "main": "index.js", + "scripts": { + "test": "jest" + }, + "dependencies": { + "@friggframework/core": "^1.0.0", + "axios": "^1.6.0", + "crypto-js": "^4.2.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "keywords": [ + "coinbase", + "cryptocurrency", + "bitcoin", + "ethereum", + "crypto", + "api", + "frigg" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/coinbase/readme.md b/packages/coinbase/readme.md new file mode 100644 index 0000000..a1e586e --- /dev/null +++ b/packages/coinbase/readme.md @@ -0,0 +1,34 @@ +# Coinbase API Module + +This module provides integration with the Coinbase cryptocurrency platform API. + +## Features + +- OAuth2 and API Key authentication +- Wallet and account management +- Buy/sell cryptocurrency orders +- Send/receive/transfer funds +- Real-time price data +- Transaction history +- Payment method management +- Deposit and withdrawal operations + +## Environment Variables + +For OAuth2: +``` +COINBASE_CLIENT_ID=your_client_id +COINBASE_CLIENT_SECRET=your_client_secret +REDIRECT_URI=your_app_redirect_uri +COINBASE_SANDBOX=true|false +``` + +For API Key authentication: +``` +COINBASE_API_KEY=your_api_key +COINBASE_API_SECRET=your_api_secret +``` + +## Usage + +See the [Frigg Framework documentation](https://docs.friggframework.org) for usage details. \ No newline at end of file diff --git a/packages/coinbase/tests/api.test.js b/packages/coinbase/tests/api.test.js new file mode 100644 index 0000000..818383b --- /dev/null +++ b/packages/coinbase/tests/api.test.js @@ -0,0 +1,80 @@ +const { Api } = require('../api'); + +describe('Coinbase API', () => { + let api; + + beforeEach(() => { + api = new Api({ + clientId: 'test_client_id', + clientSecret: 'test_client_secret', + redirectUri: 'https://example.com/callback', + sandbox: false, + }); + }); + + test('should initialize with OAuth2 configuration', () => { + expect(api.clientId).toBe('test_client_id'); + expect(api.clientSecret).toBe('test_client_secret'); + expect(api.redirectUri).toBe('https://example.com/callback'); + expect(api.baseUrl).toBe('https://api.coinbase.com'); + expect(api.client).toBeDefined(); + }); + + test('should initialize with API key configuration', () => { + const apiKeyAuth = new Api({ + apiKey: 'test_api_key', + apiSecret: 'test_api_secret', + }); + + expect(apiKeyAuth.apiKey).toBe('test_api_key'); + expect(apiKeyAuth.apiSecret).toBe('test_api_secret'); + }); + + test('should generate authorization URI', async () => { + const authUri = await api.getAuthorizationUri(); + + expect(authUri).toContain('https://www.coinbase.com/oauth/authorize'); + expect(authUri).toContain('client_id=test_client_id'); + expect(authUri).toContain('response_type=code'); + expect(authUri).toContain('redirect_uri='); + }); + + test('should construct send money request', async () => { + api.access_token = 'test_access_token'; + + // Mock the makeRequest method + api.makeRequest = jest.fn().mockResolvedValue({ + id: 'transaction-123', + type: 'send', + status: 'pending', + }); + + const transaction = await api.sendMoney('account-123', { + to: 'user@example.com', + amount: '10.00', + currency: 'USD', + description: 'Test payment', + }); + + expect(api.makeRequest).toHaveBeenCalledWith('POST', '/v2/accounts/account-123/transactions', { + type: 'send', + to: 'user@example.com', + amount: '10.00', + currency: 'USD', + description: 'Test payment', + idem: expect.stringMatching(/^send-\d+$/), + }); + expect(transaction.id).toBe('transaction-123'); + }); + + test('should verify webhook signature correctly', () => { + const payload = 'test-payload'; + const validSignature = require('crypto') + .createHmac('sha256', 'test_client_secret') + .update(payload) + .digest('hex'); + + expect(api.verifyWebhookSignature(payload, validSignature)).toBe(true); + expect(api.verifyWebhookSignature(payload, 'invalid-signature')).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/needs-updating/gorgias/.eslintrc.json b/packages/connectwise/.eslintrc.json similarity index 100% rename from packages/needs-updating/gorgias/.eslintrc.json rename to packages/connectwise/.eslintrc.json diff --git a/packages/v1-ready/connectwise/CHANGELOG.md b/packages/connectwise/CHANGELOG.md similarity index 100% rename from packages/v1-ready/connectwise/CHANGELOG.md rename to packages/connectwise/CHANGELOG.md diff --git a/packages/needs-updating/front/LICENSE.md b/packages/connectwise/LICENSE.md similarity index 100% rename from packages/needs-updating/front/LICENSE.md rename to packages/connectwise/LICENSE.md diff --git a/packages/v1-ready/connectwise/README.md b/packages/connectwise/README.md similarity index 100% rename from packages/v1-ready/connectwise/README.md rename to packages/connectwise/README.md diff --git a/packages/v1-ready/connectwise/api.js b/packages/connectwise/api.js similarity index 100% rename from packages/v1-ready/connectwise/api.js rename to packages/connectwise/api.js diff --git a/packages/v1-ready/connectwise/authFields.js b/packages/connectwise/authFields.js similarity index 100% rename from packages/v1-ready/connectwise/authFields.js rename to packages/connectwise/authFields.js diff --git a/packages/v1-ready/connectwise/defaultConfig.json b/packages/connectwise/defaultConfig.json similarity index 100% rename from packages/v1-ready/connectwise/defaultConfig.json rename to packages/connectwise/defaultConfig.json diff --git a/packages/v1-ready/connectwise/definition.js b/packages/connectwise/definition.js similarity index 100% rename from packages/v1-ready/connectwise/definition.js rename to packages/connectwise/definition.js diff --git a/packages/needs-updating/revio/formatPatchBody.js b/packages/connectwise/formatPatchBody.js similarity index 100% rename from packages/needs-updating/revio/formatPatchBody.js rename to packages/connectwise/formatPatchBody.js diff --git a/packages/v1-ready/connectwise/index.js b/packages/connectwise/index.js similarity index 100% rename from packages/v1-ready/connectwise/index.js rename to packages/connectwise/index.js diff --git a/packages/needs-updating/freshbooks/jest.config.js b/packages/connectwise/jest.config.js similarity index 100% rename from packages/needs-updating/freshbooks/jest.config.js rename to packages/connectwise/jest.config.js diff --git a/packages/v1-ready/connectwise/package.json b/packages/connectwise/package.json similarity index 100% rename from packages/v1-ready/connectwise/package.json rename to packages/connectwise/package.json diff --git a/packages/v1-ready/connectwise/tests/api.test.js b/packages/connectwise/tests/api.test.js similarity index 100% rename from packages/v1-ready/connectwise/tests/api.test.js rename to packages/connectwise/tests/api.test.js diff --git a/packages/v1-ready/connectwise/tests/auther.test.js b/packages/connectwise/tests/auther.test.js similarity index 100% rename from packages/v1-ready/connectwise/tests/auther.test.js rename to packages/connectwise/tests/auther.test.js diff --git a/packages/constantcontact/README.md b/packages/constantcontact/README.md new file mode 100644 index 0000000..7a79afd --- /dev/null +++ b/packages/constantcontact/README.md @@ -0,0 +1,5 @@ +# Constant Contact + +This is the API Module for Constant Contact that allows the [Frigg](https://friggframework.org) code to talk to the Constant Contact API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/constantcontact) diff --git a/packages/constantcontact/api.js b/packages/constantcontact/api.js new file mode 100644 index 0000000..8978611 --- /dev/null +++ b/packages/constantcontact/api.js @@ -0,0 +1,341 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.cc.email/v3'; + + this.URLs = { + // Account Info + accountInfo: '/account/summary', + + // Contact Lists + contactLists: '/contact_lists', + contactListById: (listId) => `/contact_lists/${listId}`, + + // Contacts + contacts: '/contacts', + contactById: (contactId) => `/contacts/${contactId}`, + contactCustomFields: '/contact_custom_fields', + + // Email Campaigns + emailCampaigns: '/emails', + emailCampaignById: (campaignId) => `/emails/${campaignId}`, + emailCampaignActivities: (campaignId) => `/emails/${campaignId}/activities`, + + // Activities + activities: '/activities', + activityById: (activityId) => `/activities/${activityId}`, + + // Bulk Activities + bulkImportContacts: '/activities/contacts_file_import', + bulkDeleteContacts: '/activities/remove_contacts', + + // Reporting + emailReports: '/reports/email_reports', + contactReports: '/reports/contact_reports', + + // Landing Pages + landingPages: '/landing_pages', + landingPageById: (pageId) => `/landing_pages/${pageId}`, + + // Webhooks + webhooks: '/webhooks', + webhookById: (webhookId) => `/webhooks/${webhookId}`, + + // Segments + segments: '/segments', + segmentById: (segmentId) => `/segments/${segmentId}`, + + // Tags + tags: '/contact_tags', + tagById: (tagId) => `/contact_tags/${tagId}` + }; + + this.authorizationUri = encodeURI( + `https://authz.constantcontact.com/oauth2/default/v1/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://authz.constantcontact.com/oauth2/default/v1/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.accountInfo, + }; + return this._get(options); + } + + // ************************** Contact Lists ********************************** + + async createContactList(listData) { + const options = { + url: this.baseUrl + this.URLs.contactLists, + body: listData, + }; + return this._post(options); + } + + async listContactLists(params = {}) { + const options = { + url: this.baseUrl + this.URLs.contactLists, + query: params + }; + return this._get(options); + } + + async getContactListById(listId) { + const options = { + url: this.baseUrl + this.URLs.contactListById(listId), + }; + return this._get(options); + } + + async updateContactList(listId, listData) { + const options = { + url: this.baseUrl + this.URLs.contactListById(listId), + body: listData, + }; + return this._put(options); + } + + async deleteContactList(listId) { + const options = { + url: this.baseUrl + this.URLs.contactListById(listId), + }; + return this._delete(options); + } + + // ************************** Contacts ********************************** + + async createContact(contactData) { + const options = { + url: this.baseUrl + this.URLs.contacts, + body: contactData, + }; + return this._post(options); + } + + async listContacts(params = {}) { + const options = { + url: this.baseUrl + this.URLs.contacts, + query: params + }; + return this._get(options); + } + + async getContactById(contactId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.contactById(contactId), + query: params + }; + return this._get(options); + } + + async updateContact(contactId, contactData) { + const options = { + url: this.baseUrl + this.URLs.contactById(contactId), + body: contactData, + }; + return this._put(options); + } + + async deleteContact(contactId) { + const options = { + url: this.baseUrl + this.URLs.contactById(contactId), + }; + return this._delete(options); + } + + // ************************** Email Campaigns ********************************** + + async createEmailCampaign(campaignData) { + const options = { + url: this.baseUrl + this.URLs.emailCampaigns, + body: campaignData, + }; + return this._post(options); + } + + async listEmailCampaigns(params = {}) { + const options = { + url: this.baseUrl + this.URLs.emailCampaigns, + query: params + }; + return this._get(options); + } + + async getEmailCampaignById(campaignId) { + const options = { + url: this.baseUrl + this.URLs.emailCampaignById(campaignId), + }; + return this._get(options); + } + + async updateEmailCampaign(campaignId, campaignData) { + const options = { + url: this.baseUrl + this.URLs.emailCampaignById(campaignId), + body: campaignData, + }; + return this._put(options); + } + + async deleteEmailCampaign(campaignId) { + const options = { + url: this.baseUrl + this.URLs.emailCampaignById(campaignId), + }; + return this._delete(options); + } + + async sendEmailCampaign(campaignId) { + const options = { + url: this.baseUrl + this.URLs.emailCampaignById(campaignId) + '/schedules', + body: { scheduled_date: new Date().toISOString() }, + }; + return this._post(options); + } + + async getEmailCampaignActivities(campaignId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.emailCampaignActivities(campaignId), + query: params + }; + return this._get(options); + } + + // ************************** Activities ********************************** + + async listActivities(params = {}) { + const options = { + url: this.baseUrl + this.URLs.activities, + query: params + }; + return this._get(options); + } + + async getActivityById(activityId) { + const options = { + url: this.baseUrl + this.URLs.activityById(activityId), + }; + return this._get(options); + } + + // ************************** Bulk Operations ********************************** + + async bulkImportContacts(importData) { + const options = { + url: this.baseUrl + this.URLs.bulkImportContacts, + body: importData, + }; + return this._post(options); + } + + async bulkDeleteContacts(deleteData) { + const options = { + url: this.baseUrl + this.URLs.bulkDeleteContacts, + body: deleteData, + }; + return this._post(options); + } + + // ************************** Reporting ********************************** + + async getEmailReports(params = {}) { + const options = { + url: this.baseUrl + this.URLs.emailReports, + query: params + }; + return this._get(options); + } + + async getContactReports(params = {}) { + const options = { + url: this.baseUrl + this.URLs.contactReports, + query: params + }; + return this._get(options); + } + + // ************************** Landing Pages ********************************** + + async createLandingPage(pageData) { + const options = { + url: this.baseUrl + this.URLs.landingPages, + body: pageData, + }; + return this._post(options); + } + + async listLandingPages(params = {}) { + const options = { + url: this.baseUrl + this.URLs.landingPages, + query: params + }; + return this._get(options); + } + + async getLandingPageById(pageId) { + const options = { + url: this.baseUrl + this.URLs.landingPageById(pageId), + }; + return this._get(options); + } + + async updateLandingPage(pageId, pageData) { + const options = { + url: this.baseUrl + this.URLs.landingPageById(pageId), + body: pageData, + }; + return this._put(options); + } + + async deleteLandingPage(pageId) { + const options = { + url: this.baseUrl + this.URLs.landingPageById(pageId), + }; + return this._delete(options); + } + + // ************************** Webhooks ********************************** + + async createWebhook(webhookData) { + const options = { + url: this.baseUrl + this.URLs.webhooks, + body: webhookData, + }; + return this._post(options); + } + + async listWebhooks() { + const options = { + url: this.baseUrl + this.URLs.webhooks, + }; + return this._get(options); + } + + async getWebhookById(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + }; + return this._get(options); + } + + async updateWebhook(webhookId, webhookData) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + body: webhookData, + }; + return this._put(options); + } + + async deleteWebhook(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + }; + return this._delete(options); + } +} + +module.exports = { Api }; diff --git a/packages/constantcontact/defaultConfig.json b/packages/constantcontact/defaultConfig.json new file mode 100644 index 0000000..b94299c --- /dev/null +++ b/packages/constantcontact/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "constantcontact", + "label": "Constant Contact", + "productUrl": "https://www.constantcontact.com", + "apiDocs": "https://developer.constantcontact.com", + "logoUrl": "https://friggframework.org/assets/img/constantcontact-icon.png", + "categories": [ + "Email Marketing", + "Marketing Automation", + "Communication" + ], + "description": "Constant Contact is an email marketing and digital marketing platform that helps small businesses grow their customer relationships." +} diff --git a/packages/constantcontact/definition.js b/packages/constantcontact/definition.js new file mode 100644 index 0000000..ce89ca3 --- /dev/null +++ b/packages/constantcontact/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'ConstantContact', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.account_id, user: userId}, + details: {name: userDetails.organization_name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.account_id, user: userId}, + details: {name: userDetails.organization_name} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.CONSTANT_CONTACT_CLIENT_ID, + client_secret: process.env.CONSTANT_CONTACT_CLIENT_SECRET, + scope: process.env.CONSTANT_CONTACT_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/constantcontact`, + } +}; + +module.exports = {Definition}; diff --git a/packages/constantcontact/index.js b/packages/constantcontact/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/constantcontact/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/constantcontact/jest.config.js b/packages/constantcontact/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/constantcontact/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/constantcontact/package.json b/packages/constantcontact/package.json new file mode 100644 index 0000000..c3bf191 --- /dev/null +++ b/packages/constantcontact/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-constantcontact", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Constant Contact API module that lets the Frigg Framework interact with Constant Contact", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/needs-updating/huggg/.eslintrc.json b/packages/contentful/.eslintrc.json similarity index 100% rename from packages/needs-updating/huggg/.eslintrc.json rename to packages/contentful/.eslintrc.json diff --git a/packages/v1-ready/contentful/.gitignore b/packages/contentful/.gitignore similarity index 100% rename from packages/v1-ready/contentful/.gitignore rename to packages/contentful/.gitignore diff --git a/packages/v1-ready/contentful/CHANGELOG.md b/packages/contentful/CHANGELOG.md similarity index 100% rename from packages/v1-ready/contentful/CHANGELOG.md rename to packages/contentful/CHANGELOG.md diff --git a/packages/v1-ready/42matters/LICENSE.md b/packages/contentful/LICENSE.md similarity index 100% rename from packages/v1-ready/42matters/LICENSE.md rename to packages/contentful/LICENSE.md diff --git a/packages/v1-ready/contentful/README.md b/packages/contentful/README.md similarity index 100% rename from packages/v1-ready/contentful/README.md rename to packages/contentful/README.md diff --git a/packages/v1-ready/contentful/api.js b/packages/contentful/api.js similarity index 100% rename from packages/v1-ready/contentful/api.js rename to packages/contentful/api.js diff --git a/packages/v1-ready/contentful/defaultConfig.json b/packages/contentful/defaultConfig.json similarity index 100% rename from packages/v1-ready/contentful/defaultConfig.json rename to packages/contentful/defaultConfig.json diff --git a/packages/v1-ready/contentful/definition.js b/packages/contentful/definition.js similarity index 100% rename from packages/v1-ready/contentful/definition.js rename to packages/contentful/definition.js diff --git a/packages/v1-ready/contentful/index.js b/packages/contentful/index.js similarity index 100% rename from packages/v1-ready/contentful/index.js rename to packages/contentful/index.js diff --git a/packages/v1-ready/42matters/jest.config.js b/packages/contentful/jest.config.js similarity index 100% rename from packages/v1-ready/42matters/jest.config.js rename to packages/contentful/jest.config.js diff --git a/packages/v1-ready/contentful/package.json b/packages/contentful/package.json similarity index 100% rename from packages/v1-ready/contentful/package.json rename to packages/contentful/package.json diff --git a/packages/v1-ready/contentful/tests/api.test.js b/packages/contentful/tests/api.test.js similarity index 100% rename from packages/v1-ready/contentful/tests/api.test.js rename to packages/contentful/tests/api.test.js diff --git a/packages/v1-ready/contentful/tests/auther.test.js b/packages/contentful/tests/auther.test.js similarity index 100% rename from packages/v1-ready/contentful/tests/auther.test.js rename to packages/contentful/tests/auther.test.js diff --git a/packages/v1-ready/contentful/tests/mocks/createEntryBody.json b/packages/contentful/tests/mocks/createEntryBody.json similarity index 100% rename from packages/v1-ready/contentful/tests/mocks/createEntryBody.json rename to packages/contentful/tests/mocks/createEntryBody.json diff --git a/packages/v1-ready/contentful/tests/mocks/index.js b/packages/contentful/tests/mocks/index.js similarity index 100% rename from packages/v1-ready/contentful/tests/mocks/index.js rename to packages/contentful/tests/mocks/index.js diff --git a/packages/v1-ready/contentful/tests/mocks/patchEntryBody.json b/packages/contentful/tests/mocks/patchEntryBody.json similarity index 100% rename from packages/v1-ready/contentful/tests/mocks/patchEntryBody.json rename to packages/contentful/tests/mocks/patchEntryBody.json diff --git a/packages/v1-ready/contentful/tests/mocks/updateEntryBody.json b/packages/contentful/tests/mocks/updateEntryBody.json similarity index 100% rename from packages/v1-ready/contentful/tests/mocks/updateEntryBody.json rename to packages/contentful/tests/mocks/updateEntryBody.json diff --git a/packages/needs-updating/marketo/.eslintrc.json b/packages/contentstack/.eslintrc.json similarity index 100% rename from packages/needs-updating/marketo/.eslintrc.json rename to packages/contentstack/.eslintrc.json diff --git a/packages/v1-ready/contentstack/.gitignore b/packages/contentstack/.gitignore similarity index 100% rename from packages/v1-ready/contentstack/.gitignore rename to packages/contentstack/.gitignore diff --git a/packages/v1-ready/contentstack/CHANGELOG.md b/packages/contentstack/CHANGELOG.md similarity index 100% rename from packages/v1-ready/contentstack/CHANGELOG.md rename to packages/contentstack/CHANGELOG.md diff --git a/packages/v1-ready/contentful/LICENSE.md b/packages/contentstack/LICENSE.md similarity index 100% rename from packages/v1-ready/contentful/LICENSE.md rename to packages/contentstack/LICENSE.md diff --git a/packages/v1-ready/contentstack/README.md b/packages/contentstack/README.md similarity index 100% rename from packages/v1-ready/contentstack/README.md rename to packages/contentstack/README.md diff --git a/packages/v1-ready/contentstack/api.js b/packages/contentstack/api.js similarity index 100% rename from packages/v1-ready/contentstack/api.js rename to packages/contentstack/api.js diff --git a/packages/v1-ready/contentstack/defaultConfig.json b/packages/contentstack/defaultConfig.json similarity index 100% rename from packages/v1-ready/contentstack/defaultConfig.json rename to packages/contentstack/defaultConfig.json diff --git a/packages/v1-ready/contentstack/definition.js b/packages/contentstack/definition.js similarity index 100% rename from packages/v1-ready/contentstack/definition.js rename to packages/contentstack/definition.js diff --git a/packages/v1-ready/contentstack/index.js b/packages/contentstack/index.js similarity index 100% rename from packages/v1-ready/contentstack/index.js rename to packages/contentstack/index.js diff --git a/packages/v1-ready/contentful/jest.config.js b/packages/contentstack/jest.config.js similarity index 100% rename from packages/v1-ready/contentful/jest.config.js rename to packages/contentstack/jest.config.js diff --git a/packages/v1-ready/contentstack/package.json b/packages/contentstack/package.json similarity index 100% rename from packages/v1-ready/contentstack/package.json rename to packages/contentstack/package.json diff --git a/packages/v1-ready/contentstack/tests/api.test.js b/packages/contentstack/tests/api.test.js similarity index 100% rename from packages/v1-ready/contentstack/tests/api.test.js rename to packages/contentstack/tests/api.test.js diff --git a/packages/v1-ready/contentstack/tests/auther.test.js b/packages/contentstack/tests/auther.test.js similarity index 100% rename from packages/v1-ready/contentstack/tests/auther.test.js rename to packages/contentstack/tests/auther.test.js diff --git a/packages/convertkit/README.md b/packages/convertkit/README.md new file mode 100644 index 0000000..5d47c52 --- /dev/null +++ b/packages/convertkit/README.md @@ -0,0 +1,5 @@ +# ConvertKit + +This is the API Module for ConvertKit that allows the [Frigg](https://friggframework.org) code to talk to the ConvertKit API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/convertkit) diff --git a/packages/convertkit/api.js b/packages/convertkit/api.js new file mode 100644 index 0000000..ab6fe02 --- /dev/null +++ b/packages/convertkit/api.js @@ -0,0 +1,381 @@ +const { Requester, get } = require('@friggframework/core'); + +class Api extends Requester { + constructor(params) { + super(params); + this.apiKey = get(params, 'api_key', process.env.CONVERTKIT_API_KEY); + this.apiSecret = get(params, 'api_secret', process.env.CONVERTKIT_API_SECRET); + this.baseUrl = 'https://api.convertkit.com/v3'; + + this.URLs = { + // Account + account: '/account', + + // Subscribers + subscribers: '/subscribers', + subscriberById: (subscriberId) => `/subscribers/${subscriberId}`, + subscriberTags: (subscriberId) => `/subscribers/${subscriberId}/tags`, + + // Forms + forms: '/forms', + formById: (formId) => `/forms/${formId}`, + formSubscriptions: (formId) => `/forms/${formId}/subscribe`, + formSubscribers: (formId) => `/forms/${formId}/subscriptions`, + + // Landing Pages + landingPages: '/landing_pages', + landingPageById: (pageId) => `/landing_pages/${pageId}`, + + // Tags + tags: '/tags', + tagById: (tagId) => `/tags/${tagId}`, + tagSubscriptions: (tagId) => `/tags/${tagId}/subscribe`, + tagUnsubscriptions: (tagId) => `/tags/${tagId}/unsubscribe`, + tagSubscribers: (tagId) => `/tags/${tagId}/subscriptions`, + + // Sequences + sequences: '/sequences', + sequenceById: (sequenceId) => `/sequences/${sequenceId}`, + sequenceSubscriptions: (sequenceId) => `/sequences/${sequenceId}/subscribe`, + sequenceSubscribers: (sequenceId) => `/sequences/${sequenceId}/subscriptions`, + + // Broadcasts + broadcasts: '/broadcasts', + broadcastById: (broadcastId) => `/broadcasts/${broadcastId}`, + broadcastStats: (broadcastId) => `/broadcasts/${broadcastId}/stats`, + + // Custom Fields + customFields: '/custom_fields', + customFieldById: (fieldId) => `/custom_fields/${fieldId}`, + + // Segments + segments: '/segments', + segmentById: (segmentId) => `/segments/${segmentId}`, + + // Purchases + purchases: '/purchases', + purchaseById: (purchaseId) => `/purchases/${purchaseId}`, + + // Webhooks + webhooks: '/automations/hooks', + webhookById: (webhookId) => `/automations/hooks/${webhookId}` + }; + } + + addAuthParams(options) { + // ConvertKit uses API key and secret as query parameters + const authParams = { + api_key: this.apiKey, + api_secret: this.apiSecret + }; + + options.query = { + ...authParams, + ...options.query, + }; + + const jsonHeaders = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _get(options) { + this.addAuthParams(options); + return super._get(options); + } + + async _post(options, stringify = true) { + this.addAuthParams(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + this.addAuthParams(options); + return super._put(options, stringify); + } + + async _delete(options) { + this.addAuthParams(options); + return super._delete(options); + } + + // ************************** Account ********************************** + + async getAccount() { + const options = { + url: this.baseUrl + this.URLs.account, + }; + return this._get(options); + } + + // ************************** Subscribers ********************************** + + async listSubscribers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.subscribers, + query: params + }; + return this._get(options); + } + + async getSubscriberById(subscriberId) { + const options = { + url: this.baseUrl + this.URLs.subscriberById(subscriberId), + }; + return this._get(options); + } + + async updateSubscriber(subscriberId, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.subscriberById(subscriberId), + body: subscriberData, + }; + return this._put(options); + } + + async unsubscribeSubscriber(subscriberId) { + const options = { + url: this.baseUrl + this.URLs.subscriberById(subscriberId) + '/unsubscribe', + }; + return this._put(options, false); + } + + async tagSubscriber(subscriberId, tagData) { + const options = { + url: this.baseUrl + this.URLs.subscriberTags(subscriberId), + body: tagData, + }; + return this._post(options); + } + + async untagSubscriber(subscriberId, tagData) { + const options = { + url: this.baseUrl + this.URLs.subscriberTags(subscriberId), + body: tagData, + }; + return this._delete(options); + } + + // ************************** Forms ********************************** + + async listForms() { + const options = { + url: this.baseUrl + this.URLs.forms, + }; + return this._get(options); + } + + async getFormById(formId) { + const options = { + url: this.baseUrl + this.URLs.formById(formId), + }; + return this._get(options); + } + + async subscribeToForm(formId, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.formSubscriptions(formId), + body: subscriberData, + }; + return this._post(options); + } + + async getFormSubscribers(formId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.formSubscribers(formId), + query: params + }; + return this._get(options); + } + + // ************************** Landing Pages ********************************** + + async listLandingPages() { + const options = { + url: this.baseUrl + this.URLs.landingPages, + }; + return this._get(options); + } + + async getLandingPageById(pageId) { + const options = { + url: this.baseUrl + this.URLs.landingPageById(pageId), + }; + return this._get(options); + } + + // ************************** Tags ********************************** + + async listTags() { + const options = { + url: this.baseUrl + this.URLs.tags, + }; + return this._get(options); + } + + async createTag(tagData) { + const options = { + url: this.baseUrl + this.URLs.tags, + body: tagData, + }; + return this._post(options); + } + + async getTagById(tagId) { + const options = { + url: this.baseUrl + this.URLs.tagById(tagId), + }; + return this._get(options); + } + + async subscribeToTag(tagId, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.tagSubscriptions(tagId), + body: subscriberData, + }; + return this._post(options); + } + + async unsubscribeFromTag(tagId, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.tagUnsubscriptions(tagId), + body: subscriberData, + }; + return this._post(options); + } + + async getTagSubscribers(tagId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.tagSubscribers(tagId), + query: params + }; + return this._get(options); + } + + // ************************** Sequences ********************************** + + async listSequences() { + const options = { + url: this.baseUrl + this.URLs.sequences, + }; + return this._get(options); + } + + async getSequenceById(sequenceId) { + const options = { + url: this.baseUrl + this.URLs.sequenceById(sequenceId), + }; + return this._get(options); + } + + async subscribeToSequence(sequenceId, subscriberData) { + const options = { + url: this.baseUrl + this.URLs.sequenceSubscriptions(sequenceId), + body: subscriberData, + }; + return this._post(options); + } + + async getSequenceSubscribers(sequenceId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.sequenceSubscribers(sequenceId), + query: params + }; + return this._get(options); + } + + // ************************** Broadcasts ********************************** + + async listBroadcasts() { + const options = { + url: this.baseUrl + this.URLs.broadcasts, + }; + return this._get(options); + } + + async createBroadcast(broadcastData) { + const options = { + url: this.baseUrl + this.URLs.broadcasts, + body: broadcastData, + }; + return this._post(options); + } + + async getBroadcastById(broadcastId) { + const options = { + url: this.baseUrl + this.URLs.broadcastById(broadcastId), + }; + return this._get(options); + } + + async getBroadcastStats(broadcastId) { + const options = { + url: this.baseUrl + this.URLs.broadcastStats(broadcastId), + }; + return this._get(options); + } + + // ************************** Custom Fields ********************************** + + async listCustomFields() { + const options = { + url: this.baseUrl + this.URLs.customFields, + }; + return this._get(options); + } + + async createCustomField(fieldData) { + const options = { + url: this.baseUrl + this.URLs.customFields, + body: fieldData, + }; + return this._post(options); + } + + async updateCustomField(fieldId, fieldData) { + const options = { + url: this.baseUrl + this.URLs.customFieldById(fieldId), + body: fieldData, + }; + return this._put(options); + } + + async deleteCustomField(fieldId) { + const options = { + url: this.baseUrl + this.URLs.customFieldById(fieldId), + }; + return this._delete(options); + } + + // ************************** Purchases ********************************** + + async createPurchase(purchaseData) { + const options = { + url: this.baseUrl + this.URLs.purchases, + body: purchaseData, + }; + return this._post(options); + } + + async listPurchases(params = {}) { + const options = { + url: this.baseUrl + this.URLs.purchases, + query: params + }; + return this._get(options); + } + + async getPurchaseById(purchaseId) { + const options = { + url: this.baseUrl + this.URLs.purchaseById(purchaseId), + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/convertkit/defaultConfig.json b/packages/convertkit/defaultConfig.json new file mode 100644 index 0000000..fa833e2 --- /dev/null +++ b/packages/convertkit/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "convertkit", + "label": "ConvertKit", + "productUrl": "https://convertkit.com", + "apiDocs": "https://developers.convertkit.com", + "logoUrl": "https://friggframework.org/assets/img/convertkit-icon.png", + "categories": [ + "Email Marketing", + "Creator Economy", + "Marketing Automation" + ], + "description": "ConvertKit is an email marketing platform built for creators to help them grow their audience and earn a living online." +} diff --git a/packages/convertkit/definition.js b/packages/convertkit/definition.js new file mode 100644 index 0000000..8a60a69 --- /dev/null +++ b/packages/convertkit/definition.js @@ -0,0 +1,51 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'ConvertKit', + requiredAuthMethods: { + getToken: async function (api, params) { + // ConvertKit uses API key and secret authentication + return { + access_token: params.api_key, + api_secret: params.api_secret + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const accountDetails = await api.getAccount(); + return { + identifiers: {externalId: accountDetails.account_id, user: userId}, + details: {name: accountDetails.name, email: accountDetails.primary_email_address}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'api_key', 'api_secret' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const accountDetails = await api.getAccount(); + return { + identifiers: {externalId: accountDetails.account_id, user: userId}, + details: {name: accountDetails.name} + }; + }, + testAuthRequest: async function (api) { + return api.getAccount() + }, + }, + env: { + api_key: process.env.CONVERTKIT_API_KEY, + api_secret: process.env.CONVERTKIT_API_SECRET, + } +}; + +module.exports = {Definition}; diff --git a/packages/convertkit/index.js b/packages/convertkit/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/convertkit/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/convertkit/jest.config.js b/packages/convertkit/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/convertkit/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/convertkit/package.json b/packages/convertkit/package.json new file mode 100644 index 0000000..4a2d318 --- /dev/null +++ b/packages/convertkit/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-convertkit", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "ConvertKit API module that lets the Frigg Framework interact with ConvertKit", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/needs-updating/monday/.eslintrc.json b/packages/crossbeam/.eslintrc.json similarity index 100% rename from packages/needs-updating/monday/.eslintrc.json rename to packages/crossbeam/.eslintrc.json diff --git a/packages/needs-updating/front/CHANGELOG.md b/packages/crossbeam/CHANGELOG.md similarity index 100% rename from packages/needs-updating/front/CHANGELOG.md rename to packages/crossbeam/CHANGELOG.md diff --git a/packages/needs-updating/gorgias/LICENSE.md b/packages/crossbeam/LICENSE.md similarity index 100% rename from packages/needs-updating/gorgias/LICENSE.md rename to packages/crossbeam/LICENSE.md diff --git a/packages/v1-ready/crossbeam/README.md b/packages/crossbeam/README.md similarity index 100% rename from packages/v1-ready/crossbeam/README.md rename to packages/crossbeam/README.md diff --git a/packages/v1-ready/crossbeam/api.js b/packages/crossbeam/api.js similarity index 100% rename from packages/v1-ready/crossbeam/api.js rename to packages/crossbeam/api.js diff --git a/packages/v1-ready/crossbeam/api.test.js b/packages/crossbeam/api.test.js similarity index 100% rename from packages/v1-ready/crossbeam/api.test.js rename to packages/crossbeam/api.test.js diff --git a/packages/v1-ready/crossbeam/defaultConfig.json b/packages/crossbeam/defaultConfig.json similarity index 100% rename from packages/v1-ready/crossbeam/defaultConfig.json rename to packages/crossbeam/defaultConfig.json diff --git a/packages/v1-ready/crossbeam/definition.js b/packages/crossbeam/definition.js similarity index 100% rename from packages/v1-ready/crossbeam/definition.js rename to packages/crossbeam/definition.js diff --git a/packages/v1-ready/crossbeam/index.js b/packages/crossbeam/index.js similarity index 100% rename from packages/v1-ready/crossbeam/index.js rename to packages/crossbeam/index.js diff --git a/packages/needs-updating/front/jest.config.js b/packages/crossbeam/jest.config.js similarity index 100% rename from packages/needs-updating/front/jest.config.js rename to packages/crossbeam/jest.config.js diff --git a/packages/v1-ready/crossbeam/mocks/Partners/getPartnerPopulations.js b/packages/crossbeam/mocks/Partners/getPartnerPopulations.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Partners/getPartnerPopulations.js rename to packages/crossbeam/mocks/Partners/getPartnerPopulations.js diff --git a/packages/v1-ready/crossbeam/mocks/Partners/getPartnerRecords.js b/packages/crossbeam/mocks/Partners/getPartnerRecords.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Partners/getPartnerRecords.js rename to packages/crossbeam/mocks/Partners/getPartnerRecords.js diff --git a/packages/v1-ready/crossbeam/mocks/Partners/getPartners.js b/packages/crossbeam/mocks/Partners/getPartners.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Partners/getPartners.js rename to packages/crossbeam/mocks/Partners/getPartners.js diff --git a/packages/v1-ready/crossbeam/mocks/Partners/getPopulations.js b/packages/crossbeam/mocks/Partners/getPopulations.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Partners/getPopulations.js rename to packages/crossbeam/mocks/Partners/getPopulations.js diff --git a/packages/v1-ready/crossbeam/mocks/Reports/getReportData.js b/packages/crossbeam/mocks/Reports/getReportData.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Reports/getReportData.js rename to packages/crossbeam/mocks/Reports/getReportData.js diff --git a/packages/v1-ready/crossbeam/mocks/Reports/getReports.js b/packages/crossbeam/mocks/Reports/getReports.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Reports/getReports.js rename to packages/crossbeam/mocks/Reports/getReports.js diff --git a/packages/v1-ready/crossbeam/mocks/Threads/getThreadTimelines.js b/packages/crossbeam/mocks/Threads/getThreadTimelines.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Threads/getThreadTimelines.js rename to packages/crossbeam/mocks/Threads/getThreadTimelines.js diff --git a/packages/v1-ready/crossbeam/mocks/Threads/getThreads.js b/packages/crossbeam/mocks/Threads/getThreads.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/Threads/getThreads.js rename to packages/crossbeam/mocks/Threads/getThreads.js diff --git a/packages/v1-ready/crossbeam/mocks/apiMock.js b/packages/crossbeam/mocks/apiMock.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/apiMock.js rename to packages/crossbeam/mocks/apiMock.js diff --git a/packages/v1-ready/crossbeam/mocks/getUserDetails.js b/packages/crossbeam/mocks/getUserDetails.js similarity index 100% rename from packages/v1-ready/crossbeam/mocks/getUserDetails.js rename to packages/crossbeam/mocks/getUserDetails.js diff --git a/packages/v1-ready/crossbeam/package.json b/packages/crossbeam/package.json similarity index 100% rename from packages/v1-ready/crossbeam/package.json rename to packages/crossbeam/package.json diff --git a/packages/crowdcompass/README.md b/packages/crowdcompass/README.md new file mode 100644 index 0000000..697a82a --- /dev/null +++ b/packages/crowdcompass/README.md @@ -0,0 +1,43 @@ +# CrowdCompass API Module + +This module provides integration with the CrowdCompass API for the Frigg Framework. + +## Description + +CrowdCompass provides event management and mobile app solutions for conferences and events. + +## Installation + +```bash +npm install @friggframework/api-module-crowdcompass +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-crowdcompass'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +CROWDCOMPASS_CLIENT_ID=your_client_id +CROWDCOMPASS_CLIENT_SECRET=your_client_secret +CROWDCOMPASS_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/crowdcompass/api.js b/packages/crowdcompass/api.js new file mode 100644 index 0000000..c5d46d6 --- /dev/null +++ b/packages/crowdcompass/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.crowdcompass.com/v2'; + + this.URLs = { + // User/Account info + userInfo: '/events', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://crowdcompass.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.crowdcompass.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to CrowdCompass +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/crowdcompass/defaultConfig.json b/packages/crowdcompass/defaultConfig.json new file mode 100644 index 0000000..e6d9ed5 --- /dev/null +++ b/packages/crowdcompass/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "crowdcompass", + "label": "CrowdCompass", + "productUrl": "https://crowdcompass.com/", + "apiDocs": "https://developers.crowdcompass.com/", + "logoUrl": "https://friggframework.org/assets/img/crowdcompass-icon.png", + "categories": [ + "Marketing" + ], + "subCategories": [ + "Event Management" + ], + "description": "CrowdCompass provides event management and mobile app solutions for conferences and events." +} \ No newline at end of file diff --git a/packages/crowdcompass/definition.js b/packages/crowdcompass/definition.js new file mode 100644 index 0000000..4c47c2d --- /dev/null +++ b/packages/crowdcompass/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'CrowdCompass', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'crowdcompass-account', user: userId}, + details: {name: userInfo.name || 'CrowdCompass Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'crowdcompass-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.CROWDCOMPASS_CLIENT_ID, + client_secret: process.env.CROWDCOMPASS_CLIENT_SECRET, + scope: process.env.CROWDCOMPASS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/crowdcompass`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/crowdcompass/index.js b/packages/crowdcompass/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/crowdcompass/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/crowdcompass/jest-setup.js b/packages/crowdcompass/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/crowdcompass/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/crowdcompass/jest-teardown.js b/packages/crowdcompass/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/crowdcompass/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/crowdcompass/jest.config.js b/packages/crowdcompass/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/crowdcompass/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/crowdcompass/package.json b/packages/crowdcompass/package.json new file mode 100644 index 0000000..ca99604 --- /dev/null +++ b/packages/crowdcompass/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-crowdcompass", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "CrowdCompass API module that lets the Frigg Framework interact with CrowdCompass", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/crowdcompass/test/api.test.js b/packages/crowdcompass/test/api.test.js new file mode 100644 index 0000000..8c8ed42 --- /dev/null +++ b/packages/crowdcompass/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('CrowdCompass API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/crowdcompass/test/definition.test.js b/packages/crowdcompass/test/definition.test.js new file mode 100644 index 0000000..5d9f426 --- /dev/null +++ b/packages/crowdcompass/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('CrowdCompass Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('crowdcompass'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/cubepasses/README.md b/packages/cubepasses/README.md new file mode 100644 index 0000000..7de1b95 --- /dev/null +++ b/packages/cubepasses/README.md @@ -0,0 +1,55 @@ +# CubePasses API Module + +CubePasses API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/cubepasses +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cubepasses'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +CUBEPASSES_CLIENT_ID=your_client_id +CUBEPASSES_CLIENT_SECRET=your_client_secret +CUBEPASSES_SCOPE=your_scope +CUBEPASSES_AUTH_URI=authorization_endpoint +CUBEPASSES_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +Marketing + +## License + +MIT diff --git a/packages/cubepasses/api.js b/packages/cubepasses/api.js new file mode 100644 index 0000000..7af58e5 --- /dev/null +++ b/packages/cubepasses/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'Marketing'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.CUBEPASSES_AUTH_URI; + this.tokenUri = process.env.CUBEPASSES_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'CubePasses', + MODULE_NAME: 'cubepasses', + CATEGORY: 'https://api.cubepasses.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/cubepasses/defaultConfig.json b/packages/cubepasses/defaultConfig.json new file mode 100644 index 0000000..0432220 --- /dev/null +++ b/packages/cubepasses/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "CubePasses", + "moduleName": "cubepasses", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "CubePasses API Integration Module", + "category": "Marketing", + "apiDocUrl": "https://docs.cubepasses.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cubepasses/definition.js b/packages/cubepasses/definition.js new file mode 100644 index 0000000..a773518 --- /dev/null +++ b/packages/cubepasses/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'CubePasses', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CUBEPASSES_CLIENT_ID, + client_secret: process.env.CUBEPASSES_CLIENT_SECRET, + scope: process.env.CUBEPASSES_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cubepasses`, + } +}; + +module.exports = {Definition}; diff --git a/packages/cubepasses/index.js b/packages/cubepasses/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/cubepasses/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/cubepasses/jest-setup.js b/packages/cubepasses/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/cubepasses/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/cubepasses/jest-teardown.js b/packages/cubepasses/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/cubepasses/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/cubepasses/jest.config.js b/packages/cubepasses/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/cubepasses/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/cubepasses/package.json b/packages/cubepasses/package.json new file mode 100644 index 0000000..d6332c8 --- /dev/null +++ b/packages/cubepasses/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/cubepasses", + "version": "0.0.1", + "description": "CubePasses API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "cubepasses" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/cvent/README.md b/packages/cvent/README.md new file mode 100644 index 0000000..0999cbd --- /dev/null +++ b/packages/cvent/README.md @@ -0,0 +1,34 @@ +# Cvent API Integration + +Cvent integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/cvent +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/cvent'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `CVENT_CLIENT_ID` +- `CVENT_CLIENT_SECRET` +- `CVENT_SCOPE` + +## API Documentation + +For more information about the Cvent API, visit: https://api.cvent.com/ea diff --git a/packages/cvent/api.js b/packages/cvent/api.js new file mode 100644 index 0000000..bb72cc2 --- /dev/null +++ b/packages/cvent/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class CventApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.cvent.com/ea'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: CventApi}; diff --git a/packages/cvent/defaultConfig.json b/packages/cvent/defaultConfig.json new file mode 100644 index 0000000..5bcc1c9 --- /dev/null +++ b/packages/cvent/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Cvent", + "moduleName": "cvent", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Cvent API Integration Module", + "category": "Marketing", + "apiDocUrl": "https://api.cvent.com/ea", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/cvent/definition.js b/packages/cvent/definition.js new file mode 100644 index 0000000..c9c2b62 --- /dev/null +++ b/packages/cvent/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Cvent', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.CVENT_CLIENT_ID, + client_secret: process.env.CVENT_CLIENT_SECRET, + scope: process.env.CVENT_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/cvent`, + } +}; + +module.exports = {Definition}; diff --git a/packages/cvent/index.js b/packages/cvent/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/cvent/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/cvent/package.json b/packages/cvent/package.json new file mode 100644 index 0000000..580089f --- /dev/null +++ b/packages/cvent/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/cvent", + "version": "0.0.1", + "description": "Cvent API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "cvent", + "marketing" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/datadog/.env.example b/packages/datadog/.env.example new file mode 100644 index 0000000..b17de27 --- /dev/null +++ b/packages/datadog/.env.example @@ -0,0 +1,3 @@ +# DATADOG API Configuration +DATADOG_API_KEY=your_api_key_here +DATADOG_APPLICATION_KEY=your_application_key_here diff --git a/packages/datadog/.eslintrc.json b/packages/datadog/.eslintrc.json new file mode 100644 index 0000000..cbc0fc6 --- /dev/null +++ b/packages/datadog/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "@friggframework/eslint-config" + ], + "rules": { + "no-console": "warn" + } +} \ No newline at end of file diff --git a/packages/datadog/README.md b/packages/datadog/README.md new file mode 100644 index 0000000..04aaa32 --- /dev/null +++ b/packages/datadog/README.md @@ -0,0 +1,75 @@ +# Datadog API Module + +A Frigg API Module for Datadog integration. + +## Installation + +```bash +npm install @friggframework/datadog +``` + +## Configuration + +### Environment Variables + +```bash +DATADOG_API_KEY=your_api_key +DATADOG_APPLICATION_KEY=your_application_key +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/datadog'); + +// Initialize API client +const api = new Api({ + apiKey: 'your_api_key', + applicationKey: 'your_application_key' +}); + +// Validate API key +const validation = await api.validateApiKey(); + +// Submit metrics +await api.submitMetrics({ + series: [{ + metric: 'my.metric', + points: [[Date.now() / 1000, 1]], + tags: ['environment:production'] + }] +}); +``` + +## API Methods + +### Validation +- `validateApiKey()` - Validate API credentials + +### Metrics +- `submitMetrics(data)` - Submit metrics to Datadog + +### Events +- `createEvent(event)` - Create an event +- `listEvents(params)` - List events + +### Logs +- `searchLogs(query)` - Search logs + +### Dashboards +- `listDashboards()` - List dashboards +- `createDashboard(dashboard)` - Create dashboard + +### Monitors +- `listMonitors()` - List monitors +- `createMonitor(monitor)` - Create monitor + +## Testing + +```bash +npm test +``` + +## License + +MIT diff --git a/packages/datadog/api.js b/packages/datadog/api.js new file mode 100644 index 0000000..6e4e345 --- /dev/null +++ b/packages/datadog/api.js @@ -0,0 +1,81 @@ +const { ApiKeyRequester, get, FriggError } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.datadoghq.com'; + + this.URLs = { + validate: '/api/v1/validate', + metrics: '/api/v1/series', + events: '/api/v1/events', + logs: '/api/v1/logs', + dashboards: '/api/v1/dashboard', + monitors: '/api/v1/monitor' + }; + + // Set up API key authentication + this.addAuthHeaders(); + } + + addAuthHeaders() { + if (this.apiKey) { + this.setDefaultHeaders({ + 'DD-API-KEY': this.apiKey, + 'DD-APPLICATION-KEY': this.applicationKey, + 'Content-Type': 'application/json' + }); + } + } + + static Definition = { + DISPLAY_NAME: 'Datadog', + MODULE_NAME: 'datadog', + CATEGORY: 'Monitoring', + USES_OAUTH: false + }; + + // Validation + async validateApiKey() { + return this.get(this.URLs.validate); + } + + // Metrics + async submitMetrics(data) { + return this.post(this.URLs.metrics, data); + } + + // Events + async createEvent(event) { + return this.post(this.URLs.events, event); + } + + async listEvents(params = {}) { + return this.get(this.URLs.events, params); + } + + // Logs + async searchLogs(query) { + return this.post(this.URLs.logs + '/search', { query }); + } + + // Dashboards + async listDashboards() { + return this.get(this.URLs.dashboards); + } + + async createDashboard(dashboard) { + return this.post(this.URLs.dashboards, dashboard); + } + + // Monitors + async listMonitors() { + return this.get(this.URLs.monitors); + } + + async createMonitor(monitor) { + return this.post(this.URLs.monitors, monitor); + } +} + +module.exports = { Api }; diff --git a/packages/datadog/defaultConfig.json b/packages/datadog/defaultConfig.json new file mode 100644 index 0000000..4af5687 --- /dev/null +++ b/packages/datadog/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Datadog", + "moduleName": "datadog", + "version": "0.0.1", + "supportedAuthTypes": [ + "apiKey" + ], + "docs": { + "description": "Datadog API Integration Module", + "category": "Monitoring", + "apiDocUrl": "https://docs.datadoghq.com/api/", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/datadog/definition.js b/packages/datadog/definition.js new file mode 100644 index 0000000..d81634f --- /dev/null +++ b/packages/datadog/definition.js @@ -0,0 +1,21 @@ +const { get } = require('@friggframework/core'); + +class Definition { + constructor(params) { + this.id = get(params, 'id'); + this.userId = get(params, 'userId'); + this.apiKey = get(params, 'apiKey'); + this.applicationKey = get(params, 'applicationKey'); + } + + static Config = { + name: 'datadog', + authType: 'apiKey', + env: { + api_key: process.env.DATADOG_API_KEY, + application_key: process.env.DATADOG_APPLICATION_KEY + } + }; +} + +module.exports = { Definition }; diff --git a/packages/datadog/index.js b/packages/datadog/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/datadog/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/datadog/jest-setup.js b/packages/datadog/jest-setup.js new file mode 100644 index 0000000..921ad01 --- /dev/null +++ b/packages/datadog/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); \ No newline at end of file diff --git a/packages/datadog/jest-teardown.js b/packages/datadog/jest-teardown.js new file mode 100644 index 0000000..afe6c48 --- /dev/null +++ b/packages/datadog/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; \ No newline at end of file diff --git a/packages/datadog/jest.config.js b/packages/datadog/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/datadog/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/datadog/package.json b/packages/datadog/package.json new file mode 100644 index 0000000..fa423bb --- /dev/null +++ b/packages/datadog/package.json @@ -0,0 +1,31 @@ +{ + "name": "@friggframework/datadog", + "version": "0.0.1", + "description": "Datadog API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "datadog", + "api", + "integration", + "monitoring" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/datadog/test/api.test.js b/packages/datadog/test/api.test.js new file mode 100644 index 0000000..b402d34 --- /dev/null +++ b/packages/datadog/test/api.test.js @@ -0,0 +1,54 @@ +const { Api } = require('../api'); +const { Definition } = require('../definition'); + +describe('Datadog API Tests', () => { + let api; + + beforeAll(() => { + api = new Api({ + apiKey: process.env.DATADOG_API_KEY || 'test-key', + applicationKey: process.env.DATADOG_APPLICATION_KEY || 'test-app-key' + }); + }); + + describe('Validation', () => { + test('should validate API key', async () => { + // Mock the validation endpoint + const validation = await api.validateApiKey(); + expect(validation).toBeDefined(); + }); + }); + + describe('Metrics', () => { + test('should submit metrics', async () => { + const metrics = { + series: [{ + metric: 'test.metric', + points: [[Date.now() / 1000, 1]], + tags: ['test:true'] + }] + }; + + const result = await api.submitMetrics(metrics); + expect(result).toBeDefined(); + }); + }); + + describe('Events', () => { + test('should create event', async () => { + const event = { + title: 'Test Event', + text: 'This is a test event', + tags: ['test:event'] + }; + + const result = await api.createEvent(event); + expect(result).toBeDefined(); + }); + + test('should list events', async () => { + const events = await api.listEvents({ start: Date.now() - 86400 }); + expect(Array.isArray(events) || events.events).toBeTruthy(); + }); + }); +}); diff --git a/packages/datadog/test/definition.test.js b/packages/datadog/test/definition.test.js new file mode 100644 index 0000000..eb808ad --- /dev/null +++ b/packages/datadog/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Datadog Definition Tests', () => { + test('should create definition with required fields', () => { + const params = { + id: 'test-id', + userId: 'test-user', + apiKey: 'test-key', + applicationKey: 'test-app-key' + }; + + const definition = new Definition(params); + + expect(definition.id).toBe(params.id); + expect(definition.userId).toBe(params.userId); + expect(definition.apiKey).toBe(params.apiKey); + expect(definition.applicationKey).toBe(params.applicationKey); + }); + + test('should have correct config', () => { + expect(Definition.Config.name).toBe('datadog'); + expect(Definition.Config.authType).toBe('apiKey'); + }); +}); diff --git a/packages/dealcloud/README.md b/packages/dealcloud/README.md new file mode 100644 index 0000000..289c72d --- /dev/null +++ b/packages/dealcloud/README.md @@ -0,0 +1,5 @@ +# DealCloud + +This is the API Module for DealCloud that allows the [Frigg](https://friggframework.org) code to talk to the DealCloud API. + +Read more on the [Frigg documentation website](https://docs.friggframework.org/api-modules/list/dealcloud) diff --git a/packages/dealcloud/api.js b/packages/dealcloud/api.js new file mode 100644 index 0000000..d5c8653 --- /dev/null +++ b/packages/dealcloud/api.js @@ -0,0 +1,161 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.dealcloud.com/rest/v4'; + + this.URLs = { + // Authentication + userInfo: '/users/me', + + // Entities + entities: '/entities', + entityById: (entityId) => `/entities/${entityId}`, + + // Entry Lists (records) + entryLists: '/entrylists', + entryListById: (entryListId) => `/entrylists/${entryListId}`, + entriesInList: (entryListId) => `/entrylists/${entryListId}/entries`, + + // Entries (records) + entries: '/entries', + entryById: (entryId) => `/entries/${entryId}`, + + // Custom Fields + customFields: '/customfields', + customFieldById: (fieldId) => `/customfields/${fieldId}`, + + // Files + files: '/files', + fileById: (fileId) => `/files/${fileId}`, + + // Lists + lists: '/lists', + listById: (listId) => `/lists/${listId}` + }; + + this.authorizationUri = encodeURI( + `https://api.dealcloud.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.dealcloud.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // ************************** Entities ********************************** + + async createEntity(body) { + const options = { + url: this.baseUrl + this.URLs.entities, + body: body, + }; + return this._post(options); + } + + async listEntities(params = {}) { + const options = { + url: this.baseUrl + this.URLs.entities, + query: params + }; + return this._get(options); + } + + async updateEntity(id, body) { + const options = { + url: this.baseUrl + this.URLs.entityById(id), + body: body, + }; + return this._put(options); + } + + async getEntityById(id) { + const options = { + url: this.baseUrl + this.URLs.entityById(id), + }; + return this._get(options); + } + + // ************************** Entry Lists ********************************** + + async createEntryList(body) { + const options = { + url: this.baseUrl + this.URLs.entryLists, + body: body, + }; + return this._post(options); + } + + async listEntryLists(params = {}) { + const options = { + url: this.baseUrl + this.URLs.entryLists, + query: params + }; + return this._get(options); + } + + async getEntryListById(id) { + const options = { + url: this.baseUrl + this.URLs.entryListById(id), + }; + return this._get(options); + } + + async getEntriesInList(entryListId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.entriesInList(entryListId), + query: params + }; + return this._get(options); + } + + // ************************** Entries ********************************** + + async createEntry(body) { + const options = { + url: this.baseUrl + this.URLs.entries, + body: body, + }; + return this._post(options); + } + + async listEntries(params = {}) { + const options = { + url: this.baseUrl + this.URLs.entries, + query: params + }; + return this._get(options); + } + + async updateEntry(id, body) { + const options = { + url: this.baseUrl + this.URLs.entryById(id), + body: body, + }; + return this._put(options); + } + + async deleteEntry(id) { + const options = { + url: this.baseUrl + this.URLs.entryById(id), + }; + return this._delete(options); + } + + async getEntryById(id) { + const options = { + url: this.baseUrl + this.URLs.entryById(id), + }; + return this._get(options); + } +} + +module.exports = { Api }; diff --git a/packages/dealcloud/defaultConfig.json b/packages/dealcloud/defaultConfig.json new file mode 100644 index 0000000..521cf00 --- /dev/null +++ b/packages/dealcloud/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "dealcloud", + "label": "DealCloud", + "productUrl": "https://www.dealcloud.com", + "apiDocs": "https://developer.dealcloud.com", + "logoUrl": "https://friggframework.org/assets/img/dealcloud-icon.png", + "categories": [ + "CRM", + "Deal Management", + "Investment Banking" + ], + "description": "DealCloud is a leading CRM and deal management platform for investment banking and capital markets." +} diff --git a/packages/dealcloud/definition.js b/packages/dealcloud/definition.js new file mode 100644 index 0000000..d7a1abf --- /dev/null +++ b/packages/dealcloud/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'DealCloud', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.DEALCLOUD_CLIENT_ID, + client_secret: process.env.DEALCLOUD_CLIENT_SECRET, + scope: process.env.DEALCLOUD_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/dealcloud`, + } +}; + +module.exports = {Definition}; diff --git a/packages/dealcloud/index.js b/packages/dealcloud/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/dealcloud/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/dealcloud/jest.config.js b/packages/dealcloud/jest.config.js new file mode 100644 index 0000000..4004b02 --- /dev/null +++ b/packages/dealcloud/jest.config.js @@ -0,0 +1,16 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ +module.exports = { + coverageThreshold: { + global: { + statements: 13, + branches: 0, + functions: 1, + lines: 13, + }, + }, + globalSetup: './jest-setup.js', + globalTeardown: './jest-teardown.js', +}; diff --git a/packages/dealcloud/package.json b/packages/dealcloud/package.json new file mode 100644 index 0000000..f1d7216 --- /dev/null +++ b/packages/dealcloud/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-dealcloud", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "DealCloud API module that lets the Frigg Framework interact with DealCloud", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/needs-updating/netx/.eslintrc.json b/packages/deel/.eslintrc.json similarity index 100% rename from packages/needs-updating/netx/.eslintrc.json rename to packages/deel/.eslintrc.json diff --git a/packages/v1-ready/deel/.gitignore b/packages/deel/.gitignore similarity index 100% rename from packages/v1-ready/deel/.gitignore rename to packages/deel/.gitignore diff --git a/packages/v1-ready/deel/CHANGELOG.md b/packages/deel/CHANGELOG.md similarity index 100% rename from packages/v1-ready/deel/CHANGELOG.md rename to packages/deel/CHANGELOG.md diff --git a/packages/v1-ready/contentstack/LICENSE.md b/packages/deel/LICENSE.md similarity index 100% rename from packages/v1-ready/contentstack/LICENSE.md rename to packages/deel/LICENSE.md diff --git a/packages/v1-ready/deel/README.md b/packages/deel/README.md similarity index 100% rename from packages/v1-ready/deel/README.md rename to packages/deel/README.md diff --git a/packages/v1-ready/deel/api.js b/packages/deel/api.js similarity index 100% rename from packages/v1-ready/deel/api.js rename to packages/deel/api.js diff --git a/packages/v1-ready/deel/defaultConfig.json b/packages/deel/defaultConfig.json similarity index 100% rename from packages/v1-ready/deel/defaultConfig.json rename to packages/deel/defaultConfig.json diff --git a/packages/v1-ready/deel/definition.js b/packages/deel/definition.js similarity index 100% rename from packages/v1-ready/deel/definition.js rename to packages/deel/definition.js diff --git a/packages/v1-ready/deel/index.js b/packages/deel/index.js similarity index 100% rename from packages/v1-ready/deel/index.js rename to packages/deel/index.js diff --git a/packages/v1-ready/contentstack/jest.config.js b/packages/deel/jest.config.js similarity index 100% rename from packages/v1-ready/contentstack/jest.config.js rename to packages/deel/jest.config.js diff --git a/packages/v1-ready/deel/package.json b/packages/deel/package.json similarity index 100% rename from packages/v1-ready/deel/package.json rename to packages/deel/package.json diff --git a/packages/v1-ready/deel/tests/api.test.js b/packages/deel/tests/api.test.js similarity index 100% rename from packages/v1-ready/deel/tests/api.test.js rename to packages/deel/tests/api.test.js diff --git a/packages/v1-ready/deel/tests/auther.test.js b/packages/deel/tests/auther.test.js similarity index 100% rename from packages/v1-ready/deel/tests/auther.test.js rename to packages/deel/tests/auther.test.js diff --git a/packages/deepcrawl/api.js b/packages/deepcrawl/api.js new file mode 100644 index 0000000..38bcecd --- /dev/null +++ b/packages/deepcrawl/api.js @@ -0,0 +1,153 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.deepcrawl.com/v1'; + + this.URLs = { + // User + user: '/user', + + // Projects + projects: '/projects', + projectById: (projectId) => `/projects/${projectId}`, + + // Crawls + crawls: '/projects/{project_id}/crawls', + crawlById: (crawlId) => `/crawls/${crawlId}`, + + // Reports + reports: '/crawls/{crawl_id}/reports', + reportById: (reportId) => `/reports/${reportId}`, + + // Issues + issues: '/crawls/{crawl_id}/issues', + issueById: (issueId) => `/issues/${issueId}`, + + // Pages + pages: '/crawls/{crawl_id}/pages', + pageById: (pageId) => `/pages/${pageId}`, + + // Accounts + accounts: '/accounts', + accountById: (accountId) => `/accounts/${accountId}`, + }; + + this.authorizationUri = encodeURI( + `https://app.deepcrawl.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.deepcrawl.com/oauth/token'; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + redirect_uri: this.redirect_uri, + code: code, + }, + }; + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + // User + async getUser() { + const options = { + url: this.baseUrl + this.URLs.user, + method: 'GET', + }; + return this._request(options); + } + + // Projects + async listProjects(params = {}) { + const options = { + url: this.baseUrl + this.URLs.projects, + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async getProject(projectId) { + const options = { + url: this.baseUrl + this.URLs.projectById(projectId), + method: 'GET', + }; + return this._request(options); + } + + // Crawls + async listCrawls(projectId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.crawls.replace('{project_id}', projectId), + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async getCrawl(crawlId) { + const options = { + url: this.baseUrl + this.URLs.crawlById(crawlId), + method: 'GET', + }; + return this._request(options); + } + + async startCrawl(projectId, crawlData) { + const options = { + url: this.baseUrl + this.URLs.crawls.replace('{project_id}', projectId), + method: 'POST', + json: crawlData, + }; + return this._request(options); + } + + // Reports + async listReports(crawlId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.reports.replace('{crawl_id}', crawlId), + method: 'GET', + qs: params, + }; + return this._request(options); + } + + // Issues + async listIssues(crawlId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.issues.replace('{crawl_id}', crawlId), + method: 'GET', + qs: params, + }; + return this._request(options); + } + + // Pages + async listPages(crawlId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.pages.replace('{crawl_id}', crawlId), + method: 'GET', + qs: params, + }; + return this._request(options); + } + + // User info for authentication + async getUserDetails() { + return this.getUser(); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/deepcrawl/defaultConfig.json b/packages/deepcrawl/defaultConfig.json new file mode 100644 index 0000000..dab50b5 --- /dev/null +++ b/packages/deepcrawl/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "deepcrawl", + "label": "DeepCrawl", + "productUrl": "https://www.deepcrawl.com", + "apiDocs": "https://api.deepcrawl.com/", + "logoUrl": "https://friggframework.org/assets/img/deepcrawl-icon.png", + "categories": [ + "Developer", + "Website Builders" + ], + "description": "DeepCrawl is a cloud-based website crawler that helps businesses monitor and improve their website's technical SEO performance." +} \ No newline at end of file diff --git a/packages/deepcrawl/definition.js b/packages/deepcrawl/definition.js new file mode 100644 index 0000000..eff6cfe --- /dev/null +++ b/packages/deepcrawl/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'DeepCrawl', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.DEEPCRAWL_CLIENT_ID, + client_secret: process.env.DEEPCRAWL_CLIENT_SECRET, + scope: process.env.DEEPCRAWL_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/deepcrawl`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/deepcrawl/index.js b/packages/deepcrawl/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/deepcrawl/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/discord/README.md b/packages/discord/README.md new file mode 100644 index 0000000..02451a0 --- /dev/null +++ b/packages/discord/README.md @@ -0,0 +1,105 @@ +# Discord API Module + +A comprehensive Discord API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +DISCORD_CLIENT_ID=your_discord_client_id +DISCORD_CLIENT_SECRET=your_discord_client_secret +DISCORD_SCOPE=identify email guilds +REDIRECT_URI=your_redirect_uri_base +``` + +### Getting Discord API Credentials + +1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) +2. Create a new application or select an existing one +3. Navigate to the OAuth2 section +4. Copy your Client ID and Client Secret +5. Add your redirect URI (e.g., `https://yourdomain.com/discord`) + +### Required OAuth2 Scopes + +- `identify` - Access to user's basic profile information +- `email` - Access to user's email address +- `guilds` - Access to user's guilds (servers) + +Additional available scopes: +- `guilds.join` - Join guilds on behalf of the user +- `gdm.read` - Read group DMs +- `connections` - Access to user's connections +- `bot` - Add bot to guilds (requires bot permissions) + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-discord'); + +// Initialize with credentials +const discordApi = new Api({ + client_id: process.env.DISCORD_CLIENT_ID, + client_secret: process.env.DISCORD_CLIENT_SECRET, + redirect_uri: process.env.REDIRECT_URI + '/discord', + scope: 'identify email guilds' +}); + +// Get authorization URL +const authUrl = discordApi.getAuthUri(); + +// Exchange code for tokens +const tokens = await discordApi.getTokenFromCode(authorizationCode); + +// Use the API +const user = await discordApi.getCurrentUser(); +const guilds = await discordApi.getCurrentUserGuilds(); +``` + +## Available Methods + +### User Methods +- `getCurrentUser()` - Get current user information +- `getCurrentUserGuilds()` - Get guilds the user is a member of +- `getCurrentUserConnections()` - Get user's connections (if scope granted) + +### Guild Methods +- `getGuild(guildId)` - Get guild information +- `getGuildChannels(guildId)` - Get guild channels +- `getGuildMembers(guildId, options)` - Get guild members +- `getGuildRoles(guildId)` - Get guild roles + +### Channel Methods +- `getChannel(channelId)` - Get channel information +- `getChannelMessages(channelId, options)` - Get channel messages +- `createMessage(channelId, content, options)` - Send a message +- `getMessage(channelId, messageId)` - Get specific message +- `editMessage(channelId, messageId, content, options)` - Edit a message +- `deleteMessage(channelId, messageId)` - Delete a message + +### Webhook Methods +- `getChannelWebhooks(channelId)` - Get channel webhooks +- `createWebhook(channelId, name, avatar)` - Create a webhook +- `getWebhook(webhookId)` - Get webhook information +- `modifyWebhook(webhookId, name, avatar)` - Modify a webhook +- `deleteWebhook(webhookId)` - Delete a webhook +- `executeWebhook(webhookId, webhookToken, content, options)` - Execute webhook + +## Authentication + +This module uses OAuth2 authentication. The authentication flow requires: + +1. Redirecting users to Discord's authorization URL +2. Handling the callback with the authorization code +3. Exchanging the code for access and refresh tokens + +## Rate Limiting + +Discord has strict rate limits. This module does not implement automatic rate limiting - you should implement appropriate delays and retry logic in your application. + +## Documentation + +For detailed Discord API documentation, visit: https://discord.com/developers/docs \ No newline at end of file diff --git a/packages/discord/api.js b/packages/discord/api.js new file mode 100644 index 0000000..005f673 --- /dev/null +++ b/packages/discord/api.js @@ -0,0 +1,262 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://discord.com/api/v10'; + + this.URLs = { + authorization: '/oauth2/authorize', + access_token: '/oauth2/token', + revoke_token: '/oauth2/token/revoke', + currentUser: '/users/@me', + userGuilds: '/users/@me/guilds', + userConnections: '/users/@me/connections', + guild: (guildId) => `/guilds/${guildId}`, + guildChannels: (guildId) => `/guilds/${guildId}/channels`, + guildMembers: (guildId) => `/guilds/${guildId}/members`, + guildRoles: (guildId) => `/guilds/${guildId}/roles`, + channel: (channelId) => `/channels/${channelId}`, + channelMessages: (channelId) => `/channels/${channelId}/messages`, + message: (channelId, messageId) => `/channels/${channelId}/messages/${messageId}`, + webhooks: (channelId) => `/channels/${channelId}/webhooks`, + webhook: (webhookId) => `/webhooks/${webhookId}`, + }; + + this.authorizationUri = encodeURI( + `https://discord.com/api/oauth2/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&scope=${this.scope}&state=${this.state}` + ); + this.tokenUri = 'https://discord.com/api/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Methods ********************************** + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.currentUser, + }; + return this._get(options); + } + + async getCurrentUserGuilds() { + const options = { + url: this.baseUrl + this.URLs.userGuilds, + }; + return this._get(options); + } + + async getCurrentUserConnections() { + const options = { + url: this.baseUrl + this.URLs.userConnections, + }; + return this._get(options); + } + + // ************************** Guild Methods ********************************** + + async getGuild(guildId) { + const options = { + url: this.baseUrl + this.URLs.guild(guildId), + query: { + with_counts: true, + }, + }; + return this._get(options); + } + + async getGuildChannels(guildId) { + const options = { + url: this.baseUrl + this.URLs.guildChannels(guildId), + }; + return this._get(options); + } + + async getGuildMembers(guildId, options = {}) { + const queryParams = { + limit: options.limit || 1000, + after: options.after || '0', + }; + + const requestOptions = { + url: this.baseUrl + this.URLs.guildMembers(guildId), + query: queryParams, + }; + return this._get(requestOptions); + } + + async getGuildRoles(guildId) { + const options = { + url: this.baseUrl + this.URLs.guildRoles(guildId), + }; + return this._get(options); + } + + // ************************** Channel Methods ********************************** + + async getChannel(channelId) { + const options = { + url: this.baseUrl + this.URLs.channel(channelId), + }; + return this._get(options); + } + + async getChannelMessages(channelId, options = {}) { + const queryParams = { + limit: options.limit || 50, + }; + + if (options.before) queryParams.before = options.before; + if (options.after) queryParams.after = options.after; + if (options.around) queryParams.around = options.around; + + const requestOptions = { + url: this.baseUrl + this.URLs.channelMessages(channelId), + query: queryParams, + }; + return this._get(requestOptions); + } + + async createMessage(channelId, content, options = {}) { + const body = { + content, + ...options, + }; + + const requestOptions = { + url: this.baseUrl + this.URLs.channelMessages(channelId), + body, + }; + return this._post(requestOptions); + } + + async getMessage(channelId, messageId) { + const options = { + url: this.baseUrl + this.URLs.message(channelId, messageId), + }; + return this._get(options); + } + + async editMessage(channelId, messageId, content, options = {}) { + const body = { + content, + ...options, + }; + + const requestOptions = { + url: this.baseUrl + this.URLs.message(channelId, messageId), + body, + }; + return this._patch(requestOptions); + } + + async deleteMessage(channelId, messageId) { + const options = { + url: this.baseUrl + this.URLs.message(channelId, messageId), + }; + return this._delete(options); + } + + // ************************** Webhook Methods ********************************** + + async getChannelWebhooks(channelId) { + const options = { + url: this.baseUrl + this.URLs.webhooks(channelId), + }; + return this._get(options); + } + + async createWebhook(channelId, name, avatar = null) { + const body = { + name, + }; + + if (avatar) { + body.avatar = avatar; + } + + const options = { + url: this.baseUrl + this.URLs.webhooks(channelId), + body, + }; + return this._post(options); + } + + async getWebhook(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhook(webhookId), + }; + return this._get(options); + } + + async modifyWebhook(webhookId, name, avatar = null) { + const body = { + name, + }; + + if (avatar) { + body.avatar = avatar; + } + + const options = { + url: this.baseUrl + this.URLs.webhook(webhookId), + body, + }; + return this._patch(options); + } + + async deleteWebhook(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhook(webhookId), + }; + return this._delete(options); + } + + async executeWebhook(webhookId, webhookToken, content, options = {}) { + const body = { + content, + ...options, + }; + + const requestOptions = { + url: `${this.baseUrl}/webhooks/${webhookId}/${webhookToken}`, + body, + }; + return this._post(requestOptions); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/discord/defaultConfig.json b/packages/discord/defaultConfig.json new file mode 100644 index 0000000..1c9d0ea --- /dev/null +++ b/packages/discord/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "discord", + "label": "Discord", + "productUrl": "https://discord.com", + "apiDocs": "https://discord.com/developers/docs", + "logoUrl": "https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a6918e57475a843dcf5_icon_clyde_black_RGB.svg", + "categories": [ + "Communication", + "Team Collaboration", + "Gaming", + "Community" + ], + "description": "Discord is a voice, video and text communication service to talk and hang out with your friends and communities" +} \ No newline at end of file diff --git a/packages/discord/definition.js b/packages/discord/definition.js new file mode 100644 index 0000000..d2e2b71 --- /dev/null +++ b/packages/discord/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Discord', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: { externalId: userDetails.id, user: userId }, + details: { name: userDetails.username, email: userDetails.email } + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: { externalId: userDetails.id, user: userId }, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.DISCORD_CLIENT_ID, + client_secret: process.env.DISCORD_CLIENT_SECRET, + scope: process.env.DISCORD_SCOPE || 'identify email guilds', + redirect_uri: `${process.env.REDIRECT_URI}/discord`, + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/discord/index.js b/packages/discord/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/discord/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/discord/openapi.json b/packages/discord/openapi.json new file mode 100644 index 0000000..b84e1cb --- /dev/null +++ b/packages/discord/openapi.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5558cd419c8d46bdc958064cb97f963d1ea793866414c025906ec15033512ed +size 14 diff --git a/packages/dispatchme/README.md b/packages/dispatchme/README.md new file mode 100644 index 0000000..b58419f --- /dev/null +++ b/packages/dispatchme/README.md @@ -0,0 +1,42 @@ +# Dispatch.me API Module + +Frigg API module for Dispatch.me integration. + +## Installation + +```bash +npm install @friggframework/dispatchme +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/dispatchme'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +DISPATCHME_CLIENT_ID=your_client_id +DISPATCHME_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/dispatchme/api.js b/packages/dispatchme/api.js new file mode 100644 index 0000000..c3d5990 --- /dev/null +++ b/packages/dispatchme/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.dispatch.me'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.dispatch.me/oauth/authorize'; + this.accessTokenUri = 'https://api.dispatch.me/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Dispatch.me', + MODULE_NAME: 'dispatchme', + CATEGORY: 'Productivity', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/dispatchme/defaultConfig.json b/packages/dispatchme/defaultConfig.json new file mode 100644 index 0000000..4526213 --- /dev/null +++ b/packages/dispatchme/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Dispatch.me", + "moduleName": "dispatchme", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Dispatch.me API Integration Module", + "category": "Productivity", + "apiDocUrl": "https://api.dispatch.me/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/dispatchme/definition.js b/packages/dispatchme/definition.js new file mode 100644 index 0000000..07bb8f6 --- /dev/null +++ b/packages/dispatchme/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Dispatchme', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.DISPATCHME_CLIENT_ID, + client_secret: process.env.DISPATCHME_CLIENT_SECRET, + scope: process.env.DISPATCHME_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/dispatchme`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/dispatchme/index.js b/packages/dispatchme/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/dispatchme/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/dispatchme/package.json b/packages/dispatchme/package.json new file mode 100644 index 0000000..a42811a --- /dev/null +++ b/packages/dispatchme/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/dispatchme", + "version": "0.0.1", + "description": "Dispatch.me API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "dispatchme", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/docusign/README.md b/packages/docusign/README.md new file mode 100644 index 0000000..1eb04ba --- /dev/null +++ b/packages/docusign/README.md @@ -0,0 +1,283 @@ +# DocuSign API Module + +A comprehensive DocuSign eSignature REST API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +DOCUSIGN_CLIENT_ID=your_docusign_integration_key +DOCUSIGN_CLIENT_SECRET=your_docusign_secret_key +DOCUSIGN_SCOPE=signature impersonation +DOCUSIGN_SANDBOX=true +REDIRECT_URI=your_redirect_uri_base +``` + +### Getting DocuSign API Credentials + +1. Go to [DocuSign Developer Center](https://developers.docusign.com/) +2. Sign in or create a developer account +3. Create a new app in your developer account +4. Get your Integration Key (Client ID) and Secret Key (Client Secret) +5. Set up your redirect URI (e.g., `https://yourdomain.com/docusign`) + +### Sandbox vs Production + +- Set `DOCUSIGN_SANDBOX=true` for testing with DocuSign's sandbox environment +- Set `DOCUSIGN_SANDBOX=false` for production usage + +### OAuth2 Scopes + +Required scopes for DocuSign API: +- `signature` - Create and send envelopes for signature +- `impersonation` - Act on behalf of the user + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-docusign'); + +// Initialize with credentials +const docusignApi = new Api({ + client_id: process.env.DOCUSIGN_CLIENT_ID, + client_secret: process.env.DOCUSIGN_CLIENT_SECRET, + redirect_uri: process.env.REDIRECT_URI + '/docusign', + scope: 'signature impersonation', + sandbox: process.env.DOCUSIGN_SANDBOX === 'true' +}); + +// Get authorization URL +const authUrl = docusignApi.getAuthUri(); + +// Exchange code for tokens +const tokens = await docusignApi.getTokenFromCode(authorizationCode); + +// Get user info and set account +const userInfo = await docusignApi.getUserInfo(); + +// Create and send an envelope +const envelope = await docusignApi.createSimpleEnvelope( + 'Please sign this document', + [{ email: 'signer@example.com', name: 'John Doe' }], + [{ name: 'Contract.pdf', base64Content: 'base64-encoded-pdf', extension: 'pdf' }] +); + +await docusignApi.sendEnvelope(envelope.envelopeId); +``` + +## Available Methods + +### Authentication & User Info +- `getUserInfo()` - Get user profile and account information +- `setAccountId(accountId)` - Set specific account to use + +### Envelopes Methods +- `getEnvelopes(params)` - List envelopes with filters +- `createEnvelope(envelopeData)` - Create new envelope +- `getEnvelope(envelopeId)` - Get specific envelope +- `updateEnvelope(envelopeId, envelopeData)` - Update envelope +- `deleteEnvelope(envelopeId)` - Delete envelope +- `sendEnvelope(envelopeId)` - Send envelope for signature +- `voidEnvelope(envelopeId, voidedReason)` - Void an envelope +- `createSimpleEnvelope(subject, recipients, documents)` - Helper for simple envelopes + +### Envelope Documents Methods +- `getEnvelopeDocuments(envelopeId)` - List envelope documents +- `getEnvelopeDocument(envelopeId, documentId)` - Get specific document + +### Envelope Recipients Methods +- `getEnvelopeRecipients(envelopeId)` - Get envelope recipients +- `updateEnvelopeRecipients(envelopeId, recipientsData)` - Update recipients +- `createRecipientView(envelopeId, recipientViewData)` - Create signing URL +- `getSigningUrl(envelopeId, email, name, returnUrl)` - Helper for signing URLs + +### Templates Methods +- `getTemplates(params)` - List templates +- `getTemplate(templateId)` - Get specific template +- `createTemplate(templateData)` - Create new template +- `updateTemplate(templateId, templateData)` - Update template +- `createEnvelopeFromTemplate(templateId, envelopeData)` - Create envelope from template + +### Users Methods +- `getUsers(params)` - List account users +- `getUser(userId)` - Get specific user +- `createUser(userData)` - Create new user +- `updateUser(userId, userData)` - Update user + +### Connect (Webhooks) Methods +- `getConnectConfigurations()` - List webhook configurations +- `createConnectConfiguration(connectData)` - Create webhook +- `getConnectConfiguration(connectId)` - Get webhook details +- `updateConnectConfiguration(connectId, connectData)` - Update webhook +- `deleteConnectConfiguration(connectId)` - Delete webhook + +## Usage Examples + +### Creating and Sending an Envelope +```javascript +// Create envelope with document and signer +const envelopeData = { + emailSubject: 'Please sign this contract', + status: 'created', + recipients: { + signers: [ + { + email: 'signer@example.com', + name: 'John Doe', + recipientId: '1', + routingOrder: '1', + tabs: { + signHereTabs: [ + { + documentId: '1', + pageNumber: '1', + xPosition: '100', + yPosition: '100' + } + ] + } + } + ] + }, + documents: [ + { + documentId: '1', + name: 'Contract.pdf', + documentBase64: 'base64-encoded-pdf-content', + fileExtension: 'pdf' + } + ] +}; + +const envelope = await docusignApi.createEnvelope(envelopeData); + +// Send the envelope +await docusignApi.sendEnvelope(envelope.envelopeId); +``` + +### Creating a Signing URL +```javascript +const signingUrl = await docusignApi.getSigningUrl( + envelopeId, + 'signer@example.com', + 'John Doe', + 'https://yoursite.com/signing-complete' +); + +console.log('Signing URL:', signingUrl.url); +``` + +### Working with Templates +```javascript +// Get templates +const templates = await docusignApi.getTemplates(); + +// Create envelope from template +const templateEnvelope = await docusignApi.createEnvelopeFromTemplate( + 'template-id', + { + emailSubject: 'Contract from template', + status: 'sent', + templateRoles: [ + { + email: 'signer@example.com', + name: 'John Doe', + roleName: 'Signer' + } + ] + } +); +``` + +### Setting up Webhooks +```javascript +const webhookData = { + name: 'Envelope Status Updates', + urlToPublishTo: 'https://yoursite.com/webhooks/docusign', + enableConnect: 'true', + includeDocuments: 'true', + envelopeEvents: ['sent', 'delivered', 'completed', 'declined', 'voided'], + recipientEvents: ['sent', 'delivered', 'completed', 'declined', 'authenticationfailed', 'autoresponded'] +}; + +const webhook = await docusignApi.createConnectConfiguration(webhookData); +``` + +### Monitoring Envelope Status +```javascript +const envelopes = await docusignApi.getEnvelopes({ + from_date: '2023-01-01', + status: 'completed' +}); + +envelopes.envelopes.forEach(envelope => { + console.log(`Envelope ${envelope.envelopeId}: ${envelope.status}`); +}); +``` + +### Voiding an Envelope +```javascript +await docusignApi.voidEnvelope( + envelopeId, + 'Contract terms have changed' +); +``` + +## Authentication Flow + +DocuSign uses OAuth2 Authorization Code Grant: + +1. Redirect users to DocuSign's authorization URL +2. Handle the callback with the authorization code +3. Exchange the code for access and refresh tokens +4. Get user info to determine account ID and base URI +5. Use tokens and account info for API requests + +## Error Handling + +DocuSign returns detailed error information. Always wrap API calls in try-catch blocks: + +```javascript +try { + const envelope = await docusignApi.createEnvelope(envelopeData); + console.log('Envelope created:', envelope.envelopeId); +} catch (error) { + console.error('DocuSign error:', error.message); + if (error.errorCode) { + console.error('Error code:', error.errorCode); + } +} +``` + +## Testing + +Use DocuSign's sandbox environment for testing: +- All signatures and documents are simulated +- Use demo.docusign.net for sandbox +- Test with different envelope statuses and scenarios + +## Webhooks + +DocuSign can send webhooks for various envelope and recipient events: +- `sent` - Envelope sent for signature +- `delivered` - Envelope delivered to recipient +- `completed` - All required signatures obtained +- `declined` - Recipient declined to sign +- `voided` - Envelope voided by sender + +## Document Formats + +DocuSign supports various document formats: +- PDF (recommended) +- Microsoft Word (.doc, .docx) +- Microsoft Excel (.xls, .xlsx) +- Microsoft PowerPoint (.ppt, .pptx) +- Text files (.txt) +- Images (.jpg, .png, .gif) + +## Documentation + +For detailed DocuSign API documentation, visit: https://developers.docusign.com/docs/ \ No newline at end of file diff --git a/packages/docusign/api.js b/packages/docusign/api.js new file mode 100644 index 0000000..451c1d7 --- /dev/null +++ b/packages/docusign/api.js @@ -0,0 +1,397 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.sandbox = get(params, 'sandbox', false); + this.account_id = get(params, 'account_id', null); + + // Base URLs differ for sandbox vs production + this.authBaseUrl = this.sandbox + ? 'https://account-d.docusign.com' + : 'https://account.docusign.com'; + + this.baseUrl = null; // Will be set after getting user info and account base URI + + this.URLs = { + authorization: '/oauth/auth', + access_token: '/oauth/token', + userInfo: '/oauth/userinfo', + + // Envelopes + envelopes: '/envelopes', + envelopeById: (envelopeId) => `/envelopes/${envelopeId}`, + envelopeDocuments: (envelopeId) => `/envelopes/${envelopeId}/documents`, + envelopeDocumentById: (envelopeId, documentId) => `/envelopes/${envelopeId}/documents/${documentId}`, + envelopeRecipients: (envelopeId) => `/envelopes/${envelopeId}/recipients`, + envelopeViews: (envelopeId) => `/envelopes/${envelopeId}/views/recipient`, + + // Templates + templates: '/templates', + templateById: (templateId) => `/templates/${templateId}`, + + // Users + users: '/users', + userById: (userId) => `/users/${userId}`, + + // Groups + groups: '/groups', + groupById: (groupId) => `/groups/${groupId}`, + + // Folders + folders: '/folders', + folderById: (folderId) => `/folders/${folderId}`, + + // Brand + brands: '/brands', + brandById: (brandId) => `/brands/${brandId}`, + + // Connect (webhooks) + connect: '/connect', + connectConfigurations: '/connect/configurations', + connectConfigurationById: (connectId) => `/connect/configurations/${connectId}`, + }; + + this.authorizationUri = encodeURI( + `${this.authBaseUrl}/oauth/auth?response_type=code&scope=${this.scope}&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}` + ); + this.tokenUri = this.authBaseUrl + '/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getUserInfo() { + const options = { + url: this.authBaseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + async setAccountId(accountId) { + this.account_id = accountId; + await this.setBaseUrl(); + } + + async setBaseUrl() { + if (!this.account_id) { + const userInfo = await this.getUserInfo(); + const accounts = userInfo.accounts || []; + const defaultAccount = accounts.find(acc => acc.is_default) || accounts[0]; + + if (defaultAccount) { + this.account_id = defaultAccount.account_id; + this.baseUrl = defaultAccount.base_uri + '/restapi/v2.1/accounts/' + this.account_id; + } + } else { + // Use sandbox or production base URL with account ID + const base = this.sandbox + ? 'https://demo.docusign.net' + : 'https://na1.docusign.net'; // This might vary by region + this.baseUrl = base + '/restapi/v2.1/accounts/' + this.account_id; + } + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + await this.ensureBaseUrl(); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + await this.ensureBaseUrl(); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + await this.ensureBaseUrl(); + return super._put(options, stringify); + } + + async _get(options) { + await this.ensureBaseUrl(); + return super._get(options); + } + + async _delete(options) { + await this.ensureBaseUrl(); + return super._delete(options); + } + + async ensureBaseUrl() { + if (!this.baseUrl && this.access_token) { + await this.setBaseUrl(); + } + } + + // ************************** Envelopes Methods ********************************** + + async getEnvelopes(params = {}) { + const options = { + url: this.baseUrl + this.URLs.envelopes, + query: params, + }; + return this._get(options); + } + + async createEnvelope(envelopeData) { + const options = { + url: this.baseUrl + this.URLs.envelopes, + body: envelopeData, + }; + return this._post(options); + } + + async getEnvelope(envelopeId) { + const options = { + url: this.baseUrl + this.URLs.envelopeById(envelopeId), + }; + return this._get(options); + } + + async updateEnvelope(envelopeId, envelopeData) { + const options = { + url: this.baseUrl + this.URLs.envelopeById(envelopeId), + body: envelopeData, + }; + return this._put(options); + } + + async deleteEnvelope(envelopeId) { + const options = { + url: this.baseUrl + this.URLs.envelopeById(envelopeId), + }; + return this._delete(options); + } + + async sendEnvelope(envelopeId) { + const options = { + url: this.baseUrl + this.URLs.envelopeById(envelopeId), + body: { status: 'sent' }, + }; + return this._put(options); + } + + async voidEnvelope(envelopeId, voidedReason) { + const options = { + url: this.baseUrl + this.URLs.envelopeById(envelopeId), + body: { + status: 'voided', + voidedReason: voidedReason + }, + }; + return this._put(options); + } + + // ************************** Envelope Documents Methods ********************************** + + async getEnvelopeDocuments(envelopeId) { + const options = { + url: this.baseUrl + this.URLs.envelopeDocuments(envelopeId), + }; + return this._get(options); + } + + async getEnvelopeDocument(envelopeId, documentId) { + const options = { + url: this.baseUrl + this.URLs.envelopeDocumentById(envelopeId, documentId), + }; + return this._get(options); + } + + // ************************** Envelope Recipients Methods ********************************** + + async getEnvelopeRecipients(envelopeId) { + const options = { + url: this.baseUrl + this.URLs.envelopeRecipients(envelopeId), + }; + return this._get(options); + } + + async updateEnvelopeRecipients(envelopeId, recipientsData) { + const options = { + url: this.baseUrl + this.URLs.envelopeRecipients(envelopeId), + body: recipientsData, + }; + return this._put(options); + } + + async createRecipientView(envelopeId, recipientViewData) { + const options = { + url: this.baseUrl + this.URLs.envelopeViews(envelopeId), + body: recipientViewData, + }; + return this._post(options); + } + + // ************************** Templates Methods ********************************** + + async getTemplates(params = {}) { + const options = { + url: this.baseUrl + this.URLs.templates, + query: params, + }; + return this._get(options); + } + + async getTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + }; + return this._get(options); + } + + async createTemplate(templateData) { + const options = { + url: this.baseUrl + this.URLs.templates, + body: templateData, + }; + return this._post(options); + } + + async updateTemplate(templateId, templateData) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + body: templateData, + }; + return this._put(options); + } + + async createEnvelopeFromTemplate(templateId, envelopeData) { + const templateEnvelopeData = { + ...envelopeData, + templateId: templateId, + }; + + return this.createEnvelope(templateEnvelopeData); + } + + // ************************** Users Methods ********************************** + + async getUsers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.users, + query: params, + }; + return this._get(options); + } + + async getUser(userId) { + const options = { + url: this.baseUrl + this.URLs.userById(userId), + }; + return this._get(options); + } + + async createUser(userData) { + const options = { + url: this.baseUrl + this.URLs.users, + body: userData, + }; + return this._post(options); + } + + async updateUser(userId, userData) { + const options = { + url: this.baseUrl + this.URLs.userById(userId), + body: userData, + }; + return this._put(options); + } + + // ************************** Connect (Webhooks) Methods ********************************** + + async getConnectConfigurations() { + const options = { + url: this.baseUrl + this.URLs.connectConfigurations, + }; + return this._get(options); + } + + async createConnectConfiguration(connectData) { + const options = { + url: this.baseUrl + this.URLs.connectConfigurations, + body: connectData, + }; + return this._post(options); + } + + async getConnectConfiguration(connectId) { + const options = { + url: this.baseUrl + this.URLs.connectConfigurationById(connectId), + }; + return this._get(options); + } + + async updateConnectConfiguration(connectId, connectData) { + const options = { + url: this.baseUrl + this.URLs.connectConfigurationById(connectId), + body: connectData, + }; + return this._put(options); + } + + async deleteConnectConfiguration(connectId) { + const options = { + url: this.baseUrl + this.URLs.connectConfigurationById(connectId), + }; + return this._delete(options); + } + + // ************************** Helper Methods ********************************** + + async createSimpleEnvelope(emailSubject, recipients, documents) { + const envelopeData = { + emailSubject: emailSubject, + status: 'created', + recipients: { + signers: recipients.map((recipient, index) => ({ + email: recipient.email, + name: recipient.name, + recipientId: (index + 1).toString(), + routingOrder: (index + 1).toString(), + })) + }, + documents: documents.map((doc, index) => ({ + documentId: (index + 1).toString(), + name: doc.name, + documentBase64: doc.base64Content, + fileExtension: doc.extension || 'pdf', + })) + }; + + return this.createEnvelope(envelopeData); + } + + async getSigningUrl(envelopeId, recipientEmail, recipientName, returnUrl) { + const recipientViewData = { + authenticationMethod: 'none', + email: recipientEmail, + userName: recipientName, + returnUrl: returnUrl, + clientUserId: recipientEmail, // Using email as client user ID + }; + + return this.createRecipientView(envelopeId, recipientViewData); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/docusign/defaultConfig.json b/packages/docusign/defaultConfig.json new file mode 100644 index 0000000..c5c174f --- /dev/null +++ b/packages/docusign/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "docusign", + "label": "Docusign", + "productUrl": "https://docusign.com", + "apiDocs": "https://developers.docusign.com/", + "logoUrl": "https://www.docusign.com/sites/default/files/ds-logo.svg", + "categories": [ + "Electronic Signatures", + "Document Management", + "Legal Tech", + "Business Process" + ], + "description": "Docusign is a digital transaction platform that enables electronic signatures and digital document management" +} \ No newline at end of file diff --git a/packages/docusign/definition.js b/packages/docusign/definition.js new file mode 100644 index 0000000..dde8e66 --- /dev/null +++ b/packages/docusign/definition.js @@ -0,0 +1,59 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'DocuSign', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + const accounts = userInfo.accounts || []; + const defaultAccount = accounts.find(acc => acc.is_default) || accounts[0]; + + return { + identifiers: { externalId: userInfo.sub, user: userId }, + details: { + name: userInfo.name, + email: userInfo.email, + account_id: defaultAccount?.account_id, + account_name: defaultAccount?.account_name + } + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: { externalId: userInfo.sub, user: userId }, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo(); + }, + }, + env: { + client_id: process.env.DOCUSIGN_CLIENT_ID, + client_secret: process.env.DOCUSIGN_CLIENT_SECRET, + scope: process.env.DOCUSIGN_SCOPE || 'signature impersonation', + redirect_uri: `${process.env.REDIRECT_URI}/docusign`, + sandbox: process.env.DOCUSIGN_SANDBOX === 'true', + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/docusign/index.js b/packages/docusign/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/docusign/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/docverify/README.md b/packages/docverify/README.md new file mode 100644 index 0000000..c089465 --- /dev/null +++ b/packages/docverify/README.md @@ -0,0 +1,43 @@ +# DocVerify API Module + +This module provides integration with the DocVerify API for the Frigg Framework. + +## Description + +DocVerify provides digital signature and document verification services. + +## Installation + +```bash +npm install @friggframework/api-module-docverify +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-docverify'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +DOCVERIFY_CLIENT_ID=your_client_id +DOCVERIFY_CLIENT_SECRET=your_client_secret +DOCVERIFY_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/docverify/api.js b/packages/docverify/api.js new file mode 100644 index 0000000..5df3f6c --- /dev/null +++ b/packages/docverify/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.docverify.com/v1'; + + this.URLs = { + // User/Account info + userInfo: '/account', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://secure.docverify.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://api.docverify.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to DocVerify +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/docverify/defaultConfig.json b/packages/docverify/defaultConfig.json new file mode 100644 index 0000000..c347bee --- /dev/null +++ b/packages/docverify/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "docverify", + "label": "DocVerify", + "productUrl": "https://docverify.com/", + "apiDocs": "https://developers.docverify.com/", + "logoUrl": "https://friggframework.org/assets/img/docverify-icon.png", + "categories": [ + "Productivity" + ], + "subCategories": [ + "Signatures" + ], + "description": "DocVerify provides digital signature and document verification services." +} \ No newline at end of file diff --git a/packages/docverify/definition.js b/packages/docverify/definition.js new file mode 100644 index 0000000..e88b153 --- /dev/null +++ b/packages/docverify/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'DocVerify', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'docverify-account', user: userId}, + details: {name: userInfo.name || 'DocVerify Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'docverify-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.DOCVERIFY_CLIENT_ID, + client_secret: process.env.DOCVERIFY_CLIENT_SECRET, + scope: process.env.DOCVERIFY_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/docverify`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/docverify/index.js b/packages/docverify/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/docverify/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/docverify/jest-setup.js b/packages/docverify/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/docverify/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/docverify/jest-teardown.js b/packages/docverify/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/docverify/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/docverify/jest.config.js b/packages/docverify/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/docverify/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/docverify/package.json b/packages/docverify/package.json new file mode 100644 index 0000000..7e73377 --- /dev/null +++ b/packages/docverify/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-docverify", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "DocVerify API module that lets the Frigg Framework interact with DocVerify", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/docverify/test/api.test.js b/packages/docverify/test/api.test.js new file mode 100644 index 0000000..6c7dd20 --- /dev/null +++ b/packages/docverify/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('DocVerify API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/docverify/test/definition.test.js b/packages/docverify/test/definition.test.js new file mode 100644 index 0000000..b30a97b --- /dev/null +++ b/packages/docverify/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('DocVerify Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('docverify'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/dotloop/README.md b/packages/dotloop/README.md new file mode 100644 index 0000000..fbec98e --- /dev/null +++ b/packages/dotloop/README.md @@ -0,0 +1,42 @@ +# Dotloop API Module + +Frigg API module for Dotloop integration. + +## Installation + +```bash +npm install @friggframework/dotloop +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/dotloop'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +DOTLOOP_CLIENT_ID=your_client_id +DOTLOOP_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/dotloop/api.js b/packages/dotloop/api.js new file mode 100644 index 0000000..e7b26a7 --- /dev/null +++ b/packages/dotloop/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.dotloop.com'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.dotloop.com/oauth/authorize'; + this.accessTokenUri = 'https://api.dotloop.com/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Dotloop', + MODULE_NAME: 'dotloop', + CATEGORY: 'CRM', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/dotloop/defaultConfig.json b/packages/dotloop/defaultConfig.json new file mode 100644 index 0000000..b8a9165 --- /dev/null +++ b/packages/dotloop/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Dotloop", + "moduleName": "dotloop", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Dotloop API Integration Module", + "category": "CRM", + "apiDocUrl": "https://api.dotloop.com/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/dotloop/definition.js b/packages/dotloop/definition.js new file mode 100644 index 0000000..f8df389 --- /dev/null +++ b/packages/dotloop/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Dotloop', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.DOTLOOP_CLIENT_ID, + client_secret: process.env.DOTLOOP_CLIENT_SECRET, + scope: process.env.DOTLOOP_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/dotloop`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/dotloop/index.js b/packages/dotloop/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/dotloop/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/dotloop/package.json b/packages/dotloop/package.json new file mode 100644 index 0000000..4cab796 --- /dev/null +++ b/packages/dotloop/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/dotloop", + "version": "0.0.1", + "description": "Dotloop API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "dotloop", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/drift/README.md b/packages/drift/README.md new file mode 100644 index 0000000..d3f8911 --- /dev/null +++ b/packages/drift/README.md @@ -0,0 +1,43 @@ +# Drift API Module + +This module provides integration with the Drift API for the Frigg Framework. + +## Description + +Drift is a conversational marketing and sales platform that connects businesses with customers. + +## Installation + +```bash +npm install @friggframework/api-module-drift +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-drift'); + +// Initialize the API +const api = new Api({ + client_id: 'your-client-id', + client_secret: 'your-client-secret', + redirect_uri: 'your-redirect-uri' +}); + +// Get authorization URL +const authUrl = api.getAuthUri(); +``` + +## Configuration + +Set the following environment variables: + +``` +DRIFT_CLIENT_ID=your_client_id +DRIFT_CLIENT_SECRET=your_client_secret +DRIFT_SCOPE=your_scope +``` + +## License + +MIT diff --git a/packages/drift/api.js b/packages/drift/api.js new file mode 100644 index 0000000..34f8ae8 --- /dev/null +++ b/packages/drift/api.js @@ -0,0 +1,87 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://driftapi.com'; + + this.URLs = { + // User/Account info + userInfo: '/users/me', + + // Add more endpoints specific to the API + }; + + this.authorizationUri = encodeURI( + `https://dev.drift.com/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}&scope=${this.scope}` + ); + this.tokenUri = 'https://driftapi.com/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + delete this.access_token; + return super.getTokenFromCode(code); + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + const newRefreshToken = get(params, 'refresh_token', null); + + if (newRefreshToken) { + this.refresh_token = newRefreshToken; + } + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + } + } + + async _post(options, stringify) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User Info ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + }; + return this._get(options); + } + + // Add more API methods here specific to Drift +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/drift/defaultConfig.json b/packages/drift/defaultConfig.json new file mode 100644 index 0000000..25e727f --- /dev/null +++ b/packages/drift/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "drift", + "label": "Drift", + "productUrl": "https://drift.com/", + "apiDocs": "https://developers.drift.com/", + "logoUrl": "https://friggframework.org/assets/img/drift-icon.png", + "categories": [ + "CRM" + ], + "subCategories": [ + "Customer Support" + ], + "description": "Drift is a conversational marketing and sales platform that connects businesses with customers." +} \ No newline at end of file diff --git a/packages/drift/definition.js b/packages/drift/definition.js new file mode 100644 index 0000000..76869e8 --- /dev/null +++ b/packages/drift/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Drift', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'drift-account', user: userId}, + details: {name: userInfo.name || 'Drift Account', email: userInfo.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: {externalId: userInfo.id || 'drift-account', user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.DRIFT_CLIENT_ID, + client_secret: process.env.DRIFT_CLIENT_SECRET, + scope: process.env.DRIFT_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/drift`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/drift/index.js b/packages/drift/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/drift/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/drift/jest-setup.js b/packages/drift/jest-setup.js new file mode 100644 index 0000000..32ab708 --- /dev/null +++ b/packages/drift/jest-setup.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +// Global test setup +beforeAll(async () => { + // Setup before all tests +}); + +afterAll(async () => { + // Cleanup after all tests +}); diff --git a/packages/drift/jest-teardown.js b/packages/drift/jest-teardown.js new file mode 100644 index 0000000..d69c71e --- /dev/null +++ b/packages/drift/jest-teardown.js @@ -0,0 +1,4 @@ +module.exports = async () => { + // Global teardown + console.log('Tests completed'); +}; diff --git a/packages/drift/jest.config.js b/packages/drift/jest.config.js new file mode 100644 index 0000000..5d2ff6d --- /dev/null +++ b/packages/drift/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx}', + '!**/node_modules/**', + '!**/test/**', + '!**/tests/**', + '!jest.config.js', + '!jest-setup.js', + '!jest-teardown.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: [ + '**/test/**/*.js', + '**/tests/**/*.js', + '**/*.test.js' + ], + setupFilesAfterEnv: ['/jest-setup.js'], + globalTeardown: '/jest-teardown.js' +}; diff --git a/packages/drift/package.json b/packages/drift/package.json new file mode 100644 index 0000000..6efd8bd --- /dev/null +++ b/packages/drift/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-drift", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Drift API module that lets the Frigg Framework interact with Drift", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "npx jest" + }, + "author": "Frigg Framework", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.2.2", + "@friggframework/prettier-config": "^1.2.2", + "@friggframework/test": "^1.2.2", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + }, + "dependencies": { + "@friggframework/core": "^1.2.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/drift/test/api.test.js b/packages/drift/test/api.test.js new file mode 100644 index 0000000..c5c3ba4 --- /dev/null +++ b/packages/drift/test/api.test.js @@ -0,0 +1,45 @@ +const { Api } = require('../api'); + +describe('Drift API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uri: 'http://localhost:3000/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct properties', () => { + expect(api).toBeDefined(); + expect(api.client_id).toBe('test-client-id'); + expect(api.client_secret).toBe('test-client-secret'); + expect(api.redirect_uri).toBe('http://localhost:3000/callback'); + }); + }); + + describe('getAuthUri', () => { + test('should return authorization URI', () => { + const authUri = api.getAuthUri(); + expect(authUri).toBeDefined(); + expect(typeof authUri).toBe('string'); + expect(authUri).toContain('client_id=test-client-id'); + }); + }); + + describe('setTokens', () => { + test('should set access token', async () => { + const tokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600 + }; + + await api.setTokens(tokens); + expect(api.access_token).toBe('test-access-token'); + expect(api.refresh_token).toBe('test-refresh-token'); + }); + }); +}); diff --git a/packages/drift/test/definition.test.js b/packages/drift/test/definition.test.js new file mode 100644 index 0000000..9ab3300 --- /dev/null +++ b/packages/drift/test/definition.test.js @@ -0,0 +1,24 @@ +const { Definition } = require('../definition'); + +describe('Drift Definition', () => { + test('should have required properties', () => { + expect(Definition).toBeDefined(); + expect(Definition.API).toBeDefined(); + expect(Definition.getName).toBeDefined(); + expect(Definition.moduleName).toBeDefined(); + expect(Definition.requiredAuthMethods).toBeDefined(); + }); + + test('getName should return module name', () => { + const name = Definition.getName(); + expect(name).toBe('drift'); + }); + + test('should have required auth methods', () => { + const { requiredAuthMethods } = Definition; + expect(requiredAuthMethods.getToken).toBeDefined(); + expect(requiredAuthMethods.getEntityDetails).toBeDefined(); + expect(requiredAuthMethods.getCredentialDetails).toBeDefined(); + expect(requiredAuthMethods.testAuthRequest).toBeDefined(); + }); +}); diff --git a/packages/dropbox/api.js b/packages/dropbox/api.js new file mode 100644 index 0000000..8cc0c68 --- /dev/null +++ b/packages/dropbox/api.js @@ -0,0 +1,119 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// Dropbox API v2 +// https://www.dropbox.com/developers/documentation +// Core resources: files, sharing, users, team + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.dropboxapi.com/2'; + this.contentBaseUrl = 'https://content.dropboxapi.com/2'; + + this.URLs = { + // Users + getCurrentAccount: '/users/get_current_account', + + // Files + listFolder: '/files/list_folder', + getMetadata: '/files/get_metadata', + upload: '/files/upload', + download: '/files/download', + + // Sharing + createSharedLink: '/sharing/create_shared_link_with_settings', + listSharedLinks: '/sharing/list_shared_links', + }; + + this.authorizationUri = encodeURI( + `https://www.dropbox.com/oauth2/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&state=${this.state}` + ); + this.tokenUri = 'https://api.dropboxapi.com/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri, + }, + }; + + const response = await this._post(options); + await this.setTokens(response); + return response; + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + this.refresh_token = get(params, 'refresh_token'); + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addAuthHeaders(options) { + const authHeaders = { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.getCurrentAccount, + }; + return this._post(options); + } + + async listFolder(path = '', params = {}) { + const options = { + url: this.baseUrl + this.URLs.listFolder, + body: { path, ...params }, + }; + return this._post(options); + } + + async uploadFile(path, fileContent) { + const options = { + url: this.contentBaseUrl + this.URLs.upload, + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'Dropbox-API-Arg': JSON.stringify({ path, mode: 'add', autorename: true }), + 'Content-Type': 'application/octet-stream', + }, + body: fileContent, + }; + return this._post(options, false); + } + + async createSharedLink(path, settings = {}) { + const options = { + url: this.baseUrl + this.URLs.createSharedLink, + body: { path, settings }, + }; + return this._post(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/dropbox/defaultConfig.json b/packages/dropbox/defaultConfig.json new file mode 100644 index 0000000..33f6bdb --- /dev/null +++ b/packages/dropbox/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "dropbox", + "label": "Dropbox", + "productUrl": "https://dropbox.com", + "apiDocs": "https://www.dropbox.com/developers/documentation", + "logoUrl": "https://cfl.dropboxstatic.com/static/images/logo_catalog/dropbox_logo_glyph_blue_m1@2x.png", + "categories": [ + "Cloud Storage", + "File Management", + "File Synchronization", + "Collaboration" + ], + "description": "Dropbox is a cloud storage service that allows users to store, sync, and share files across devices and with others." +} \ No newline at end of file diff --git a/packages/dropbox/definition.js b/packages/dropbox/definition.js new file mode 100644 index 0000000..c79b6b8 --- /dev/null +++ b/packages/dropbox/definition.js @@ -0,0 +1,43 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Dropbox', + requiredAuthMethods: { + getToken: async (api, params) => { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.account_id, user: userId }, + details: { name: userDetails.name.display_name, email: userDetails.email }, + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token'], + entity: [], + }, + getCredentialDetails: async (api, userId) => { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.account_id, user: userId }, + details: {}, + }; + }, + testAuthRequest: async (api) => api.getUserDetails(), + }, + env: { + client_id: process.env.DROPBOX_CLIENT_ID, + client_secret: process.env.DROPBOX_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/dropbox`, + }, +}; + +module.exports = { Definition }; diff --git a/packages/dropbox/index.js b/packages/dropbox/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/dropbox/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/drupal/README.md b/packages/drupal/README.md new file mode 100644 index 0000000..63247ac --- /dev/null +++ b/packages/drupal/README.md @@ -0,0 +1,42 @@ +# Drupal API Module + +Frigg API module for Drupal integration. + +## Installation + +```bash +npm install @friggframework/drupal +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/drupal'); + +// Initialize API client +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret' +}); +``` + +## Configuration + +Set the following environment variables: + +``` +DRUPAL_CLIENT_ID=your_client_id +DRUPAL_CLIENT_SECRET=your_client_secret +``` + +## API Methods + +- `getCurrentUser()` - Get current user information +- `listItems()` - List items +- `createItem(data)` - Create new item +- `updateItem(id, data)` - Update existing item +- `deleteItem(id)` - Delete item + +## License + +MIT diff --git a/packages/drupal/api.js b/packages/drupal/api.js new file mode 100644 index 0000000..941efcc --- /dev/null +++ b/packages/drupal/api.js @@ -0,0 +1,79 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.drupal.org'; + + this.URLs = { + me: '/user/profile', + list: '/items', + create: '/items', + update: '/items/:id', + delete: '/items/:id' + }; + + this.authorizationUri = 'https://api.drupal.org/oauth/authorize'; + this.accessTokenUri = 'https://api.drupal.org/oauth/token'; + } + + static Definition = { + DISPLAY_NAME: 'Drupal', + MODULE_NAME: 'drupal', + CATEGORY: 'Developer', + USES_OAUTH: true + }; + + // OAuth2 Implementation + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + scope: this.scope || 'read write' + }; + } + + async getTokenFromCode(code) { + const body = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }; + + const options = { + body: body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return this.post(this.accessTokenUri, options); + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listItems(params = {}) { + return this.get(this.URLs.list, { params }); + } + + async createItem(data) { + return this.post(this.URLs.create, data); + } + + async updateItem(id, data) { + const url = this.URLs.update.replace(':id', id); + return this.put(url, data); + } + + async deleteItem(id) { + const url = this.URLs.delete.replace(':id', id); + return this.delete(url); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/drupal/defaultConfig.json b/packages/drupal/defaultConfig.json new file mode 100644 index 0000000..6949f43 --- /dev/null +++ b/packages/drupal/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Drupal", + "moduleName": "drupal", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Drupal API Integration Module", + "category": "Developer", + "apiDocUrl": "https://api.drupal.org/docs", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/drupal/definition.js b/packages/drupal/definition.js new file mode 100644 index 0000000..69ec618 --- /dev/null +++ b/packages/drupal/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Drupal', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {name: userDetails.name || userDetails.email || userDetails.display_name}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id || userDetails.user_id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.DRUPAL_CLIENT_ID, + client_secret: process.env.DRUPAL_CLIENT_SECRET, + scope: process.env.DRUPAL_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/drupal`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/drupal/index.js b/packages/drupal/index.js new file mode 100644 index 0000000..a53bf55 --- /dev/null +++ b/packages/drupal/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; \ No newline at end of file diff --git a/packages/drupal/package.json b/packages/drupal/package.json new file mode 100644 index 0000000..fef2096 --- /dev/null +++ b/packages/drupal/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/drupal", + "version": "0.0.1", + "description": "Drupal API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "drupal", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0", + "eslint": "^8.0.0", + "dotenv": "^16.0.0" + } +} \ No newline at end of file diff --git a/packages/ebanx/README.md b/packages/ebanx/README.md new file mode 100644 index 0000000..81d4f0d --- /dev/null +++ b/packages/ebanx/README.md @@ -0,0 +1,34 @@ +# Ebanx API Integration + +Ebanx integration module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/ebanx +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/ebanx'); + +// Initialize API instance +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); +``` + +## Configuration + +Set the following environment variables: + +- `EBANX_CLIENT_ID` +- `EBANX_CLIENT_SECRET` +- `EBANX_SCOPE` + +## API Documentation + +For more information about the Ebanx API, visit: https://api.ebanx.com diff --git a/packages/ebanx/api.js b/packages/ebanx/api.js new file mode 100644 index 0000000..a38149c --- /dev/null +++ b/packages/ebanx/api.js @@ -0,0 +1,95 @@ +const {OAuth2Requester} = require('@friggframework/core'); + +class EbanxApi extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.ebanx.com'; + + // OAuth2 configuration + this.authorizationUri = `${this.baseUrl}/oauth/authorize`; + this.tokenUri = `${this.baseUrl}/oauth/token`; + this.revokeUri = `${this.baseUrl}/oauth/revoke`; + + this.URLs = { + userInfo: '/user', + // Add more endpoints as needed + }; + } + + async getAuthorizationRequirements() { + return { + url: this.authorizationUri, + type: 'oauth2', + clientId: this.client_id, + scope: this.scope || 'read', + redirectUri: this.redirect_uri + }; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + method: 'GET', + headers: this._buildHeaders() + }; + + return this._request(options); + } + + async refreshToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + form: { + grant_type: 'refresh_token', + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.refresh_token + } + }; + + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + _buildHeaders() { + return { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + } +} + +module.exports = {Api: EbanxApi}; diff --git a/packages/ebanx/defaultConfig.json b/packages/ebanx/defaultConfig.json new file mode 100644 index 0000000..3d50553 --- /dev/null +++ b/packages/ebanx/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Ebanx", + "moduleName": "ebanx", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Ebanx API Integration Module", + "category": "Unnamed record", + "apiDocUrl": "https://api.ebanx.com", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/ebanx/definition.js b/packages/ebanx/definition.js new file mode 100644 index 0000000..995c70a --- /dev/null +++ b/packages/ebanx/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Ebanx', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.EBANX_CLIENT_ID, + client_secret: process.env.EBANX_CLIENT_SECRET, + scope: process.env.EBANX_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/ebanx`, + } +}; + +module.exports = {Definition}; diff --git a/packages/ebanx/index.js b/packages/ebanx/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/ebanx/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/ebanx/package.json b/packages/ebanx/package.json new file mode 100644 index 0000000..f461195 --- /dev/null +++ b/packages/ebanx/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/ebanx", + "version": "0.0.1", + "description": "Ebanx API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "eslint": "^8.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "ebanx", + "unnamed record" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/enreach-formerly-herobase/README.md b/packages/enreach-formerly-herobase/README.md new file mode 100644 index 0000000..e8c6b1f --- /dev/null +++ b/packages/enreach-formerly-herobase/README.md @@ -0,0 +1,55 @@ +# Enreach (formerly Herobase) API Module + +Enreach (formerly Herobase) API Integration Module for the Frigg Framework. + +## Installation + +```bash +npm install @friggframework/enreach-formerly-herobase +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/enreach-formerly-herobase'); + +// Initialize API +const api = new Api({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uri: 'your_redirect_uri' +}); + +// Get current user +const user = await api.getCurrentUser(); +console.log(user); +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +ENREACH_FORMERLY_HEROBASE_CLIENT_ID=your_client_id +ENREACH_FORMERLY_HEROBASE_CLIENT_SECRET=your_client_secret +ENREACH_FORMERLY_HEROBASE_SCOPE=your_scope +ENREACH_FORMERLY_HEROBASE_AUTH_URI=authorization_endpoint +ENREACH_FORMERLY_HEROBASE_TOKEN_URI=token_endpoint +REDIRECT_URI=your_base_redirect_uri +``` + +## Development + +```bash +npm test +npm run test:watch +npm run test:coverage +``` + +## Category + +CRM + +## License + +MIT diff --git a/packages/enreach-formerly-herobase/api.js b/packages/enreach-formerly-herobase/api.js new file mode 100644 index 0000000..efa6e38 --- /dev/null +++ b/packages/enreach-formerly-herobase/api.js @@ -0,0 +1,73 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'CRM'; + + this.URLs = { + me: '/me', + users: '/users', + // Add more endpoints here + }; + + this.authorizationUri = process.env.ENREACH_FORMERLY_HEROBASE_AUTH_URI; + this.tokenUri = process.env.ENREACH_FORMERLY_HEROBASE_TOKEN_URI; + } + + static Definition = { + DISPLAY_NAME: 'Enreach (formerly Herobase)', + MODULE_NAME: 'enreach-formerly-herobase', + CATEGORY: 'https://api.enreachformerlyherobase.com', + USES_OAUTH: true + }; + + async getAuthUri() { + const { client_id, redirect_uri, scopes } = this.config; + const params = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes.join(' '), + access_type: 'offline', + prompt: 'consent' + }); + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const { client_id, client_secret, redirect_uri } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'authorization_code', + code, + client_id, + client_secret, + redirect_uri + }); + return response; + } + + async refreshAccessToken(refreshToken) { + const { client_id, client_secret } = this.config; + const response = await this.post(this.tokenUri, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id, + client_secret + }); + return response; + } + + // API Methods + async getCurrentUser() { + return this.get(this.URLs.me); + } + + async listUsers(params = {}) { + return this.get(this.URLs.users, params); + } + + // Add more API methods here based on the API documentation +} + +module.exports = { Api }; diff --git a/packages/enreach-formerly-herobase/defaultConfig.json b/packages/enreach-formerly-herobase/defaultConfig.json new file mode 100644 index 0000000..a07d101 --- /dev/null +++ b/packages/enreach-formerly-herobase/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Enreach (formerly Herobase)", + "moduleName": "enreach-formerly-herobase", + "version": "0.0.1", + "supportedAuthTypes": [ + "oauth2" + ], + "docs": { + "description": "Enreach (formerly Herobase) API Integration Module", + "category": "CRM", + "apiDocUrl": "https://docs.enreach-formerly-herobase.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/enreach-formerly-herobase/definition.js b/packages/enreach-formerly-herobase/definition.js new file mode 100644 index 0000000..c5f661c --- /dev/null +++ b/packages/enreach-formerly-herobase/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Enreach (formerly Herobase)', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name || userDetails.email}, + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getCurrentUser(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + }, + env: { + client_id: process.env.ENREACH_FORMERLY_HEROBASE_CLIENT_ID, + client_secret: process.env.ENREACH_FORMERLY_HEROBASE_CLIENT_SECRET, + scope: process.env.ENREACH_FORMERLY_HEROBASE_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/enreach-formerly-herobase`, + } +}; + +module.exports = {Definition}; diff --git a/packages/enreach-formerly-herobase/index.js b/packages/enreach-formerly-herobase/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/enreach-formerly-herobase/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/enreach-formerly-herobase/jest-setup.js b/packages/enreach-formerly-herobase/jest-setup.js new file mode 100644 index 0000000..f851825 --- /dev/null +++ b/packages/enreach-formerly-herobase/jest-setup.js @@ -0,0 +1 @@ +require('dotenv').config(); diff --git a/packages/enreach-formerly-herobase/jest-teardown.js b/packages/enreach-formerly-herobase/jest-teardown.js new file mode 100644 index 0000000..881057a --- /dev/null +++ b/packages/enreach-formerly-herobase/jest-teardown.js @@ -0,0 +1,3 @@ +module.exports = async () => { + // Global teardown +}; diff --git a/packages/enreach-formerly-herobase/jest.config.js b/packages/enreach-formerly-herobase/jest.config.js new file mode 100644 index 0000000..6a2c507 --- /dev/null +++ b/packages/enreach-formerly-herobase/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + coverageDirectory: 'coverage', + collectCoverageFrom: [ + '**/*.js', + '!**/node_modules/**', + '!**/coverage/**', + '!**/jest.config.js', + '!**/jest-*.js' + ], + setupFilesAfterEnv: ['./jest-setup.js'], + globalTeardown: './jest-teardown.js' +}; diff --git a/packages/enreach-formerly-herobase/package.json b/packages/enreach-formerly-herobase/package.json new file mode 100644 index 0000000..2dcf80e --- /dev/null +++ b/packages/enreach-formerly-herobase/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/enreach-formerly-herobase", + "version": "0.0.1", + "description": "Enreach (formerly Herobase) API Integration Module", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0", + "dotenv": "^16.0.0" + }, + "keywords": [ + "frigg", + "api", + "integration", + "enreach-formerly-herobase" + ], + "author": "Frigg", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/etsy/api.js b/packages/etsy/api.js new file mode 100644 index 0000000..656fc86 --- /dev/null +++ b/packages/etsy/api.js @@ -0,0 +1,664 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// Etsy Open API v3 client +// Supports OAuth2 authentication +// Documentation: https://developers.etsy.com/documentation/ + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://openapi.etsy.com/v3'; + this.client_id = get(params, 'client_id', process.env.ETSY_CLIENT_ID); + this.client_secret = get(params, 'client_secret', process.env.ETSY_CLIENT_SECRET); + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + + // OAuth endpoints + this.authorizationUri = 'https://www.etsy.com/oauth/connect'; + this.tokenUri = 'https://api.etsy.com/v3/public/oauth/token'; + + this.URLs = { + // Application + ping: '/application/ping', + + // Shops + shops: '/application/shops', + shopById: (shopId) => `/application/shops/${shopId}`, + shopSections: (shopId) => `/application/shops/${shopId}/sections`, + shopSectionById: (shopId, sectionId) => `/application/shops/${shopId}/sections/${sectionId}`, + shopPolicies: (shopId) => `/application/shops/${shopId}/policies`, + shopReceipts: (shopId) => `/application/shops/${shopId}/receipts`, + shopReceiptById: (shopId, receiptId) => `/application/shops/${shopId}/receipts/${receiptId}`, + + // Listings + listings: '/application/listings', + listingById: (listingId) => `/application/listings/${listingId}`, + listingsByShop: (shopId) => `/application/shops/${shopId}/listings`, + listingImages: (listingId) => `/application/listings/${listingId}/images`, + listingImageById: (listingId, imageId) => `/application/listings/${listingId}/images/${imageId}`, + listingInventory: (listingId) => `/application/listings/${listingId}/inventory`, + listingProducts: (listingId) => `/application/listings/${listingId}/products`, + listingReviews: (listingId) => `/application/listings/${listingId}/reviews`, + listingVideos: (listingId) => `/application/listings/${listingId}/videos`, + listingTranslation: (listingId, language) => `/application/listings/${listingId}/translations/${language}`, + listingVariationImages: (listingId) => `/application/listings/${listingId}/variation-images`, + + // User/Shop Management + user: '/application/user', + userProfile: '/application/user/profile', + userAddress: '/application/user/addresses', + userAddressById: (addressId) => `/application/user/addresses/${addressId}`, + userAccount: '/application/user/account', + + // Shop Management + myShops: '/application/user/shops', + myShopById: (shopId) => `/application/user/shops/${shopId}`, + myShopListings: (shopId) => `/application/user/shops/${shopId}/listings`, + myShopReceipts: (shopId) => `/application/user/shops/${shopId}/receipts`, + myShopReceiptById: (shopId, receiptId) => `/application/user/shops/${shopId}/receipts/${receiptId}`, + + // Payments + shopPaymentAccountLedgerEntries: (shopId) => `/application/shops/${shopId}/payment-account/ledger-entries`, + shopPaymentAccountLedgerEntry: (shopId, entryId) => `/application/shops/${shopId}/payment-account/ledger-entries/${entryId}`, + shopPaymentAccountLedgerEntryPayments: (shopId, entryId) => `/application/shops/${shopId}/payment-account/ledger-entries/${entryId}/payments`, + + // Shipping + shippingCarriers: '/application/shipping-carriers', + shippingTemplates: (shopId) => `/application/shops/${shopId}/shipping-templates`, + shippingTemplateById: (shopId, templateId) => `/application/shops/${shopId}/shipping-templates/${templateId}`, + shippingTemplateEntries: (shopId, templateId) => `/application/shops/${shopId}/shipping-templates/${templateId}/entries`, + shippingTemplateUpgrades: (shopId, templateId) => `/application/shops/${shopId}/shipping-templates/${templateId}/upgrades`, + + // Taxonomy + taxonomy: '/application/seller-taxonomy/nodes', + taxonomyNode: (taxonomyId) => `/application/seller-taxonomy/nodes/${taxonomyId}`, + taxonomyNodeProperties: (taxonomyId) => `/application/seller-taxonomy/nodes/${taxonomyId}/properties`, + + // Shop Production Partners + shopProductionPartners: (shopId) => `/application/shops/${shopId}/production-partners`, + + // Categories and Attributes + buyerTaxonomy: '/application/buyer-taxonomy/nodes', + buyerTaxonomyNode: (taxonomyId) => `/application/buyer-taxonomy/nodes/${taxonomyId}`, + buyerTaxonomyNodeProperties: (taxonomyId) => `/application/buyer-taxonomy/nodes/${taxonomyId}/properties`, + + // Reviews + reviewsByShop: (shopId) => `/application/shops/${shopId}/reviews`, + reviewById: (shopId, reviewId) => `/application/shops/${shopId}/reviews/${reviewId}`, + + // Favorites + userFavoriteListings: '/application/user/favorites/listings', + userFavoriteListingById: (listingId) => `/application/user/favorites/listings/${listingId}`, + + // Conversations + conversations: '/application/user/conversations', + conversationById: (conversationId) => `/application/user/conversations/${conversationId}`, + conversationMessages: (conversationId) => `/application/user/conversations/${conversationId}/messages`, + + // Orders + shopReceipts: (shopId) => `/application/shops/${shopId}/receipts`, + shopReceiptById: (shopId, receiptId) => `/application/shops/${shopId}/receipts/${receiptId}`, + receiptShipments: (shopId, receiptId) => `/application/shops/${shopId}/receipts/${receiptId}/shipments`, + receiptTransactions: (shopId, receiptId) => `/application/shops/${shopId}/receipts/${receiptId}/transactions`, + receiptTransactionById: (shopId, receiptId, transactionId) => `/application/shops/${shopId}/receipts/${receiptId}/transactions/${transactionId}`, + }; + + // Default scopes for Etsy API + this.scope = get(params, 'scope', 'email_r profile_r shops_r listings_r'); + } + + // Generate OAuth authorization URL + getAuthUri() { + const params = new URLSearchParams({ + response_type: 'code', + client_id: this.client_id, + redirect_uri: this.redirect_uri, + scope: this.scope, + state: this.state || 'random_state_string', + }); + + return `${this.authorizationUri}?${params.toString()}`; + } + + // Exchange authorization code for access token + async getTokenFromCode(code) { + const tokenData = { + grant_type: 'authorization_code', + client_id: this.client_id, + redirect_uri: this.redirect_uri, + code: code, + }; + + const options = { + url: this.tokenUri, + body: tokenData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64')}`, + }, + }; + + const response = await this._post(options, false); + await this.setTokens(response); + return response; + } + + // Refresh access token + async refreshAccessToken() { + if (!this.refresh_token) { + throw new Error('No refresh token available'); + } + + const tokenData = { + grant_type: 'refresh_token', + refresh_token: this.refresh_token, + }; + + const options = { + url: this.tokenUri, + body: tokenData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64')}`, + }, + }; + + const response = await this._post(options, false); + await this.setTokens(response); + return response; + } + + // Set access and refresh tokens + async setTokens(tokenResponse) { + this.access_token = tokenResponse.access_token; + if (tokenResponse.refresh_token) { + this.refresh_token = tokenResponse.refresh_token; + } + + if (tokenResponse.expires_in) { + this.accessTokenExpire = new Date(Date.now() + tokenResponse.expires_in * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + // Add authentication headers + addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + } + + async _get(options) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Application ********************************** + + async ping() { + const options = { + url: this.URLs.ping, + }; + return this._get(options); + } + + // ************************** User Methods ********************************** + + async getUserProfile() { + const options = { + url: this.URLs.userProfile, + }; + return this._get(options); + } + + async getUser() { + const options = { + url: this.URLs.user, + }; + return this._get(options); + } + + async getUserAccount() { + const options = { + url: this.URLs.userAccount, + }; + return this._get(options); + } + + async getUserAddresses() { + const options = { + url: this.URLs.userAddress, + }; + return this._get(options); + } + + async getUserAddressById(addressId) { + const options = { + url: this.URLs.userAddressById(addressId), + }; + return this._get(options); + } + + // ************************** Shops ********************************** + + async getShopById(shopId, params = {}) { + const options = { + url: this.URLs.shopById(shopId), + query: params + }; + return this._get(options); + } + + async findShops(params = {}) { + const options = { + url: this.URLs.shops, + query: params + }; + return this._get(options); + } + + async getMyShops() { + const options = { + url: this.URLs.myShops, + }; + return this._get(options); + } + + async getShopSections(shopId) { + const options = { + url: this.URLs.shopSections(shopId), + }; + return this._get(options); + } + + async createShopSection(shopId, sectionData) { + const options = { + url: this.URLs.shopSections(shopId), + body: sectionData, + }; + return this._post(options); + } + + async updateShopSection(shopId, sectionId, sectionData) { + const options = { + url: this.URLs.shopSectionById(shopId, sectionId), + body: sectionData, + }; + return this._put(options); + } + + async deleteShopSection(shopId, sectionId) { + const options = { + url: this.URLs.shopSectionById(shopId, sectionId), + }; + return this._delete(options); + } + + async getShopPolicies(shopId) { + const options = { + url: this.URLs.shopPolicies(shopId), + }; + return this._get(options); + } + + // ************************** Listings ********************************** + + async createListing(shopId, listingData) { + const options = { + url: this.URLs.myShopListings(shopId), + body: listingData, + }; + return this._post(options); + } + + async getListingsByShop(shopId, params = {}) { + const options = { + url: this.URLs.listingsByShop(shopId), + query: params + }; + return this._get(options); + } + + async getMyShopListings(shopId, params = {}) { + const options = { + url: this.URLs.myShopListings(shopId), + query: params + }; + return this._get(options); + } + + async getListingById(listingId, params = {}) { + const options = { + url: this.URLs.listingById(listingId), + query: params + }; + return this._get(options); + } + + async updateListing(listingId, listingData) { + const options = { + url: this.URLs.listingById(listingId), + body: listingData, + }; + return this._put(options); + } + + async deleteListing(listingId) { + const options = { + url: this.URLs.listingById(listingId), + }; + return this._delete(options); + } + + async getListingImages(listingId) { + const options = { + url: this.URLs.listingImages(listingId), + }; + return this._get(options); + } + + async uploadListingImage(listingId, imageData) { + const options = { + url: this.URLs.listingImages(listingId), + body: imageData, + }; + return this._post(options); + } + + async deleteListingImage(listingId, imageId) { + const options = { + url: this.URLs.listingImageById(listingId, imageId), + }; + return this._delete(options); + } + + async getListingInventory(listingId) { + const options = { + url: this.URLs.listingInventory(listingId), + }; + return this._get(options); + } + + async updateListingInventory(listingId, inventoryData) { + const options = { + url: this.URLs.listingInventory(listingId), + body: inventoryData, + }; + return this._put(options); + } + + async getListingProducts(listingId) { + const options = { + url: this.URLs.listingProducts(listingId), + }; + return this._get(options); + } + + async getListingReviews(listingId, params = {}) { + const options = { + url: this.URLs.listingReviews(listingId), + query: params + }; + return this._get(options); + } + + // ************************** Orders / Receipts ********************************** + + async getShopReceipts(shopId, params = {}) { + const options = { + url: this.URLs.shopReceipts(shopId), + query: params + }; + return this._get(options); + } + + async getShopReceiptById(shopId, receiptId) { + const options = { + url: this.URLs.shopReceiptById(shopId, receiptId), + }; + return this._get(options); + } + + async updateShopReceipt(shopId, receiptId, receiptData) { + const options = { + url: this.URLs.shopReceiptById(shopId, receiptId), + body: receiptData, + }; + return this._put(options); + } + + async getReceiptTransactions(shopId, receiptId) { + const options = { + url: this.URLs.receiptTransactions(shopId, receiptId), + }; + return this._get(options); + } + + async getReceiptTransactionById(shopId, receiptId, transactionId) { + const options = { + url: this.URLs.receiptTransactionById(shopId, receiptId, transactionId), + }; + return this._get(options); + } + + async createReceiptShipment(shopId, receiptId, shipmentData) { + const options = { + url: this.URLs.receiptShipments(shopId, receiptId), + body: shipmentData, + }; + return this._post(options); + } + + async getReceiptShipments(shopId, receiptId) { + const options = { + url: this.URLs.receiptShipments(shopId, receiptId), + }; + return this._get(options); + } + + // ************************** Reviews ********************************** + + async getShopReviews(shopId, params = {}) { + const options = { + url: this.URLs.reviewsByShop(shopId), + query: params + }; + return this._get(options); + } + + async getShopReviewById(shopId, reviewId) { + const options = { + url: this.URLs.reviewById(shopId, reviewId), + }; + return this._get(options); + } + + // ************************** Shipping ********************************** + + async getShippingCarriers() { + const options = { + url: this.URLs.shippingCarriers, + }; + return this._get(options); + } + + async getShippingTemplates(shopId) { + const options = { + url: this.URLs.shippingTemplates(shopId), + }; + return this._get(options); + } + + async createShippingTemplate(shopId, templateData) { + const options = { + url: this.URLs.shippingTemplates(shopId), + body: templateData, + }; + return this._post(options); + } + + async getShippingTemplateById(shopId, templateId) { + const options = { + url: this.URLs.shippingTemplateById(shopId, templateId), + }; + return this._get(options); + } + + async updateShippingTemplate(shopId, templateId, templateData) { + const options = { + url: this.URLs.shippingTemplateById(shopId, templateId), + body: templateData, + }; + return this._put(options); + } + + async deleteShippingTemplate(shopId, templateId) { + const options = { + url: this.URLs.shippingTemplateById(shopId, templateId), + }; + return this._delete(options); + } + + async getShippingTemplateEntries(shopId, templateId) { + const options = { + url: this.URLs.shippingTemplateEntries(shopId, templateId), + }; + return this._get(options); + } + + // ************************** Taxonomy ********************************** + + async getSellerTaxonomy() { + const options = { + url: this.URLs.taxonomy, + }; + return this._get(options); + } + + async getSellerTaxonomyNode(taxonomyId) { + const options = { + url: this.URLs.taxonomyNode(taxonomyId), + }; + return this._get(options); + } + + async getSellerTaxonomyNodeProperties(taxonomyId) { + const options = { + url: this.URLs.taxonomyNodeProperties(taxonomyId), + }; + return this._get(options); + } + + async getBuyerTaxonomy() { + const options = { + url: this.URLs.buyerTaxonomy, + }; + return this._get(options); + } + + async getBuyerTaxonomyNode(taxonomyId) { + const options = { + url: this.URLs.buyerTaxonomyNode(taxonomyId), + }; + return this._get(options); + } + + // ************************** Favorites ********************************** + + async getUserFavoriteListings(params = {}) { + const options = { + url: this.URLs.userFavoriteListings, + query: params + }; + return this._get(options); + } + + async addUserFavoriteListing(listingId) { + const options = { + url: this.URLs.userFavoriteListingById(listingId), + body: {}, + }; + return this._post(options); + } + + async removeUserFavoriteListing(listingId) { + const options = { + url: this.URLs.userFavoriteListingById(listingId), + }; + return this._delete(options); + } + + // ************************** Conversations ********************************** + + async getConversations(params = {}) { + const options = { + url: this.URLs.conversations, + query: params + }; + return this._get(options); + } + + async getConversationById(conversationId) { + const options = { + url: this.URLs.conversationById(conversationId), + }; + return this._get(options); + } + + async getConversationMessages(conversationId, params = {}) { + const options = { + url: this.URLs.conversationMessages(conversationId), + query: params + }; + return this._get(options); + } + + // ************************** Payments ********************************** + + async getShopPaymentAccountLedgerEntries(shopId, params = {}) { + const options = { + url: this.URLs.shopPaymentAccountLedgerEntries(shopId), + query: params + }; + return this._get(options); + } + + async getShopPaymentAccountLedgerEntry(shopId, entryId) { + const options = { + url: this.URLs.shopPaymentAccountLedgerEntry(shopId, entryId), + }; + return this._get(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/etsy/defaultConfig.json b/packages/etsy/defaultConfig.json new file mode 100644 index 0000000..cea236d --- /dev/null +++ b/packages/etsy/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "etsy", + "label": "Etsy", + "productUrl": "https://www.etsy.com", + "apiDocs": "https://developers.etsy.com/", + "logoUrl": "https://friggframework.org/assets/img/etsy-icon.png", + "categories": [ + "E-commerce", + "Marketplace" + ], + "description": "Etsy is a global marketplace for unique and creative goods, connecting millions of buyers and sellers around the world." +} \ No newline at end of file diff --git a/packages/etsy/definition.js b/packages/etsy/definition.js new file mode 100644 index 0000000..0b0c749 --- /dev/null +++ b/packages/etsy/definition.js @@ -0,0 +1,76 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Etsy', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + + if (!code) { + throw new Error('Missing authorization code for Etsy OAuth'); + } + + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userProfile = await api.getUserProfile(); + const user = await api.getUser(); + + return { + identifiers: {externalId: user.user_id.toString(), user: userId}, + details: { + name: userProfile.first_name && userProfile.last_name + ? `${userProfile.first_name} ${userProfile.last_name}` + : user.login_name, + login_name: user.login_name, + user_id: user.user_id, + email: userProfile.email || '', + bio: userProfile.bio || '', + location: userProfile.location || '', + image_url_75x75: userProfile.image_url_75x75 || '', + profile_url: `https://www.etsy.com/people/${user.login_name}` + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token', 'token_type', 'expires_in' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const user = await api.getUser(); + const userProfile = await api.getUserProfile(); + + return { + identifiers: {externalId: user.user_id.toString(), user: userId}, + details: { + name: userProfile.first_name && userProfile.last_name + ? `${userProfile.first_name} ${userProfile.last_name}` + : user.login_name, + login_name: user.login_name, + user_id: user.user_id + } + }; + }, + testAuthRequest: async function (api) { + return api.ping() + }, + }, + env: { + client_id: process.env.ETSY_CLIENT_ID, + client_secret: process.env.ETSY_CLIENT_SECRET, + scope: process.env.ETSY_SCOPE || 'email_r profile_r shops_r listings_r', + redirect_uri: `${process.env.REDIRECT_URI}/etsy`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/etsy/index.js b/packages/etsy/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/etsy/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/etsy/jest.config.js b/packages/etsy/jest.config.js new file mode 100644 index 0000000..fa8c051 --- /dev/null +++ b/packages/etsy/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEnv: ['/tests/setup.js'] +}; \ No newline at end of file diff --git a/packages/etsy/package.json b/packages/etsy/package.json new file mode 100644 index 0000000..0d3c9ba --- /dev/null +++ b/packages/etsy/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-etsy", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Etsy API module that lets the Frigg Framework interact with Etsy", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/etsy/tests/api.test.js b/packages/etsy/tests/api.test.js new file mode 100644 index 0000000..02b34dd --- /dev/null +++ b/packages/etsy/tests/api.test.js @@ -0,0 +1,91 @@ +const { Api } = require('../api'); + +describe('Etsy API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test_client_id', + client_secret: 'test_client_secret', + access_token: 'test_access_token', + redirect_uri: 'https://example.com/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct credentials', () => { + expect(api.client_id).toBe('test_client_id'); + expect(api.client_secret).toBe('test_client_secret'); + expect(api.access_token).toBe('test_access_token'); + expect(api.baseUrl).toBe('https://openapi.etsy.com/v3'); + }); + + test('should set OAuth endpoints correctly', () => { + expect(api.authorizationUri).toBe('https://www.etsy.com/oauth/connect'); + expect(api.tokenUri).toBe('https://api.etsy.com/v3/public/oauth/token'); + }); + }); + + describe('Authentication', () => { + test('should generate correct auth URI', () => { + const authUri = api.getAuthUri(); + + expect(authUri).toContain('https://www.etsy.com/oauth/connect'); + expect(authUri).toContain('client_id=test_client_id'); + expect(authUri).toContain('response_type=code'); + expect(authUri).toContain('scope=email_r%20profile_r%20shops_r%20listings_r'); + }); + + test('should add correct auth headers', () => { + const options = { headers: {} }; + api.addAuthHeaders(options); + + expect(options.headers.Authorization).toBe('Bearer test_access_token'); + expect(options.headers['Content-Type']).toBe('application/json'); + expect(options.headers.Accept).toBe('application/json'); + }); + }); + + describe('URL Construction', () => { + test('should construct shop URLs correctly', () => { + expect(api.URLs.shops).toBe('/application/shops'); + expect(api.URLs.shopById(123)).toBe('/application/shops/123'); + expect(api.URLs.shopSections(123)).toBe('/application/shops/123/sections'); + }); + + test('should construct listing URLs correctly', () => { + expect(api.URLs.listings).toBe('/application/listings'); + expect(api.URLs.listingById(456)).toBe('/application/listings/456'); + expect(api.URLs.listingImages(456)).toBe('/application/listings/456/images'); + expect(api.URLs.listingsByShop(123)).toBe('/application/shops/123/listings'); + }); + + test('should construct user URLs correctly', () => { + expect(api.URLs.user).toBe('/application/user'); + expect(api.URLs.userProfile).toBe('/application/user/profile'); + expect(api.URLs.myShops).toBe('/application/user/shops'); + }); + + test('should construct receipt/order URLs correctly', () => { + expect(api.URLs.shopReceipts(123)).toBe('/application/shops/123/receipts'); + expect(api.URLs.shopReceiptById(123, 456)).toBe('/application/shops/123/receipts/456'); + expect(api.URLs.receiptTransactions(123, 456)).toBe('/application/shops/123/receipts/456/transactions'); + }); + }); + + describe('Scope Handling', () => { + test('should use default scope if none provided', () => { + expect(api.scope).toBe('email_r profile_r shops_r listings_r'); + }); + + test('should use custom scope if provided', () => { + const customApi = new Api({ + scope: 'email_r profile_r', + client_id: 'test', + client_secret: 'test' + }); + + expect(customApi.scope).toBe('email_r profile_r'); + }); + }); +}); \ No newline at end of file diff --git a/packages/etsy/tests/setup.js b/packages/etsy/tests/setup.js new file mode 100644 index 0000000..ab02bab --- /dev/null +++ b/packages/etsy/tests/setup.js @@ -0,0 +1,12 @@ +// Test setup file for Etsy API module +require('dotenv').config(); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; \ No newline at end of file diff --git a/packages/eventbrite/api.js b/packages/eventbrite/api.js new file mode 100644 index 0000000..5324de3 --- /dev/null +++ b/packages/eventbrite/api.js @@ -0,0 +1,165 @@ +const { OAuth2Requester } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + this.baseUrl = 'https://www.eventbriteapi.com/v3'; + + this.URLs = { + // User + user: '/users/me', + + // Events + events: '/events', + eventById: (eventId) => `/events/${eventId}`, + myEvents: '/users/me/events', + + // Orders + orders: '/events/{event_id}/orders', + orderById: (orderId) => `/orders/${orderId}`, + + // Attendees + attendees: '/events/{event_id}/attendees', + attendeeById: (attendeeId) => `/attendees/${attendeeId}`, + + // Tickets + ticketClasses: '/events/{event_id}/ticket_classes', + ticketClassById: (ticketClassId) => `/ticket_classes/${ticketClassId}`, + + // Venues + venues: '/venues', + venueById: (venueId) => `/venues/${venueId}`, + + // Organizations + organizations: '/users/me/organizations', + organizationById: (organizationId) => `/organizations/${organizationId}`, + + // Webhooks + webhooks: '/webhooks', + webhookById: (webhookId) => `/webhooks/${webhookId}`, + }; + + this.authorizationUri = encodeURI( + `https://www.eventbrite.com/oauth/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&state=${this.state}` + ); + this.tokenUri = 'https://www.eventbrite.com/oauth/token'; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + redirect_uri: this.redirect_uri, + code: code, + }, + }; + const response = await this._request(options); + await this.setTokens(response); + return response; + } + + // User + async getUser() { + const options = { + url: this.baseUrl + this.URLs.user, + method: 'GET', + }; + return this._request(options); + } + + // Events + async listEvents(params = {}) { + const options = { + url: this.baseUrl + this.URLs.events, + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async getMyEvents(params = {}) { + const options = { + url: this.baseUrl + this.URLs.myEvents, + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async getEvent(eventId) { + const options = { + url: this.baseUrl + this.URLs.eventById(eventId), + method: 'GET', + }; + return this._request(options); + } + + async createEvent(eventData) { + const options = { + url: this.baseUrl + this.URLs.events, + method: 'POST', + json: eventData, + }; + return this._request(options); + } + + async updateEvent(eventId, eventData) { + const options = { + url: this.baseUrl + this.URLs.eventById(eventId), + method: 'POST', + json: eventData, + }; + return this._request(options); + } + + // Orders + async getEventOrders(eventId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.orders.replace('{event_id}', eventId), + method: 'GET', + qs: params, + }; + return this._request(options); + } + + async getOrder(orderId) { + const options = { + url: this.baseUrl + this.URLs.orderById(orderId), + method: 'GET', + }; + return this._request(options); + } + + // Attendees + async getEventAttendees(eventId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.attendees.replace('{event_id}', eventId), + method: 'GET', + qs: params, + }; + return this._request(options); + } + + // Organizations + async getOrganizations() { + const options = { + url: this.baseUrl + this.URLs.organizations, + method: 'GET', + }; + return this._request(options); + } + + // User info for authentication + async getUserDetails() { + return this.getUser(); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/eventbrite/defaultConfig.json b/packages/eventbrite/defaultConfig.json new file mode 100644 index 0000000..2682341 --- /dev/null +++ b/packages/eventbrite/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "eventbrite", + "label": "EventBrite", + "productUrl": "https://www.eventbrite.com", + "apiDocs": "https://www.eventbrite.com/platform/", + "logoUrl": "https://friggframework.org/assets/img/eventbrite-icon.png", + "categories": [ + "Marketing", + "Event Management" + ], + "description": "Eventbrite is a global self-service ticketing platform for live experiences that allows anyone to create, share, find and attend events that fuel their passions and enrich their lives." +} \ No newline at end of file diff --git a/packages/eventbrite/definition.js b/packages/eventbrite/definition.js new file mode 100644 index 0000000..97ce3ab --- /dev/null +++ b/packages/eventbrite/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'EventBrite', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {name: userDetails.name, email: userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.EVENTBRITE_CLIENT_ID, + client_secret: process.env.EVENTBRITE_CLIENT_SECRET, + scope: process.env.EVENTBRITE_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/eventbrite`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/eventbrite/index.js b/packages/eventbrite/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/eventbrite/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/evernote/api.js b/packages/evernote/api.js new file mode 100644 index 0000000..a25cefb --- /dev/null +++ b/packages/evernote/api.js @@ -0,0 +1,746 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// Evernote API client +// Supports OAuth2 authentication +// Documentation: https://dev.evernote.com/doc/ + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + // Determine environment (sandbox or production) + this.isSandbox = get(params, 'sandbox', false); + + if (this.isSandbox) { + this.baseUrl = 'https://sandbox.evernote.com'; + this.authorizationUri = 'https://sandbox.evernote.com/OAuth.action'; + } else { + this.baseUrl = 'https://www.evernote.com'; + this.authorizationUri = 'https://www.evernote.com/OAuth.action'; + } + + this.apiUrl = `${this.baseUrl}/shard/s1/notestore`; + this.userStoreUrl = `${this.baseUrl}/edam/user`; + + this.client_id = get(params, 'client_id', process.env.EVERNOTE_CLIENT_ID); + this.client_secret = get(params, 'client_secret', process.env.EVERNOTE_CLIENT_SECRET); + this.access_token = get(params, 'access_token', null); + this.noteStoreUrl = get(params, 'noteStoreUrl', null); + this.webApiUrlPrefix = get(params, 'webApiUrlPrefix', null); + + // OAuth1.0a parameters (Evernote uses OAuth 1.0a) + this.oauth_token = get(params, 'oauth_token', null); + this.oauth_token_secret = get(params, 'oauth_token_secret', null); + this.oauth_verifier = get(params, 'oauth_verifier', null); + + this.URLs = { + // User Store API + userStore: '/edam/user', + checkVersion: '/edam/user/checkVersion', + getBootstrapInfo: '/edam/user/getBootstrapInfo', + getUser: '/edam/user/getUser', + getPublicUserInfo: '/edam/user/getPublicUserInfo', + getPremiumInfo: '/edam/user/getPremiumInfo', + getNoteStoreUrl: '/edam/user/getNoteStoreUrl', + + // Note Store API (these are relative to noteStoreUrl) + noteStore: '', + listNotebooks: '/listNotebooks', + getDefaultNotebook: '/getDefaultNotebook', + createNotebook: '/createNotebook', + updateNotebook: '/updateNotebook', + expungeNotebook: '/expungeNotebook', + + // Notes + createNote: '/createNote', + updateNote: '/updateNote', + deleteNote: '/deleteNote', + expungeNote: '/expungeNote', + getNote: '/getNote', + getNoteContent: '/getNoteContent', + getNoteSearchText: '/getNoteSearchText', + getNoteTagNames: '/getNoteTagNames', + getNoteAttributes: '/getNoteAttributes', + + // Search + findNotes: '/findNotes', + findNotesMetadata: '/findNotesMetadata', + findNotesCounts: '/findNotesCounts', + findNotesWithResultSpec: '/findNotesWithResultSpec', + + // Tags + listTags: '/listTags', + listTagsByNotebook: '/listTagsByNotebook', + getTag: '/getTag', + createTag: '/createTag', + updateTag: '/updateTag', + untagAll: '/untagAll', + expungeTag: '/expungeTag', + + // Resources (attachments) + createResource: '/createResource', + updateResource: '/updateResource', + getResource: '/getResource', + getResourceData: '/getResourceData', + getResourceByHash: '/getResourceByHash', + getResourceAttributes: '/getResourceAttributes', + + // Saved Searches + listSearches: '/listSearches', + getSearch: '/getSearch', + createSearch: '/createSearch', + updateSearch: '/updateSearch', + expungeSearch: '/expungeSearch', + + // Linked notebooks (shared notebooks) + listLinkedNotebooks: '/listLinkedNotebooks', + getLinkedNotebook: '/getLinkedNotebook', + createLinkedNotebook: '/createLinkedNotebook', + updateLinkedNotebook: '/updateLinkedNotebook', + expungeLinkedNotebook: '/expungeLinkedNotebook', + + // Shared notebooks + shareNotebook: '/shareNotebook', + createSharedNotebook: '/createSharedNotebook', + updateSharedNotebook: '/updateSharedNotebook', + setSharedNotebookRecipientSettings: '/setSharedNotebookRecipientSettings', + sendMessageToSharedNotebookMembers: '/sendMessageToSharedNotebookMembers', + listSharedNotebooks: '/listSharedNotebooks', + expungeSharedNotebooks: '/expungeSharedNotebooks', + + // Business/Teams + getSharedNotebookByAuth: '/getSharedNotebookByAuth', + emailNote: '/emailNote', + shareNote: '/shareNote', + stopSharingNote: '/stopSharingNote', + authenticateToSharedNotebook: '/authenticateToSharedNotebook', + + // Sync + getSyncState: '/getSyncState', + getSyncChunk: '/getSyncChunk', + getFilteredSyncChunk: '/getFilteredSyncChunk', + getLinkedNotebookSyncState: '/getLinkedNotebookSyncState', + getLinkedNotebookSyncChunk: '/getLinkedNotebookSyncChunk', + + // Webhooks + createWebhook: '/createWebhook', + deleteWebhook: '/deleteWebhook', + listWebhooks: '/listWebhooks', + + // OAuth + requestToken: '/oauth', + authorize: '/OAuth.action', + accessToken: '/oauth', + }; + + // ENML (Evernote Markup Language) helper + this.enmlHeader = ''; + this.enmlFooter = ''; + } + + // OAuth 1.0a flow for Evernote + async getRequestToken() { + const options = { + url: `${this.baseUrl}${this.URLs.requestToken}`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + 'oauth_callback': this.redirect_uri, + 'oauth_consumer_key': this.client_id, + 'oauth_signature_method': 'HMAC-SHA1', + 'oauth_timestamp': Math.floor(Date.now() / 1000), + 'oauth_nonce': Math.random().toString(36).substring(2, 15), + 'oauth_version': '1.0' + }) + }; + + // Note: Full OAuth 1.0a signature generation would be needed here + // This is a simplified version - production code should use a proper OAuth library + return this._post(options, false); + } + + getAuthUri(requestToken) { + const params = new URLSearchParams({ + oauth_token: requestToken, + oauth_callback: this.redirect_uri, + }); + + return `${this.authorizationUri}?${params.toString()}`; + } + + async getAccessToken(requestToken, requestTokenSecret, verifier) { + const options = { + url: `${this.baseUrl}${this.URLs.accessToken}`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + 'oauth_token': requestToken, + 'oauth_verifier': verifier, + 'oauth_consumer_key': this.client_id, + 'oauth_signature_method': 'HMAC-SHA1', + 'oauth_timestamp': Math.floor(Date.now() / 1000), + 'oauth_nonce': Math.random().toString(36).substring(2, 15), + 'oauth_version': '1.0' + }) + }; + + // Note: Full OAuth 1.0a signature generation would be needed here + const response = await this._post(options, false); + await this.setTokens(response); + return response; + } + + async setTokens(tokenResponse) { + this.access_token = tokenResponse.oauth_token; + this.oauth_token_secret = tokenResponse.oauth_token_secret; + this.noteStoreUrl = tokenResponse.edam_noteStoreUrl; + this.webApiUrlPrefix = tokenResponse.edam_webApiUrlPrefix; + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + // Add authentication headers for API requests + addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + } + + async _get(options, useNoteStore = false) { + const baseUrl = useNoteStore && this.noteStoreUrl ? this.noteStoreUrl : this.baseUrl; + options.url = baseUrl + options.url; + this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true, useNoteStore = false) { + const baseUrl = useNoteStore && this.noteStoreUrl ? this.noteStoreUrl : this.baseUrl; + options.url = baseUrl + options.url; + this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true, useNoteStore = false) { + const baseUrl = useNoteStore && this.noteStoreUrl ? this.noteStoreUrl : this.baseUrl; + options.url = baseUrl + options.url; + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _delete(options, useNoteStore = false) { + const baseUrl = useNoteStore && this.noteStoreUrl ? this.noteStoreUrl : this.baseUrl; + options.url = baseUrl + options.url; + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** User Store Methods ********************************** + + async getUser() { + const options = { + url: this.URLs.getUser, + }; + return this._get(options); + } + + async getBootstrapInfo(locale = 'en') { + const options = { + url: this.URLs.getBootstrapInfo, + query: { locale } + }; + return this._get(options); + } + + async getPremiumInfo() { + const options = { + url: this.URLs.getPremiumInfo, + }; + return this._get(options); + } + + async getNoteStoreUrl() { + const options = { + url: this.URLs.getNoteStoreUrl, + }; + return this._get(options); + } + + // ************************** Notebooks ********************************** + + async listNotebooks() { + const options = { + url: this.URLs.listNotebooks, + }; + return this._get(options, true); + } + + async getDefaultNotebook() { + const options = { + url: this.URLs.getDefaultNotebook, + }; + return this._get(options, true); + } + + async createNotebook(notebookData) { + const options = { + url: this.URLs.createNotebook, + body: notebookData, + }; + return this._post(options, true, true); + } + + async updateNotebook(notebookData) { + const options = { + url: this.URLs.updateNotebook, + body: notebookData, + }; + return this._post(options, true, true); + } + + async expungeNotebook(notebookGuid) { + const options = { + url: this.URLs.expungeNotebook, + body: { guid: notebookGuid }, + }; + return this._post(options, true, true); + } + + // ************************** Notes ********************************** + + async createNote(noteData) { + // Ensure content is wrapped in ENML + if (noteData.content && !noteData.content.includes('')) { + noteData.content = this.enmlHeader + noteData.content + this.enmlFooter; + } + + const options = { + url: this.URLs.createNote, + body: noteData, + }; + return this._post(options, true, true); + } + + async updateNote(noteData) { + // Ensure content is wrapped in ENML + if (noteData.content && !noteData.content.includes('')) { + noteData.content = this.enmlHeader + noteData.content + this.enmlFooter; + } + + const options = { + url: this.URLs.updateNote, + body: noteData, + }; + return this._post(options, true, true); + } + + async getNote(noteGuid, withContent = true, withResourcesData = false, withResourcesRecognition = false, withResourcesAlternateData = false) { + const options = { + url: this.URLs.getNote, + body: { + guid: noteGuid, + withContent, + withResourcesData, + withResourcesRecognition, + withResourcesAlternateData + }, + }; + return this._post(options, true, true); + } + + async getNoteContent(noteGuid) { + const options = { + url: this.URLs.getNoteContent, + body: { guid: noteGuid }, + }; + return this._post(options, true, true); + } + + async deleteNote(noteGuid) { + const options = { + url: this.URLs.deleteNote, + body: { guid: noteGuid }, + }; + return this._post(options, true, true); + } + + async expungeNote(noteGuid) { + const options = { + url: this.URLs.expungeNote, + body: { guid: noteGuid }, + }; + return this._post(options, true, true); + } + + async getNoteTagNames(noteGuid) { + const options = { + url: this.URLs.getNoteTagNames, + body: { guid: noteGuid }, + }; + return this._post(options, true, true); + } + + // ************************** Search ********************************** + + async findNotes(filter, offset = 0, maxNotes = 100) { + const options = { + url: this.URLs.findNotes, + body: { + filter: { + query: filter, + ascending: false, + order: 1, // CREATED + }, + offset, + maxNotes + }, + }; + return this._post(options, true, true); + } + + async findNotesMetadata(filter, offset = 0, maxNotes = 100, resultSpec = {}) { + const options = { + url: this.URLs.findNotesMetadata, + body: { + filter: { + query: filter, + ascending: false, + order: 1, // CREATED + }, + offset, + maxNotes, + resultSpec: { + includeTitle: true, + includeContentLength: true, + includeCreated: true, + includeUpdated: true, + includeDeleted: false, + includeUpdateSequenceNum: true, + includeNotebookGuid: true, + includeTagGuids: true, + includeAttributes: true, + includeLargestResourceMime: true, + includeLargestResourceSize: true, + ...resultSpec + } + }, + }; + return this._post(options, true, true); + } + + async searchNotes(query, notebookGuid = null, tagGuids = [], offset = 0, maxNotes = 100) { + let filter = query; + + if (notebookGuid) { + filter += ` notebook:"${notebookGuid}"`; + } + + if (tagGuids && tagGuids.length > 0) { + filter += ` tag:"${tagGuids.join('" tag:"')}"`; + } + + return this.findNotesMetadata(filter, offset, maxNotes); + } + + // ************************** Tags ********************************** + + async listTags() { + const options = { + url: this.URLs.listTags, + }; + return this._get(options, true); + } + + async listTagsByNotebook(notebookGuid) { + const options = { + url: this.URLs.listTagsByNotebook, + body: { notebookGuid }, + }; + return this._post(options, true, true); + } + + async getTag(tagGuid) { + const options = { + url: this.URLs.getTag, + body: { guid: tagGuid }, + }; + return this._post(options, true, true); + } + + async createTag(tagData) { + const options = { + url: this.URLs.createTag, + body: tagData, + }; + return this._post(options, true, true); + } + + async updateTag(tagData) { + const options = { + url: this.URLs.updateTag, + body: tagData, + }; + return this._post(options, true, true); + } + + async expungeTag(tagGuid) { + const options = { + url: this.URLs.expungeTag, + body: { guid: tagGuid }, + }; + return this._post(options, true, true); + } + + // ************************** Resources (Attachments) ********************************** + + async createResource(resourceData) { + const options = { + url: this.URLs.createResource, + body: resourceData, + }; + return this._post(options, true, true); + } + + async getResource(resourceGuid, withData = true, withRecognition = false, withAttributes = true, withAlternateData = false) { + const options = { + url: this.URLs.getResource, + body: { + guid: resourceGuid, + withData, + withRecognition, + withAttributes, + withAlternateData + }, + }; + return this._post(options, true, true); + } + + async getResourceData(resourceGuid) { + const options = { + url: this.URLs.getResourceData, + body: { guid: resourceGuid }, + }; + return this._post(options, true, true); + } + + async updateResource(resourceData) { + const options = { + url: this.URLs.updateResource, + body: resourceData, + }; + return this._post(options, true, true); + } + + // ************************** Saved Searches ********************************** + + async listSearches() { + const options = { + url: this.URLs.listSearches, + }; + return this._get(options, true); + } + + async getSearch(searchGuid) { + const options = { + url: this.URLs.getSearch, + body: { guid: searchGuid }, + }; + return this._post(options, true, true); + } + + async createSearch(searchData) { + const options = { + url: this.URLs.createSearch, + body: searchData, + }; + return this._post(options, true, true); + } + + async updateSearch(searchData) { + const options = { + url: this.URLs.updateSearch, + body: searchData, + }; + return this._post(options, true, true); + } + + async expungeSearch(searchGuid) { + const options = { + url: this.URLs.expungeSearch, + body: { guid: searchGuid }, + }; + return this._post(options, true, true); + } + + // ************************** Sharing ********************************** + + async shareNotebook(notebookGuid, message = '') { + const options = { + url: this.URLs.shareNotebook, + body: { + guid: notebookGuid, + message + }, + }; + return this._post(options, true, true); + } + + async shareNote(noteGuid) { + const options = { + url: this.URLs.shareNote, + body: { guid: noteGuid }, + }; + return this._post(options, true, true); + } + + async stopSharingNote(noteGuid) { + const options = { + url: this.URLs.stopSharingNote, + body: { guid: noteGuid }, + }; + return this._post(options, true, true); + } + + async listSharedNotebooks() { + const options = { + url: this.URLs.listSharedNotebooks, + }; + return this._get(options, true); + } + + async createSharedNotebook(sharedNotebookData) { + const options = { + url: this.URLs.createSharedNotebook, + body: sharedNotebookData, + }; + return this._post(options, true, true); + } + + async emailNote(noteGuid, message, recipients, ccAddresses = []) { + const options = { + url: this.URLs.emailNote, + body: { + guid: noteGuid, + message, + recipients, + ccAddresses + }, + }; + return this._post(options, true, true); + } + + // ************************** Sync ********************************** + + async getSyncState() { + const options = { + url: this.URLs.getSyncState, + }; + return this._get(options, true); + } + + async getSyncChunk(afterUSN, maxEntries = 100, fullSyncOnly = false) { + const options = { + url: this.URLs.getSyncChunk, + body: { + afterUSN, + maxEntries, + fullSyncOnly + }, + }; + return this._post(options, true, true); + } + + // ************************** Helper Methods ********************************** + + // Convert plain text to ENML + textToENML(text) { + const escaped = text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + return this.enmlHeader + escaped + this.enmlFooter; + } + + // Convert HTML to ENML (basic conversion) + htmlToENML(html) { + // Basic HTML to ENML conversion + // In production, you'd want a more robust HTML parser + const enmlContent = html + .replace(/<(?!\/?(br|p|div|span|b|i|u|s|strike|strong|em|font|a|img|ul|ol|li|table|tr|td|th|tbody|thead|tfoot|h1|h2|h3|h4|h5|h6|blockquote|cite|abbr|acronym|del|ins|sub|sup|tt|code|kbd|samp|var)(\s|\/|>))/gi, '<') + .replace(/style\s*=\s*["'][^"']*["']/gi, '') // Remove style attributes + .replace(/class\s*=\s*["'][^"']*["']/gi, '') // Remove class attributes + .replace(/id\s*=\s*["'][^"']*["']/gi, ''); // Remove id attributes + + return this.enmlHeader + enmlContent + this.enmlFooter; + } + + // Extract plain text from ENML + enmlToText(enml) { + return enml + .replace(/<[^>]*>/g, '') // Remove all tags + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/ /g, ' ') + .trim(); + } + + // Create a simple text note + async createTextNote(title, content, notebookGuid = null, tagNames = []) { + const noteData = { + title, + content: this.textToENML(content), + tagNames + }; + + if (notebookGuid) { + noteData.notebookGuid = notebookGuid; + } + + return this.createNote(noteData); + } + + // Create an HTML note + async createHtmlNote(title, htmlContent, notebookGuid = null, tagNames = []) { + const noteData = { + title, + content: this.htmlToENML(htmlContent), + tagNames + }; + + if (notebookGuid) { + noteData.notebookGuid = notebookGuid; + } + + return this.createNote(noteData); + } + + // Get all notes in a notebook + async getNotesInNotebook(notebookGuid, maxNotes = 100) { + return this.searchNotes('', notebookGuid, [], 0, maxNotes); + } + + // Get notes by tag + async getNotesByTag(tagName, maxNotes = 100) { + return this.searchNotes(`tag:"${tagName}"`, null, [], 0, maxNotes); + } + + // Get recent notes + async getRecentNotes(days = 7, maxNotes = 50) { + const date = new Date(); + date.setDate(date.getDate() - days); + const dateStr = date.toISOString().split('T')[0]; + + return this.searchNotes(`created:${dateStr}`, null, [], 0, maxNotes); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/evernote/defaultConfig.json b/packages/evernote/defaultConfig.json new file mode 100644 index 0000000..082a90d --- /dev/null +++ b/packages/evernote/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "evernote", + "label": "Evernote", + "productUrl": "https://evernote.com", + "apiDocs": "https://dev.evernote.com/", + "logoUrl": "https://friggframework.org/assets/img/evernote-icon.png", + "categories": [ + "Productivity", + "Note Taking", + "Document Management" + ], + "description": "Evernote is a note-taking and organization application that helps users capture, organize, and share notes across devices and platforms." +} \ No newline at end of file diff --git a/packages/evernote/definition.js b/packages/evernote/definition.js new file mode 100644 index 0000000..ca5b241 --- /dev/null +++ b/packages/evernote/definition.js @@ -0,0 +1,82 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Evernote', + requiredAuthMethods: { + getToken: async function (api, params) { + // Evernote uses OAuth 1.0a, so the flow is different + const oauth_token = get(params.data, 'oauth_token'); + const oauth_verifier = get(params.data, 'oauth_verifier'); + + if (!oauth_token || !oauth_verifier) { + throw new Error('Missing required Evernote OAuth parameters: oauth_token and oauth_verifier'); + } + + // In a real implementation, you'd need to store the request token secret from the initial request + const requestTokenSecret = get(params.data, 'oauth_token_secret'); + if (!requestTokenSecret) { + throw new Error('Missing oauth_token_secret from initial OAuth request'); + } + + return api.getAccessToken(oauth_token, requestTokenSecret, oauth_verifier); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const user = await api.getUser(); + const noteStoreUrl = await api.getNoteStoreUrl(); + + return { + identifiers: {externalId: user.id.toString(), user: userId}, + details: { + name: user.name, + username: user.username, + email: user.email || '', + timezone: user.timezone, + privilege: user.privilege, + serviceLevel: user.serviceLevel, + created: user.created, + updated: user.updated, + noteStoreUrl: noteStoreUrl, + webApiUrlPrefix: user.webApiUrlPrefix || tokenResponse.edam_webApiUrlPrefix + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'oauth_token_secret', 'noteStoreUrl', 'webApiUrlPrefix' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const user = await api.getUser(); + + return { + identifiers: {externalId: user.id.toString(), user: userId}, + details: { + name: user.name, + username: user.username, + email: user.email || '', + serviceLevel: user.serviceLevel + } + }; + }, + testAuthRequest: async function (api) { + return api.getUser() + }, + }, + env: { + client_id: process.env.EVERNOTE_CLIENT_ID, + client_secret: process.env.EVERNOTE_CLIENT_SECRET, + sandbox: process.env.EVERNOTE_SANDBOX === 'true', + redirect_uri: `${process.env.REDIRECT_URI}/evernote`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/evernote/index.js b/packages/evernote/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/evernote/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/evernote/jest.config.js b/packages/evernote/jest.config.js new file mode 100644 index 0000000..fa8c051 --- /dev/null +++ b/packages/evernote/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEnv: ['/tests/setup.js'] +}; \ No newline at end of file diff --git a/packages/evernote/package.json b/packages/evernote/package.json new file mode 100644 index 0000000..b4f6185 --- /dev/null +++ b/packages/evernote/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-evernote", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Evernote API module that lets the Frigg Framework interact with Evernote", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/evernote/tests/api.test.js b/packages/evernote/tests/api.test.js new file mode 100644 index 0000000..2653c0b --- /dev/null +++ b/packages/evernote/tests/api.test.js @@ -0,0 +1,144 @@ +const { Api } = require('../api'); + +describe('Evernote API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test_client_id', + client_secret: 'test_client_secret', + access_token: 'test_access_token', + noteStoreUrl: 'https://sandbox.evernote.com/shard/s1/notestore', + sandbox: true + }); + }); + + describe('Constructor', () => { + test('should initialize with sandbox environment', () => { + expect(api.isSandbox).toBe(true); + expect(api.baseUrl).toBe('https://sandbox.evernote.com'); + expect(api.authorizationUri).toBe('https://sandbox.evernote.com/OAuth.action'); + }); + + test('should initialize with production environment', () => { + const prodApi = new Api({ + client_id: 'test_client_id', + client_secret: 'test_client_secret', + sandbox: false + }); + + expect(prodApi.isSandbox).toBe(false); + expect(prodApi.baseUrl).toBe('https://www.evernote.com'); + expect(prodApi.authorizationUri).toBe('https://www.evernote.com/OAuth.action'); + }); + + test('should set API URLs correctly', () => { + expect(api.apiUrl).toBe('https://sandbox.evernote.com/shard/s1/notestore'); + expect(api.userStoreUrl).toBe('https://sandbox.evernote.com/edam/user'); + }); + }); + + describe('Authentication', () => { + test('should generate correct auth URI', () => { + const authUri = api.getAuthUri('test_request_token'); + + expect(authUri).toContain('https://sandbox.evernote.com/OAuth.action'); + expect(authUri).toContain('oauth_token=test_request_token'); + }); + + test('should add correct auth headers', () => { + const options = { headers: {} }; + api.addAuthHeaders(options); + + expect(options.headers.Authorization).toBe('Bearer test_access_token'); + expect(options.headers['Content-Type']).toBe('application/json'); + }); + }); + + describe('URL Construction', () => { + test('should construct user store URLs correctly', () => { + expect(api.URLs.getUser).toBe('/edam/user/getUser'); + expect(api.URLs.getNoteStoreUrl).toBe('/edam/user/getNoteStoreUrl'); + expect(api.URLs.getPremiumInfo).toBe('/edam/user/getPremiumInfo'); + }); + + test('should construct note store URLs correctly', () => { + expect(api.URLs.listNotebooks).toBe('/listNotebooks'); + expect(api.URLs.createNote).toBe('/createNote'); + expect(api.URLs.findNotes).toBe('/findNotes'); + expect(api.URLs.listTags).toBe('/listTags'); + }); + }); + + describe('ENML Helpers', () => { + test('should wrap text in ENML correctly', () => { + const text = 'Hello World'; + const enml = api.textToENML(text); + + expect(enml).toContain(''); + expect(enml).toContain(''); + expect(enml).toContain('Hello World'); + }); + + test('should escape HTML entities in text', () => { + const text = 'Hello & World'; + const enml = api.textToENML(text); + + expect(enml).toContain('<script>'); + expect(enml).toContain('&'); + expect(enml).not.toContain(' + + + + + + \ No newline at end of file diff --git a/packages/miro/coverage/base.css b/packages/miro/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/packages/miro/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/packages/miro/coverage/block-navigation.js b/packages/miro/coverage/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/packages/miro/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/packages/miro/coverage/favicon.png b/packages/miro/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/packages/miro/coverage/favicon.png differ diff --git a/packages/miro/coverage/index.html b/packages/miro/coverage/index.html new file mode 100644 index 0000000..eb4f7be --- /dev/null +++ b/packages/miro/coverage/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 16.59% + Statements + 41/247 +
+ + +
+ 15.15% + Branches + 5/33 +
+ + +
+ 22.04% + Functions + 28/127 +
+ + +
+ 16.59% + Lines + 41/247 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
api.js +
+
16.59%41/24715.15%5/3322.04%28/12716.59%41/247
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/miro/coverage/lcov-report/api.js.html b/packages/miro/coverage/lcov-report/api.js.html new file mode 100644 index 0000000..dfbaab6 --- /dev/null +++ b/packages/miro/coverage/lcov-report/api.js.html @@ -0,0 +1,2659 @@ + + + + + + Code coverage report for api.js + + + + + + + + + +
+
+

All files api.js

+
+ +
+ 16.59% + Statements + 41/247 +
+ + +
+ 15.15% + Branches + 5/33 +
+ + +
+ 22.04% + Functions + 28/127 +
+ + +
+ 16.59% + Lines + 41/247 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +8591x +  +  +  +  +  +  +  +22x +  +22x +22x +22x +22x +22x +  +  +22x +22x +  +22x +  +  +1x +  +  +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +1x +1x +1x +1x +  +  +1x +1x +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +22x +  +  +  +  +2x +2x +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x
const { OAuth2Requester, get } = require('@friggframework/core');
+ 
+// Miro REST API v2 client
+// Supports OAuth2 authentication
+// Documentation: https://developers.miro.com/reference/api-reference
+ 
+class Api extends OAuth2Requester {
+    constructor(params) {
+        super(params);
+        
+        this.baseUrl = 'https://api.miro.com/v2';
+        this.client_id = get(params, 'client_id', process.env.MIRO_CLIENT_ID);
+        this.client_secret = get(params, 'client_secret', process.env.MIRO_CLIENT_SECRET);
+        this.access_token = get(params, 'access_token', null);
+        this.refresh_token = get(params, 'refresh_token', null);
+        
+        // OAuth endpoints
+        this.authorizationUri = 'https://miro.com/oauth/authorize';
+        this.tokenUri = 'https://api.miro.com/v1/oauth/token';
+ 
+        this.URLs = {
+            // Boards
+            boards: '/boards',
+            boardById: (boardId) => `/boards/${boardId}`,
+            
+            // Board Items
+            boardItems: (boardId) => `/boards/${boardId}/items`,
+            boardItemById: (boardId, itemId) => `/boards/${boardId}/items/${itemId}`,
+            
+            // Specific Item Types
+            stickyNotes: (boardId) => `/boards/${boardId}/sticky_notes`,
+            stickyNoteById: (boardId, itemId) => `/boards/${boardId}/sticky_notes/${itemId}`,
+            shapes: (boardId) => `/boards/${boardId}/shapes`,
+            shapeById: (boardId, itemId) => `/boards/${boardId}/shapes/${itemId}`,
+            texts: (boardId) => `/boards/${boardId}/texts`,
+            textById: (boardId, itemId) => `/boards/${boardId}/texts/${itemId}`,
+            images: (boardId) => `/boards/${boardId}/images`,
+            imageById: (boardId, itemId) => `/boards/${boardId}/images/${itemId}`,
+            documents: (boardId) => `/boards/${boardId}/documents`,
+            documentById: (boardId, itemId) => `/boards/${boardId}/documents/${itemId}`,
+            embeds: (boardId) => `/boards/${boardId}/embeds`,
+            embedById: (boardId, itemId) => `/boards/${boardId}/embeds/${itemId}`,
+            frames: (boardId) => `/boards/${boardId}/frames`,
+            frameById: (boardId, itemId) => `/boards/${boardId}/frames/${itemId}`,
+            connectors: (boardId) => `/boards/${boardId}/connectors`,
+            connectorById: (boardId, itemId) => `/boards/${boardId}/connectors/${itemId}`,
+            
+            // Tags
+            tags: (boardId) => `/boards/${boardId}/tags`,
+            tagById: (boardId, tagId) => `/boards/${boardId}/tags/${tagId}`,
+            
+            // Teams
+            teams: '/teams',
+            teamById: (teamId) => `/teams/${teamId}`,
+            teamMembers: (teamId) => `/teams/${teamId}/members`,
+            teamMemberById: (teamId, memberId) => `/teams/${teamId}/members/${memberId}`,
+            
+            // Organizations
+            organizations: '/organizations',
+            organizationById: (orgId) => `/organizations/${orgId}`,
+            organizationMembers: (orgId) => `/organizations/${orgId}/members`,
+            organizationMemberById: (orgId, memberId) => `/organizations/${orgId}/members/${memberId}`,
+            organizationTeams: (orgId) => `/organizations/${orgId}/teams`,
+            
+            // Enterprise (Admin APIs)
+            enterprise: '/enterprise',
+            enterpriseUsers: '/enterprise/users',
+            enterpriseUserById: (userId) => `/enterprise/users/${userId}`,
+            enterpriseAuditLogs: '/enterprise/audit-logs',
+            
+            // App Cards and Data
+            appCards: (boardId) => `/boards/${boardId}/app_cards`,
+            appCardById: (boardId, itemId) => `/boards/${boardId}/app_cards/${itemId}`,
+            
+            // Comments
+            boardComments: (boardId) => `/boards/${boardId}/comments`,
+            itemComments: (boardId, itemId) => `/boards/${boardId}/items/${itemId}/comments`,
+            commentById: (boardId, commentId) => `/boards/${boardId}/comments/${commentId}`,
+            
+            // Webhooks
+            webhooks: '/webhooks',
+            webhookById: (webhookId) => `/webhooks/${webhookId}`,
+            
+            // Templates
+            templates: '/templates',
+            templateById: (templateId) => `/templates/${templateId}`,
+            
+            // User info
+            userInfo: '/users/me',
+        };
+ 
+        // Default scopes
+        this.scope = get(params, 'scope', 'boards:read boards:write');
+    }
+ 
+    // Generate OAuth authorization URL
+    getAuthUri(scopes = null) {
+        const requestedScopes = scopes || this.scope;
+        const params = new URLSearchParams({
+            response_type: 'code',
+            client_id: this.client_id,
+            redirect_uri: this.redirect_uri,
+            scope: requestedScopes,
+            state: this.state || 'random_state_string',
+        });
+        
+        return `${this.authorizationUri}?${params.toString()}`;
+    }
+ 
+    // Exchange authorization code for access token
+    async getTokenFromCode(code) {
+        const tokenData = {
+            grant_type: 'authorization_code',
+            client_id: this.client_id,
+            client_secret: this.client_secret,
+            code: code,
+            redirect_uri: this.redirect_uri,
+        };
+ 
+        const options = {
+            url: this.tokenUri,
+            body: tokenData,
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded',
+                'Accept': 'application/json',
+            },
+        };
+ 
+        const response = await this._post(options, false);
+        await this.setTokens(response);
+        return response;
+    }
+ 
+    // Refresh access token
+    async refreshAccessToken() {
+        if (!this.refresh_token) {
+            throw new Error('No refresh token available');
+        }
+ 
+        const tokenData = {
+            grant_type: 'refresh_token',
+            client_id: this.client_id,
+            client_secret: this.client_secret,
+            refresh_token: this.refresh_token,
+        };
+ 
+        const options = {
+            url: this.tokenUri,
+            body: tokenData,
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded',
+                'Accept': 'application/json',
+            },
+        };
+ 
+        const response = await this._post(options, false);
+        await this.setTokens(response);
+        return response;
+    }
+ 
+    // Set access and refresh tokens
+    async setTokens(tokenResponse) {
+        this.access_token = tokenResponse.access_token;
+        if (tokenResponse.refresh_token) {
+            this.refresh_token = tokenResponse.refresh_token;
+        }
+        
+        if (tokenResponse.expires_in) {
+            this.accessTokenExpire = new Date(Date.now() + tokenResponse.expires_in * 1000);
+        }
+ 
+        await this.notify(this.DLGT_TOKEN_UPDATE);
+    }
+ 
+    // Add authentication headers
+    addAuthHeaders(options) {
+        options.headers = {
+            ...options.headers,
+            'Authorization': `Bearer ${this.access_token}`,
+            'Content-Type': 'application/json',
+            'Accept': 'application/json',
+        };
+    }
+ 
+    async _get(options) {
+        options.url = this.baseUrl + options.url;
+        this.addAuthHeaders(options);
+        return super._get(options);
+    }
+ 
+    async _post(options, stringify = true) {
+        options.url = this.baseUrl + options.url;
+        this.addAuthHeaders(options);
+        return super._post(options, stringify);
+    }
+ 
+    async _put(options, stringify = true) {
+        options.url = this.baseUrl + options.url;
+        this.addAuthHeaders(options);
+        return super._put(options, stringify);
+    }
+ 
+    async _patch(options, stringify = true) {
+        options.url = this.baseUrl + options.url;
+        this.addAuthHeaders(options);
+        return super._patch(options, stringify);
+    }
+ 
+    async _delete(options) {
+        options.url = this.baseUrl + options.url;
+        this.addAuthHeaders(options);
+        return super._delete(options);
+    }
+ 
+    // **************************   User Info   **********************************
+ 
+    async getUserInfo() {
+        const options = {
+            url: this.URLs.userInfo,
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Boards   **********************************
+ 
+    async createBoard(boardData) {
+        const options = {
+            url: this.URLs.boards,
+            body: boardData,
+        };
+        return this._post(options);
+    }
+ 
+    async getBoards(params = {}) {
+        const options = {
+            url: this.URLs.boards,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getBoardById(boardId) {
+        const options = {
+            url: this.URLs.boardById(boardId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateBoard(boardId, boardData) {
+        const options = {
+            url: this.URLs.boardById(boardId),
+            body: boardData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteBoard(boardId) {
+        const options = {
+            url: this.URLs.boardById(boardId),
+        };
+        return this._delete(options);
+    }
+ 
+    async copyBoard(boardId, copyData) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/copy`,
+            body: copyData,
+        };
+        return this._post(options);
+    }
+ 
+    async shareBoardWithTeam(boardId, teamShareData) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/share`,
+            body: teamShareData,
+        };
+        return this._post(options);
+    }
+ 
+    // **************************   Board Items (Generic)   **********************************
+ 
+    async getBoardItems(boardId, params = {}) {
+        const options = {
+            url: this.URLs.boardItems(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getBoardItemById(boardId, itemId) {
+        const options = {
+            url: this.URLs.boardItemById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateBoardItem(boardId, itemId, itemData) {
+        const options = {
+            url: this.URLs.boardItemById(boardId, itemId),
+            body: itemData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteBoardItem(boardId, itemId) {
+        const options = {
+            url: this.URLs.boardItemById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Sticky Notes   **********************************
+ 
+    async createStickyNote(boardId, stickyNoteData) {
+        const options = {
+            url: this.URLs.stickyNotes(boardId),
+            body: stickyNoteData,
+        };
+        return this._post(options);
+    }
+ 
+    async getStickyNotes(boardId, params = {}) {
+        const options = {
+            url: this.URLs.stickyNotes(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getStickyNoteById(boardId, itemId) {
+        const options = {
+            url: this.URLs.stickyNoteById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateStickyNote(boardId, itemId, stickyNoteData) {
+        const options = {
+            url: this.URLs.stickyNoteById(boardId, itemId),
+            body: stickyNoteData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteStickyNote(boardId, itemId) {
+        const options = {
+            url: this.URLs.stickyNoteById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Shapes   **********************************
+ 
+    async createShape(boardId, shapeData) {
+        const options = {
+            url: this.URLs.shapes(boardId),
+            body: shapeData,
+        };
+        return this._post(options);
+    }
+ 
+    async getShapes(boardId, params = {}) {
+        const options = {
+            url: this.URLs.shapes(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getShapeById(boardId, itemId) {
+        const options = {
+            url: this.URLs.shapeById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateShape(boardId, itemId, shapeData) {
+        const options = {
+            url: this.URLs.shapeById(boardId, itemId),
+            body: shapeData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteShape(boardId, itemId) {
+        const options = {
+            url: this.URLs.shapeById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Text Items   **********************************
+ 
+    async createText(boardId, textData) {
+        const options = {
+            url: this.URLs.texts(boardId),
+            body: textData,
+        };
+        return this._post(options);
+    }
+ 
+    async getTexts(boardId, params = {}) {
+        const options = {
+            url: this.URLs.texts(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getTextById(boardId, itemId) {
+        const options = {
+            url: this.URLs.textById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateText(boardId, itemId, textData) {
+        const options = {
+            url: this.URLs.textById(boardId, itemId),
+            body: textData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteText(boardId, itemId) {
+        const options = {
+            url: this.URLs.textById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Images   **********************************
+ 
+    async createImage(boardId, imageData) {
+        const options = {
+            url: this.URLs.images(boardId),
+            body: imageData,
+        };
+        return this._post(options);
+    }
+ 
+    async getImages(boardId, params = {}) {
+        const options = {
+            url: this.URLs.images(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getImageById(boardId, itemId) {
+        const options = {
+            url: this.URLs.imageById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateImage(boardId, itemId, imageData) {
+        const options = {
+            url: this.URLs.imageById(boardId, itemId),
+            body: imageData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteImage(boardId, itemId) {
+        const options = {
+            url: this.URLs.imageById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Frames   **********************************
+ 
+    async createFrame(boardId, frameData) {
+        const options = {
+            url: this.URLs.frames(boardId),
+            body: frameData,
+        };
+        return this._post(options);
+    }
+ 
+    async getFrames(boardId, params = {}) {
+        const options = {
+            url: this.URLs.frames(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getFrameById(boardId, itemId) {
+        const options = {
+            url: this.URLs.frameById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateFrame(boardId, itemId, frameData) {
+        const options = {
+            url: this.URLs.frameById(boardId, itemId),
+            body: frameData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteFrame(boardId, itemId) {
+        const options = {
+            url: this.URLs.frameById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Connectors   **********************************
+ 
+    async createConnector(boardId, connectorData) {
+        const options = {
+            url: this.URLs.connectors(boardId),
+            body: connectorData,
+        };
+        return this._post(options);
+    }
+ 
+    async getConnectors(boardId, params = {}) {
+        const options = {
+            url: this.URLs.connectors(boardId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getConnectorById(boardId, itemId) {
+        const options = {
+            url: this.URLs.connectorById(boardId, itemId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateConnector(boardId, itemId, connectorData) {
+        const options = {
+            url: this.URLs.connectorById(boardId, itemId),
+            body: connectorData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteConnector(boardId, itemId) {
+        const options = {
+            url: this.URLs.connectorById(boardId, itemId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Tags   **********************************
+ 
+    async createTag(boardId, tagData) {
+        const options = {
+            url: this.URLs.tags(boardId),
+            body: tagData,
+        };
+        return this._post(options);
+    }
+ 
+    async getTags(boardId) {
+        const options = {
+            url: this.URLs.tags(boardId),
+        };
+        return this._get(options);
+    }
+ 
+    async getTagById(boardId, tagId) {
+        const options = {
+            url: this.URLs.tagById(boardId, tagId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateTag(boardId, tagId, tagData) {
+        const options = {
+            url: this.URLs.tagById(boardId, tagId),
+            body: tagData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteTag(boardId, tagId) {
+        const options = {
+            url: this.URLs.tagById(boardId, tagId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Teams   **********************************
+ 
+    async getTeams() {
+        const options = {
+            url: this.URLs.teams,
+        };
+        return this._get(options);
+    }
+ 
+    async getTeamById(teamId) {
+        const options = {
+            url: this.URLs.teamById(teamId),
+        };
+        return this._get(options);
+    }
+ 
+    async getTeamMembers(teamId) {
+        const options = {
+            url: this.URLs.teamMembers(teamId),
+        };
+        return this._get(options);
+    }
+ 
+    async getTeamMemberById(teamId, memberId) {
+        const options = {
+            url: this.URLs.teamMemberById(teamId, memberId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateTeamMember(teamId, memberId, memberData) {
+        const options = {
+            url: this.URLs.teamMemberById(teamId, memberId),
+            body: memberData,
+        };
+        return this._patch(options);
+    }
+ 
+    async removeTeamMember(teamId, memberId) {
+        const options = {
+            url: this.URLs.teamMemberById(teamId, memberId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Comments   **********************************
+ 
+    async createComment(boardId, commentData, itemId = null) {
+        const url = itemId ? this.URLs.itemComments(boardId, itemId) : this.URLs.boardComments(boardId);
+        const options = {
+            url: url,
+            body: commentData,
+        };
+        return this._post(options);
+    }
+ 
+    async getComments(boardId, itemId = null, params = {}) {
+        const url = itemId ? this.URLs.itemComments(boardId, itemId) : this.URLs.boardComments(boardId);
+        const options = {
+            url: url,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getCommentById(boardId, commentId) {
+        const options = {
+            url: this.URLs.commentById(boardId, commentId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateComment(boardId, commentId, commentData) {
+        const options = {
+            url: this.URLs.commentById(boardId, commentId),
+            body: commentData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteComment(boardId, commentId) {
+        const options = {
+            url: this.URLs.commentById(boardId, commentId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Webhooks   **********************************
+ 
+    async createWebhook(webhookData) {
+        const options = {
+            url: this.URLs.webhooks,
+            body: webhookData,
+        };
+        return this._post(options);
+    }
+ 
+    async getWebhooks() {
+        const options = {
+            url: this.URLs.webhooks,
+        };
+        return this._get(options);
+    }
+ 
+    async getWebhookById(webhookId) {
+        const options = {
+            url: this.URLs.webhookById(webhookId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateWebhook(webhookId, webhookData) {
+        const options = {
+            url: this.URLs.webhookById(webhookId),
+            body: webhookData,
+        };
+        return this._patch(options);
+    }
+ 
+    async deleteWebhook(webhookId) {
+        const options = {
+            url: this.URLs.webhookById(webhookId),
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Templates   **********************************
+ 
+    async getTemplates(params = {}) {
+        const options = {
+            url: this.URLs.templates,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getTemplateById(templateId) {
+        const options = {
+            url: this.URLs.templateById(templateId),
+        };
+        return this._get(options);
+    }
+ 
+    async createBoardFromTemplate(templateId, boardData) {
+        const options = {
+            url: `${this.URLs.templateById(templateId)}/create-board`,
+            body: boardData,
+        };
+        return this._post(options);
+    }
+ 
+    // **************************   Advanced Features   **********************************
+ 
+    // Bulk operations
+    async bulkCreateItems(boardId, itemsData) {
+        const options = {
+            url: `${this.URLs.boardItems(boardId)}/bulk`,
+            body: { data: itemsData },
+        };
+        return this._post(options);
+    }
+ 
+    async bulkUpdateItems(boardId, itemsData) {
+        const options = {
+            url: `${this.URLs.boardItems(boardId)}/bulk`,
+            body: { data: itemsData },
+        };
+        return this._patch(options);
+    }
+ 
+    async bulkDeleteItems(boardId, itemIds) {
+        const options = {
+            url: `${this.URLs.boardItems(boardId)}/bulk`,
+            body: { data: itemIds.map(id => ({ id })) },
+        };
+        return this._delete(options);
+    }
+ 
+    // Search within board
+    async searchBoardItems(boardId, query, params = {}) {
+        const searchParams = {
+            ...params,
+            query: query
+        };
+        
+        const options = {
+            url: this.URLs.boardItems(boardId),
+            query: searchParams
+        };
+        return this._get(options);
+    }
+ 
+    // Export board
+    async exportBoard(boardId, format = 'pdf', params = {}) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/export`,
+            body: {
+                format: format,
+                ...params
+            },
+        };
+        return this._post(options);
+    }
+ 
+    // Get board analytics/stats
+    async getBoardStats(boardId) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/stats`,
+        };
+        return this._get(options);
+    }
+ 
+    // Collaboration features
+    async getBoardCollaborators(boardId) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/members`,
+        };
+        return this._get(options);
+    }
+ 
+    async inviteCollaborator(boardId, invitationData) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/members`,
+            body: invitationData,
+        };
+        return this._post(options);
+    }
+ 
+    async updateCollaboratorPermissions(boardId, memberId, permissionsData) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/members/${memberId}`,
+            body: permissionsData,
+        };
+        return this._patch(options);
+    }
+ 
+    async removeCollaborator(boardId, memberId) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/members/${memberId}`,
+        };
+        return this._delete(options);
+    }
+ 
+    // Board versions/snapshots
+    async createBoardSnapshot(boardId, snapshotData) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/snapshots`,
+            body: snapshotData,
+        };
+        return this._post(options);
+    }
+ 
+    async getBoardSnapshots(boardId) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/snapshots`,
+        };
+        return this._get(options);
+    }
+ 
+    async restoreBoardSnapshot(boardId, snapshotId) {
+        const options = {
+            url: `${this.URLs.boardById(boardId)}/snapshots/${snapshotId}/restore`,
+            body: {},
+        };
+        return this._post(options);
+    }
+}
+ 
+module.exports = { Api };
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/miro/coverage/lcov-report/base.css b/packages/miro/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/packages/miro/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/packages/miro/coverage/lcov-report/block-navigation.js b/packages/miro/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/packages/miro/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/packages/miro/coverage/lcov-report/favicon.png b/packages/miro/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/packages/miro/coverage/lcov-report/favicon.png differ diff --git a/packages/miro/coverage/lcov-report/index.html b/packages/miro/coverage/lcov-report/index.html new file mode 100644 index 0000000..650a04b --- /dev/null +++ b/packages/miro/coverage/lcov-report/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 16.59% + Statements + 41/247 +
+ + +
+ 15.15% + Branches + 5/33 +
+ + +
+ 22.04% + Functions + 28/127 +
+ + +
+ 16.59% + Lines + 41/247 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
api.js +
+
16.59%41/24715.15%5/3322.04%28/12716.59%41/247
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/miro/coverage/lcov-report/prettify.css b/packages/miro/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/packages/miro/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/packages/miro/coverage/lcov-report/prettify.js b/packages/miro/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/packages/miro/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/packages/miro/coverage/lcov-report/sort-arrow-sprite.png b/packages/miro/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/packages/miro/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/packages/miro/coverage/lcov-report/sorter.js b/packages/miro/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/packages/miro/coverage/lcov-report/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/packages/miro/coverage/lcov.info b/packages/miro/coverage/lcov.info new file mode 100644 index 0000000..3c543d2 --- /dev/null +++ b/packages/miro/coverage/lcov.info @@ -0,0 +1,543 @@ +TN: +SF:api.js +FN:8,(anonymous_0) +FN:24,(anonymous_1) +FN:27,(anonymous_2) +FN:28,(anonymous_3) +FN:31,(anonymous_4) +FN:32,(anonymous_5) +FN:33,(anonymous_6) +FN:34,(anonymous_7) +FN:35,(anonymous_8) +FN:36,(anonymous_9) +FN:37,(anonymous_10) +FN:38,(anonymous_11) +FN:39,(anonymous_12) +FN:40,(anonymous_13) +FN:41,(anonymous_14) +FN:42,(anonymous_15) +FN:43,(anonymous_16) +FN:44,(anonymous_17) +FN:45,(anonymous_18) +FN:46,(anonymous_19) +FN:49,(anonymous_20) +FN:50,(anonymous_21) +FN:54,(anonymous_22) +FN:55,(anonymous_23) +FN:56,(anonymous_24) +FN:60,(anonymous_25) +FN:61,(anonymous_26) +FN:62,(anonymous_27) +FN:63,(anonymous_28) +FN:68,(anonymous_29) +FN:72,(anonymous_30) +FN:73,(anonymous_31) +FN:76,(anonymous_32) +FN:77,(anonymous_33) +FN:78,(anonymous_34) +FN:82,(anonymous_35) +FN:86,(anonymous_36) +FN:97,(anonymous_37) +FN:111,(anonymous_38) +FN:135,(anonymous_39) +FN:162,(anonymous_40) +FN:176,(anonymous_41) +FN:185,(anonymous_42) +FN:191,(anonymous_43) +FN:197,(anonymous_44) +FN:203,(anonymous_45) +FN:209,(anonymous_46) +FN:217,(anonymous_47) +FN:226,(anonymous_48) +FN:234,(anonymous_49) +FN:242,(anonymous_50) +FN:249,(anonymous_51) +FN:257,(anonymous_52) +FN:264,(anonymous_53) +FN:272,(anonymous_54) +FN:282,(anonymous_55) +FN:290,(anonymous_56) +FN:297,(anonymous_57) +FN:305,(anonymous_58) +FN:314,(anonymous_59) +FN:322,(anonymous_60) +FN:330,(anonymous_61) +FN:337,(anonymous_62) +FN:345,(anonymous_63) +FN:354,(anonymous_64) +FN:362,(anonymous_65) +FN:370,(anonymous_66) +FN:377,(anonymous_67) +FN:385,(anonymous_68) +FN:394,(anonymous_69) +FN:402,(anonymous_70) +FN:410,(anonymous_71) +FN:417,(anonymous_72) +FN:425,(anonymous_73) +FN:434,(anonymous_74) +FN:442,(anonymous_75) +FN:450,(anonymous_76) +FN:457,(anonymous_77) +FN:465,(anonymous_78) +FN:474,(anonymous_79) +FN:482,(anonymous_80) +FN:490,(anonymous_81) +FN:497,(anonymous_82) +FN:505,(anonymous_83) +FN:514,(anonymous_84) +FN:522,(anonymous_85) +FN:530,(anonymous_86) +FN:537,(anonymous_87) +FN:545,(anonymous_88) +FN:554,(anonymous_89) +FN:562,(anonymous_90) +FN:569,(anonymous_91) +FN:576,(anonymous_92) +FN:584,(anonymous_93) +FN:593,(anonymous_94) +FN:600,(anonymous_95) +FN:607,(anonymous_96) +FN:614,(anonymous_97) +FN:621,(anonymous_98) +FN:629,(anonymous_99) +FN:638,(anonymous_100) +FN:647,(anonymous_101) +FN:656,(anonymous_102) +FN:663,(anonymous_103) +FN:671,(anonymous_104) +FN:680,(anonymous_105) +FN:688,(anonymous_106) +FN:695,(anonymous_107) +FN:702,(anonymous_108) +FN:710,(anonymous_109) +FN:719,(anonymous_110) +FN:727,(anonymous_111) +FN:734,(anonymous_112) +FN:745,(anonymous_113) +FN:753,(anonymous_114) +FN:761,(anonymous_115) +FN:764,(anonymous_116) +FN:770,(anonymous_117) +FN:784,(anonymous_118) +FN:796,(anonymous_119) +FN:804,(anonymous_120) +FN:811,(anonymous_121) +FN:819,(anonymous_122) +FN:827,(anonymous_123) +FN:835,(anonymous_124) +FN:843,(anonymous_125) +FN:850,(anonymous_126) +FNF:127 +FNH:28 +FNDA:22,(anonymous_0) +FNDA:1,(anonymous_1) +FNDA:1,(anonymous_2) +FNDA:1,(anonymous_3) +FNDA:1,(anonymous_4) +FNDA:1,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:1,(anonymous_7) +FNDA:1,(anonymous_8) +FNDA:1,(anonymous_9) +FNDA:1,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:1,(anonymous_16) +FNDA:1,(anonymous_17) +FNDA:1,(anonymous_18) +FNDA:1,(anonymous_19) +FNDA:1,(anonymous_20) +FNDA:1,(anonymous_21) +FNDA:1,(anonymous_22) +FNDA:1,(anonymous_23) +FNDA:1,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:1,(anonymous_32) +FNDA:1,(anonymous_33) +FNDA:1,(anonymous_34) +FNDA:1,(anonymous_35) +FNDA:1,(anonymous_36) +FNDA:2,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:1,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +FNDA:0,(anonymous_44) +FNDA:0,(anonymous_45) +FNDA:0,(anonymous_46) +FNDA:0,(anonymous_47) +FNDA:0,(anonymous_48) +FNDA:0,(anonymous_49) +FNDA:0,(anonymous_50) +FNDA:0,(anonymous_51) +FNDA:0,(anonymous_52) +FNDA:0,(anonymous_53) +FNDA:0,(anonymous_54) +FNDA:0,(anonymous_55) +FNDA:0,(anonymous_56) +FNDA:0,(anonymous_57) +FNDA:0,(anonymous_58) +FNDA:0,(anonymous_59) +FNDA:0,(anonymous_60) +FNDA:0,(anonymous_61) +FNDA:0,(anonymous_62) +FNDA:0,(anonymous_63) +FNDA:0,(anonymous_64) +FNDA:0,(anonymous_65) +FNDA:0,(anonymous_66) +FNDA:0,(anonymous_67) +FNDA:0,(anonymous_68) +FNDA:0,(anonymous_69) +FNDA:0,(anonymous_70) +FNDA:0,(anonymous_71) +FNDA:0,(anonymous_72) +FNDA:0,(anonymous_73) +FNDA:0,(anonymous_74) +FNDA:0,(anonymous_75) +FNDA:0,(anonymous_76) +FNDA:0,(anonymous_77) +FNDA:0,(anonymous_78) +FNDA:0,(anonymous_79) +FNDA:0,(anonymous_80) +FNDA:0,(anonymous_81) +FNDA:0,(anonymous_82) +FNDA:0,(anonymous_83) +FNDA:0,(anonymous_84) +FNDA:0,(anonymous_85) +FNDA:0,(anonymous_86) +FNDA:0,(anonymous_87) +FNDA:0,(anonymous_88) +FNDA:0,(anonymous_89) +FNDA:0,(anonymous_90) +FNDA:0,(anonymous_91) +FNDA:0,(anonymous_92) +FNDA:0,(anonymous_93) +FNDA:0,(anonymous_94) +FNDA:0,(anonymous_95) +FNDA:0,(anonymous_96) +FNDA:0,(anonymous_97) +FNDA:0,(anonymous_98) +FNDA:0,(anonymous_99) +FNDA:0,(anonymous_100) +FNDA:0,(anonymous_101) +FNDA:0,(anonymous_102) +FNDA:0,(anonymous_103) +FNDA:0,(anonymous_104) +FNDA:0,(anonymous_105) +FNDA:0,(anonymous_106) +FNDA:0,(anonymous_107) +FNDA:0,(anonymous_108) +FNDA:0,(anonymous_109) +FNDA:0,(anonymous_110) +FNDA:0,(anonymous_111) +FNDA:0,(anonymous_112) +FNDA:0,(anonymous_113) +FNDA:0,(anonymous_114) +FNDA:0,(anonymous_115) +FNDA:0,(anonymous_116) +FNDA:0,(anonymous_117) +FNDA:0,(anonymous_118) +FNDA:0,(anonymous_119) +FNDA:0,(anonymous_120) +FNDA:0,(anonymous_121) +FNDA:0,(anonymous_122) +FNDA:0,(anonymous_123) +FNDA:0,(anonymous_124) +FNDA:0,(anonymous_125) +FNDA:0,(anonymous_126) +DA:1,1 +DA:9,22 +DA:11,22 +DA:12,22 +DA:13,22 +DA:14,22 +DA:15,22 +DA:18,22 +DA:19,22 +DA:21,22 +DA:24,1 +DA:27,1 +DA:28,1 +DA:31,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,1 +DA:44,1 +DA:45,1 +DA:46,1 +DA:49,1 +DA:50,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:68,0 +DA:72,0 +DA:73,0 +DA:76,1 +DA:77,1 +DA:78,1 +DA:82,1 +DA:86,1 +DA:93,22 +DA:98,2 +DA:99,2 +DA:107,2 +DA:112,0 +DA:120,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:136,0 +DA:137,0 +DA:140,0 +DA:147,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:168,0 +DA:169,0 +DA:172,0 +DA:177,1 +DA:186,0 +DA:187,0 +DA:188,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:218,0 +DA:221,0 +DA:227,0 +DA:231,0 +DA:235,0 +DA:239,0 +DA:243,0 +DA:246,0 +DA:250,0 +DA:254,0 +DA:258,0 +DA:261,0 +DA:265,0 +DA:269,0 +DA:273,0 +DA:277,0 +DA:283,0 +DA:287,0 +DA:291,0 +DA:294,0 +DA:298,0 +DA:302,0 +DA:306,0 +DA:309,0 +DA:315,0 +DA:319,0 +DA:323,0 +DA:327,0 +DA:331,0 +DA:334,0 +DA:338,0 +DA:342,0 +DA:346,0 +DA:349,0 +DA:355,0 +DA:359,0 +DA:363,0 +DA:367,0 +DA:371,0 +DA:374,0 +DA:378,0 +DA:382,0 +DA:386,0 +DA:389,0 +DA:395,0 +DA:399,0 +DA:403,0 +DA:407,0 +DA:411,0 +DA:414,0 +DA:418,0 +DA:422,0 +DA:426,0 +DA:429,0 +DA:435,0 +DA:439,0 +DA:443,0 +DA:447,0 +DA:451,0 +DA:454,0 +DA:458,0 +DA:462,0 +DA:466,0 +DA:469,0 +DA:475,0 +DA:479,0 +DA:483,0 +DA:487,0 +DA:491,0 +DA:494,0 +DA:498,0 +DA:502,0 +DA:506,0 +DA:509,0 +DA:515,0 +DA:519,0 +DA:523,0 +DA:527,0 +DA:531,0 +DA:534,0 +DA:538,0 +DA:542,0 +DA:546,0 +DA:549,0 +DA:555,0 +DA:559,0 +DA:563,0 +DA:566,0 +DA:570,0 +DA:573,0 +DA:577,0 +DA:581,0 +DA:585,0 +DA:588,0 +DA:594,0 +DA:597,0 +DA:601,0 +DA:604,0 +DA:608,0 +DA:611,0 +DA:615,0 +DA:618,0 +DA:622,0 +DA:626,0 +DA:630,0 +DA:633,0 +DA:639,0 +DA:640,0 +DA:644,0 +DA:648,0 +DA:649,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:664,0 +DA:668,0 +DA:672,0 +DA:675,0 +DA:681,0 +DA:685,0 +DA:689,0 +DA:692,0 +DA:696,0 +DA:699,0 +DA:703,0 +DA:707,0 +DA:711,0 +DA:714,0 +DA:720,0 +DA:724,0 +DA:728,0 +DA:731,0 +DA:735,0 +DA:739,0 +DA:746,0 +DA:750,0 +DA:754,0 +DA:758,0 +DA:762,0 +DA:764,0 +DA:766,0 +DA:771,0 +DA:776,0 +DA:780,0 +DA:785,0 +DA:792,0 +DA:797,0 +DA:800,0 +DA:805,0 +DA:808,0 +DA:812,0 +DA:816,0 +DA:820,0 +DA:824,0 +DA:828,0 +DA:831,0 +DA:836,0 +DA:840,0 +DA:844,0 +DA:847,0 +DA:851,0 +DA:855,0 +DA:859,1 +LF:247 +LH:41 +BRDA:97,0,0,1 +BRDA:98,1,0,2 +BRDA:98,1,1,1 +BRDA:104,2,0,2 +BRDA:104,2,1,2 +BRDA:136,3,0,0 +BRDA:136,3,1,0 +BRDA:164,4,0,0 +BRDA:164,4,1,0 +BRDA:168,5,0,0 +BRDA:168,5,1,0 +BRDA:191,6,0,0 +BRDA:197,7,0,0 +BRDA:203,8,0,0 +BRDA:234,9,0,0 +BRDA:282,10,0,0 +BRDA:322,11,0,0 +BRDA:362,12,0,0 +BRDA:402,13,0,0 +BRDA:442,14,0,0 +BRDA:482,15,0,0 +BRDA:522,16,0,0 +BRDA:638,17,0,0 +BRDA:639,18,0,0 +BRDA:639,18,1,0 +BRDA:647,19,0,0 +BRDA:647,20,0,0 +BRDA:648,21,0,0 +BRDA:648,21,1,0 +BRDA:719,22,0,0 +BRDA:770,23,0,0 +BRDA:784,24,0,0 +BRDA:784,25,0,0 +BRF:33 +BRH:5 +end_of_record diff --git a/packages/miro/coverage/prettify.css b/packages/miro/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/packages/miro/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/packages/miro/coverage/prettify.js b/packages/miro/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/packages/miro/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/packages/miro/coverage/sort-arrow-sprite.png b/packages/miro/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/packages/miro/coverage/sort-arrow-sprite.png differ diff --git a/packages/miro/coverage/sorter.js b/packages/miro/coverage/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/packages/miro/coverage/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/packages/miro/defaultConfig.json b/packages/miro/defaultConfig.json new file mode 100644 index 0000000..6ffd76b --- /dev/null +++ b/packages/miro/defaultConfig.json @@ -0,0 +1,13 @@ +{ + "name": "miro", + "label": "Miro", + "productUrl": "https://miro.com", + "apiDocs": "https://developers.miro.com/", + "logoUrl": "https://friggframework.org/assets/img/miro-icon.png", + "categories": [ + "Productivity", + "Collaboration", + "Visual Design" + ], + "description": "Miro is a collaborative online whiteboard platform that enables teams to work together visually through brainstorming, planning, and design thinking." +} \ No newline at end of file diff --git a/packages/miro/definition.js b/packages/miro/definition.js new file mode 100644 index 0000000..a034ca2 --- /dev/null +++ b/packages/miro/definition.js @@ -0,0 +1,75 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Miro', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + + if (!code) { + throw new Error('Missing authorization code for Miro OAuth'); + } + + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + + return { + identifiers: {externalId: userInfo.id, user: userId}, + details: { + name: userInfo.name, + email: userInfo.email, + picture: userInfo.picture || '', + industry: userInfo.industry || '', + company: userInfo.company || '', + companySize: userInfo.companySize || '', + timeZone: userInfo.timeZone || '', + locale: userInfo.locale || '', + type: userInfo.type, + state: userInfo.state, + createdAt: userInfo.createdAt, + modifiedAt: userInfo.modifiedAt + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token', 'token_type', 'expires_in' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + + return { + identifiers: {externalId: userInfo.id, user: userId}, + details: { + name: userInfo.name, + email: userInfo.email, + company: userInfo.company || '', + type: userInfo.type + } + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo() + }, + }, + env: { + client_id: process.env.MIRO_CLIENT_ID, + client_secret: process.env.MIRO_CLIENT_SECRET, + scope: process.env.MIRO_SCOPE || 'boards:read boards:write', + redirect_uri: `${process.env.REDIRECT_URI}/miro`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/miro/index.js b/packages/miro/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/miro/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/miro/jest.config.js b/packages/miro/jest.config.js new file mode 100644 index 0000000..fa8c051 --- /dev/null +++ b/packages/miro/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEnv: ['/tests/setup.js'] +}; \ No newline at end of file diff --git a/packages/miro/package.json b/packages/miro/package.json new file mode 100644 index 0000000..bcb87f3 --- /dev/null +++ b/packages/miro/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-miro", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Miro API module that lets the Frigg Framework interact with Miro", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/miro/tests/api.test.js b/packages/miro/tests/api.test.js new file mode 100644 index 0000000..2841f40 --- /dev/null +++ b/packages/miro/tests/api.test.js @@ -0,0 +1,146 @@ +const { Api } = require('../api'); + +describe('Miro API', () => { + let api; + + beforeEach(() => { + api = new Api({ + client_id: 'test_client_id', + client_secret: 'test_client_secret', + access_token: 'test_access_token', + redirect_uri: 'https://example.com/callback' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct credentials', () => { + expect(api.client_id).toBe('test_client_id'); + expect(api.client_secret).toBe('test_client_secret'); + expect(api.access_token).toBe('test_access_token'); + expect(api.baseUrl).toBe('https://api.miro.com/v2'); + }); + + test('should set OAuth endpoints correctly', () => { + expect(api.authorizationUri).toBe('https://miro.com/oauth/authorize'); + expect(api.tokenUri).toBe('https://api.miro.com/v1/oauth/token'); + }); + + test('should set default scope', () => { + expect(api.scope).toBe('boards:read boards:write'); + }); + }); + + describe('Authentication', () => { + test('should generate correct auth URI', () => { + const authUri = api.getAuthUri(); + + expect(authUri).toContain('https://miro.com/oauth/authorize'); + expect(authUri).toContain('client_id=test_client_id'); + expect(authUri).toContain('response_type=code'); + // URL encoding can use either %20 or + for spaces + expect(authUri).toMatch(/scope=boards%3Aread[\+%20]boards%3Awrite/); + }); + + test('should generate auth URI with custom scopes', () => { + const authUri = api.getAuthUri('boards:read teams:read'); + + // URL encoding can use either %20 or + for spaces + expect(authUri).toMatch(/scope=boards%3Aread[\+%20]teams%3Aread/); + }); + + test('should add correct auth headers', () => { + const options = { headers: {} }; + api.addAuthHeaders(options); + + expect(options.headers.Authorization).toBe('Bearer test_access_token'); + expect(options.headers['Content-Type']).toBe('application/json'); + expect(options.headers.Accept).toBe('application/json'); + }); + }); + + describe('URL Construction', () => { + test('should construct board URLs correctly', () => { + expect(api.URLs.boards).toBe('/boards'); + expect(api.URLs.boardById('123')).toBe('/boards/123'); + }); + + test('should construct board items URLs correctly', () => { + expect(api.URLs.boardItems('123')).toBe('/boards/123/items'); + expect(api.URLs.boardItemById('123', '456')).toBe('/boards/123/items/456'); + }); + + test('should construct sticky notes URLs correctly', () => { + expect(api.URLs.stickyNotes('123')).toBe('/boards/123/sticky_notes'); + expect(api.URLs.stickyNoteById('123', '456')).toBe('/boards/123/sticky_notes/456'); + }); + + test('should construct shapes URLs correctly', () => { + expect(api.URLs.shapes('123')).toBe('/boards/123/shapes'); + expect(api.URLs.shapeById('123', '456')).toBe('/boards/123/shapes/456'); + }); + + test('should construct texts URLs correctly', () => { + expect(api.URLs.texts('123')).toBe('/boards/123/texts'); + expect(api.URLs.textById('123', '456')).toBe('/boards/123/texts/456'); + }); + + test('should construct images URLs correctly', () => { + expect(api.URLs.images('123')).toBe('/boards/123/images'); + expect(api.URLs.imageById('123', '456')).toBe('/boards/123/images/456'); + }); + + test('should construct frames URLs correctly', () => { + expect(api.URLs.frames('123')).toBe('/boards/123/frames'); + expect(api.URLs.frameById('123', '456')).toBe('/boards/123/frames/456'); + }); + + test('should construct connectors URLs correctly', () => { + expect(api.URLs.connectors('123')).toBe('/boards/123/connectors'); + expect(api.URLs.connectorById('123', '456')).toBe('/boards/123/connectors/456'); + }); + + test('should construct tags URLs correctly', () => { + expect(api.URLs.tags('123')).toBe('/boards/123/tags'); + expect(api.URLs.tagById('123', '456')).toBe('/boards/123/tags/456'); + }); + + test('should construct teams URLs correctly', () => { + expect(api.URLs.teams).toBe('/teams'); + expect(api.URLs.teamById('123')).toBe('/teams/123'); + expect(api.URLs.teamMembers('123')).toBe('/teams/123/members'); + expect(api.URLs.teamMemberById('123', '456')).toBe('/teams/123/members/456'); + }); + + test('should construct comments URLs correctly', () => { + expect(api.URLs.boardComments('123')).toBe('/boards/123/comments'); + expect(api.URLs.itemComments('123', '456')).toBe('/boards/123/items/456/comments'); + expect(api.URLs.commentById('123', '456')).toBe('/boards/123/comments/456'); + }); + + test('should construct webhooks URLs correctly', () => { + expect(api.URLs.webhooks).toBe('/webhooks'); + expect(api.URLs.webhookById('123')).toBe('/webhooks/123'); + }); + + test('should construct templates URLs correctly', () => { + expect(api.URLs.templates).toBe('/templates'); + expect(api.URLs.templateById('123')).toBe('/templates/123'); + }); + + test('should construct user info URL correctly', () => { + expect(api.URLs.userInfo).toBe('/users/me'); + }); + }); + + describe('Scope Handling', () => { + test('should use custom scope if provided in constructor', () => { + const customApi = new Api({ + scope: 'boards:read teams:read', + client_id: 'test', + client_secret: 'test' + }); + + expect(customApi.scope).toBe('boards:read teams:read'); + }); + }); +}); \ No newline at end of file diff --git a/packages/miro/tests/setup.js b/packages/miro/tests/setup.js new file mode 100644 index 0000000..6238ad0 --- /dev/null +++ b/packages/miro/tests/setup.js @@ -0,0 +1,12 @@ +// Test setup file for Miro API module +require('dotenv').config(); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; \ No newline at end of file diff --git a/packages/mixpanel/README.md b/packages/mixpanel/README.md new file mode 100644 index 0000000..9588cad --- /dev/null +++ b/packages/mixpanel/README.md @@ -0,0 +1,327 @@ +# Mixpanel API Module + +This module provides a v1-ready integration with Mixpanel's product analytics platform using Service Account authentication. + +## Installation + +```bash +npm install @friggframework/api-module-mixpanel +``` + +## Features + +- Service Account authentication (recommended) +- Event tracking and batch operations +- User profile management +- Group analytics +- JQL (JSON Query Language) queries +- Funnel and retention analysis +- Data export capabilities +- Cohort management +- Annotations and insights + +## Authentication + +Mixpanel uses two types of authentication: +1. **Service Account** (recommended) - For server-side API calls +2. **Project Token** - For event tracking and client-side operations + +### Service Account Setup +1. Go to your Mixpanel Organization Settings +2. Create a new Service Account +3. Save the username and secret securely +4. Grant appropriate project permissions + +## Quick Start + +### Initialize the Integration + +```javascript +const { Definition } = require('@friggframework/api-module-mixpanel'); + +const mixpanel = new Definition({ + serviceAccountUsername: 'your-service-account-username', + serviceAccountSecret: 'your-service-account-secret', + projectToken: 'your-project-token' +}); +``` + +## API Methods + +### Event Tracking + +#### Track Single Event +```javascript +await mixpanel.track({ + event: 'Purchase Completed', + distinct_id: 'user-123', + properties: { + amount: 99.99, + currency: 'USD', + items: ['product-1', 'product-2'] + } +}); +``` + +#### Track Batch Events +```javascript +await mixpanel.trackBatch([ + { + event: 'Page View', + distinct_id: 'user-123', + properties: { page: '/home' } + }, + { + event: 'Button Click', + distinct_id: 'user-123', + properties: { button: 'signup' } + } +]); +``` + +### User Profile Management + +#### Update User Profile +```javascript +await mixpanel.updateProfile({ + distinct_id: 'user-123', + properties: { + $name: 'John Doe', + $email: 'john@example.com', + plan: 'premium', + signup_date: '2024-01-01' + } +}); +``` + +#### Batch Update Profiles +```javascript +await mixpanel.updateProfilesBatch([ + { + distinct_id: 'user-123', + properties: { plan: 'premium' } + }, + { + distinct_id: 'user-456', + properties: { plan: 'basic' } + } +]); +``` + +### Group Analytics + +#### Update Group Profile +```javascript +await mixpanel.updateGroup({ + group_key: 'company', + group_id: 'company-123', + properties: { + name: 'Acme Corp', + plan: 'enterprise', + employees: 500 + } +}); +``` + +### Analytics Queries + +#### JQL Query +```javascript +const results = await mixpanel.queryJQL({ + params: { + from_date: '2024-01-01', + to_date: '2024-01-31', + event_selectors: [{ + event: 'Purchase Completed' + }] + } +}); +``` + +#### Funnel Analysis +```javascript +const funnel = await mixpanel.getFunnel({ + project_id: 123456, + funnel_id: 789, + from_date: '2024-01-01', + to_date: '2024-01-31' +}); +``` + +#### Retention Analysis +```javascript +const retention = await mixpanel.getRetention({ + project_id: 123456, + from_date: '2024-01-01', + to_date: '2024-01-31', + event: 'Sign Up', + born_event: 'Sign Up' +}); +``` + +### Data Export + +#### Export Events +```javascript +const events = await mixpanel.exportEvents({ + from_date: '2024-01-01', + to_date: '2024-01-31', + event: ['Purchase Completed', 'Sign Up'] +}); +``` + +#### Export People +```javascript +const people = await mixpanel.exportPeople({ + project_id: 123456, + filter_by_cohort: { id: 456 } +}); +``` + +### Project Management + +#### List Projects +```javascript +const projects = await mixpanel.listProjects(); +``` + +#### Get Project Details +```javascript +const project = await mixpanel.getProject(123456); +``` + +### Cohorts + +#### Get Cohorts +```javascript +const cohorts = await mixpanel.getCohorts(123456); +``` + +#### Create Cohort +```javascript +const cohort = await mixpanel.createCohort(123456, { + name: 'High Value Users', + description: 'Users who spent over $100', + filter: { + filter: { + selector: { + property: 'total_spent', + operator: '>', + values: [100] + } + } + } +}); +``` + +### Annotations + +#### Get Annotations +```javascript +const annotations = await mixpanel.getAnnotations({ + project_id: 123456, + from_date: '2024-01-01', + to_date: '2024-01-31' +}); +``` + +#### Create Annotation +```javascript +const annotation = await mixpanel.createAnnotation({ + project_id: 123456, + date: '2024-01-15', + description: 'Launched new feature' +}); +``` + +### Additional Analytics + +#### Segmentation +```javascript +const segmentation = await mixpanel.api.getSegmentation({ + project_id: 123456, + event: 'Purchase Completed', + from_date: '2024-01-01', + to_date: '2024-01-31', + unit: 'day' +}); +``` + +#### Top Events +```javascript +const topEvents = await mixpanel.api.getTopEvents({ + project_id: 123456, + type: 'general' +}); +``` + +#### Property Values +```javascript +const values = await mixpanel.api.getPropertyValues({ + project_id: 123456, + event: 'Purchase Completed', + property: 'product_category' +}); +``` + +## Error Handling + +```javascript +try { + await mixpanel.track({ + event: 'Test Event', + distinct_id: 'test-user' + }); +} catch (error) { + if (error.status === 401) { + console.error('Authentication failed - check credentials'); + } else if (error.status === 429) { + console.error('Rate limit exceeded'); + } else { + console.error('Mixpanel API Error:', error); + } +} +``` + +## Testing Authentication + +```javascript +const testResult = await mixpanel.testAuth(); +if (testResult.success) { + console.log('Authentication successful!', testResult.data); +} else { + console.error('Authentication failed:', testResult.message); +} +``` + +## Best Practices + +1. **Use Service Accounts** for server-side operations +2. **Batch Operations** when tracking multiple events to reduce API calls +3. **Set distinct_id** consistently for accurate user tracking +4. **Use Groups** for B2B analytics (companies, teams, etc.) +5. **Export Data** regularly for backups and advanced analysis + +## Rate Limits + +- Standard rate limits apply based on your Mixpanel plan +- Use batch endpoints to optimize API usage +- Implement exponential backoff for rate limit errors + +## GDPR Compliance + +Delete user data for GDPR compliance: +```javascript +await mixpanel.api.deleteUserData({ + project_id: 123456, + distinct_ids: ['user-123', 'user-456'], + compliance_type: 'gdpr' +}); +``` + +## Resources + +- [Mixpanel API Documentation](https://developer.mixpanel.com/reference) +- [Service Accounts Guide](https://developer.mixpanel.com/reference/service-accounts) +- [JQL Reference](https://developer.mixpanel.com/docs/jql-overview) +- [Event Tracking Guide](https://developer.mixpanel.com/docs/javascript) \ No newline at end of file diff --git a/packages/mixpanel/api.js b/packages/mixpanel/api.js new file mode 100644 index 0000000..a17f67a --- /dev/null +++ b/packages/mixpanel/api.js @@ -0,0 +1,312 @@ +const { ApiClass } = require('@friggframework/core'); + +class MixpanelApi extends ApiClass { + constructor(params) { + super(params); + this.baseUrl = 'https://api.mixpanel.com'; + this.ingestionUrl = 'https://api.mixpanel.com'; + this.projectToken = params.projectToken; + this.serviceAccountUsername = params.serviceAccountUsername; + this.serviceAccountSecret = params.serviceAccountSecret; + + // Set up basic auth for service account + if (this.serviceAccountUsername && this.serviceAccountSecret) { + this.authHeader = 'Basic ' + Buffer.from( + `${this.serviceAccountUsername}:${this.serviceAccountSecret}` + ).toString('base64'); + } + } + + /** + * Get authorization headers + * @returns {Object} Headers with authorization + */ + _getAuthHeaders() { + return { + 'Authorization': this.authHeader, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + } + + /** + * Track a single event + * @param {Object} event - Event data with properties + * @returns {Promise} Tracking response + */ + async track(event) { + const data = [{ + event: event.event, + properties: { + ...event.properties, + token: this.projectToken, + time: event.time || Math.floor(Date.now() / 1000), + distinct_id: event.distinct_id || event.properties.distinct_id + } + }]; + + const url = `${this.ingestionUrl}/import`; + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Track multiple events in batch + * @param {Array} events - Array of event objects + * @returns {Promise} Batch tracking response + */ + async trackBatch(events) { + const data = events.map(event => ({ + event: event.event, + properties: { + ...event.properties, + token: this.projectToken, + time: event.time || Math.floor(Date.now() / 1000), + distinct_id: event.distinct_id || event.properties.distinct_id + } + })); + + const url = `${this.ingestionUrl}/import`; + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Update user profile + * @param {Object} profile - Profile data + * @returns {Promise} Profile update response + */ + async updateProfile(profile) { + const data = [{ + $token: this.projectToken, + $distinct_id: profile.distinct_id, + $set: profile.properties || {}, + $ip: profile.ip, + $time: profile.time || Math.floor(Date.now() / 1000) + }]; + + const url = `${this.ingestionUrl}/engage`; + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Update multiple profiles in batch + * @param {Array} profiles - Array of profile objects + * @returns {Promise} Batch profile update response + */ + async updateProfilesBatch(profiles) { + const data = profiles.map(profile => ({ + $token: this.projectToken, + $distinct_id: profile.distinct_id, + $set: profile.properties || {}, + $ip: profile.ip, + $time: profile.time || Math.floor(Date.now() / 1000) + })); + + const url = `${this.ingestionUrl}/engage`; + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Update group profile + * @param {Object} group - Group data + * @returns {Promise} Group update response + */ + async updateGroup(group) { + const data = [{ + $token: this.projectToken, + $group_key: group.group_key, + $group_id: group.group_id, + $set: group.properties || {}, + $time: group.time || Math.floor(Date.now() / 1000) + }]; + + const url = `${this.ingestionUrl}/groups`; + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Query using JQL (JSON Query Language) + * @param {Object} params - JQL query parameters + * @returns {Promise} Query results + */ + async queryJQL(params) { + const url = `${this.baseUrl}/api/2.0/jql`; + return this._post(url, params, { headers: this._getAuthHeaders() }); + } + + /** + * Get funnel analysis + * @param {Object} params - Funnel parameters + * @returns {Promise} Funnel data + */ + async getFunnel(params) { + const url = `${this.baseUrl}/api/2.0/funnels`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get retention analysis + * @param {Object} params - Retention parameters + * @returns {Promise} Retention data + */ + async getRetention(params) { + const url = `${this.baseUrl}/api/2.0/retention`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get insights (saved reports) + * @param {number} projectId - Project ID + * @returns {Promise} List of insights + */ + async getInsights(projectId) { + const url = `${this.baseUrl}/api/2.0/insights`; + const queryString = new URLSearchParams({ project_id: projectId }).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Export raw event data + * @param {Object} params - Export parameters + * @returns {Promise} Export data + */ + async exportEvents(params) { + const url = `${this.baseUrl}/api/2.0/export`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Export people profiles + * @param {Object} params - Export parameters + * @returns {Promise} People data + */ + async exportPeople(params) { + const url = `${this.baseUrl}/api/2.0/engage`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get project details + * @param {number} projectId - Project ID + * @returns {Promise} Project details + */ + async getProject(projectId) { + const url = `${this.baseUrl}/api/app/projects/${projectId}`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * List all projects + * @returns {Promise} List of projects + */ + async listProjects() { + const url = `${this.baseUrl}/api/app/me`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get cohorts + * @param {number} projectId - Project ID + * @returns {Promise} List of cohorts + */ + async getCohorts(projectId) { + const url = `${this.baseUrl}/api/2.0/cohorts/list`; + const queryString = new URLSearchParams({ project_id: projectId }).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Create a cohort + * @param {number} projectId - Project ID + * @param {Object} cohort - Cohort configuration + * @returns {Promise} Created cohort + */ + async createCohort(projectId, cohort) { + const url = `${this.baseUrl}/api/2.0/cohorts/create`; + const data = { + project_id: projectId, + ...cohort + }; + return this._post(url, data, { headers: this._getAuthHeaders() }); + } + + /** + * Get annotations + * @param {Object} params - Query parameters + * @returns {Promise} List of annotations + */ + async getAnnotations(params) { + const url = `${this.baseUrl}/api/2.0/annotations`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Create an annotation + * @param {Object} annotation - Annotation data + * @returns {Promise} Created annotation + */ + async createAnnotation(annotation) { + const url = `${this.baseUrl}/api/2.0/annotations/create`; + return this._post(url, annotation, { headers: this._getAuthHeaders() }); + } + + /** + * Delete user data (GDPR compliance) + * @param {Object} params - Deletion parameters + * @returns {Promise} Deletion response + */ + async deleteUserData(params) { + const url = `${this.baseUrl}/api/2.0/engage/delete`; + return this._post(url, params, { headers: this._getAuthHeaders() }); + } + + /** + * Get segmentation data + * @param {Object} params - Segmentation parameters + * @returns {Promise} Segmentation results + */ + async getSegmentation(params) { + const url = `${this.baseUrl}/api/2.0/segmentation`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get property values for an event + * @param {Object} params - Parameters including event name + * @returns {Promise} Property values + */ + async getPropertyValues(params) { + const url = `${this.baseUrl}/api/2.0/events/properties/values`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get top events + * @param {Object} params - Query parameters + * @returns {Promise} Top events + */ + async getTopEvents(params) { + const url = `${this.baseUrl}/api/2.0/events/names`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Create or update lookup table + * @param {Object} table - Lookup table data + * @returns {Promise} Table response + */ + async updateLookupTable(table) { + const url = `${this.baseUrl}/api/2.0/lookup-tables`; + return this._post(url, table, { headers: this._getAuthHeaders() }); + } +} + +module.exports = MixpanelApi; \ No newline at end of file diff --git a/packages/mixpanel/defaultConfig.json b/packages/mixpanel/defaultConfig.json new file mode 100644 index 0000000..43d0994 --- /dev/null +++ b/packages/mixpanel/defaultConfig.json @@ -0,0 +1,91 @@ +{ + "name": "Mixpanel", + "version": "1.0.0", + "category": "Analytics", + "type": "mixpanel", + "description": "Product analytics platform for tracking user interactions and behavior", + "documentation": "https://mixpanel.com/docs", + "apiDocs": "https://developer.mixpanel.com/reference", + "authentication": { + "types": [ + { + "type": "serviceAccount", + "name": "Service Account", + "description": "Recommended for server-side API operations", + "fields": [ + "serviceAccountUsername", + "serviceAccountSecret" + ] + }, + { + "type": "projectToken", + "name": "Project Token", + "description": "For event tracking and client-side operations", + "fields": [ + "projectToken" + ] + } + ] + }, + "endpoints": { + "api": "https://api.mixpanel.com", + "ingestion": "https://api.mixpanel.com", + "euApi": "https://eu.mixpanel.com", + "euIngestion": "https://eu.mixpanel.com" + }, + "features": [ + "Event tracking", + "User profiles", + "Group analytics", + "Funnel analysis", + "Retention analysis", + "JQL queries", + "Cohort management", + "Data export", + "Real-time analytics", + "Custom properties", + "Annotations", + "Lookup tables" + ], + "limitations": [ + "Data retention based on plan", + "API rate limits", + "Maximum event size 1MB", + "Property name limitations" + ], + "pricing": "Free tier up to 20M events/month, paid plans for higher volume", + "rateLimits": { + "ingestion": "2000 requests/minute per project", + "export": "60 requests/hour", + "query": "5 concurrent queries" + }, + "supportedRegions": [ + "US", + "EU" + ], + "dataRetention": { + "free": "90 days", + "growth": "1 year", + "enterprise": "Unlimited" + }, + "webhooks": true, + "sdks": [ + "JavaScript", + "iOS", + "Android", + "React Native", + "Python", + "Ruby", + "PHP", + "Java", + "Node.js", + "Unity", + "Flutter" + ], + "compliance": [ + "GDPR", + "CCPA", + "SOC 2", + "EU-US Privacy Shield" + ] +} \ No newline at end of file diff --git a/packages/mixpanel/definition.js b/packages/mixpanel/definition.js new file mode 100644 index 0000000..aad7d81 --- /dev/null +++ b/packages/mixpanel/definition.js @@ -0,0 +1,198 @@ +const { Integration } = require('@friggframework/module-plugin'); +const ApiClass = require('./api'); + +class MixpanelIntegration extends Integration { + static name = 'Mixpanel'; + static category = 'Analytics'; + static catalogDescription = 'Product analytics platform for tracking user interactions and behavior'; + static version = '1.0.0'; + static referenceUrl = 'https://mixpanel.com'; + static apiDocs = 'https://developer.mixpanel.com/reference'; + + /** + * Constructor for MixpanelIntegration + * @param {Object} params - Should include serviceAccountUsername, serviceAccountSecret, and projectToken + */ + constructor(params) { + super(params); + this.api = new ApiClass(params); + } + + /** + * Track an event + * @param {Object} event - Event data + * @returns {Promise} Tracking response + */ + async track(event) { + return this.api.track(event); + } + + /** + * Track multiple events in batch + * @param {Array} events - Array of event objects + * @returns {Promise} Batch tracking response + */ + async trackBatch(events) { + return this.api.trackBatch(events); + } + + /** + * Update user profile + * @param {Object} profile - Profile data + * @returns {Promise} Profile update response + */ + async updateProfile(profile) { + return this.api.updateProfile(profile); + } + + /** + * Update multiple profiles in batch + * @param {Array} profiles - Array of profile objects + * @returns {Promise} Batch profile update response + */ + async updateProfilesBatch(profiles) { + return this.api.updateProfilesBatch(profiles); + } + + /** + * Create or update a group profile + * @param {Object} group - Group data + * @returns {Promise} Group update response + */ + async updateGroup(group) { + return this.api.updateGroup(group); + } + + /** + * Query JQL (JSON Query Language) for analytics + * @param {Object} params - JQL query parameters + * @returns {Promise} Query results + */ + async queryJQL(params) { + return this.api.queryJQL(params); + } + + /** + * Get funnel analysis + * @param {Object} params - Funnel parameters + * @returns {Promise} Funnel data + */ + async getFunnel(params) { + return this.api.getFunnel(params); + } + + /** + * Get retention analysis + * @param {Object} params - Retention parameters + * @returns {Promise} Retention data + */ + async getRetention(params) { + return this.api.getRetention(params); + } + + /** + * Get insights (saved reports) + * @param {number} projectId - Project ID + * @returns {Promise} List of insights + */ + async getInsights(projectId) { + return this.api.getInsights(projectId); + } + + /** + * Export raw event data + * @param {Object} params - Export parameters + * @returns {Promise} Export data + */ + async exportEvents(params) { + return this.api.exportEvents(params); + } + + /** + * Export people profiles + * @param {Object} params - Export parameters + * @returns {Promise} People data + */ + async exportPeople(params) { + return this.api.exportPeople(params); + } + + /** + * Get project details + * @param {number} projectId - Project ID + * @returns {Promise} Project details + */ + async getProject(projectId) { + return this.api.getProject(projectId); + } + + /** + * List all projects + * @returns {Promise} List of projects + */ + async listProjects() { + return this.api.listProjects(); + } + + /** + * Get cohorts + * @param {number} projectId - Project ID + * @returns {Promise} List of cohorts + */ + async getCohorts(projectId) { + return this.api.getCohorts(projectId); + } + + /** + * Create a cohort + * @param {number} projectId - Project ID + * @param {Object} cohort - Cohort configuration + * @returns {Promise} Created cohort + */ + async createCohort(projectId, cohort) { + return this.api.createCohort(projectId, cohort); + } + + /** + * Get annotations + * @param {Object} params - Query parameters + * @returns {Promise} List of annotations + */ + async getAnnotations(params) { + return this.api.getAnnotations(params); + } + + /** + * Create an annotation + * @param {Object} annotation - Annotation data + * @returns {Promise} Created annotation + */ + async createAnnotation(annotation) { + return this.api.createAnnotation(annotation); + } + + /** + * Test authentication + * @returns {Promise} Test result + */ + async testAuth() { + try { + const projects = await this.listProjects(); + return { + success: true, + message: 'Authentication successful', + data: { + projectsCount: projects.length + } + }; + } catch (error) { + return { + success: false, + message: `Authentication failed: ${error.message}`, + error: error + }; + } + } +} + +module.exports = MixpanelIntegration; \ No newline at end of file diff --git a/packages/mixpanel/index.js b/packages/mixpanel/index.js new file mode 100644 index 0000000..2f5c456 --- /dev/null +++ b/packages/mixpanel/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/v1-ready/monday/README.md b/packages/monday/README.md similarity index 100% rename from packages/v1-ready/monday/README.md rename to packages/monday/README.md diff --git a/packages/v1-ready/monday/fenestra/platform.fenestra.yaml b/packages/monday/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/monday/fenestra/platform.fenestra.yaml rename to packages/monday/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/monday/fenestra/schemas/monday.com-validation.json b/packages/monday/fenestra/schemas/monday.com-validation.json similarity index 100% rename from packages/v1-ready/monday/fenestra/schemas/monday.com-validation.json rename to packages/monday/fenestra/schemas/monday.com-validation.json diff --git a/packages/v1-ready/monday/index.js b/packages/monday/index.js similarity index 100% rename from packages/v1-ready/monday/index.js rename to packages/monday/index.js diff --git a/packages/v1-ready/monday/package.json b/packages/monday/package.json similarity index 100% rename from packages/v1-ready/monday/package.json rename to packages/monday/package.json diff --git a/packages/needs-updating/activecampaign/index.js b/packages/needs-updating/activecampaign/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/activecampaign/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/activecampaign/jest-setup.js b/packages/needs-updating/activecampaign/jest-setup.js deleted file mode 100644 index 65abfad..0000000 --- a/packages/needs-updating/activecampaign/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const { globalSetup } = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/needs-updating/activecampaign/jest-teardown.js b/packages/needs-updating/activecampaign/jest-teardown.js deleted file mode 100644 index d0c6426..0000000 --- a/packages/needs-updating/activecampaign/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const { globalTeardown } = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/activecampaign/manager.js b/packages/needs-updating/activecampaign/manager.js deleted file mode 100644 index cf4cb72..0000000 --- a/packages/needs-updating/activecampaign/manager.js +++ /dev/null @@ -1,126 +0,0 @@ -const _ = require('lodash'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const AuthFields = require('./authFields'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - let activeCampaignParams; - - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - if (instance.entity.credential) { - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - activeCampaignParams = { - apiKey: instance.credential.api_key, - apiUrl: instance.credential.api_url, - }; - } - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - activeCampaignParams = { - apiKey: instance.credential.api_key, - apiUrl: instance.credential.api_url, - }; - } - if (activeCampaignParams) { - instance.api = await new Api(activeCampaignParams); - } - - return instance; - } - - async getAuthorizationRequirements(params) { - // see parent docs. only use these three top level keys - return { - url: null, - type: ModuleConstants.authType.apiKey, - data: { - jsonSchema: AuthFields.jsonSchema, - uiSchema: AuthFields.uiSchema, - }, - }; - } - - async processAuthorizationCallback(params) { - const apiUrl = get(params.data, 'apiUrl'); - const apiKey = get(params.data, 'apiKey'); - this.api = new Api({apiUrl, apiKey}); - const userDetails = await this.api.getUserDetails(); - - const byUserId = {user: this.userId}; - const credentials = await this.credentialMO.list(byUserId); - - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - const credential = await this.credentialMO.upsert(byUserId, { - user: this.userId, - api_url: apiUrl, - api_key: apiKey, - }); - - const byUserIdAndCredential = { - ...byUserId, - credential: credential.id, - }; - const entity = await this.entityMO.upsert(byUserIdAndCredential, { - user: this.userId, - credential: credential.id, - name: userDetails.user.username, - externalId: userDetails.user.id, - }); - - return { - entity_id: entity.id, - credential_id: credential.id, - type: Manager.getName(), - }; - } - - async testAuth() { - await this.api.getUserDetails(); - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/activecampaign/models/credential.js b/packages/needs-updating/activecampaign/models/credential.js deleted file mode 100644 index 8ff4c26..0000000 --- a/packages/needs-updating/activecampaign/models/credential.js +++ /dev/null @@ -1,18 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - api_key: { - type: String, - trim: true, - lhEncrypt: true, - }, - api_url: { - type: String, - required: true, - }, -}); -const name = 'ActiveCampaignCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/activecampaign/models/entity.js b/packages/needs-updating/activecampaign/models/entity.js deleted file mode 100644 index f2bd9bc..0000000 --- a/packages/needs-updating/activecampaign/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'ActiveCampaignEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/airwallex/index.js b/packages/needs-updating/airwallex/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/airwallex/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/airwallex/jest-setup.js b/packages/needs-updating/airwallex/jest-setup.js deleted file mode 100644 index b47d77e..0000000 --- a/packages/needs-updating/airwallex/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const { globalSetup } = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/airwallex/jest-teardown.js b/packages/needs-updating/airwallex/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/airwallex/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/airwallex/manager.js b/packages/needs-updating/airwallex/manager.js deleted file mode 100644 index fd4b2a5..0000000 --- a/packages/needs-updating/airwallex/manager.js +++ /dev/null @@ -1,133 +0,0 @@ -const {ModuleManager} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); - -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - const apiParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.access_token = credential.access_token; - apiParams.id_token = credential.id_token; - apiParams.expires_in = credential.accessExpiresIn; - } - - instance.api = await new Api(apiParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: 'oauth2', - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - const userDetails = await this.api.getTokenIdentity(); - - let credentials = await this.credentialMO.list({user: this.userId}); - - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - let entity = await this.entityMO.getByUserId(this.userId); - - if (!entity) { - entity = await this.entityMO.create({ - user: this.userId, - credential: credentials[0]._id, - externalId: userDetails.companyId, - name: userDetails.companyName, - }); - } - - return { - credential_id: credentials[0]._id, - entity_id: entity._id, - type: Manager.getName(), - }; - } - - async testAuth() { - await this.api.getTokenIdentity(); - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId, - access_token: this.api.access_token, - id_token: this.api.id_token, - // expires_in: this.api.accessExpiresIn, - auth_is_valid: true, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] === null && delete updatedToken[k] - ); - - let credential = await this.entityMO.getByUserId(this.userId); - - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/airwallex/models/credential.js b/packages/needs-updating/airwallex/models/credential.js deleted file mode 100644 index 19a048b..0000000 --- a/packages/needs-updating/airwallex/models/credential.js +++ /dev/null @@ -1,18 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - api_key: { - type: String, - trim: true, - lhEncrypt: true, - }, - api_url: { - type: String, - required: true, - }, -}); -const name = 'AirwallexCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/airwallex/models/entity.js b/packages/needs-updating/airwallex/models/entity.js deleted file mode 100644 index c05f79a..0000000 --- a/packages/needs-updating/airwallex/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'AirwallexEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/attentive/index.js b/packages/needs-updating/attentive/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/attentive/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/attentive/jest-setup.js b/packages/needs-updating/attentive/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/attentive/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/attentive/jest-teardown.js b/packages/needs-updating/attentive/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/attentive/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/attentive/manager.js b/packages/needs-updating/attentive/manager.js deleted file mode 100644 index 322f7bf..0000000 --- a/packages/needs-updating/attentive/manager.js +++ /dev/null @@ -1,134 +0,0 @@ -const {ModuleManager} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); - -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - const attentiveParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - attentiveParams.access_token = credential.access_token; - attentiveParams.id_token = credential.id_token; - attentiveParams.expires_in = credential.accessExpiresIn; - } - - instance.api = await new Api(attentiveParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: 'oauth2', - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - const userDetails = await this.api.getTokenIdentity(); - - let credentials = await this.credentialMO.list({user: this.userId}); - - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - let entity = await this.entityMO.getByUserId(this.userId); - - if (!entity) { - entity = await this.entityMO.create({ - user: this.userId, - credential: credentials[0]._id, - externalId: userDetails.companyId, - name: userDetails.companyName, - domain: userDetails.attentiveDomainName, - }); - } - - return { - credential_id: credentials[0]._id, - entity_id: entity._id, - type: Manager.getName(), - }; - } - - async testAuth() { - await this.api.getTokenIdentity(); - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId, - access_token: this.api.access_token, - id_token: this.api.id_token, - // expires_in: this.api.accessExpiresIn, - auth_is_valid: true, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] === null && delete updatedToken[k] - ); - - let credential = await this.entityMO.getByUserId(this.userId); - - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/attentive/models/credential.js b/packages/needs-updating/attentive/models/credential.js deleted file mode 100644 index fe15fb3..0000000 --- a/packages/needs-updating/attentive/models/credential.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: {type: String, trim: true, lhEncrypt: true}, - id_token: {type: String, trim: true, lhEncrypt: true}, - token_type: {type: String, default: 'Bearer'}, - expires_in: {type: Number}, -}); -const name = 'AttentiveCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/attentive/models/entity.js b/packages/needs-updating/attentive/models/entity.js deleted file mode 100644 index 533a4af..0000000 --- a/packages/needs-updating/attentive/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'AttentiveEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/clyde/index.js b/packages/needs-updating/clyde/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/clyde/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/clyde/jest-setup.js b/packages/needs-updating/clyde/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/clyde/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/clyde/jest-teardown.js b/packages/needs-updating/clyde/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/clyde/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/clyde/manager.js b/packages/needs-updating/clyde/manager.js deleted file mode 100644 index a609855..0000000 --- a/packages/needs-updating/clyde/manager.js +++ /dev/null @@ -1,198 +0,0 @@ -const {debug, flushDebugLog, get} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants -} = require('@friggframework/core'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - // All async code here - - // initializes the Api - const apiParams = { - delegate: instance, - }; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.clientKey = credential.clientKey; - apiParams.secret = credential.secret; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.listProducts()) validAuth = true; - } catch (e) { - flushDebugLog(e); - } - return validAuth; - } - - async getAuthorizationRequirements(params) { - return { - url: null, - type: ModuleConstants.authType.basic, - data: { - jsonSchema: { - type: 'object', - required: ['clientKey', 'secret'], - properties: { - clientKey: { - type: 'string', - title: 'Client Key', - }, - secret: { - type: 'string', - title: 'Secret', - }, - }, - }, - uiSchema: { - clientKey: { - 'ui:help': - 'To obtain your Client Key and Secret, log in and head to settings. You can find your Keys in the "Developers" section.', - 'ui:placeholder': 'Client Key', - }, - secret: { - 'ui:widget': 'password', - 'ui:help': - 'Your secret is obtained along with your Client Key', - 'ui:placeholder': 'secret', - }, - }, - }, - }; - } - - async processAuthorizationCallback(params) { - const clientKey = get(params.data, 'clientKey'); - const secret = get(params.data, 'secret'); - this.api.setClientKey(clientKey); - this.api.setSecret(secret); - await this.testAuth(); - - await this.findOrCreateCredential({ - clientKey, - secret, - }); - - await this.findOrCreateEntity({ - clientKey, - }); - - return { - entity_id: this.entity.id, - credential_id: this.credential.id, - type: Manager.getName(), - }; - } - - async findOrCreateCredential(params) { - const clientKey = get(params, 'clientKey'); - const secret = get(params, 'secret'); - - const search = await this.credentialMO.list({ - user: this.userId, - clientKey, - }); - - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - user: this.userId, - clientKey, - secret, - }; - this.credential = await this.credentialMO.create(createObj); - } else if (search.length === 1) { - this.credential = search[0]; - } else { - debug( - 'Multiple entities found with the same Client Key:', - clientKey - ); - } - } - - async findOrCreateEntity(params) { - const clientKey = get(params, 'clientKey'); - - const search = await this.entityMO.list({ - user: this.userId, - externalId: clientKey, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name: clientKey, - externalId: clientKey, - }; - this.entity = await this.entityMO.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug( - 'Multiple entities found with the same Client Key:', - clientKey - ); - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - this.credential = undefined; - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/clyde/models/credential.js b/packages/needs-updating/clyde/models/credential.js deleted file mode 100644 index 5412d37..0000000 --- a/packages/needs-updating/clyde/models/credential.js +++ /dev/null @@ -1,21 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - // Clyde Access Details - clientKey: { - type: String, - trim: true, - unique: true, - }, - secret: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'ClydeCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/clyde/models/entity.js b/packages/needs-updating/clyde/models/entity.js deleted file mode 100644 index 573b068..0000000 --- a/packages/needs-updating/clyde/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'ClydeEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/fastspring-iq/index.js b/packages/needs-updating/fastspring-iq/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/fastspring-iq/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/fastspring-iq/jest-setup.js b/packages/needs-updating/fastspring-iq/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/fastspring-iq/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/fastspring-iq/jest-teardown.js b/packages/needs-updating/fastspring-iq/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/fastspring-iq/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/fastspring-iq/manager.js b/packages/needs-updating/fastspring-iq/manager.js deleted file mode 100644 index 7b95558..0000000 --- a/packages/needs-updating/fastspring-iq/manager.js +++ /dev/null @@ -1,171 +0,0 @@ -const {ModuleManager, get} = require('@friggframework/core'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const {Api} = require('./api'); - -// name used as the entity type -const MANAGER_NAME = 'fastspring-iq'; - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return MANAGER_NAME; - } - - static async getInstance(params) { - const instance = new this(params); - - const apiParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.accessToken = credential.accessToken; - apiParams.refreshToken = credential.refreshToken; - apiParams.accessTokenExpire = credential.accessTokenExpire; - apiParams.refreshTokenExpire = credential.refreshTokenExpire; - apiParams.realmId = credential.realmId; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.getOrganizationDetails()) validAuth = true; - } catch (e) { - console.log(e); - } - return validAuth; - } - - async getAuthorizationRequirements() { - return { - url: this.api.getAuthorizationUri(), - type: 'oauth2', - }; - } - - async processAuthorizationCallback(params) { - const data = get(params, 'data'); - const code = get(data, 'code'); - - await this.getAccessToken(code); - const entity = await Entity.findByUserId(this.userId); - return { - id: entity.id, - type: Manager.getName(), - }; - } - - //------------------------------------------------------------ - - checkUserAuthorized() { - return this.api.isAuthenticated(); - } - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await Entity.findByUserId(this.userId); - if (entity.credential) { - await Credential.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - getOrCreateClient() { - } - - async getAccessToken(code) { - await this.api.getTokenFromCode(code); - } - - async sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - async receiveNotification(notifier, delegateString, object = null) { - try { - // throw new Error("Whats the stack trace here?"); - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - console.log(`should update the token: ${object}`); - const updatedToken = { - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - accessTokenExpire: this.api.accessTokenExpire, - }; - - // We shouldn't ever get to this point vv but just in case - if (!this.entity) { - this.throwException( - 'No entity found on the Manager during Token Update... something is wrong' - ); - } - const {credentials} = this.entity; - const credentialObject = {}; - - // First check to see if there are any credentials stored on the Entity - if (!credentials) { - // If no credentials stored, then we create our first one and push it to the array - credentialObject.credential = await Credential.create( - updatedToken - ); - credentialObject.type = this.type; - await Entity.model.updateOne( - {_id: this.entity.id}, - {$push: {credentials: credentialObject}} - ); - } else { - // If there ARE some credentials stored, then we need to figure out the one we have is the same type - const existingCred = credentials.find( - (credential) => credential.type === this.type - ); - // If there are any existingCredentials of the same type, then we update that credential and call it a day - if (existingCred) { - this.credential = await Credential.findOneAndUpdate( - {_id: existingCred.credential}, - {$set: updatedToken}, - {useFindAndModify: true, new: true} - ); - } else { - // If there are no existing credentials by that type, create and add to the array - credentialObject.credential = - await Credential.create(updatedToken); - credentialObject.type = this.type; - await Entity.model.findOneAndUpdate( - {_id: this.entity.id}, - {$push: {credentials: credentialObject}} - ); - } - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - console.log(this.checkUserAuthorized()); - } - } - } catch (e) { - console.log('error yo'); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/fastspring-iq/models/credential.js b/packages/needs-updating/fastspring-iq/models/credential.js deleted file mode 100644 index c69ec28..0000000 --- a/packages/needs-updating/fastspring-iq/models/credential.js +++ /dev/null @@ -1,14 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: {type: String, lhEncrypt: true}, - refreshToken: {type: String, lhEncrypt: true}, - accessTokenExpire: {type: String}, - refreshTokenExpire: {type: String}, -}); - -const name = 'FastSpringIQCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/fastspring-iq/models/entity.js b/packages/needs-updating/fastspring-iq/models/entity.js deleted file mode 100644 index e3bb404..0000000 --- a/packages/needs-updating/fastspring-iq/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'FastSpringIQEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/freshbooks/index.js b/packages/needs-updating/freshbooks/index.js deleted file mode 100644 index a915b0e..0000000 --- a/packages/needs-updating/freshbooks/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const Manager = require('./manager'); -const Config = require('./defaultConfig'); -const {Definition} = require('./definition'); - -module.exports = { - Api, - Credential, - Entity, - Config, - Manager, - Definition, -}; diff --git a/packages/needs-updating/freshbooks/jest-setup.js b/packages/needs-updating/freshbooks/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/needs-updating/freshbooks/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/needs-updating/freshbooks/jest-teardown.js b/packages/needs-updating/freshbooks/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/freshbooks/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/freshbooks/manager.js b/packages/needs-updating/freshbooks/manager.js deleted file mode 100644 index a1fa2eb..0000000 --- a/packages/needs-updating/freshbooks/manager.js +++ /dev/null @@ -1,240 +0,0 @@ -const {ModuleManager, get, debug, flushDebugLog} = require('@friggframework/core'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const {IndividualUser} = require('./models/IndividualUser'); -const {Api} = require('./api'); -const config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - this.api = new Api({}); - this.credential = null; - this.entity = null; - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return config.name; - } - - static async getInstance(params) { - const instance = new this(params); - // All async code here - - // initializes the Api - let credential, entity; - const apiParams = { - delegate: instance, - client_id: process.env.FRESHBOOKS_CLIENT_ID, - client_secret: process.env.FRESHBOOKS_CLIENT_SECRET, - redirect_uri: `${process.env.REDIRECT_URI}/freshbooks`, - }; - - if (params.entityId) { - entity = await Entity.findOne({_id: params.entityId}); - if (!entity) - throw new Error( - `Freshbooks Module: getInstance: No entity found for id: ${params.entityId}` - ); - - credential = await Credential.findOne({_id: entity.credential}); - } - instance.api = new Api(apiParams); - if (entity) { - instance.setEntity(entity); - } - if (credential) { - instance.setCredential(credential); - } - - return instance; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.getUserInfo()) validAuth = true; - } catch (e) { - flushDebugLog(e); - } - return validAuth; - } - - getAuthorizationRequirements(params) { - return { - url: this.api.getAuthUri(params), - type: 'oauth2', - }; - } - - async processAuthorizationCallback(params) { - console.log('processAuthorizationCallback', JSON.stringify(params)); - const code = get(params.data, 'code'); - if (!code) throw new Error('Node valid params.data.code'); - - await this.getAccessToken(code); - - await this.findOrCreateEntity({ - externalId: String(params.data.account_id || params.data.appOrgId), - subType: params.data.subType, - }); - - if (!this.credential) { - throw new Error( - `Freshbooks Module: processAuthorizationCallback: No credential set.` - ); - } - - return { - entity_id: this.entity.id, - credential_id: this.credential.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(data) { - const {externalId, subType} = data; - - const search = await Entity.find({ - externalId, - subType, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - if (!this.credential) { - throw new Error( - `Freshbooks Module: No credential set when creating entity for externalId: ${externalId}` - ); - } - - const createObj = { - credential: this.credential.id, - user: this.userId, - externalId, - subType, - }; - this.setEntity(await Entity.create(createObj)); - } else if (search.length === 1) { - this.setEntity(search[0]); - } else { - debug( - 'Multiple entities found with the same externalId:', - externalId - ); - throw new Error( - `Multiple entities found with the same externalId: ${externalId}` - ); - } - return this.entity; - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api({}); - - // delete credentials from the database - const entity = await Entity.findByUserId(this.userId); - if (entity.credential) { - await Credential.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - this.credential = null; - } - - setEntity(entity) { - this.entity = entity; - this.api.setAccountId(entity.externalId); - } - - setCredential(credential) { - this.credential = credential; - this.api.setAccessToken(credential.access_token); - this.api.setRefreshToken(credential.refresh_token); - } - - async getAccessToken(code) { - return this.api.getTokenFromCode(code); - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - debug(`should update the token: ${object}`); - - const userDetails = await this.api.getUserInfo(); - console.log('userDetails', userDetails); - - const updatedToken = { - externalId: userDetails.id, - appUserId: userDetails.id, - access_token: this.api.access_token, - refresh_token: this.api.refresh_token, - expires_at: String(this.api.accessTokenExpire), - // portalId: userDetails.portalId, - auth_is_valid: true, - }; - - if (!this.credential) { - console.log('Credential not found, searching by external ID', userDetails.id); - const credentialSearch = await Credential.find({ - externalId: userDetails.id, - }); - if (credentialSearch.length === 0) { - let user = await IndividualUser.getUserByAppUserId( - updatedToken.appUserId - ); - if (!user) { - user = await IndividualUser.create({ - name: userDetails.user_name, - email: userDetails.user_email, - externalId: userDetails.id, - appUserId: updatedToken.appUserId, - organizationUser: null, - }); - } - - this.credential = await Credential.create({ - ...updatedToken, - user: user._id, - }); - } else if (credentialSearch.length === 1) { - console.log('Credential user', credentialSearch[0].user); - console.log('Updating credential', credentialSearch[0]._id); - this.credential = await Credential.update( - {_id: credentialSearch[0]._id}, - updatedToken - ); - } else { - // Handling multiple credentials found with an error for the time being - console.log( - 'Multiple credentials found with the same external ID:', - updatedToken.externalId, - ); - } - } else { - this.credential = await Credential.update( - this.credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/freshbooks/models/IndividualUser.js b/packages/needs-updating/freshbooks/models/IndividualUser.js deleted file mode 100644 index aa2fcab..0000000 --- a/packages/needs-updating/freshbooks/models/IndividualUser.js +++ /dev/null @@ -1,66 +0,0 @@ -const {mongoose} = require('@friggframework/core'); -const {Schema, model, models} = mongoose; -const schema1 = new Schema({}, {timestamps: true}) - -const User = models.User || model('User', schema1) - -const schema = new Schema({ - appUserId: {type: String, unique: true}, - name: {type: String}, - email: {type: String}, - organizationUser: {type: Schema.Types.ObjectId, ref: 'User'}, -}); - -schema.static({ - //decimals: 10, - /*update: async function (id, options) { - if ('password' in options) { - options.hashword = await bcrypt.hashSync( - options.password, - parseInt(this.decimals) - ); - delete options.password; - } - return this.findOneAndUpdate( - {_id: id}, - options, - {new: true, useFindAndModify: true} - ); - },*/ - getUserByUsername: async function (username) { - let getByUser; - try { - getByUser = await this.find({username}); - } catch (e) { - console.log('oops'); - } - - if (getByUser.length > 1) { - throw new Error( - 'Unique username or email? Please reach out to our developers' - ); - } - - if (getByUser.length === 1) { - return getByUser[0]; - } - }, - getUserByAppUserId: async function (appUserId) { - const getByUser = await this.find({appUserId}); - - if (getByUser.length > 1) { - throw new Error( - 'Supposedly using a unique appUserId? Please reach out to our developers' - ); - } - - if (getByUser.length === 1) { - return getByUser[0]; - } - }, -}); - -const IndividualUser = - User.discriminators?.IndividualUser || - User.discriminator('IndividualUser', schema); -module.exports = {IndividualUser}; diff --git a/packages/needs-updating/freshbooks/models/credential.js b/packages/needs-updating/freshbooks/models/credential.js deleted file mode 100644 index 027dc4f..0000000 --- a/packages/needs-updating/freshbooks/models/credential.js +++ /dev/null @@ -1,23 +0,0 @@ -const {Credential: Parent, mongoose} = require('@friggframework/core'); -const {Schema} = mongoose; - -const schema = new Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - refresh_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - access_token_expire: {type: Date}, - expires_at: {type: Date}, - externalId: {type: String}, -}); - -const name = 'FreshbooksCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/freshbooks/models/entity.js b/packages/needs-updating/freshbooks/models/entity.js deleted file mode 100644 index 672b41d..0000000 --- a/packages/needs-updating/freshbooks/models/entity.js +++ /dev/null @@ -1,18 +0,0 @@ -const {Entity: Parent, mongoose} = require('@friggframework/core'); -const {Schema} = mongoose; - -const schema = new Schema({ - account_id: { - type: String, - }, - externalId: { - type: String, - }, - title: { - type: String, - } -}); -const name = 'FreshBookEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/front/index.js b/packages/needs-updating/front/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/front/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/front/jest-setup.js b/packages/needs-updating/front/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/front/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/front/jest-teardown.js b/packages/needs-updating/front/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/front/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/front/manager.js b/packages/needs-updating/front/manager.js deleted file mode 100644 index feab469..0000000 --- a/packages/needs-updating/front/manager.js +++ /dev/null @@ -1,181 +0,0 @@ -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential.js'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const _ = require('lodash'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - async testAuth() { - await this.api.listContacts(); - } - - static async getInstance(params) { - let instance = new this(params); - - // initializes the Api - const frontParams = {delegate: instance}; - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - frontParams.access_token = instance.credential.access_token; - frontParams.refresh_token = instance.credential.refresh_token; - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - frontParams.access_token = instance.credential.access_token; - frontParams.refresh_token = instance.credential.refresh_token; - } - instance.api = await new Api(frontParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: await this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - - let credentials = await this.credentialMO.list({user: this.userId}); - - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - let entity = await this.entityMO.getByUserId(this.userId); - - return { - credential_id: credentials[0]._id, - entity_id: entity._id, - type: Manager.getName(), - }; - } - - async getEntityOptions() { - // No entity options to get. Probably won't even hit this - return []; - } - - async findOrCreateEntity(data) { - // Creating entity in send with credential creation... Just do a find - return this.entityMO.getByUserId(data.userId); - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - // todo update the database - const userDetails = await this.api.getTokenIdentity(); - const updatedToken = { - user: this.userId.toString(), - access_token: this.api.access_token, - refresh_token: this.api.refresh_token, - auth_is_valid: true, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] == null && delete updatedToken[k] - ); - - let entity = await this.entityMO.getByUserId(this.userId); - if (!entity) { - entity = await this.entityMO.create({ - user: this.userId, - externalId: userDetails.id, - name: userDetails.name, - }); - } - - let {credential} = entity; - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential, - updatedToken - ); - } - await this.entityMO.update(entity.id, {credential}); - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - return this.markCredentialsInvalid(); - } - } - } - - async mark_credentials_invalid() { - let credentials = await this.credentialMO.list({user: this.userId}); - if (credentials.length === 1) { - return await this.credentialMO.update(credentials[0]._id, { - auth_is_valid: false, - }); - } else if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } else if (credentials.length === 0) { - throw new Error( - 'How are we marking nonexistant credentials invalid???' - ); - } - } - - async listAllContacts(next = null) { - const results = await this.api.listContacts(next); - if (results._pagination) { - if (results._pagination.next) { - const next_page = await this.listAllContacts( - results._pagination.next - ); - results._results = results._results.concat(next_page); - } - } - return results._results; - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/front/models/credential.js b/packages/needs-updating/front/models/credential.js deleted file mode 100644 index 62a57d0..0000000 --- a/packages/needs-updating/front/models/credential.js +++ /dev/null @@ -1,14 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: {type: String, trim: true, lhEncrypt: true}, - refresh_token: {type: String, trim: true, lhEncrypt: true}, - auth_is_valid: {type: Boolean, default: true}, -}); - -const name = 'FrontCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/front/models/entity.js b/packages/needs-updating/front/models/entity.js deleted file mode 100644 index b0c27be..0000000 --- a/packages/needs-updating/front/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'FrontEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/gorgias/index.js b/packages/needs-updating/gorgias/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/gorgias/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/gorgias/jest-setup.js b/packages/needs-updating/gorgias/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/gorgias/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/gorgias/jest-teardown.js b/packages/needs-updating/gorgias/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/gorgias/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/gorgias/manager.js b/packages/needs-updating/gorgias/manager.js deleted file mode 100644 index 7034bce..0000000 --- a/packages/needs-updating/gorgias/manager.js +++ /dev/null @@ -1,209 +0,0 @@ -const { - ModuleManager, - ModuleConstants, - debug -} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - // initializes the Api - const apiParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.access_token = credential.accessToken; - apiParams.refresh_token = credential.refreshToken; - apiParams.subdomain = credential.subdomain; - apiParams.apiKey = credential.apiKey; - apiParams.apiUserEmail = credential.apiUserEmail; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.getAccountDetails()) validAuth = true; - } catch (e) { - debug(e); - } - return validAuth; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.getAuthUri(), - type: ModuleConstants.authType.oauth2, - data: { - jsonSchema: { - type: 'object', - required: ['subdomain'], - properties: { - subdomain: { - type: 'string', - title: 'Subdomain', - }, - }, - }, - uiSchema: { - subdomain: { - 'ui:help': 'The Subdomain for your Application login.', - 'ui:placeholder': '{{subdomain}}.gorgias.com', - }, - }, - }, - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - this.api.setSubdomain(get(params.data, 'subdomain')); - - await this.getAccessToken(code); - - await this.testAuth(); - - await this.findOrCreateEntity({ - subdomain: this.api.subdomain, - }); - - return { - entity_id: this.entity.id, - credential_id: this.credential.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const domainName = get(params, 'subdomain'); - - const search = await this.entityMO.list({ - user: this.userId, - externalId: domainName, - }); - if (search.length === 0) { - const createObj = { - credential: this.credential.id, - user: this.userId, - name: domainName, - externalId: domainName, - }; - this.entity = await this.entityMO.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug( - 'Multiple entities found with the same subdomain:', - domainName - ); - throw new Error( - `Multiple entities found with the same subdomain: ${domainName}` - ); - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - this.credential = undefined; - } - - async getAccessToken(code) { - return this.api.getTokenFromCodeBasicAuthHeader(code); - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - debug(`should update the token: ${object}`); - const updatedToken = { - user: this.userId, - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - accessTokenExpire: this.api.accessTokenExpire, - subdomain: this.api.subdomain, - apiKey: this.api.apiKey, - apiUserEmail: this.api.apiUserEmail, - auth_is_valid: true, - }; - - if (!this.credential) { - let credentialSearch = await this.credentialMO.list({ - subdomain: this.api.subdomain, - }); - if (credentialSearch.length === 0) { - this.credential = await this.credentialMO.create( - updatedToken - ); - } else if (credentialSearch.length === 1) { - if ( - credentialSearch[0].user.toString() === this.userId - ) { - this.credential = await this.credentialMO.update( - credentialSearch[0], - updatedToken - ); - } else { - debug( - `Somebody else already created a credential with the same domain: ${this.api.subdomain}` - ); - } - } else { - // Handling multiple credentials found with an error for the time being - let message = `Multiple credentials found with the same account: ${this.api.subdomain}`; - debug(message); - throw new Error(message); - } - } else { - this.credential = await this.credentialMO.update( - this.credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/gorgias/models/credential.js b/packages/needs-updating/gorgias/models/credential.js deleted file mode 100644 index 3de01b2..0000000 --- a/packages/needs-updating/gorgias/models/credential.js +++ /dev/null @@ -1,26 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - subdomain: { - type: String, - trim: true, - }, - accessTokenExpire: {type: Date}, - expires_at: {type: Date}, -}); - -const name = 'GorgiasCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/gorgias/models/entity.js b/packages/needs-updating/gorgias/models/entity.js deleted file mode 100644 index e88ab23..0000000 --- a/packages/needs-updating/gorgias/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'GorgiasEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/huggg/index.js b/packages/needs-updating/huggg/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/huggg/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/huggg/jest-setup.js b/packages/needs-updating/huggg/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/huggg/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/huggg/jest-teardown.js b/packages/needs-updating/huggg/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/huggg/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/huggg/manager.js b/packages/needs-updating/huggg/manager.js deleted file mode 100644 index e09fda3..0000000 --- a/packages/needs-updating/huggg/manager.js +++ /dev/null @@ -1,73 +0,0 @@ -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const AuthFields = require('./authFields'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - const hugggParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - let credential = await instance.credentialMO.get( - instance.entity.credential - ); - hugggParams.access_token = credential.access_token; - hugggParams.refresh_token = credential.refresh_token; - hugggParams.username = credential.username; - hugggParams.password = credential.password; - } else if (params.credentialId) { - let credential = await instance.credentialMO.get( - params.credentialId - ); - hugggParams.access_token = credential.access_token; - hugggParams.refresh_token = credential.refresh_token; - hugggParams.username = credential.username; - hugggParams.password = credential.password; - } - instance.api = await new Api(hugggParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: null, - type: ModuleConstants.authType.basic, - data: { - fields: AuthFields.hugggAuthorizationFields, //TODO Let's refactor to use JSON Schema - }, - }; - } - - async processAuthorizationCallback(params) { - await this.api.getTokenFromClientCredentials(); - this.api.username = get(params, 'username'); - this.api.password = get(params, 'password'); - await this.api.getTokenFromUsernamePassword(); - //TODO add async testAuth() - - const credentials = await this.credentialMO.list({user: this.userId}); - const entitySearch = await this.entityMO.list({user: this.userId}); - let entity; - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/huggg/models/credential.js b/packages/needs-updating/huggg/models/credential.js deleted file mode 100644 index 35ba7a3..0000000 --- a/packages/needs-updating/huggg/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'HugggCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/huggg/models/entity.js b/packages/needs-updating/huggg/models/entity.js deleted file mode 100644 index b8878b1..0000000 --- a/packages/needs-updating/huggg/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'HugggEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/marketo/index.js b/packages/needs-updating/marketo/index.js deleted file mode 100644 index 7691d95..0000000 --- a/packages/needs-updating/marketo/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./credential'); -const {Entity} = require('./entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/marketo/jest-setup.js b/packages/needs-updating/marketo/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/marketo/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/marketo/jest-teardown.js b/packages/needs-updating/marketo/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/marketo/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/marketo/manager.js b/packages/needs-updating/marketo/manager.js deleted file mode 100644 index 126aa38..0000000 --- a/packages/needs-updating/marketo/manager.js +++ /dev/null @@ -1,218 +0,0 @@ -const {ModuleManager} = require('@friggframework/core'); -const {Api} = require('./api.js'); -const {Entity} = require('./entity'); -const {Credential} = require('./credential.js'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Credential = Credential; - static Entity = Entity; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params = {}) { - // if there's an entityId, retrieving the entity and retrieving the credential connected to the entity, then passing whatever is needed into the args for the API - // if there's a credentialId, retrieving the credential and passing those as args for the API constructor (default to entity... so an else if here) - // Instantiating the API class with the args passed in, appending it to the instance of the manager (instance.api), and returning the instance. - // (Noting for posterity... this is definitely based on a different module that uses a tangled concept... should be cleaner and hence the refactor you'll see in the crossbeam API module. Also may benefit from just removing altogether from the Manager class and making it part of the ModuleManager base class) - - const instance = new Manager(params); - - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - - if (!instance.entity) { - throw new Error(`No entity exists with ID ${params.entityId}`); - } - - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - - if (!instance.credential) { - throw new Error('Marketo credentials not found'); - } - } - - instance.api = new Api({ - delegate: instance, - munchkin_id: instance.entity?.munchkin_id, - client_id: instance.credential?.client_id, - client_secret: instance.credential?.client_secret, - }); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - type: 'Form', - data: { - jsonSchema: { - title: 'Authorization Credentials', - description: 'A simple form example.', - type: 'object', - required: ['munchkin_id', 'services'], - properties: { - munchkin_id: { - type: 'string', - title: 'Please enter your Munchkin ID.', - }, - services: { - type: 'array', - title: 'Services', - items: { - type: 'object', - properties: { - name: { - type: 'string', - title: 'Please enter the name of the service.', - }, - client_id: { - type: 'string', - title: 'Please enter the client_id for the service.', - }, - client_secret: { - type: 'string', - title: 'Please enter the client_secret for the service.', - }, - }, - required: [ - 'name', - 'client_id', - 'client_secret', - ], - }, - }, - }, - }, - uiSchema: { - 'ui:order': ['munchkin_id', 'services'], - munchkin_id: { - 'ui:help': 'The Munchkin ID for the Marketo account', - }, - services: { - 'ui:help': 'Please add 1 or more services', - }, - }, - }, - }; - } - - async processAuthorizationCallback(params) { - // Update the API class (as needed) with params.data to do whatever is needed to get an authenticated request to the API. For OAuth this means generateTokenFromCode, for your client_credentials grant it means a slide modification. - - const {munchkin_id, services} = params.data; - const created = []; - - for (const service of services) { - const {service_name, client_id, client_secret} = service; - - const credential = await this.credentialMO.upsert( - { - user: this.userId, - client_id, - }, - { - client_secret, - } - ); - - const entity = await this.entityMO.upsert( - { - munchkin_id, - externalId: client_id, - user: this.userId, - }, - { - name: service_name, - credential: credential._id, - } - ); - - created.push({entity, credential}); - } - - // TODO how to pick one? - const {entity, credential} = created[0]; - - this.api.munchkin_id = entity.munchkin_id; - this.api.client_id = credential.client_id; - this.api.client_secret = credential.client_secret; - - // testAuth to confirm valid credentials - await this.testAuth(); - - // If the Entity : Credential relationship is 1:1, then searchOrCreateEntity. In some cases, this means using an API request to look up the externalId and name for the Entity (typically an "Account ID" and "Account Name"), in other cases you'll have this info as part of the params.data object. - - // Return the credential_id, entity_id (null if not available yet), and type (just the Manager Name) as an object. - return { - type: Manager.getName(), - entity_id: entity._id, - credential_id: credential._id, - }; - } - - async testAuth() { - await this.api.refreshAuth(); - - const response = await this.api.describeLeads(); - - if (this.api.checkExpired(response)) { - throw new Error('Not authenticated to Marketo'); - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // Wipe API connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (!(notifier instanceof Api)) return; - - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } else if (delegateString === this.api.DLGT_INVALID_AUTH) { - const credentials = await this.credentialMO.list({ - user: this.userId, - }); - if (credentials.length === 1) { - return (this.credential = this.credentialMO.update( - credentials[0]._id, - {auth_is_valid: false} - )); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } else if (credentials.length === 0) { - throw new Error( - 'How are we marking nonexistant credentials invalid???' - ); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/monday/CHANGELOG.md b/packages/needs-updating/monday/CHANGELOG.md deleted file mode 100644 index 23a97f6..0000000 --- a/packages/needs-updating/monday/CHANGELOG.md +++ /dev/null @@ -1,210 +0,0 @@ -# v0.10.0 (Wed Mar 20 2024) - -:tada: This release contains work from new contributors! :tada: - -Thanks for all your work! - -:heart: Nicolas Leal ([@nicolasmelo1](https://github.com/nicolasmelo1)) - -:heart: nmilcoff ([@nmilcoff](https://github.com/nmilcoff)) - -#### 🚀 Enhancement - -#### 🐛 Bug Fix - -- correct some bad automated edits, though they are not in relevant - files ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 4 - -- [@MichaelRyanWebber](https://github.com/MichaelRyanWebber) -- Nicolas Leal ([@nicolasmelo1](https://github.com/nicolasmelo1)) -- nmilcoff ([@nmilcoff](https://github.com/nmilcoff)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.9.0 (Wed Sep 06 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.26 (Thu Jun 08 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.25 (Thu May 25 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.24 (Tue Apr 04 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.23 (Tue Feb 21 2023) - -#### 🐛 Bug Fix - -- Merge branch 'main' into hubspot-updates ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.21 (Tue Jan 31 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.19 (Wed Jan 11 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.18 (Tue Jan 10 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, Jonathan O'Donnell ([@joncodo](https://github.com/joncodo)), for all your work! - -#### 🐛 Bug Fix - -- Merge branch 'main' of github.com:friggframework/frigg into doc-updates ([@joncodo](https://github.com/joncodo)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Jonathan O'Donnell ([@joncodo](https://github.com/joncodo)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.17 (Mon Jan 09 2023) - -#### 🐛 Bug Fix - -- Merge remote-tracking branch 'origin/main' into - gitbook-updates [#48](https://github.com/friggframework/frigg/pull/48) ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) -- A lot of changes all rolled into - one [#21](https://github.com/friggframework/frigg/pull/21) ([@seanspeaks](https://github.com/seanspeaks)) -- Updated API modules with support for sls offline, and made sure optional chaining with discriminators was in - place ([@seanspeaks](https://github.com/seanspeaks)) -- Fixing dependencies across all API Modules ([@seanspeaks](https://github.com/seanspeaks)) -- More import issues (Exports are named objects, imports needed to object - destructure) ([@seanspeaks](https://github.com/seanspeaks)) -- Updates to API Modules for proper export/imports ([@seanspeaks](https://github.com/seanspeaks)) -- Continued updating of references and refactoring API modules ([@seanspeaks](https://github.com/seanspeaks)) -- Merge remote-tracking branch 'origin/main' into - simplify-mongoose-models ([@seanspeaks](https://github.com/seanspeaks)) -- Update all api modules to use module-plugin models ([@seanspeaks](https://github.com/seanspeaks)) -- Add READMEs for all packages and - api-modules [#20](https://github.com/friggframework/frigg/pull/20) ([@seanspeaks](https://github.com/seanspeaks)) -- Add READMEs for all packages and api-modules ([@seanspeaks](https://github.com/seanspeaks)) - -#### ⚠️ Pushed to `main` - -- Merge branch 'main' into gitbook-updates ([@seanspeaks](https://github.com/seanspeaks)) -- Finish initial formatting and publishing of all modules ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.14 (Tue Dec 06 2022) - -#### 🐛 Bug Fix - -- fix modules to - @friggframework [#74](https://github.com/friggframework/frigg/pull/74) ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) -- Sheehan Toufiq Khan ([@sheehantoufiq](https://github.com/sheehantoufiq)) - ---- - -# v0.8.11 (Mon Sep 19 2022) - -#### 🐛 Bug Fix - -- Test environment setup for all - modules [#49](https://github.com/friggframework/frigg/pull/49) ([@seanspeaks](https://github.com/seanspeaks)) -- Test environment setup for all modules ([@seanspeaks](https://github.com/seanspeaks)) -- Merge remote-tracking branch 'origin/main' into - gitbook-updates [#48](https://github.com/friggframework/frigg/pull/48) ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.10 (Thu Sep 01 2022) - -#### 🐛 Bug Fix - -- version bumped to address tag - issue [#43](https://github.com/friggframework/frigg/pull/43) ([@seanspeaks](https://github.com/seanspeaks)) -- version bumped ([@seanspeaks](https://github.com/seanspeaks)) -- Publish ([@seanspeaks](https://github.com/seanspeaks)) -- Add nx and - licenses [#37](https://github.com/friggframework/frigg/pull/37) ([@seanspeaks](https://github.com/seanspeaks)) -- MIT to all packages ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) diff --git a/packages/needs-updating/monday/README.md b/packages/needs-updating/monday/README.md deleted file mode 100644 index 41a6dc0..0000000 --- a/packages/needs-updating/monday/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# monday - -This is the API Module for monday that allows the [Frigg](https://friggframework.org) code to talk to the monday API. - -Read more on the [Frigg documentation site](https://docs.friggframework.org/api-modules/list/monday \ No newline at end of file diff --git a/packages/needs-updating/monday/api.js b/packages/needs-updating/monday/api.js deleted file mode 100644 index 34a80e7..0000000 --- a/packages/needs-updating/monday/api.js +++ /dev/null @@ -1,133 +0,0 @@ -const {get, OAuth2Requester} = require('@friggframework/core'); -const mondaySdk = require('monday-sdk-js'); - -const monday = mondaySdk(); - -class Api extends OAuth2Requester { - constructor(params) { - super(params); - - this.client_id = process.env.MONDAY_CLIENT_ID; - this.client_secret = process.env.MONDAY_CLIENT_SECRET; - this.redirect_uri = `${process.env.REDIRECT_URI}/monday`; - this.scopes = process.env.MONDAY_SCOPES; - - this.authorizationUri = encodeURI( - `https://auth.monday.com/oauth2/authorize?state=app:MONDAY&client_id=${this.client_id}&response_type=code&scope=${this.scopes}&redirect_uri=${this.redirect_uri}` - ); - this.tokenUri = 'https://auth.monday.com/oauth2/token'; - - this.access_token = get(params, 'access_token', null); - this.refresh_token = get(params, 'refresh_token', null); - if (this.access_token) { - monday.setToken(this.access_token); - } - } - - async setTokens(params) { - await super.setTokens(params); - monday.setToken(this.access_token); - } - - async getAccount() { - return monday.api('{account {id, name}}'); - } - - async query(params) { - const query = get(params, 'query'); - const options = get(params, 'options', {}); - return monday.api(query, options); - } - - async getBoards() { - return monday.api('{boards {name, id}}'); - } - - async createColumn(params) { - const boardId = get(params, 'boardId'); - const title = get(params, 'title'); - const type = get(params, 'type'); - - const query = `mutation { - create_column (board_id: ${boardId}, title: ${JSON.stringify( - title - )}, column_type: ${type}) { - id, title }}`; - const res = await monday.api(query); - return res.data.create_column; - } - - // TODO this is too specific to the Crossbeam integration. Need to make it a bit more generic, - // or describe how it's intended to be helpful - async createItem(params) { - const boardId = get(params, 'boardId'); - const itemName = get(params, 'itemName'); - const columnValues = get(params, 'columnValues'); - - const columnIdQuery = `query {boards (ids: ${boardId}){columns{id title type}}}`; - const columnResponse = await monday.api(columnIdQuery); - const {columns} = columnResponse.data.boards[0]; - const newColumnValues = {}; - Object.keys(columnValues).map((value) => { - const foundColumn = columns.find((col) => col.title === value); - if (foundColumn) - newColumnValues[foundColumn.id] = columnValues[value]; - }); - - const query = `mutation { - create_item (board_id: ${boardId}, item_name: ${JSON.stringify( - itemName - )}, column_values: ${JSON.stringify(JSON.stringify(newColumnValues))}) { - id }}`; - return monday.api(query); - } - - async updateItem(params) { - const boardId = get(params, 'boardId'); - const itemId = get(params, 'itemId'); - const columnValues = get(params, 'columnValues'); - - const columnIdQuery = `query {boards (ids: ${boardId}){columns{id title type}}}`; - const columnResponse = await monday.api(columnIdQuery); - const {columns} = columnResponse.data.boards[0]; - const newColumnValues = {}; - Object.keys(columnValues).map((value) => { - const foundColumn = columns.find((col) => col.title === value); - if (foundColumn) - newColumnValues[foundColumn.id] = columnValues[value]; - }); - - const query = `mutation { - change_multiple_column_values (board_id: ${boardId}, item_id: ${itemId}, column_values: ${JSON.stringify( - JSON.stringify(newColumnValues) - )}) { - id }}`; - return monday.api(query); - } - - async createBoard(params) { - const name = get(params, 'name'); - const kind = get(params, 'kind'); - - const query = - 'mutation ($boardName: String! $boardKind: BoardKind!) { create_board (board_name: $boardName, board_kind: $boardKind) {id, name}}'; - const options = { - variables: { - boardName: name, - boardKind: kind, - }, - }; - const res = await monday.api(query, options); - return res.data.create_board; - } - - async getBoardColumns(params) { - const boardId = get(params, 'boardId'); - const columnIdQuery = `query {boards (ids: ${boardId}){columns{id title type}}}`; - const columnResponse = await monday.api(columnIdQuery); - const {columns} = columnResponse.data.boards[0]; - return columns; - } -} - -module.exports = {Api}; diff --git a/packages/needs-updating/monday/defaultConfig.json b/packages/needs-updating/monday/defaultConfig.json deleted file mode 100644 index 9590881..0000000 --- a/packages/needs-updating/monday/defaultConfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "monday", - "label": "monday", - "productUrl": "https://monday.com", - "apiDocs": "https://developer.monday.com", - "logoUrl": "https://friggframework.org/assets/img/mondaycom-icon.jpeg", - "categories": [ - "Project Management", - "Work OS" - ], - "description": "monday.com" -} diff --git a/packages/needs-updating/monday/index.js b/packages/needs-updating/monday/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/monday/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/monday/jest-setup.js b/packages/needs-updating/monday/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/monday/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/monday/jest-teardown.js b/packages/needs-updating/monday/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/monday/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/monday/manager.js b/packages/needs-updating/monday/manager.js deleted file mode 100644 index ae81a7f..0000000 --- a/packages/needs-updating/monday/manager.js +++ /dev/null @@ -1,180 +0,0 @@ -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential.js'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - let instance = new this(params); - - // initializes the Api - const mondayParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - if (instance.entity.credential) { - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - mondayParams.access_token = instance.credential.access_token; - mondayParams.refresh_token = instance.credential.refresh_token; - } - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - mondayParams.access_token = instance.credential.access_token; - mondayParams.refresh_token = instance.credential.refresh_token; - } - instance.api = await new Api(mondayParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: await this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - - // Gotta search for credential since it's not returned by the functions - let credentials = await this.credentialMO.list({user: this.userId}); - - // TODO schema? - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - const accountDetails = await this.api.getAccount(); - - const {id: accountId, name: accountName} = - accountDetails.data.account; - const entity = await this.findOrCreateEntity({ - accountName, - accountId, - credentialId: credentials[0].id, - }); - - this.credential = credentials[0]; - this.entity = entity; - - return { - // id: entity.id, - credential_id: credentials[0].id, - entity_id: entity.id, - type: Manager.getName(), - }; - } - - async testAuth() { - await this.api.getAccount(); - } - - async getEntityOptions() { - let options = []; - return options; - } - - async findOrCreateEntity(params) { - const accountId = get(params, 'accountId'); - const accountName = get(params, 'accountName'); - const credentialId = get(params, 'credentialId'); - - let search = await this.entityMO.list({ - user: this.userId, - externalId: accountId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - let createObj = { - credential: credentialId, - user: this.userId, - name: accountName, - externalId: accountId, - }; - return this.entityMO.create(createObj); - } else if (search.length === 1) { - return search[0]; - } else { - throw new Error( - `Multiple entities found with the same organization ID: ${data.organization_id}` - ); - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId, - access_token: this.api.access_token, - }; - - let credentials = await this.credentialMO.list({ - user: this.userId, - }); - let credential; - if (credentials.length === 1) { - credential = credentials[0]; - } else if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential._id, - updatedToken - ); - } - // await this.entityMO.update(entity.id, { credential }); - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/monday/models/credential.js b/packages/needs-updating/monday/models/credential.js deleted file mode 100644 index 741ee75..0000000 --- a/packages/needs-updating/monday/models/credential.js +++ /dev/null @@ -1,15 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'MondayCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/monday/models/entity.js b/packages/needs-updating/monday/models/entity.js deleted file mode 100644 index b38efc6..0000000 --- a/packages/needs-updating/monday/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'MondayEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/monday/test/Api.test.js b/packages/needs-updating/monday/test/Api.test.js deleted file mode 100644 index a1ae6a5..0000000 --- a/packages/needs-updating/monday/test/Api.test.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @group interactive - */ - -require('../../../../test/utils/TestUtils'); - -const Authenticator = require('../../../../test/utils/Authenticator'); -const MondayApiClass = require('../api.js'); - -describe('Monday API', () => { - let testContext; - - beforeEach(() => { - testContext = {}; - }); - - const mondayApi = new MondayApiClass(); - beforeAll(async () => { - const url = mondayApi.authorizationUri; - const response = await Authenticator.oauth2(url); - const baseArr = response.base.split('/'); - response.entityType = baseArr[baseArr.length - 1]; - delete response.base; - - const token = await mondayApi.getTokenFromCode(response.data.code); - }); - - describe('Get Account Info', () => { - it('should get account info', async () => { - const response = await mondayApi.getAccount(); - expect(response.data).toHaveProperty('account'); - expect(response.data).toHaveProperty('account.id'); - expect(response.data).toHaveProperty('account.name'); - return response; - }); - }); - - describe('Get List of Boards', () => { - it('should get board data', async () => { - const response = await mondayApi.getBoards(); - expect(response).toHaveProperty('account_id'); - expect(response.data).toHaveProperty('boards'); - expect(response.data).toHaveProperty('boards[0].name'); - - // not sure what this does or why it's important to the test.. - // but it keeps failing, so commenting out - // await mondayApi.query({ query: '{boards { name }}' }); - - return response; - }); - }); - - describe('Board Creation', () => { - it('should get create a board', async () => { - const query = - 'mutation ($boardName: String!) { create_board (board_name: $boardName, board_kind: public) {id, name}}'; - const accountBoard = await mondayApi.query({ - query, - options: {variables: {boardName: 'Test Board'}}, - }); - testContext.boardId = accountBoard.data.create_board.id; - }); - - it('should add columns to the created board', async () => { - const columns = [ - {title: 'First Name', type: 'text'}, - {title: 'Last Name', type: 'text'}, - {title: 'Email', type: 'text'}, - {title: 'Title', type: 'text'}, - {title: 'partner', type: 'text'}, - {title: 'partner_population', type: 'text'}, - {title: 'population', type: 'text'}, - ]; - - for (const column of columns) { - const res = await mondayApi.createColumn({ - title: column.title, - type: column.type, - boardId: testContext.boardId, - }); - } - }); - it('should add items to the created board', async () => { - const items = [ - { - itemName: 'Sean Matthews', - columnValues: { - 'Last Name': 'Matthews', - }, - }, - { - itemName: 'Nicole Charest', - columnValues: { - 'Last Name': 'Charest', - }, - }, - ]; - - for (const item of items) { - const res = await mondayApi.createItem({ - itemName: item.itemName, - columnValues: item.columnValues, - boardId: testContext.boardId, - }); - } - }); - - it('should delete/clean up the created board', async () => { - }); - }); -}); diff --git a/packages/needs-updating/monday/test/Manager.test.js b/packages/needs-updating/monday/test/Manager.test.js deleted file mode 100644 index 0db90ca..0000000 --- a/packages/needs-updating/monday/test/Manager.test.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @group interactive - */ - -require('../../../../test/utils/TestUtils'); -const chai = require('chai'); - -const {expect} = chai; -const chaiAsPromised = require('chai-as-promised'); -chai.use(require('chai-url')); - -chai.use(chaiAsPromised); - -const Authenticator = require('../../../../test/utils/Authenticator'); -const MondayManager = require('../../../managers/entities/MondayManager.js'); -const TestUtils = require('../../../../test/utils/TestUtils'); - -describe.skip('Monday Manager', () => { - let mondayManager; - let authorizeUrl; - beforeAll(async () => { - this.userManager = await TestUtils.getLoggedInTestUserManagerInstance(); - // TODO verify instance with API class associated - mondayManager = await MondayManager.getInstance({ - userId: this.userManager.getUserId(), - }); - - const res = await mondayManager.getAuthorizationRequirements(); - - chai.assert.hasAnyKeys(res, ['url', 'type']); - authorizeUrl = res.url; - - const response = await Authenticator.oauth2(authorizeUrl); - const baseArr = response.base.split('/'); - response.entityType = baseArr[baseArr.length - 1]; - delete response.base; - - const ids = await mondayManager.processAuthorizationCallback({ - userId: this.userManager.getUserId(), - data: response.data, - }); - - // TODO Should not be empty (any key) - chai.assert.hasAllKeys(ids, ['credential_id', 'entity_id', 'type']); - }); - - it('Should get Auth Requirements, go through OAuth Flow, and processAuthorizationCallback', async () => { - // Hope the before works! - }); - - it('should reinstantiate with an entity ID', async () => { - const newManager = await MondayManager.getInstance({ - userId: this.userManager.getUserId(), - entityId: mondayManager.entity._id, - }); - newManager.api.access_token.should.equal( - mondayManager.api.access_token - ); - // newManager.api.refresh_token.should.equal(mondayManager.api.refresh_token); - // newManager.api.organization_id.should.equal(mondayManager.api.organization_id); - newManager.entity._id - .toString() - .should.equal(mondayManager.entity._id.toString()); - newManager.credential._id - .toString() - .should.equal(mondayManager.credential._id.toString()); - }); - - it('should reinstantiate with a credential ID', async () => { - const newManager = await MondayManager.getInstance({ - userId: this.userManager.getUserId(), - credentialId: mondayManager.credential._id, - }); - newManager.api.access_token.should.equal( - mondayManager.api.access_token - ); - // newManager.api.refresh_token.should.equal(mondayManager.api.refresh_token); - newManager.credential._id - .toString() - .should.equal(mondayManager.credential._id.toString()); - }); -}); diff --git a/packages/needs-updating/netx/index.js b/packages/needs-updating/netx/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/netx/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/netx/jest-setup.js b/packages/needs-updating/netx/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/netx/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/netx/jest-teardown.js b/packages/needs-updating/netx/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/netx/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/netx/manager.js b/packages/needs-updating/netx/manager.js deleted file mode 100644 index fd4b2a5..0000000 --- a/packages/needs-updating/netx/manager.js +++ /dev/null @@ -1,133 +0,0 @@ -const {ModuleManager} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); - -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - const apiParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.access_token = credential.access_token; - apiParams.id_token = credential.id_token; - apiParams.expires_in = credential.accessExpiresIn; - } - - instance.api = await new Api(apiParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: 'oauth2', - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - const userDetails = await this.api.getTokenIdentity(); - - let credentials = await this.credentialMO.list({user: this.userId}); - - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - let entity = await this.entityMO.getByUserId(this.userId); - - if (!entity) { - entity = await this.entityMO.create({ - user: this.userId, - credential: credentials[0]._id, - externalId: userDetails.companyId, - name: userDetails.companyName, - }); - } - - return { - credential_id: credentials[0]._id, - entity_id: entity._id, - type: Manager.getName(), - }; - } - - async testAuth() { - await this.api.getTokenIdentity(); - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId, - access_token: this.api.access_token, - id_token: this.api.id_token, - // expires_in: this.api.accessExpiresIn, - auth_is_valid: true, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] === null && delete updatedToken[k] - ); - - let credential = await this.entityMO.getByUserId(this.userId); - - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/netx/models/credential.js b/packages/needs-updating/netx/models/credential.js deleted file mode 100644 index 26a0ad6..0000000 --- a/packages/needs-updating/netx/models/credential.js +++ /dev/null @@ -1,15 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'NetXCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/netx/models/entity.js b/packages/needs-updating/netx/models/entity.js deleted file mode 100644 index 7983f50..0000000 --- a/packages/needs-updating/netx/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'NetXEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/outreach/index.js b/packages/needs-updating/outreach/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/outreach/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/outreach/jest-setup.js b/packages/needs-updating/outreach/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/outreach/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/outreach/jest-teardown.js b/packages/needs-updating/outreach/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/outreach/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/outreach/manager.js b/packages/needs-updating/outreach/manager.js deleted file mode 100644 index 2715ca8..0000000 --- a/packages/needs-updating/outreach/manager.js +++ /dev/null @@ -1,183 +0,0 @@ -const { - ModuleManager, - ModuleConstants, - flushDebugLog, - debug -} = require('@friggframework/core'); -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - const oParams = {delegate: instance}; - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - oParams.access_token = instance.credential.accessToken; - oParams.refresh_token = instance.credential.refreshToken; - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - oParams.access_token = instance.credential.accessToken; - oParams.refresh_token = instance.credential.refreshToken; - } - instance.api = await new Api(oParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.getUser()) validAuth = true; - } catch (e) { - await this.markCredentialsInvalid(); - flushDebugLog(e); - } - return validAuth; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - await this.api.getTokenFromCode(code); - await this.testAuth(); - - const userProfile = await this.api.getUser(); - await this.findOrCreateEntity({ - org_uuid: userProfile.org_uuid, - org_name: userProfile.org_name, - }); - - return { - credential_id: this.credential.id, - entity_id: this.entity.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const org_uuid = get(params, 'org_uuid'); - const org_name = get(params, 'org_name'); - - const search = await this.entityMO.list({ - user: this.userId, - externalId: org_uuid, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name: org_name, - externalId: org_uuid, - }; - this.entity = await this.entityMO.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug('Multiple entities found with the same Org ID:', org_uuid); - } - - return { - entity_id: this.entity.id, - }; - } - - async deauthorize() { - this.api = new Api(); - - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const userProfile = await this.api.getUser(); - const updatedToken = { - user: this.userId, - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - accessTokenExpire: this.api.accessTokenExpire, - externalId: userProfile.user_id, - auth_is_valid: true, - }; - - if (!this.credential) { - const credentialSearch = await this.credentialMO.list({ - externalId: userProfile.user_id, - }); - if (credentialSearch.length === 0) { - this.credential = await this.credentialMO.create( - updatedToken - ); - } else if (credentialSearch.length === 1) { - if ( - credentialSearch[0].user.toString() === this.userId - ) { - this.credential = await this.credentialMO.update( - credentialSearch[0], - updatedToken - ); - } else { - debug( - 'Somebody else already created a credential with the same User ID:', - userProfile.user_id - ); - } - } else { - // Handling multiple credentials found with an error for the time being - debug( - 'Multiple credentials found with the same User ID:', - userProfile.user_id - ); - } - } else { - this.credential = await this.credentialMO.update( - this.credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/outreach/models/credential.js b/packages/needs-updating/outreach/models/credential.js deleted file mode 100644 index bcebf2a..0000000 --- a/packages/needs-updating/outreach/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'OutreachCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/outreach/models/entity.js b/packages/needs-updating/outreach/models/entity.js deleted file mode 100644 index b851e96..0000000 --- a/packages/needs-updating/outreach/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'OutreachEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/personio/index.js b/packages/needs-updating/personio/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/personio/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/personio/jest-setup.js b/packages/needs-updating/personio/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/personio/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/personio/jest-teardown.js b/packages/needs-updating/personio/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/personio/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/personio/manager.js b/packages/needs-updating/personio/manager.js deleted file mode 100644 index 438df9c..0000000 --- a/packages/needs-updating/personio/manager.js +++ /dev/null @@ -1,147 +0,0 @@ -// Scaffolded from -const _ = require('lodash'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const AuthFields = require('./authFields'); - -const MANAGER_NAME = 'Personio'; - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return MANAGER_NAME; - } - - static async getInstance(params) { - const instance = new this(params); - - let personioParams; - - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - if (instance.entity.credential) { - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - personioParams = { - clientId: instance.credential.clientId, - clientSecret: instance.credential.clientSecret, - companyId: instance.credential.companyId, - accessToken: instance.credential.accessToken, - subdomain: instance.credential.subdomain, - }; - } - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - personioParams = { - clientId: instance.credential.clientId, - clientSecret: instance.credential.clientSecret, - companyId: instance.credential.companyId, - accessToken: instance.credential.accessToken, - subdomain: instance.credential.subdomain, - }; - } - if (personioParams) { - instance.api = await new Api(personioParams); - } - - return instance; - } - - async getAuthorizationRequirements(params) { - // see parent docs. only use these three top level keys - return { - url: null, - type: ModuleConstants.authType.apiKey, - data: { - jsonSchema: AuthFields.jsonSchema, - uiSchema: AuthFields.uiSchema, - }, - }; - } - - async processAuthorizationCallback(params) { - const clientId = get(params.data, 'clientId'); - const clientSecret = get(params.data, 'clientSecret'); - const companyId = get(params.data, 'companyId'); - const accessToken = get(params.data, 'accessToken'); - const subdomain = get(params.data, 'subdomain'); - this.api = new Api({ - clientId, - clientSecret, - companyId, - accessToken, - subdomain, - }); - const userDetails = await this.api.getUserDetails(); - - const byUserId = {user: this.userId}; - const credentials = await this.credentialMO.list(byUserId); - - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - const credential = await this.credentialMO.upsert(byUserId, { - user: this.userId, - client_id: clientId, - client_secret: clientSecret, - company_id: companyId, - access_token: accessToken, - subdomain: subdomain, - }); - - const byUserIdAndCredential = { - ...byUserId, - credential: credential.id, - }; - const entity = await this.entityMO.upsert(byUserIdAndCredential, { - user: this.userId, - credential: credential.id, - name: userDetails.user.username, - externalId: userDetails.user.id, - }); - - return { - entity_id: entity.id, - credential_id: credential.id, - type: Manager.getName(), - }; - } - - async testAuth() { - // TODO - this method doesn't exist in API - await this.api.getUserDetails(); - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/personio/models/credential.js b/packages/needs-updating/personio/models/credential.js deleted file mode 100644 index eb05ac6..0000000 --- a/packages/needs-updating/personio/models/credential.js +++ /dev/null @@ -1,15 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'PersonioCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/personio/models/entity.js b/packages/needs-updating/personio/models/entity.js deleted file mode 100644 index be29240..0000000 --- a/packages/needs-updating/personio/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'PersonioEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/pipedrive/README.md b/packages/needs-updating/pipedrive/README.md deleted file mode 100644 index 0ae7cf3..0000000 --- a/packages/needs-updating/pipedrive/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# pipedrive - -This is the API Module for pipedrive that allows the [Frigg](https://friggframework.org) code to talk to the pipedrive -API. - -Read more on the [Frigg documentation site](https://docs.friggframework.org/api-modules/list/pipedrive \ No newline at end of file diff --git a/packages/needs-updating/pipedrive/api.js b/packages/needs-updating/pipedrive/api.js deleted file mode 100644 index 6a3f9ce..0000000 --- a/packages/needs-updating/pipedrive/api.js +++ /dev/null @@ -1,121 +0,0 @@ -const {get, OAuth2Requester} = require('@friggframework/core'); - -class Api extends OAuth2Requester { - constructor(params) { - super(params); - - this.access_token = get(params, 'access_token', null); - this.refresh_token = get(params, 'refresh_token', null); - this.companyDomain = get(params, 'companyDomain', null); - this.baseURL = () => `${this.companyDomain}/api`; - - this.client_id = process.env.PIPEDRIVE_CLIENT_ID; - this.client_secret = process.env.PIPEDRIVE_CLIENT_SECRET; - this.redirect_uri = `${process.env.REDIRECT_URI}/pipedrive`; - this.scopes = process.env.PIPEDRIVE_SCOPES; - - this.URLs = { - activities: '/v1/activities', - activityFields: '/v1/activityFields', - activityById: (activityId) => `/v1/activities/${activityId}`, - getUser: '/v1/users/me', - users: '/v1/users', - deals: '/v1/deals', - }; - - this.authorizationUri = encodeURI( - `https://oauth.pipedrive.com/oauth/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&scope=${this.scopes}` - ); - - this.tokenUri = 'https://oauth.pipedrive.com/oauth/token'; - } - - async setTokens(params) { - await this.setCompanyDomain(params.api_domain); - return super.setTokens(params); - } - - async setCompanyDomain(companyDomain) { - this.companyDomain = companyDomain; - } - - // ************************** Deals ********************************** - async listDeals() { - const options = { - url: this.baseURL() + this.URLs.deals, - }; - const res = await this._get(options); - return res; - } - - // ************************** Activities ********************************** - async listActivityFields() { - const options = { - url: this.baseURL() + this.URLs.activityFields, - }; - const res = await this._get(options); - return res; - } - - async listActivities(params) { - const options = { - url: this.baseURL() + this.URLs.activities, - }; - if (params.query) { - options.query = params.query; - } - const res = await this._get(options); - return res; - } - - async deleteActivity(activityId) { - const options = { - url: this.baseURL() + this.URLs.activityById(activityId), - }; - const res = await this._delete(options); - return res; - } - - async updateActivity(activityId, task) { - const options = { - url: this.baseURL() + this.URLs.activityById(activityId), - body: task, - }; - const res = await this._patch(options); - return res; - } - - async createActivity(params) { - const dealId = get(params, 'dealId', null); - const subject = get(params, 'subject'); - const type = get(params, 'type'); - const options = { - url: this.baseURL() + this.URLs.activities, - body: {...params}, - headers: { - 'Content-Type': 'application/json', - }, - }; - const res = await this._post(options); - return res; - } - - // ************************** Users ********************************** - async getUser() { - const options = { - url: this.baseURL() + this.URLs.getUser, - }; - const res = await this._get(options); - return res; - } - - async listUsers() { - const options = { - url: this.baseURL() + this.URLs.users, - }; - const res = await this._get(options); - return res; - } -} - -module.exports = {Api}; diff --git a/packages/needs-updating/pipedrive/index.js b/packages/needs-updating/pipedrive/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/pipedrive/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/pipedrive/jest-setup.js b/packages/needs-updating/pipedrive/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/pipedrive/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/pipedrive/jest-teardown.js b/packages/needs-updating/pipedrive/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/pipedrive/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/pipedrive/manager.js b/packages/needs-updating/pipedrive/manager.js deleted file mode 100644 index eef514c..0000000 --- a/packages/needs-updating/pipedrive/manager.js +++ /dev/null @@ -1,193 +0,0 @@ -const { - ModuleManager, - ModuleConstants, - flushDebugLog, - debug -} = require('@friggframework/core'); -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static - Entity = Entity; - - static - Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static - async getInstance(params) { - const instance = new this(params); - - const apiParams = {delegate: instance}; - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - } - if (instance.entity?.credential) { - apiParams.access_token = instance.credential.accessToken; - apiParams.refresh_token = instance.credential.refreshToken; - apiParams.companyDomain = instance.credential.companyDomain; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.getUser()) validAuth = true; - } catch (e) { - await this.markCredentialsInvalid(); - flushDebugLog(e); - } - return validAuth; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - await this.api.getTokenFromCode(code); - await this.testAuth(); - - const userProfile = await this.api.getUser(); - await this.findOrCreateEntity({ - companyId: userProfile.data.company_id, - companyName: userProfile.data.company_name, - }); - - return { - credential_id: this.credential.id, - entity_id: this.entity.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const companyId = get(params, 'companyId'); - const companyName = get(params, 'companyName'); - - const search = await this.entityMO.list({ - user: this.userId, - externalId: companyId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name: companyName, - externalId: companyId, - }; - this.entity = await this.entityMO.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug( - 'Multiple entities found with the same Company ID:', - companyId - ); - } - - return { - entity_id: this.entity.id, - }; - } - - async deauthorize() { - this.api = new Api(); - - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const userProfile = await this.api.getUser(); - const pipedriveUserId = userProfile.data.id; - const updatedToken = { - user: this.userId, - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - accessTokenExpire: this.api.accessTokenExpire, - externalId: pipedriveUserId, - companyDomain: object.api_domain, - auth_is_valid: true, - }; - - if (!this.credential) { - let credentialSearch = await this.credentialMO.list({ - externalId: pipedriveUserId, - }); - if (credentialSearch.length === 0) { - this.credential = await this.credentialMO.create( - updatedToken - ); - } else if (credentialSearch.length === 1) { - if ( - credentialSearch[0].user.toString() === - this.userId.toString() - ) { - this.credential = await this.credentialMO.update( - credentialSearch[0], - updatedToken - ); - } else { - debug( - 'Somebody else already created a credential with the same User ID:', - pipedriveUserId - ); - } - } else { - // Handling multiple credentials found with an error for the time being - debug( - 'Multiple credentials found with the same User ID:', - pipedriveUserId - ); - } - } else { - this.credential = await this.credentialMO.update( - this.credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/pipedrive/models/credential.js b/packages/needs-updating/pipedrive/models/credential.js deleted file mode 100644 index 29afde1..0000000 --- a/packages/needs-updating/pipedrive/models/credential.js +++ /dev/null @@ -1,21 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - companyDomain: {type: String}, -}); - -const name = 'PipedriveCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/pipedrive/models/entity.js b/packages/needs-updating/pipedrive/models/entity.js deleted file mode 100644 index 6e02de8..0000000 --- a/packages/needs-updating/pipedrive/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'PipedriveEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/qbo/index.js b/packages/needs-updating/qbo/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/qbo/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/qbo/jest-setup.js b/packages/needs-updating/qbo/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/qbo/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/qbo/jest-teardown.js b/packages/needs-updating/qbo/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/qbo/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/qbo/manager.js b/packages/needs-updating/qbo/manager.js deleted file mode 100644 index c83e696..0000000 --- a/packages/needs-updating/qbo/manager.js +++ /dev/null @@ -1,134 +0,0 @@ -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - // initializes the Api - const apiParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.accessToken = credential.accessToken; - apiParams.refreshToken = credential.refreshToken; - apiParams.accessTokenExpire = credential.accessTokenExpire; - apiParams.refreshTokenExpire = credential.refreshTokenExpire; - apiParams.realmId = credential.realmId; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.getAuthorizationUri(), - type: ModuleConstants.authType.oauth2, - }; - } - - async processAuthorizationCallback(params) { - const userId = get(params, 'userId'); - const data = get(params, 'data'); - const code = get(data, 'code'); - const realmId = get(data, 'realmId'); - - await this.getAccessToken(code, realmId); - const entity = await this.entityMO.getByUserId(this.userId); - return { - id: entity.id, - type: Manager.getName(), - }; - } - - //------------------------------------------------------------ - - checkUserAuthorized() { - return this.api.isAuthenticated(); - } - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - getOrCreateClient() { - } - - async getAccessToken(code, realmId) { - await this.api.getAccessToken(code, realmId); - } - - async sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - // todo update the database - const updatedToken = { - accessToken: this.api.accessToken, - refreshToken: this.api.refreshToken, - accessTokenExpire: this.api.accessTokenExpire, - refreshTokenExpire: this.api.refreshTokenExpire, - realmId: this.api.realmId, - }; - let entity = await this.entityMO.getByUserId(this.userId); - - if (!entity) { - entity = await this.entityMO.create({ - user: this.userId, - }); - } - let {credential} = entity; - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential, - updatedToken - ); - } - await this.entityMO.update(entity.id, {credential}); - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/qbo/models/credential.js b/packages/needs-updating/qbo/models/credential.js deleted file mode 100644 index be07098..0000000 --- a/packages/needs-updating/qbo/models/credential.js +++ /dev/null @@ -1,16 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - realmId: {type: String, required: true}, - accessToken: {type: String, trim: true, lhEncrypt: true}, - refreshToken: {type: String, trime: true, lhEncrypt: true}, - accessTokenExpire: {type: String}, - refreshTokenExpire: {type: String}, -}); - -const name = 'QBOCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/qbo/models/entity.js b/packages/needs-updating/qbo/models/entity.js deleted file mode 100644 index 1bb075d..0000000 --- a/packages/needs-updating/qbo/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'QBOEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/revio/index.js b/packages/needs-updating/revio/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/revio/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/revio/jest-setup.js b/packages/needs-updating/revio/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/revio/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/revio/jest-teardown.js b/packages/needs-updating/revio/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/revio/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/revio/manager.js b/packages/needs-updating/revio/manager.js deleted file mode 100644 index a83f1c7..0000000 --- a/packages/needs-updating/revio/manager.js +++ /dev/null @@ -1,178 +0,0 @@ -const moment = require('moment'); -const { - ModuleManager, - ModuleConstants -} = require('@friggframework/core'); -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const AuthFields = require('./authFields'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - apiParams.access_token = credential.accessToken; - apiParams.refresh_token = credential.refreshToken; - apiParams.subdomain = credential.subdomain; - apiParams.apiKey = credential.apiKey; - apiParams.apiUserEmail = credential.apiUserEmail; - } - - instance.api = await new Api({ - delegate: instance, - ...instance.credential, - }); - - return instance; - } - - async processAuthorizationCallback(params) { - // verify credentials - const username = get(params.data, 'username'); - const password = get(params.data, 'password'); - const client_code = get(params.data, 'client_code'); - - this.api = new Api({username, password, client_code}); - const billProfile = await this.api.getBillProfile(); // May have a 401 error we'll need to catch in the route for bad credentials - - // add credentials to db - let entity = await this.entityMO.getByUserId(this.userId); - if (!entity) { - entity = await this.entityMO.create({user: this.userId}); - } - - let {credential} = entity; - if (!credential) { - credential = await this.credentialMO.create({ - username, - password, - client_code, - }); - } else { - credential = await this.credentialMO.update(credential, { - username, - password, - client_code, - }); - } - await this.entityMO.update(entity.id, {credential}); - return { - id: entity._id, - type: Manager.getName(), - }; - } - - async getAuthorizationRequirements(params) { - // see parent docs. only use these three top level keys - return { - url: null, - type: ModuleConstants.authType.basic, - data: { - // fields: AuthFields.revioAuthorizationFields, - jsonSchema: AuthFields.jsonSchema, - uiSchema: AuthFields.uiSchema, - }, - }; - } - - async notify(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === 'TOKEN_UPDATE') { - const updatedToken = { - user_name: object.user_name, - user_code: object.user_code, - password: object.password, - }; - this.revIoCredentials = await this.RevIoMO.update( - this.revIoCredentials.id, - updatedToken - ); - } - } - } - - async listAllCustomers(limit, page) { - const req = await this.api.getCustomers({ - 'search.page_size': limit, - 'search.page': page, - }); - - let customers = req.records; - if (req.has_more) { - const nextPages = await this.listAllCustomers(limit, page + 1); - customers = customers.concat(nextPages); - } - return customers; - } - - async listFilteredCustomers(limit, page, filter) { - const date = moment(filter.startDate).format('YYYY-MM-DDThh:mm:ss'); - const req = await this.api.getCustomers({ - page_size: limit, - page, - updated_date_start: date, - }); - // let req = await this.api.getCustomers({page_size: limit, page: page, created_date_start: date}); - let customers = req.records; - if (req.has_more) { - const nextPages = await this.listFilteredCustomers( - limit, - page + 1, - filter - ); - customers = customers.concat(nextPages); - } - return customers; - } - - async listAllContacts(limit, page) { - const req = await this.api.getContacts({page_size: limit, page}); - let contacts = req.records; - if (req.has_more) { - const nextPages = await this.listAllContacts(limit, page + 1); - contacts = contacts.concat(nextPages); - } - return contacts; - } - - async listFilteredContacts(limit, page, filter) { - const date = moment(filter.startDate).format('YYYY-MM-DDThh:mm:ss'); - const req = await this.api.getContacts({ - page_size: limit, - page, - created_date_start: date, - }); - let contacts = req.records; - if (req.has_more) { - const nextPages = await this.listFilteredContacts( - limit, - page + 1, - filter - ); - contacts = contacts.concat(nextPages); - } - return contacts; - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/revio/models/credential.js b/packages/needs-updating/revio/models/credential.js deleted file mode 100644 index 7ba108b..0000000 --- a/packages/needs-updating/revio/models/credential.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - username: {type: String, required: true, unique: true}, - password: {type: String, required: true, lhEncrypt: true}, - client_code: {type: String, required: true, lhEncrypt: true}, -}); - -const name = 'RevioCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/revio/models/entity.js b/packages/needs-updating/revio/models/entity.js deleted file mode 100644 index feba558..0000000 --- a/packages/needs-updating/revio/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'RevioEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/rollworks/index.js b/packages/needs-updating/rollworks/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/rollworks/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/rollworks/jest-setup.js b/packages/needs-updating/rollworks/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/rollworks/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/rollworks/jest-teardown.js b/packages/needs-updating/rollworks/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/rollworks/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/rollworks/manager.js b/packages/needs-updating/rollworks/manager.js deleted file mode 100644 index 5d7d3c9..0000000 --- a/packages/needs-updating/rollworks/manager.js +++ /dev/null @@ -1,208 +0,0 @@ -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - // initializes the Api - const rollworksParams = {delegate: instance}; - - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - if (instance.entity.credential) { - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - rollworksParams.access_token = instance.credential.access_token; - rollworksParams.refresh_token = - instance.credential.refresh_token; - } - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - rollworksParams.access_token = instance.credential.access_token; - rollworksParams.refresh_token = instance.credential.refresh_token; - } - - instance.api = await new Api(rollworksParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - - // Gotta search for credential since it's not returned by the functions - const credentials = await this.credentialMO.list({user: this.userId}); - - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - const accountDetails = await this.api.getOrganization(); - const {eid, name} = accountDetails.results; - const entity = await this.findOrCreateEntity({ - accountName: name, - accountId: eid, - credentialId: credentials[0]._id, - }); - - this.credential = credentials[0]; - this.entity = entity; - - return { - // id: entity.id, - credential_id: credentials[0]._id, - entity_id: entity.id, - type: Manager.getName(), - }; - } - - async testAuth() { - await this.api.getOrganization(); - } - - async getEntityOptions() { - const options = []; - return options; - } - - async findOrCreateEntity(params) { - const accountId = get(params, 'accountId'); - const accountName = get(params, 'accountName'); - const credentialId = get(params, 'credentialId'); - - const search = await this.entityMO.list({ - externalId: accountId, - user: this.userId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: credentialId, - user: this.userId, - name: accountName, - externalId: accountId, - }; - return this.entityMO.create(createObj); - } - if (search.length === 1) { - return search[0]; - } - - throw new Error( - `Multiple entities found with the same organization ID: ${data.organization_id}` - ); - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - // todo update the database - const updatedToken = { - user: this.userId, - access_token: this.api.access_token, - refresh_token: this.api.refresh_token, - expires_at: this.api.accessTokenExpire, - // refreshTokenExpire: this.api.refreshTokenExpire, - }; - // let entity = await this.entityMO.getByUserId(this.userId); - - // if (!entity) { - // entity = await this.entityMO.create({ user: this.userId }); - // } - // let { credential } = entity; - const credentials = await this.credentialMO.list({ - user: this.userId, - }); - let credential; - if (credentials.length === 1) { - credential = credentials[0]; - } else if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential._id, - updatedToken - ); - } - // await this.entityMO.update(entity.id, { credential }); - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - - if (delegateString === this.api.DLGT_INVALID_AUTH) { - const credentials = await this.credentialMO.list({ - user: this.userId, - }); - if (credentials.length === 1) { - return this.credentialMO.update(credentials[0]._id, { - auth_is_valid: false, - }); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } else if (credentials.length === 0) { - throw new Error( - 'How are we marking nonexistant credentials invalid???' - ); - } - } - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/rollworks/models/credential.js b/packages/needs-updating/rollworks/models/credential.js deleted file mode 100644 index b2b988d..0000000 --- a/packages/needs-updating/rollworks/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - refresh_token: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'RollWorksCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/rollworks/models/entity.js b/packages/needs-updating/rollworks/models/entity.js deleted file mode 100644 index cbbaaf8..0000000 --- a/packages/needs-updating/rollworks/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'RollWorksEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/salesloft/index.js b/packages/needs-updating/salesloft/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/salesloft/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/salesloft/jest-setup.js b/packages/needs-updating/salesloft/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/salesloft/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/salesloft/jest-teardown.js b/packages/needs-updating/salesloft/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/salesloft/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/salesloft/manager.js b/packages/needs-updating/salesloft/manager.js deleted file mode 100644 index ebd5619..0000000 --- a/packages/needs-updating/salesloft/manager.js +++ /dev/null @@ -1,159 +0,0 @@ -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/core'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - const slParams = {delegate: instance}; - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - slParams.access_token = credential.access_token; - slParams.refresh_token = credential.refresh_token; - } else if (params.credentialId) { - const credential = await instance.credentialMO.get( - params.credentialId - ); - slParams.access_token = credential.access_token; - slParams.refresh_token = credential.refresh_token; - } - instance.api = await new Api(slParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: await this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async testAuth() { - await this.api.getTeam(); - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - const response = await this.api.getTokenFromCode(code); - - const credentials = await this.credentialMO.list({user: this.userId}); - const entitySearch = await this.entityMO.list({user: this.userId}); - let entity; - - await this.testAuth(); - - const teamDetails = await this.api.getTeam(); - - if (entitySearch.length === 0) { - const createObj = { - credential: credentials[0]._id, - user: this.userId, - name: teamDetails.data.name, - externalId: teamDetails.data.id, - }; - entity = await this.entityMO.create(createObj); - } else { - entity = entitySearch[0]; - } - - if (credentials.length === 0) { - throw new Error('Credential failed to create'); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - - return { - credential_id: credentials[0].id, - entity_id: entity.id, - type: Manager.getName(), - }; - } - - async deauthorize() { - this.api = new Api(); - - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId, - access_token: this.api.access_token, - refresh_token: this.api.refresh_token, - expires_at: this.api.accessTokenExpire, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] === null && delete updatedToken[k] - ); - const credentials = await this.credentialMO.list({ - user: this.userId, - }); - let credential; - if (credentials.length === 1) { - credential = credentials[0]; - } else if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } - if (!credential) { - credential = await this.credentialMO.create(updatedToken); - } else { - credential = await this.credentialMO.update( - credential._id, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - } - } - - async mark_credentials_invalid() { - const credentials = await this.credentialMO.list({user: this.userId}); - if (credentials.length === 1) { - return await this.credentialMO.update(credential[0]._id, { - auth_is_valid: false, - }); - } - if (credentials.length > 1) { - throw new Error('User has multiple credentials???'); - } else if (credentials.lenth === 0) { - throw new Error( - 'How are we marking noexistant credentials invalid???' - ); - } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/salesloft/manager.test.js b/packages/needs-updating/salesloft/manager.test.js deleted file mode 100644 index b4c8e08..0000000 --- a/packages/needs-updating/salesloft/manager.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const Manager = require('./manager'); -const mongoose = require('mongoose'); -const config = require('./defaultConfig.json'); - -describe(`Should fully test the ${config.label} Manager`, () => { - let manager, userManager; - - beforeAll(async () => { - await mongoose.connect(process.env.MONGO_URI); - manager = await Manager.getInstance({ - userId: new mongoose.Types.ObjectId(), - }); - }); - - afterAll(async () => { - await Manager.Credential.deleteMany(); - await Manager.Entity.deleteMany(); - await mongoose.disconnect(); - }); - - it('should return auth requirements', async () => { - const requirements = await manager.getAuthorizationRequirements(); - expect(requirements).exists; - expect(requirements.type).toEqual('oauth2'); - }); -}); diff --git a/packages/needs-updating/salesloft/models/credential.js b/packages/needs-updating/salesloft/models/credential.js deleted file mode 100644 index caa2eb6..0000000 --- a/packages/needs-updating/salesloft/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - refresh_token: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'SalesloftCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/salesloft/models/entity.js b/packages/needs-updating/salesloft/models/entity.js deleted file mode 100644 index 883822f..0000000 --- a/packages/needs-updating/salesloft/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'SalesloftEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/sharepoint/index.js b/packages/needs-updating/sharepoint/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/sharepoint/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/sharepoint/jest-setup.js b/packages/needs-updating/sharepoint/jest-setup.js deleted file mode 100644 index 62b3b30..0000000 --- a/packages/needs-updating/sharepoint/jest-setup.js +++ /dev/null @@ -1,13 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -const dotenv = require('dotenv'); - -const parsed = { - SHAREPOINT_SCOPE: 'sharepoint_scope_test', - SHAREPOINT_CLIENT_ID: 'sharepoint_client_id_test', - SHAREPOINT_CLIENT_SECRET: 'sharepoint_client_secret_test', - REDIRECT_URI: 'http://redirect_uri_test' -}; - -dotenv.populate(process.env, parsed); - -module.exports = globalSetup; diff --git a/packages/needs-updating/sharepoint/jest-teardown.js b/packages/needs-updating/sharepoint/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/sharepoint/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/sharepoint/manager.js b/packages/needs-updating/sharepoint/manager.js deleted file mode 100644 index 8444446..0000000 --- a/packages/needs-updating/sharepoint/manager.js +++ /dev/null @@ -1,193 +0,0 @@ -const {ModuleManager, get, debug, flushDebugLog} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const Config = require('./defaultConfig'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - // All async code here - - // initializes the Api - const sharepointParams = { - client_id: process.env.SHAREPOINT_CLIENT_ID, - client_secret: process.env.SHAREPOINT_CLIENT_SECRET, - redirect_uri: `${process.env.REDIRECT_URI}/microsoft-sharepoint`, - scope: process.env.SHAREPOINT_SCOPE, - forceConsent: true, - delegate: instance, - }; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - const credential = await Credential.findById( - instance.entity.credential - ); - instance.credential = credential; - sharepointParams.access_token = credential.accessToken; - sharepointParams.refresh_token = credential.refreshToken; - } - instance.api = new Api(sharepointParams); - - return instance; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.listSites()) validAuth = true; - } catch (e) { - flushDebugLog(e); - } - return validAuth; - } - - getAuthorizationRequirements() { - return { - url: this.api.getAuthUri(), - type: 'oauth2', - }; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code', 'test'); - await this.api.getTokenFromCode(code); - const authCheck = await this.testAuth(); - if (!authCheck) throw new Error('Authentication failed'); - - const userDetails = await this.api.getUser(); - // TODO determine if there's a good flag to make for this, where we have individual tokens vs. org/tenant tokens - // The issue here is that the Entity should reflect "on whose behalf are we making api requests", and in the - // individual user case, it's a user. In the org/tenant case, it's a tenant. - // The catch is that personal microsoft users do not have an org. So the graph API throws a 500 error. - // const orgDetails = await this.api.getOrganization(); - - await this.findOrCreateEntity({ - externalId: userDetails.id, - name: `${userDetails.displayName} (${userDetails.userPrincipalName})` - }); - return { - entity_id: this.entity.id, - credential_id: this.credential.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const externalId = get(params, 'externalId'); - const name = get(params, 'name'); - - // TODO-new... this doesn't allow for multiple entities for a specific User. - const search = await Entity.find({ - user: this.userId, - externalId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name, - externalId, - }; - this.entity = await Entity.create(createObj); - } else if (search.length === 1) { - this.entity = await Entity.findOneAndUpdate( - {_id: search[0]}, - { - $set: { - credential: this.credential.id - } - }, - {useFindAndModify: true, new: true} - ); - } else { - const message = 'Multiple entities found with the same external ID: ' + externalId; - debug(message); - throw new Error(message); - } - } - - //------------------------------------------------------------ - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId.toString(), - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - auth_is_valid: true, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] == null && delete updatedToken[k] - ); - // TODO-new globally... multiple credentials should be allowed, this is 1:1 - if (!this.credential) { - let credentialSearch = await Credential.find({ - user: this.userId.toString(), - }); - if (credentialSearch.length === 0) { - this.credential = await Credential.create(updatedToken); - } else if (credentialSearch.length === 1) { - this.credential = await Credential.findOneAndUpdate( - {_id: credentialSearch[0]}, - {$set: updatedToken}, - {useFindAndModify: true, new: true} - ); - } else { - // Handling multiple credentials found with an error for the time being - debug( - 'Multiple credentials found with the same user ID: ' + this.userId - ); - } - } else { - this.credential = await Credential.findOneAndUpdate( - {_id: this.credential}, - {$set: updatedToken}, - {useFindAndModify: true, new: true} - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - return this.markCredentialsInvalid(); - } - } - } - - // TODO-new (globally) normalize "deauthorization" - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await Entity.findByUserId(this.userId); - if (entity.credential) { - await Credential.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - this.credential = undefined; - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/sharepoint/models/credential.js b/packages/needs-updating/sharepoint/models/credential.js deleted file mode 100644 index 2c9faa2..0000000 --- a/packages/needs-updating/sharepoint/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - required: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - required: true, - lhEncrypt: true, - }, -}); - -const name = 'MicrosoftSharePointCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/sharepoint/models/entity.js b/packages/needs-updating/sharepoint/models/entity.js deleted file mode 100644 index 275e378..0000000 --- a/packages/needs-updating/sharepoint/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'MicrosoftSharePointEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/slack/CHANGELOG.md b/packages/needs-updating/slack/CHANGELOG.md deleted file mode 100644 index 8adb196..0000000 --- a/packages/needs-updating/slack/CHANGELOG.md +++ /dev/null @@ -1,706 +0,0 @@ -# v1.1.3 (Tue Aug 06 2024) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, Fer Riffel ([@FerRiffel-LeftHook](https://github.com/FerRiffel-LeftHook)), for all your work! - -#### 🐛 Bug Fix - -- Updated icons for all Frigg API modules [#14](https://github.com/friggframework/api-module-library/pull/14) ([@FerRiffel-LeftHook](https://github.com/FerRiffel-LeftHook)) -- Update icons for all Frigg API modules ([@FerRiffel-LeftHook](https://github.com/FerRiffel-LeftHook)) - -#### Authors: 1 - -- Fer Riffel ([@FerRiffel-LeftHook](https://github.com/FerRiffel-LeftHook)) - ---- - -# v1.1.0 (Wed Mar 20 2024) - -:tada: This release contains work from new contributors! :tada: - -Thanks for all your work! - -:heart: Nicolas Leal ([@nicolasmelo1](https://github.com/nicolasmelo1)) - -:heart: nmilcoff ([@nmilcoff](https://github.com/nmilcoff)) - -#### 🚀 Enhancement - -#### 🐛 Bug Fix - -- Preparing auto for managing "major old - versions" [#271](https://github.com/friggframework/frigg/pull/271) ([@seanspeaks](https://github.com/seanspeaks)) -- correct some bad automated edits, though they are not in relevant - files ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber)) -- Fix the redirect_uri passed by the user in getInstance - method [#255](https://github.com/friggframework/frigg/pull/255) ([@leofmds](https://github.com/leofmds)) -- Fix the redirect_uri passed by the user in getInstance method ([@leofmds](https://github.com/leofmds)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 5 - -- [@MichaelRyanWebber](https://github.com/MichaelRyanWebber) -- Leonardo Ferreira ([@leofmds](https://github.com/leofmds)) -- Nicolas Leal ([@nicolasmelo1](https://github.com/nicolasmelo1)) -- nmilcoff ([@nmilcoff](https://github.com/nmilcoff)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.2.10 (Tue Mar 19 2024) - -#### 🐛 Bug Fix - -- update hubspot and slack versions to addres publishing - issue [#273](https://github.com/friggframework/frigg/pull/273) ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber)) -- update hubspot and slack versions as they seem to be causing an - issue ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber)) -- Fix the redirect_uri passed by the user in getInstance - method [#255](https://github.com/friggframework/frigg/pull/255) ([@leofmds](https://github.com/leofmds)) -- Fix the redirect_uri passed by the user in getInstance method ([@leofmds](https://github.com/leofmds)) - -#### Authors: 2 - -- [@MichaelRyanWebber](https://github.com/MichaelRyanWebber) -- Leonardo Ferreira ([@leofmds](https://github.com/leofmds)) - ---- - -# v0.2.5 (Thu Sep 28 2023) - -#### 🐛 Bug Fix - -- Fix to team entity creation - user should be null instead of - 0 [#223](https://github.com/friggframework/frigg/pull/223) ([@leofmds](https://github.com/leofmds)) -- Fix to team entity creation - user should be null instead of 0 ([@leofmds](https://github.com/leofmds)) - -#### Authors: 1 - -- Leonardo Ferreira ([@leofmds](https://github.com/leofmds)) - ---- - -# v0.2.4 (Mon Sep 25 2023) - -#### 🐛 Bug Fix - -- Feature/Add getChannelMembers - endpoint [#222](https://github.com/friggframework/frigg/pull/222) ([@msalvatti](https://github.com/msalvatti)) -- Fix/Name on channel members test fixed ([@msalvatti](https://github.com/msalvatti)) -- Feature/Add getChannelMembers endpoint ([@msalvatti](https://github.com/msalvatti)) - -#### Authors: 1 - -- Maximiliano Salvatti ([@msalvatti](https://github.com/msalvatti)) - ---- - -# v0.2.3 (Thu Sep 21 2023) - -#### 🐛 Bug Fix - -- Add the team entity to Slack if it doesn't exists and return it in - pr… [#221](https://github.com/friggframework/frigg/pull/221) ([@leofmds](https://github.com/leofmds)) -- Adding proposed changes ([@leofmds](https://github.com/leofmds)) -- Renaming team_entity to team_entity_id ([@leofmds](https://github.com/leofmds)) -- Returning auth info in Slack processAuthorizationCallback ([@leofmds](https://github.com/leofmds)) -- Add the team entity to Slack if it doesn't exists and return it in - processAuthorizationCallback ([@leofmds](https://github.com/leofmds)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Leonardo Ferreira ([@leofmds](https://github.com/leofmds)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.2.2 (Mon Sep 18 2023) - -#### 🐛 Bug Fix - -- Add state and user_scope variables to Slack auth - URI [#220](https://github.com/friggframework/frigg/pull/220) ([@leofmds](https://github.com/leofmds)) -- Add state and user_scope variables to Slack auth URI ([@leofmds](https://github.com/leofmds)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Leonardo Ferreira ([@leofmds](https://github.com/leofmds)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.2.1 (Thu Sep 14 2023) - -#### 🐛 Bug Fix - -- Fix/Add headers - charset [#219](https://github.com/friggframework/frigg/pull/219) ([@msalvatti](https://github.com/msalvatti)) -- Fix/Add headers charset ([@msalvatti](https://github.com/msalvatti)) - -#### Authors: 1 - -- Maximiliano Salvatti ([@msalvatti](https://github.com/msalvatti)) - ---- - -# v0.2.0 (Wed Sep 06 2023) - -#### 🚀 Enhancement - -- Slack lookup by externalId, remove the user requirement from Mongoose DB - models [#218](https://github.com/friggframework/frigg/pull/218) ([@seanspeaks](https://github.com/seanspeaks)) - -#### 🐛 Bug Fix - -- Vestiges ([@seanspeaks](https://github.com/seanspeaks)) -- Looking good ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.36 (Tue Aug 22 2023) - -#### 🐛 Bug Fix - -- Add link unfurling - endpoint [#215](https://github.com/friggframework/frigg/pull/215) ([@leofmds](https://github.com/leofmds)) -- Add link unfurling endpoint ([@leofmds](https://github.com/leofmds)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Leonardo Ferreira ([@leofmds](https://github.com/leofmds)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.35 (Thu Jun 08 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.34 (Thu May 25 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.33 (Thu Apr 13 2023) - -#### 🐛 Bug Fix - -- fix slack channel history - call [#148](https://github.com/friggframework/frigg/pull/148) ([@debbie-yu](https://github.com/debbie-yu)) -- don't stringify slack body ([@debbie-yu](https://github.com/debbie-yu)) -- use qs to set slack post - body [#147](https://github.com/friggframework/frigg/pull/147) ([@debbie-yu](https://github.com/debbie-yu)) -- set slack post body - correctly [#147](https://github.com/friggframework/frigg/pull/147) ([@debbie-yu](https://github.com/debbie-yu)) -- call getChannelHistory with right - content-type [#147](https://github.com/friggframework/frigg/pull/147) ([@debbie-yu](https://github.com/debbie-yu)) -- Merge branch 'main' of https://github.com/friggframework/frigg into - debbie.yu/fix-slack-history [#147](https://github.com/friggframework/frigg/pull/147) ([@debbie-yu](https://github.com/debbie-yu)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- [@debbie-yu](https://github.com/debbie-yu) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.32 (Thu Apr 13 2023) - -#### 🐛 Bug Fix - -- Fix Slack getChannelHistory - call [#147](https://github.com/friggframework/frigg/pull/147) ([@debbie-yu](https://github.com/debbie-yu)) -- use qs to set slack post body ([@debbie-yu](https://github.com/debbie-yu)) -- set slack post body correctly ([@debbie-yu](https://github.com/debbie-yu)) -- call getChannelHistory with right content-type ([@debbie-yu](https://github.com/debbie-yu)) -- Merge branch 'main' of https://github.com/friggframework/frigg into - debbie.yu/fix-slack-history [#145](https://github.com/friggframework/frigg/pull/145) ([@debbie-yu](https://github.com/debbie-yu)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- [@debbie-yu](https://github.com/debbie-yu) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.31 (Mon Apr 10 2023) - -#### 🐛 Bug Fix - -- Added some tests and change API request - method [#146](https://github.com/friggframework/frigg/pull/146) ([@seanspeaks](https://github.com/seanspeaks)) -- "Get" is a POST ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.30 (Tue Apr 04 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, null[@debbie-yu](https://github.com/debbie-yu), for all your work! - -#### 🐛 Bug Fix - -- Adding new IntegrationMapping - collection [#142](https://github.com/friggframework/frigg/pull/142) ([@debbie-yu](https://github.com/debbie-yu)) -- correct IntegrationMapping discriminator ([@debbie-yu](https://github.com/debbie-yu)) -- Merge branch 'main' of https://github.com/friggframework/frigg into - debbie.yu/integration-mapping ([@debbie-yu](https://github.com/debbie-yu)) -- addressing PR feedback and adding unit tests around IntegrationMapping ([@debbie-yu](https://github.com/debbie-yu)) -- adding new IntegrationMapping collection to better handle keeping track of mappings for - integrations ([@debbie-yu](https://github.com/debbie-yu)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- [@debbie-yu](https://github.com/debbie-yu) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.29 (Sat Apr 01 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, null[@debbie-yu](https://github.com/debbie-yu), for all your work! - -#### 🐛 Bug Fix - -- add token_revoked event for - slack [#135](https://github.com/friggframework/frigg/pull/135) ([@debbie-yu](https://github.com/debbie-yu)) -- add token_revoked event ([@debbie-yu](https://github.com/debbie-yu)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- [@debbie-yu](https://github.com/debbie-yu) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.28 (Tue Mar 28 2023) - -#### 🐛 Bug Fix - -- Trailing slash for - path [#140](https://github.com/friggframework/frigg/pull/140) ([@seanspeaks](https://github.com/seanspeaks)) -- Trailing slash for path ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.27 (Fri Mar 24 2023) - -#### 🐛 Bug Fix - -- Update api.js [#139](https://github.com/friggframework/frigg/pull/139) ([@seanspeaks](https://github.com/seanspeaks)) -- Update api.js ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.26 (Thu Mar 23 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, Roberto Oliveros ([@roboli](https://github.com/roboli)), for all your work! - -#### 🐛 Bug Fix - -- Implement postEphemeral method for - Slack [#137](https://github.com/friggframework/frigg/pull/137) ([@roboli](https://github.com/roboli)) -- Implement postEphemeral method for Slack ([@roboli](https://github.com/roboli)) - -#### Authors: 1 - -- Roberto Oliveros ([@roboli](https://github.com/roboli)) - ---- - -# v0.1.25 (Tue Feb 21 2023) - -#### 🐛 Bug Fix - -- Updates to HubSpot - Module [#132](https://github.com/friggframework/frigg/pull/132) ([@seanspeaks](https://github.com/seanspeaks)) -- Merge branch 'main' into hubspot-updates ([@seanspeaks](https://github.com/seanspeaks)) -- WIP Updates... cleanup, simplify, and fix ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.24 (Mon Feb 13 2023) - -#### 🐛 Bug Fix - -- Support as-user-workflow-schemas and connection - info [#129](https://github.com/friggframework/frigg/pull/129) ([@seanspeaks](https://github.com/seanspeaks)) -- Slack: Add User Lookup by - ID [#128](https://github.com/friggframework/frigg/pull/128) ([@seanspeaks](https://github.com/seanspeaks)) -- User Info Lookup ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.23 (Wed Feb 08 2023) - -#### 🐛 Bug Fix - -- Quick hits, view - endpoints [#126](https://github.com/friggframework/frigg/pull/126) ([@seanspeaks](https://github.com/seanspeaks)) -- Quick hits, view endpoints ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.22 (Wed Feb 08 2023) - -#### 🐛 Bug Fix - -- Fixed a Slack bug via requester - improvement [#125](https://github.com/friggframework/frigg/pull/125) ([@seanspeaks](https://github.com/seanspeaks)) -- Fixed a Slack bug via requester improvement ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.21 (Thu Feb 02 2023) - -#### 🐛 Bug Fix - -- More unit test - reasons [#116](https://github.com/friggframework/frigg/pull/116) ([@seanspeaks](https://github.com/seanspeaks)) -- More unit test reasons ([@seanspeaks](https://github.com/seanspeaks)) -- this is why we unit - test [#115](https://github.com/friggframework/frigg/pull/115) ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.20 (Wed Feb 01 2023) - -#### 🐛 Bug Fix - -- this is why we unit - test [#115](https://github.com/friggframework/frigg/pull/115) ([@seanspeaks](https://github.com/seanspeaks)) -- this is why we unit test ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.19 (Wed Feb 01 2023) - -#### 🐛 Bug Fix - -- Channel management API - Methods [#113](https://github.com/friggframework/frigg/pull/113) ([@seanspeaks](https://github.com/seanspeaks)) -- Channel management API Methods ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.18 (Tue Jan 31 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, null[@li-sherry](https://github.com/li-sherry), for all your work! - -#### 🐛 Bug Fix - -- change to - x-www-form-urlencoded [#107](https://github.com/friggframework/frigg/pull/107) ([@li-sherry](https://github.com/li-sherry) - vedant@vedant.agrawal) -- remove body, set email as query param ([@li-sherry](https://github.com/li-sherry)) -- passing email as query param (vedant@vedant.agrawal) -- change to x-www-form-urlencoded ([@li-sherry](https://github.com/li-sherry)) -- Merge branch 'vedantagrawal/additional-ironclad-endpoints' into - AddSlackLookupUsersByEmail [#105](https://github.com/friggframework/frigg/pull/105) ([@li-sherry](https://github.com/li-sherry)) -- add - lookupUsersByEmail [#106](https://github.com/friggframework/frigg/pull/106) ([@li-sherry](https://github.com/li-sherry)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 3 - -- [@li-sherry](https://github.com/li-sherry) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) -- Vedant Agrawal (vedant@vedant.agrawal) - ---- - -# v0.1.17 (Tue Jan 31 2023) - -#### 🐛 Bug Fix - -- Updates/api module - yotpo [#108](https://github.com/friggframework/frigg/pull/108) ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.16 (Tue Jan 31 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, null[@li-sherry](https://github.com/li-sherry), for all your work! - -#### 🐛 Bug Fix - -- add lookupUsersByEmail [#106](https://github.com/friggframework/frigg/pull/106) ( - vedant@vedant.agrawal [@li-sherry](https://github.com/li-sherry)) -- Merge branch 'vedantagrawal/additional-ironclad-endpoints' into - AddSlackLookupUsersByEmail [#105](https://github.com/friggframework/frigg/pull/105) ([@li-sherry](https://github.com/li-sherry)) -- add lookupUsersByEmail ([@li-sherry](https://github.com/li-sherry)) - -#### Authors: 2 - -- [@li-sherry](https://github.com/li-sherry) -- Vedant Agrawal (vedant@vedant.agrawal) - ---- - -# v0.1.15 (Tue Jan 24 2023) - -#### 🐛 Bug Fix - -- .update is not a mongoose model - method [#104](https://github.com/friggframework/frigg/pull/104) ([@seanspeaks](https://github.com/seanspeaks)) -- .update is not a mongoose model method ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.14 (Fri Jan 20 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, null[@debbie-yu](https://github.com/debbie-yu), for all your work! - -#### 🐛 Bug Fix - -- log error thrown from slack get token from code - call [#101](https://github.com/friggframework/frigg/pull/101) ([@debbie-yu](https://github.com/debbie-yu)) -- log error thrown from slack get token from code call ([@debbie-yu](https://github.com/debbie-yu)) - -#### Authors: 1 - -- [@debbie-yu](https://github.com/debbie-yu) - ---- - -# v0.1.13 (Wed Jan 18 2023) - -#### 🐛 Bug Fix - -- Ironclad and slack - updates [#96](https://github.com/friggframework/frigg/pull/96) ([@seanspeaks](https://github.com/seanspeaks)) -- Fetch Error fix to also log a body if already used (and passed in) ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.12 (Thu Jan 12 2023) - -#### 🐛 Bug Fix - -- Slack authUri [#94](https://github.com/friggframework/frigg/pull/94) ([@seanspeaks](https://github.com/seanspeaks)) -- authUri ([@seanspeaks](https://github.com/seanspeaks)) -- slack redirect - URI [#93](https://github.com/friggframework/frigg/pull/93) ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.11 (Thu Jan 12 2023) - -#### 🐛 Bug Fix - -- slack redirect - URI [#93](https://github.com/friggframework/frigg/pull/93) ([@seanspeaks](https://github.com/seanspeaks)) -- slack redirect URI ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.10 (Thu Jan 12 2023) - -#### 🐛 Bug Fix - -- Slack, Ironclad, and "IntegrationManager" - updates [#92](https://github.com/friggframework/frigg/pull/92) ([@seanspeaks](https://github.com/seanspeaks)) -- - - Update Slack to retrieve workspace information and use OAuth2Requester's standard getAuthFromCode (name is wrong), - and we were storing the wrong information ([@seanspeaks](https://github.com/seanspeaks)) -- Update Slack module ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.9 (Wed Jan 11 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.8 (Tue Jan 10 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, Jonathan O'Donnell ([@joncodo](https://github.com/joncodo)), for all your work! - -#### 🐛 Bug Fix - -- Merge branch 'main' of github.com:friggframework/frigg into doc-updates ([@joncodo](https://github.com/joncodo)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Jonathan O'Donnell ([@joncodo](https://github.com/joncodo)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.7 (Mon Jan 09 2023) - -#### ⚠️ Pushed to `main` - -- Merge branch 'main' into gitbook-updates ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.1.4 (Tue Dec 06 2022) - -#### 🐛 Bug Fix - -- fix modules to - @friggframework [#74](https://github.com/friggframework/frigg/pull/74) ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) -- Sheehan Toufiq Khan ([@sheehantoufiq](https://github.com/sheehantoufiq)) - ---- - -# v0.1.2 (Thu Oct 13 2022) - -#### 🐛 Bug Fix - -- Api module library - slack [#52](https://github.com/friggframework/frigg/pull/52) ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- changelog update ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- missing files, slack manager updates ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- merge conflicts ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- merge conflift, bug fixes, code cleanup ([@sheehantoufiq](https://github.com/sheehantoufiq)) - -#### Authors: 1 - -- Sheehan Toufiq Khan ([@sheehantoufiq](https://github.com/sheehantoufiq)) - ---- - -# v0.1.0 (Oct 12 2022) - -- Code cleanup -- Bug fixes - -# v0.1.0 (Oct 6 2022) - -- Api Module -- Manager - -# v0.0.1 (Sep 27 2022) - -#### Generated - -- Initialized from template diff --git a/packages/needs-updating/slack/README.md b/packages/needs-updating/slack/README.md deleted file mode 100644 index 7b2eb47..0000000 --- a/packages/needs-updating/slack/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Frigg API Module Slack - -A quick description of Slack. - -Expected environment variables - -``` -SLACK_CLIENT_ID="Slack app client ID" -SLACK_CLIENT_SECRET="Slack app client secret" -SLACK_SCOPE="Slack bot scopes, comma separated" -SLACK_USER_SCOPE="Slack user scopes, comma separated" -``` -## Fenestra UI Extensions - -This module includes Fenestra specifications for Slack UI extensibility. - -### Available Extension Types -See `fenestra/platform.fenestra.yaml` for complete specification. - -### Examples -Check `fenestra/examples/` directory for implementation examples. - diff --git a/packages/needs-updating/slack/api.js b/packages/needs-updating/slack/api.js deleted file mode 100644 index 000735f..0000000 --- a/packages/needs-updating/slack/api.js +++ /dev/null @@ -1,615 +0,0 @@ -const {FetchError, get, OAuth2Requester} = require('@friggframework/core'); -const qs = require('qs'); - - -class Api extends OAuth2Requester { - constructor(params) { - super(params); - - this.baseUrl = 'https://slack.com/api'; - this.client_id = process.env.SLACK_CLIENT_ID; - this.client_secret = process.env.SLACK_CLIENT_SECRET; - // this.client_id = get(params, 'client_id'); - // this.client_secret = get(params, 'client_secret'); - this.scope = process.env.SLACK_SCOPE; - this.user_scope = process.env.SLACK_USER_SCOPE || ''; - this.redirect_uri = get( - params, - 'redirect_uri', - `${process.env.REDIRECT_URI}/slack` - ); - this.access_token = get(params, 'access_token', null); - - this.URLs = { - // Auth - authorize: 'https://slack.com/oauth/v2/authorize', - access_token: '/oauth.v2.access', - authTest: '/auth.test', - listTeams: '/auth.teams.list', - - // Channels or Conversations - getChannel: '/conversations.info', - listChannels: '/conversations.list', - createChannel: '/conversations.create', - updateChannel: '/conversations.update', - closeChannel: '/conversations.close', - archiveChannel: '/conversations.archive', - inviteUsersToChannel: '/conversations.invite', - renameChannel: '/conversations.rename', - getChannelHistory: '/conversations.history', - getChannelMembers: '/conversations.members', - - // Chats - getMessagePermalink: '/chat.getPermalink', - postMessage: '/chat.postMessage', - postEphemeral: '/chat.postEphemeral', - updateMessage: '/chat.update', - deleteMessage: '/chat.delete', - postUnfurl: '/chat.unfurl', - - // Files - getFile: '/files.info', // Gets information about a file. - listFiles: '/files.list', // List for a team, in a channel, or from a user with applied filters. - uploadFile: '/files.upload', // Uploads a file - deleteFile: '/files.delete', // Deletes a file. - getFileUploadURLExternal: '/files.completeUploadExternal', // Gets a URL for an edge external upload - completeFileUploadExternal: '/files.getUploadURLExternal', // Finishes an upload started with getUploadURLExternal - getRemoteFile: '/files.remote.info', // Gets information about a remote file - listRemoteFiles: '/files.remote.list', // Lists remote files - addRemoteFile: '/files.remote.add', // Adds a remote file - updateRemoteFile: '/files.remote.update', // Updates a remote file - removeRemoteFile: '/files.remote.remove', // Removes a remote file - shareRemoteFile: '/files.remote.share', // Shares a remote file - revokeFilePublicURL: '/files.revokePublicURL', // Revokes public/external sharing access for a file - sharedFilePublicURL: '/files.sharedPublicURL', // Enables a file for public/external sharing. - - // Users - lookupUserByEmail: '/users.lookupByEmail', - getUserProfileById: '/users.profile.get', - getUserById: '/users.info', - - // Views - openView: '/views.open', - publishView: '/views.publish', - updateView: '/views.update', - pushView: '/views.push', - }; - - this.tokenUri = this.baseUrl + this.URLs.access_token; - } - - async setTokens(params) { - this.access_token = get(params, 'access_token'); - this.refresh_token = get(params, 'refresh_token', null); - const authedUser = get(params, 'authed_user', null); - const accessExpiresIn = get(params, 'expires_in', null); - const refreshExpiresIn = get( - params, - 'x_refresh_token_expires_in', - null - ); - - if (authedUser && authedUser.access_token) { - this.access_token = authedUser.access_token; - } - - this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); - this.refreshTokenExpire = new Date(Date.now() + refreshExpiresIn * 1000); - - await this.notify(this.DLGT_TOKEN_UPDATE); - } - - async _request(url, options, i = 0) { - let encodedUrl = encodeURI(url); - if (options.query) { - let queryBuild = '?'; - for (const key in options.query) { - queryBuild += `${encodeURIComponent(key)}=${encodeURIComponent( - options.query[key] - )}&`; - } - encodedUrl += queryBuild.slice(0, -1); - } - - options.headers = await this.addAuthHeaders(options.headers); - - const response = await this.fetch(encodedUrl, options); - const parsedResponse = await this.parsedBody(response); - const {status} = response; - const {ok, error} = parsedResponse; - console.log(parsedResponse); - - // If the status is retriable and there are back off requests left, retry the request - if ((status === 429 || status >= 500) && i < this.backOff.length) { - const delay = this.backOff[i] * 1000; - await new Promise((resolve) => setTimeout(resolve, delay)); - return this._request(url, options, i + 1); - } else if ( - parsedResponse.error === 'invalid_auth' || - parsedResponse.error === 'auth_expired' || - parsedResponse.error === 'token_expired' || - parsedResponse.error === 'token_revoked' - ) { - if (!this.isRefreshable || this.refreshCount > 0) { - await this.notify(this.DLGT_INVALID_AUTH); - } else { - this.refreshCount++; - await this.refreshAuth(); - return this._request(url, options, i + 1); // Retries - } - } - - // If the error wasn't retried, throw. - if (!ok) { - throw await FetchError.create({ - resource: encodedUrl, - init: options, - response, - body: parsedResponse, - }); - } - - return parsedResponse; - } - - async addAuthHeaders(headers) { - if (this.access_token) { - headers.Authorization = `Bearer ${this.access_token}`; - } - if (!headers['Content-Type']) - headers['Content-Type'] = 'application/json'; - if (!headers['Accept']) headers['Accept'] = 'application/json'; - - return headers; - } - - getAuthorizationUri() { - const authUri = encodeURI( - `${this.URLs.authorize}?state=${this.state}&client_id=${this.client_id}&scope=${this.scope}&user_scope=${this.user_scope}&redirect_uri=${this.redirect_uri}` - ); - return authUri; - } - - // for backwards compatibility - getAuthUri() { - return this.getAuthorizationUri(); - } - - async listTeams() { - const options = { - url: this.baseUrl + this.URLs.listTeams, - }; - const response = await this._get(options); - return response; - } - - async authTest() { - const options = { - url: this.baseUrl + this.URLs.authTest, - body: null, - }; - const response = await this._post(options); - return response; - } - - async lookupUserByEmail(email) { - const options = { - url: this.baseUrl + this.URLs.lookupUserByEmail + `?email=${email}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json', - }, - }; - const response = await this._get(options); - return response; - } - - async getUserProfileById(userId) { - const options = { - url: - this.baseUrl + this.URLs.getUserProfileById + `?user=${userId}`, - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }; - const response = await this._get(options); - return response; - } - - async getUserById(userId) { - const options = { - url: this.baseUrl + this.URLs.getUserById + `?user=${userId}`, - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }; - const response = await this._get(options); - return response; - } - - // Args: - // channel: string, required - // message_ts: string, required - async getMessagePermalink(query) { - const options = { - url: this.baseUrl + this.URLs.getMessagePermalink, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // channel: string, required - // At least one required: - // attachments: string - // blocks: blocks[] as string - // text: string - // as_user: boolean, optional - // icon_emoji: string, optional - // icon_url: string, optional - // link_names: boolean, optional - // metadata: string, optional - // mrkdwn: boolean, optional - // parse: string, optional - // reply_broadcast: boolean, optional - // thread_ts: string, optional - // unfurl_links: boolean, optional - // unfurl_media: boolean, optional - // username: string, optional - async postMessage(body) { - const options = { - url: this.baseUrl + this.URLs.postMessage, - body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - const response = await this._post(options); - return response; - } - - async postUnfurl(body) { - const options = { - url: this.baseUrl + this.URLs.postUnfurl, - body, - }; - const response = await this._post(options); - return response; - } - - // Args: - // channel: string, required - // user: string, required - // At least one required: - // attachments: string - // blocks: blocks[] as string - // text: string - // as_user: boolean, optional - // icon_emoji: string, optional - // icon_url: string, optional - // link_names: boolean, optional - // parse: string, optional - // thread_ts: string, optional - // username: string, optional - async postEphemeral(body) { - const options = { - url: this.baseUrl + this.URLs.postEphemeral, - body, - }; - const response = await this._post(options); - return response; - } - - // Args: - // channel: string, required - // ts: string, required - // as_user: boolean, optional - // attachments: string, optional - // blocks: blocks[] as string, optional - // file_ids: array, optional - // link_names: boolean, optional - // metadata: string, optional - // parse: string, optional - // reply_broadcast: boolean, optional - // text: string, optional - async updateMessage(body) { - const options = { - url: this.baseUrl + this.URLs.updateMessage, - body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - const response = await this._post(options); - return response; - } - - // Args: - // channel: string, required - // ts: string, required - // as_user: boolean, required - async deleteMessage(body) { - const options = { - url: this.baseUrl + this.URLs.deleteMessage, - body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - const response = await this._post(options); - return response; - } - - // Args: - // count: integer, optional - // cursor: string, optional - // limit: integer, optional - // page: integer, optional - async getFile(query) { - const options = { - url: this.baseUrl + this.URLs.getFile, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // channel: string, optional - // count: integer, optional - // files: string, optional - // page: integer, optional - // show_files_hidden_by_limit: boolean, optional - // team_id: string, optional - // ts_from: string, optional - // ts_to: string, optional - // types: string, optional - // user: string, optional - async listFiles(query) { - const options = { - url: this.baseUrl + this.URLs.listFiles, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // channels: string, optional - // content: string, optional - If omitting this parameter, you must submit file. - // file: file, optional - If omitting this parameter, you must submit content. - // filename: string, optional - // filetype: string, optional - // initial_comment: string, optional - // thread_ts: string, optional - // title: string, optional - async uploadFile(body) { - const options = { - url: this.baseUrl + this.URLs.uploadFile, - body, - }; - const response = await this._post(options); - return response; - } - - // Args: - // file: string, required - async deleteFile(body) { - const options = { - url: this.baseUrl + this.URLs.deleteFile, - body, - }; - const response = await this._post(options); - return response; - } - - // Args: - // external_id: string, optional - // file: string, optional - async getRemoteFile(query) { - const options = { - url: this.baseUrl + this.URLs.getRemoteFile, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // channel: string, optional - // cursor: string, optional - // limit: integer, optional - // ts_from: string, optional - // ts_to: string, optional - async listRemoteFiles(query) { - const options = { - url: this.baseUrl + this.URLs.listRemoteFiles, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // title: string, required - // external_id: string, required - // external_url: string, required - // filetype: string, optional - // indexable_file_contents: file, optional - // preview_image: file, optional - async addRemoteFile(query) { - const options = { - url: this.baseUrl + this.URLs.addRemoteFile, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // channels: string, required - // external_id: string, optional - // file: string, optional - async shareRemoteFile(query) { - const options = { - url: this.baseUrl + this.URLs.shareRemoteFile, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // file: string, optional - // external_id: string, optional - // title: string, optional - // external_url: string, optional - // filetype: string, optional - // indexable_file_contents: file, optional - // preview_image: file, optional - async updateRemoteFile(query) { - const options = { - url: this.baseUrl + this.URLs.updateRemoteFile, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // external_id: string, optional - // file: string, optional - async removeRemoteFile(query) { - const options = { - url: this.baseUrl + this.URLs.removeRemoteFile, - query, - }; - const response = await this._get(options); - return response; - } - - // Args: - // name: string, required - // is_private: boolean, optional - async createChannel(body) { - const options = { - url: this.baseUrl + this.URLs.createChannel, - body, - }; - const response = await this._post(options); - return response; - } - - // Args: - // channel: string, required - // users: array, required - async inviteUsersToChannel(body) { - const options = { - url: this.baseUrl + this.URLs.inviteUsersToChannel, - body, - }; - const response = await this._post(options); - return response; - } - - // Args: - // channel: string, required - // cursor: string, optional - // limit: integer, optional - // latest: integer, optional - // oldest: integer, optional - // inclusive: boolean, optional - async getChannelHistory(body) { - const options = { - url: this.baseUrl + this.URLs.getChannelHistory, - body: qs.stringify(body), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }; - const response = await this._post(options, false); - return response; - } - - // Args: - // channel: string, required - // cursor: string, optional - // limit: integer, optional - async getChannelMembers(query) { - const options = { - url: this.baseUrl + this.URLs.getChannelMembers, - query, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }; - const response = await this._get(options); - return response; - } - - async listChannels(query) { - const options = { - url: this.baseUrl + this.URLs.listChannels, - query, - }; - const response = await this._get(options); - return response; - } - - // unfurl_links: boolean, optional - - // Args: - // Need args from Slack - async openView(body) { - const options = { - url: this.baseUrl + this.URLs.openView, - body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - const response = await this._post(options); - return response; - } - - // Need args from Slack - async updateView(body) { - const options = { - url: this.baseUrl + this.URLs.updateView, - body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - const response = await this._post(options); - return response; - } - - // Need args from Slack - async pushView(body) { - const options = { - url: this.baseUrl + this.URLs.pushView, - body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - const response = await this._post(options); - return response; - } - - // Need args from Slack - async publishView(body) { - const options = { - url: this.baseUrl + this.URLs.publishView, - body, - }; - const response = await this._post(options); - return response; - } -} - -module.exports = {Api}; diff --git a/packages/needs-updating/slack/authFields.js b/packages/needs-updating/slack/authFields.js deleted file mode 100644 index a1e40e9..0000000 --- a/packages/needs-updating/slack/authFields.js +++ /dev/null @@ -1,6 +0,0 @@ -const AuthFields = { - jsonSchema: {}, - uiSchema: {}, -}; - -module.exports = {AuthFields}; diff --git a/packages/needs-updating/slack/defaultConfig.json b/packages/needs-updating/slack/defaultConfig.json deleted file mode 100644 index 99b52df..0000000 --- a/packages/needs-updating/slack/defaultConfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "slack", - "label": "slack", - "productUrl": "", - "apiDocs": "", - "logoUrl": "https://friggframework.org/assets/img/slack-icon.jpeg", - "categories": [], - "description": "slack" -} diff --git a/packages/needs-updating/slack/definition.js b/packages/needs-updating/slack/definition.js deleted file mode 100644 index d6ba8b6..0000000 --- a/packages/needs-updating/slack/definition.js +++ /dev/null @@ -1,68 +0,0 @@ -require('dotenv').config(); -const {Api} = require('./api'); -const {get, flushDebugLog} = require("@friggframework/core"); -const config = require('./defaultConfig.json') - -const Definition = { - API: Api, - getName: function () { - return config.name - }, - moduleName: config.name,//maybe not required - requiredAuthMethods: { - getToken: async function (api, params) { - const code = get(params.data, 'code', null); - - let authInfo; - try { - authInfo = await api.getTokenFromCode(code); - } catch (e) { - flushDebugLog(e); - throw new Error('Auth Error'); - } - const authRes = await this.testAuth(); - if (!authRes) throw new Error('Auth Error'); - api.authed_user = authInfo.authed_user; - api.team_id = authInfo.team.id; - api.team_name = authInfo.team.name; - - }, - apiPropertiesToPersist: { - credential: [ - 'access_token', 'refresh_token', - ], - entity: ['team_id'], - }, - getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { - const isUserScopeAuthorized = api.authed_user && api.authed_user.access_token; - - const externalId = isUserScopeAuthorized ? - api.authed_user.id : api.teamId; - - return { - identifiers: {externalId, user: userId}, - details: {name: api.team_name} - } - }, - getCredentialDetails: async function (api, userId) { - const workspaceInfo = await api.authTest(); - const isTeamUser = workspaceInfo['bot_user_id']; - const externalId = isTeamUser ? get(workspaceInfo, 'team_id') : get(workspaceInfo, 'user_id'); - return { - identifiers: {externalId}, - details: {} - }; - }, - testAuthRequest: async function (api) { - return api.authTest(); - }, - }, - env: { - client_id: process.env.SLACK_CLIENT_ID, - client_secret: process.env.SLACK_CLIENT_SECRET, - redirect_uri: `${process.env.REDIRECT_URI}/slack`, - scope: process.env.SLACK_SCOPE, - } -}; - -module.exports = {Definition}; diff --git a/packages/needs-updating/slack/fenestra/examples/slack-app.fenestra.yaml b/packages/needs-updating/slack/fenestra/examples/slack-app.fenestra.yaml deleted file mode 100644 index 51fbcfb..0000000 --- a/packages/needs-updating/slack/fenestra/examples/slack-app.fenestra.yaml +++ /dev/null @@ -1,253 +0,0 @@ -# Slack App - Fenestra Specification Example -fenestra: 1.0.0 -info: - title: Task Manager for Slack - version: 2.1.0 - description: | - A comprehensive task management app for Slack teams. Create, assign, and track - tasks directly within Slack using slash commands, interactive modals, and home tabs. - contact: - name: Task Manager Support - email: support@taskmanager.example - url: https://taskmanager.example/support - license: - name: MIT - url: https://opensource.org/licenses/MIT - -extension: - type: embedded - rendering: - mode: schema - schema: - format: block-kit - version: "1.0" - endpoint: https://api.taskmanager.example/slack/ui - templates: - - name: task-modal - description: Modal for creating/editing tasks - schema: - type: modal - title: - type: plain_text - text: Create Task - blocks: - - type: input - element: - type: plain_text_input - action_id: task_title - label: - type: plain_text - text: Task Title - - type: input - element: - type: datepicker - action_id: due_date - label: - type: plain_text - text: Due Date - - name: home-tab - description: App home tab showing user's tasks - schema: - type: home - blocks: - - type: section - text: - type: mrkdwn - text: "*Your Tasks*" - - type: divider - - type: section - text: - type: mrkdwn - text: "Loading tasks..." - - communication: - channels: - - type: http - config: - baseUrl: https://api.taskmanager.example - endpoints: - - path: /slack/events - method: POST - description: Slack Events API endpoint - - path: /slack/interactive - method: POST - description: Interactive component endpoint - - path: /slack/slash-commands - method: POST - description: Slash command endpoint - - events: - - name: app_home_opened - direction: incoming - description: User opened the app home tab - payload: - type: object - properties: - user: - type: string - channel: - type: string - tab: - type: string - enum: [home, messages] - - - name: view_submission - direction: incoming - description: Modal form submitted - payload: - type: object - properties: - view: - type: object - properties: - id: - type: string - state: - type: object - description: Form state values - - - name: block_actions - direction: incoming - description: User interacted with a Block Kit element - payload: - type: object - properties: - actions: - type: array - items: - type: object - properties: - action_id: - type: string - value: - type: string - - capabilities: - storage: - platform: true - quota: "10MB" - api: - platformData: - - users.read - - conversations.read - - chat.write - externalRequests: true - ui: - modals: true - notifications: true - shortcuts: true - slashCommands: - - /task - - /tasks - compute: - serverless: true - webhooks: true - - triggers: - - type: manual - config: - slashCommands: - - command: /task - description: Create or manage tasks - usageHint: "[create|list|assign]" - shortcuts: - - name: Create Task - type: global - callback_id: create_task_shortcut - - - type: contextual - config: - messageActions: - - name: Create Task from Message - callback_id: create_from_message - - - type: event - config: - events: - - app_home_opened - - app_mention - - message.channels - - context: - required: - - workspace_id - - user_id - - channel_id - optional: - - team_id - - enterprise_id - - message_ts - - lifecycle: - install: - oauth: - scopes: - bot: - - commands - - chat:write - - users:read - - app_mentions:read - user: - - search:read - redirectUrl: https://taskmanager.example/slack/oauth/callback - - uninstall: - webhook: https://api.taskmanager.example/slack/uninstall - - update: - automatic: true - notification: true - -security: - - oauth2: - - commands - - chat:write - - users:read - -deployment: - hosting: marketplace - distribution: - platform: slack-app-directory - appId: A0123456789 - manifest: - display_information: - name: Task Manager - description: Manage tasks within Slack - background_color: "#1A1D21" - long_description: | - Task Manager helps teams stay organized by providing - comprehensive task management directly within Slack. - settings: - org_deploy_enabled: true - socket_mode_enabled: false - token_rotation_enabled: true - features: - app_home: - home_tab_enabled: true - messages_tab_enabled: false - bot_user: - display_name: Task Manager - always_online: true - shortcuts: - - name: Create Task - type: global - callback_id: create_task_shortcut - slash_commands: - - command: /task - url: https://api.taskmanager.example/slack/slash-commands - description: Create or manage tasks - usage_hint: "[create|list|assign]" - should_escape: false - -externalDocs: - description: Complete Task Manager documentation - url: https://docs.taskmanager.example/slack-app - -tags: - - name: productivity - - name: task-management - - name: team-collaboration - -x-slack-app-id: A0123456789 -x-slack-verification-token: ${SLACK_VERIFICATION_TOKEN} -x-slack-signing-secret: ${SLACK_SIGNING_SECRET} \ No newline at end of file diff --git a/packages/needs-updating/slack/fenestra/examples/slack-extension.json b/packages/needs-updating/slack/fenestra/examples/slack-extension.json deleted file mode 100644 index a3ac06f..0000000 --- a/packages/needs-updating/slack/fenestra/examples/slack-extension.json +++ /dev/null @@ -1,388 +0,0 @@ -{ - "$schema": "https://frigg.cloud/schemas/fenestra/v1/manifest.json", - "fenestra": { - "version": "1.0", - "id": "660e8400-e29b-41d4-a716-446655440001", - "name": "Team Collaboration Hub", - "description": "Enhanced team collaboration features with CRM integration for Slack", - "author": { - "name": "Frigg Cloud Team", - "email": "extensions@frigg.cloud", - "url": "https://frigg.cloud" - }, - "version": "1.5.0", - "icon": "https://frigg.cloud/assets/icons/collab-hub.png", - "permissions": [ - "data:read", - "data:write", - "ui:modal", - "ui:notification", - "api:external", - "storage:cloud" - ], - "platforms": { - "slack": { - "appId": "${SLACK_APP_ID}", - "signingSecret": "${SLACK_SIGNING_SECRET}", - "scopes": { - "bot": [ - "channels:history", - "channels:read", - "chat:write", - "commands", - "users:read", - "users:read.email" - ], - "user": [ - "channels:history", - "channels:read" - ] - } - } - }, - "extensions": [ - { - "id": "crm-lookup-command", - "type": "action", - "name": "CRM Lookup", - "description": "Quick CRM record lookup from Slack", - "locations": ["slash-command"], - "actionType": "slash-command", - "label": "/crm", - "handler": { - "type": "api", - "config": { - "endpoint": "https://api.frigg.cloud/slack/commands/crm", - "method": "POST", - "responseType": "ephemeral" - } - }, - "triggers": { - "events": ["slash_command"] - }, - "data": { - "requirements": [ - { - "entity": "slack_user", - "fields": ["id", "email", "name"] - } - ] - } - }, - { - "id": "customer-context-action", - "type": "action", - "name": "View Customer Context", - "description": "View CRM data for customers mentioned in messages", - "locations": ["message.action"], - "actionType": "message-action", - "label": "View in CRM", - "icon": "user-circle", - "handler": { - "type": "modal", - "config": { - "callback_id": "customer_context_modal", - "title": "Customer Context", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "Loading customer information..." - } - } - ] - } - }, - "permissions": ["data:read"] - }, - { - "id": "deal-notification-bot", - "type": "widget", - "name": "Deal Notifications", - "description": "Real-time deal updates in Slack channels", - "locations": ["channel.tab"], - "widgetType": "custom", - "interactive": true, - "component": { - "type": "native", - "source": "slack-blocks", - "config": { - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": "Deal Activity Feed" - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Recent Deal Updates*" - } - }, - { - "type": "divider" - } - ], - "home_tab": true, - "messages_tab": false - } - }, - "dataSource": { - "type": "webhook", - "endpoint": "https://api.frigg.cloud/slack/webhooks/deals", - "events": ["deal.created", "deal.updated", "deal.closed"] - }, - "updateStrategy": "realtime", - "data": { - "subscriptions": [ - { - "entity": "deal", - "events": ["create", "update"], - "filters": [ - { - "field": "stage", - "operator": "in", - "value": ["closedwon", "closedlost"] - } - ], - "webhook": "https://api.frigg.cloud/slack/webhooks/deal-updates" - } - ] - } - }, - { - "id": "team-dashboard-home", - "type": "panel", - "name": "Team Dashboard", - "description": "Comprehensive team performance dashboard", - "locations": ["app-home.tab"], - "position": "tab", - "component": { - "type": "native", - "source": "slack-home", - "config": { - "view": { - "type": "home", - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": "Team Performance Dashboard" - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Weekly Summary*\n• Deals Closed: 12 ($245,000)\n• New Leads: 45\n• Activities: 234" - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "View Details" - }, - "action_id": "view_details" - } - }, - { - "type": "actions", - "elements": [ - { - "type": "button", - "text": { - "type": "plain_text", - "text": "Create Deal" - }, - "style": "primary", - "action_id": "create_deal" - }, - { - "type": "button", - "text": { - "type": "plain_text", - "text": "View Pipeline" - }, - "action_id": "view_pipeline" - } - ] - } - ] - } - } - }, - "refreshInterval": 300 - }, - { - "id": "smart-notifications", - "type": "widget", - "name": "Smart Notifications", - "description": "AI-powered notification filtering and routing", - "locations": ["global.shortcut"], - "widgetType": "custom", - "interactive": false, - "component": { - "type": "native", - "source": "notification-engine", - "config": { - "rules": [ - { - "name": "High-value deals", - "condition": "deal.amount > 50000", - "channel": "#sales-wins", - "template": ":moneybag: New high-value deal: {deal.name} - ${deal.amount}" - }, - { - "name": "Customer escalations", - "condition": "ticket.priority = 'high' AND ticket.source = 'customer'", - "channel": "#customer-success", - "template": ":warning: Customer escalation: {ticket.subject}" - } - ] - } - }, - "dataSource": { - "type": "api", - "endpoint": "/api/notifications/smart", - "params": { - "ai_filtering": true, - "priority_threshold": "medium" - } - }, - "updateStrategy": "realtime" - }, - { - "id": "workflow-automation", - "type": "action", - "name": "Workflow Builder", - "description": "Create automated workflows between Slack and CRM", - "locations": ["global.shortcut"], - "actionType": "global-shortcut", - "label": "Create Workflow", - "shortcut": "Cmd+Shift+W", - "handler": { - "type": "modal", - "config": { - "callback_id": "workflow_builder", - "title": "Workflow Builder", - "submit": { - "type": "plain_text", - "text": "Create Workflow" - }, - "blocks": [ - { - "type": "input", - "label": { - "type": "plain_text", - "text": "Workflow Name" - }, - "element": { - "type": "plain_text_input", - "action_id": "workflow_name" - } - }, - { - "type": "input", - "label": { - "type": "plain_text", - "text": "Trigger" - }, - "element": { - "type": "static_select", - "action_id": "workflow_trigger", - "options": [ - { - "text": { - "type": "plain_text", - "text": "New message in channel" - }, - "value": "message" - }, - { - "text": { - "type": "plain_text", - "text": "Reaction added" - }, - "value": "reaction" - }, - { - "text": { - "type": "plain_text", - "text": "Scheduled time" - }, - "value": "schedule" - } - ] - } - } - ] - } - } - } - ], - "settings": { - "configurable": true, - "schema": { - "type": "object", - "properties": { - "defaultChannel": { - "type": "string", - "title": "Default Notification Channel", - "description": "Default Slack channel for CRM notifications", - "default": "#general", - "pattern": "^#[a-z0-9-]+$" - }, - "notificationLevel": { - "type": "string", - "title": "Notification Level", - "description": "Control notification frequency", - "default": "important", - "enum": ["all", "important", "critical"], - "ui": { - "widget": "select", - "help": "All: Every update, Important: Significant changes, Critical: Urgent only" - } - }, - "enableAI": { - "type": "boolean", - "title": "Enable AI Features", - "description": "Use AI for smart notifications and suggestions", - "default": true - }, - "workingHours": { - "type": "object", - "title": "Working Hours", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "timezone": { - "type": "string", - "default": "America/New_York" - }, - "start": { - "type": "string", - "default": "09:00" - }, - "end": { - "type": "string", - "default": "17:00" - } - } - } - } - } - }, - "lifecycle": { - "install": "https://api.frigg.cloud/webhooks/slack/install", - "uninstall": "https://api.frigg.cloud/webhooks/slack/uninstall", - "update": "https://api.frigg.cloud/webhooks/slack/update" - } - } -} \ No newline at end of file diff --git a/packages/needs-updating/slack/fenestra/platform.fenestra.yaml b/packages/needs-updating/slack/fenestra/platform.fenestra.yaml deleted file mode 100644 index 908b1ca..0000000 --- a/packages/needs-updating/slack/fenestra/platform.fenestra.yaml +++ /dev/null @@ -1,496 +0,0 @@ -# Slack Platform - Fenestra Specification -fenestra: "1.0.0" -platform: - name: Slack - description: All varieties of available Slack UI extensibility, from Bot interactions to Block Kit interfaces, Workflow steps, and App Home experiences - version: "1.7" - baseUrl: "https://slack.com/api" - documentation: "https://api.slack.com" - marketplace: "https://slack.com/apps" - support: "https://api.slack.com/support" - -extensionTypes: - bot-interaction: - name: Bot Interactions - description: Conversational interfaces that respond to user messages and interactions - contexts: - - direct-message - - channel - - group-message - - thread - rendering: - - text-response - - block-kit-message - - rich-media - - ephemeral-message - communication: - - events-api - - web-api - - socket-mode - - webhooks - capabilities: - - message-posting - - file-sharing - - user-lookup - - channel-management - - conversation-history - triggers: - - mention - - direct-message - - keyword - - reaction - - app-mention - examples: - - name: Support Bot - description: Handles customer support requests and ticket creation - responseType: "interactive-blocks" - - name: Standup Bot - description: Collects daily standup responses from team members - responseType: "ephemeral-forms" - - block-kit-ui: - name: Block Kit Interfaces - description: Rich interactive interfaces using Slack's Block Kit framework - contexts: - - message - - modal - - home-tab - - workflow-step - - app-mention-response - rendering: - - block-kit-schema - - interactive-components - - form-elements - communication: - - interactive-components - - view-submissions - - block-actions - capabilities: - - form-collection - - data-display - - user-interaction - - conditional-logic - - dynamic-content - triggers: - - button-click - - dropdown-select - - modal-open - - date-picker - - overflow-menu - examples: - - name: Project Creation Modal - description: Complex form for creating new projects with multiple fields - blockTypes: ["input", "section", "actions", "datepicker"] - - slash-command: - name: Slash Commands - description: Custom commands triggered by typing / in the message input - contexts: - - message-composer - - any-channel - - direct-message - rendering: - - ephemeral-response - - in-channel-response - - delayed-response - - modal-trigger - communication: - - command-webhook - - response-url - - follow-up-messages - capabilities: - - command-processing - - response-posting - - parameter-parsing - - help-text - triggers: - - slash-command-invocation - - command-with-parameters - examples: - - name: "/standup" - description: Starts daily standup process for team - responseType: "modal" - parameters: ["team", "date"] - - workflow-step: - name: Workflow Steps - description: Custom steps that can be added to Slack Workflow Builder - contexts: - - workflow-builder - - automation-workflows - - scheduled-workflows - rendering: - - step-configuration - - step-execution - - step-results - communication: - - step-webhook - - completion-callback - - error-handling - capabilities: - - data-transformation - - external-integration - - conditional-branching - - input-validation - triggers: - - workflow-execution - - scheduled-trigger - - event-trigger - examples: - - name: Approval Step - description: Sends approval request to manager before proceeding - stepType: "approval" - inputFields: ["approver", "message", "timeout"] - - home-tab: - name: App Home - description: Dedicated space for app interaction accessible from App Home - contexts: - - app-home - - user-personal-space - rendering: - - block-kit-view - - dynamic-content - - personalized-interface - communication: - - app-home-events - - interactive-components - - view-updates - capabilities: - - personalized-content - - persistent-state - - user-onboarding - - dashboard-display - triggers: - - home-tab-open - - user-interaction - - periodic-refresh - examples: - - name: Task Dashboard - description: Personal task management interface - features: ["task-list", "quick-actions", "progress-tracking"] - - shortcuts: - name: Shortcuts - description: Quick actions accessible from various contexts in Slack - contexts: - - message-action - - global-shortcut - - message-composer - rendering: - - modal-dialog - - direct-action - - in-place-edit - communication: - - shortcut-webhook - - interactive-response - - action-completion - capabilities: - - context-access - - quick-actions - - message-processing - - bulk-operations - triggers: - - shortcut-invocation - - context-menu-selection - examples: - - name: "Create Task from Message" - description: Converts any message into a task - shortcutType: "message" - modalRequired: true - - events-subscription: - name: Events Subscription - description: Real-time notifications for workspace events - contexts: - - workspace-events - - user-events - - channel-events - - message-events - rendering: - - webhook-payload - - event-processing - communication: - - events-api - - webhook-delivery - - retry-mechanism - capabilities: - - real-time-monitoring - - event-filtering - - batch-processing - - custom-reactions - triggers: - - user-action - - system-event - - scheduled-event - examples: - - name: Onboarding Tracker - description: Monitors new user activity and sends welcome messages - events: ["team_join", "first_message", "profile_change"] - - calls-integration: - name: Calls Integration - description: Custom calling experiences within Slack - contexts: - - call-interface - - video-call - - screen-share - rendering: - - call-ui - - controls-overlay - - call-preview - communication: - - calls-api - - real-time-media - - call-events - capabilities: - - call-initiation - - call-control - - recording-access - - screen-sharing - triggers: - - call-button-click - - slash-command - - workflow-step - examples: - - name: Custom Video Provider - description: Integration with third-party video calling service - provider: "custom-webrtc" - -communication: - events-api: - description: Real-time event notifications via HTTP webhooks - delivery: - - webhook - - request-url - events: - - app_mention - - message.channels - - message.groups - - message.im - - team_join - - channel_created - - reaction_added - retryPolicy: "exponential-backoff" - verification: "request-signing" - - web-api: - description: RESTful API for Slack operations and data access - baseUrl: "https://slack.com/api" - authentication: - - bot-token - - user-token - - app-token - rateLimit: "tier-based limits" - methods: - - chat.postMessage - - conversations.list - - users.info - - files.upload - - views.open - - workflows.stepCompleted - - socket-mode: - description: WebSocket connection for real-time events without public endpoints - authentication: - - app-token - benefits: - - no-public-endpoint - - real-time-delivery - - reduced-latency - events: "same as Events API" - connection: "websocket" - - interactive-components: - description: Handles user interactions with Block Kit elements - delivery: "webhook" - interactions: - - button-clicks - - select-menus - - datepickers - - modal-submissions - - view-closed - verification: "request-signing" - -authentication: - oauth2: - authorizationUrl: "https://slack.com/oauth/v2/authorize" - tokenUrl: "https://slack.com/api/oauth.v2.access" - scopes: - bot: - - app_mentions:read - - channels:history - - channels:read - - chat:write - - commands - - files:read - - groups:history - - im:history - - reactions:read - - users:read - - workflows:read - user: - - channels:read - - chat:write - - files:read - - users:read - flow: "authorization_code" - - bot-token: - format: "xoxb-*" - description: "Bot-level permissions for app functionality" - scope: "bot-scoped permissions" - usage: "server-to-server API calls" - - user-token: - format: "xoxp-*" - description: "User-level permissions for on-behalf-of actions" - scope: "user-scoped permissions" - usage: "user-context API calls" - - app-token: - format: "xapp-*" - description: "App-level token for Socket Mode connections" - scope: "app-level permissions" - usage: "websocket connections" - -deployment: - app-directory: - name: "Slack App Directory" - url: "https://slack.com/apps" - reviewProcess: true - categories: - - productivity - - developer-tools - - communication - - project-management - - analytics - - social-fun - distribution: "public" - installation: "oauth-flow" - - enterprise-grid: - name: "Enterprise Grid" - distribution: "organization-wide" - adminApproval: "required" - policies: "org-level-control" - installation: "admin-managed" - - direct-install: - name: "Direct Install" - distribution: "workspace-specific" - oauth: "required" - scope: "single-workspace" - - custom-integration: - name: "Custom Integration" - description: "Legacy integration method" - status: "deprecated" - recommendation: "migrate-to-apps" - -sdks: - bolt-javascript: - name: "Bolt Framework for JavaScript" - url: "https://github.com/slackapi/bolt-js" - language: "javascript" - features: - - event-handling - - interactive-components - - slash-commands - - middleware-support - - typescript-support - - bolt-python: - name: "Bolt Framework for Python" - url: "https://github.com/slackapi/bolt-python" - language: "python" - features: - - async-support - - flask-integration - - django-integration - - middleware-system - - web-api-clients: - name: "Web API Client Libraries" - languages: - - javascript: "@slack/web-api" - - python: "slack-sdk" - - java: "slack-api-client" - - csharp: "SlackAPI" - features: - - method-calls - - error-handling - - rate-limiting - - type-safety - - block-kit-builder: - name: "Block Kit Builder" - url: "https://app.slack.com/block-kit-builder" - type: "visual-designer" - features: - - drag-drop-interface - - code-generation - - preview-mode - - template-library - - slack-cli: - name: "Slack CLI" - url: "https://api.slack.com/automation/cli" - features: - - app-scaffolding - - local-development - - deployment-tools - - function-testing - -examples: - productivity-bot: - name: "Team Productivity Bot" - description: "Helps teams track goals and manage daily standups" - types: - - bot-interaction - - slash-command - - home-tab - - workflow-step - features: - - daily-standup-collection - - goal-tracking - - team-analytics - - reminder-notifications - - approval-workflow: - name: "Approval Workflow Steps" - description: "Custom approval steps for various business processes" - types: - - workflow-step - - block-kit-ui - features: - - multi-level-approval - - timeout-handling - - notification-escalation - - audit-trail - - customer-support: - name: "Customer Support Integration" - description: "Connects Slack with external support ticket systems" - types: - - events-subscription - - shortcuts - - bot-interaction - features: - - ticket-creation - - status-updates - - customer-context - - team-notifications - -tags: - - messaging - - collaboration - - automation - - productivity - - workflow - - real-time - - notifications - -x-slack-manifest-version: "1.7" -x-slack-distribution-method: "app-directory" -x-slack-socket-mode-supported: true \ No newline at end of file diff --git a/packages/needs-updating/slack/fenestra/schemas/slack-validation.json b/packages/needs-updating/slack/fenestra/schemas/slack-validation.json deleted file mode 100644 index 3a43c78..0000000 --- a/packages/needs-updating/slack/fenestra/schemas/slack-validation.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Slack Fenestra Validation Schema", - "description": "Validation schema for Slack Fenestra specifications", - "type": "object", - "properties": { - "fenestra": { - "type": "string", - "pattern": "^1\.0\.0$" - }, - "platform": { - "type": "object", - "required": ["name", "description"] - } - }, - "required": ["fenestra", "platform"] -} diff --git a/packages/needs-updating/slack/index.js b/packages/needs-updating/slack/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/slack/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/slack/jest-setup.js b/packages/needs-updating/slack/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/slack/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/slack/jest-teardown.js b/packages/needs-updating/slack/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/slack/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/slack/manager.js b/packages/needs-updating/slack/manager.js deleted file mode 100644 index b225d4e..0000000 --- a/packages/needs-updating/slack/manager.js +++ /dev/null @@ -1,233 +0,0 @@ -const { - get, debug, flushDebugLog, - ModuleManager, - ModuleConstants -} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const {ConfigFields} = require('./authFields'); -const Config = require('./defaultConfig.json'); -const {IntegrationMapping} = require('./models/integrationMapping'); -const moment = require("moment/moment"); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - static IntegrationMapping = IntegrationMapping; - - constructor(params) { - super(params); - this.redirect_uri = get(params, 'redirect_uri', null); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - let instance = new this(params); - - const apiParams = {delegate: instance}; - if (instance.redirect_uri) apiParams.redirect_uri = instance.redirect_uri; - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - instance.credential = await Credential.findById( - instance.entity.credential - ); - apiParams.access_token = instance.credential.access_token; - apiParams.refresh_token = instance.credential.refresh_token; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: await this.api.getAuthUri(), - type: ModuleConstants.authType.oauth2, - data: {}, - }; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.authTest()) validAuth = true; - } catch (e) { - flushDebugLog(e); - } - return validAuth; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code', null); - - // For OAuth2, generate the token and store in this.credential and the DB - let authInfo; - try { - authInfo = await this.api.getTokenFromCode(code); - } catch (e) { - flushDebugLog(e); - throw new Error('Auth Error'); - } - const authRes = await this.testAuth(); - if (!authRes) throw new Error('Auth Error'); - - const isUserScopeAuthorized = authInfo.authed_user && authInfo.authed_user.access_token; - const teamId = authInfo.team.id; - // get entity identifying information from the api. If we have the user access token, - // it means we should store it along with their id - const externalId = isUserScopeAuthorized ? - authInfo.authed_user.id : teamId; - - await this.findOrCreateUserEntity({ - externalId: externalId, - name: authInfo.team.name, - }); - - const returnObj = { - type: Manager.getName(), - credential_id: this.credential.id, - entity_id: this.entity.id, - team_entity_id: null, - auth_info: authInfo, - }; - - if (isUserScopeAuthorized) { - const teamEntity = await this.createAndReturnTeamEntity({ - authInfo, - teamId, - }); - - returnObj.team_entity_id = teamEntity.id; - } - - return returnObj; - } - - async createAndReturnTeamEntity({ - authInfo, - teamId, - }) { - let teamEntity = await Entity.findOne({ - externalId: teamId - }); - if (!teamEntity) { - const credential = await Credential.create({ - access_token: authInfo.access_token, - refresh_token: authInfo.refresh_token || null, - externalId: teamId, - auth_is_valid: true, - }); - - // create team entity - const createObj = { - credential: credential.id, - user: null, - name: authInfo.team.name, - externalId: teamId, - }; - teamEntity = await Entity.create(createObj); - } - return teamEntity; - } - - async getEntityOptions() { - // No entity options to get. Probably won't even hit this - return []; - } - - async findOrCreateUserEntity(params) { - const externalId = get(params, 'externalId', null); - const name = get(params, 'name', null); - - const search = await Entity.find({ - externalId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this?.userId, - name, - externalId, - }; - this.entity = await Entity.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug( - 'Multiple entities found with the same external ID:', - externalId - ); - throw new Error( - 'Multiple entities found with the same external ID' - ); - } - } - - async deauthorize() { - this.api = new Api(); - - // delete credentials from the database - const entity = await Entity.find({user: this.userId}); - if (entity.credential) { - await Credential.deleteOne({_id: entity.credential}); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (!(notifier instanceof Api)) { - // no-op - } else if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - await this.updateOrCreateCredential(); - } else if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } else if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - - async updateOrCreateCredential() { - const workspaceInfo = await this.api.authTest(); - const isTeamUser = workspaceInfo['bot_user_id']; - const externalId = isTeamUser ? get(workspaceInfo, 'team_id') : get(workspaceInfo, 'user_id'); - const updatedToken = { - access_token: this.api.access_token, - refresh_token: this.api.refresh_token, - externalId, - auth_is_valid: true, - }; - - // search for a credential for this externalId - // skip if we already have a credential - if (!this.credential) { - const credentialSearch = await Credential.find({externalId}); - if (credentialSearch.length > 1) { - debug( - `Multiple credentials found with same externalId: ${externalId}` - ); - } else if (credentialSearch.length === 1) { - // found exactly one credential with this externalId - this.credential = credentialSearch[0]; - } else { - // found no credential with this externalId (match none for insert) - this.credential = {$exists: false}; - } - } - // update credential or create if none was found - // NOTE: upsert skips validation - this.credential = await Credential.findOneAndUpdate( - {_id: this.credential}, - {$set: updatedToken}, - {useFindAndModify: true, new: true, upsert: true} - ); - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/slack/models/credential.js b/packages/needs-updating/slack/models/credential.js deleted file mode 100644 index f2f6cdd..0000000 --- a/packages/needs-updating/slack/models/credential.js +++ /dev/null @@ -1,19 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - refresh_token: { - type: String, - trim: true, - lhEncrypt: true, - }, -}); - -const name = 'SlackCredential'; -const Credential = Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/slack/models/entity.js b/packages/needs-updating/slack/models/entity.js deleted file mode 100644 index b3ae9e8..0000000 --- a/packages/needs-updating/slack/models/entity.js +++ /dev/null @@ -1,10 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; - -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'SlackEntity'; -const Entity = Parent.discriminators?.[name] || Parent.discriminator(name, schema); - -module.exports = {Entity}; diff --git a/packages/needs-updating/slack/models/integrationMapping.js b/packages/needs-updating/slack/models/integrationMapping.js deleted file mode 100644 index 2f75442..0000000 --- a/packages/needs-updating/slack/models/integrationMapping.js +++ /dev/null @@ -1,10 +0,0 @@ -const {IntegrationMapping: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'SlackMessage'; -const IntegrationMapping = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {IntegrationMapping}; diff --git a/packages/needs-updating/slack/package.json b/packages/needs-updating/slack/package.json deleted file mode 100644 index cb38bfa..0000000 --- a/packages/needs-updating/slack/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@friggframework/api-module-slack", - "version": "1.1.3", - "prettier": "@friggframework/prettier-config", - "description": "", - "main": "index.js", - "scripts": { - "lint:fix": "prettier --write --loglevel error . && eslint . --fix", - "test": "jest" - }, - "author": "", - "license": "MIT", - "devDependencies": { - "@friggframework/devtools": "^1.1.2", - "@friggframework/test": "^1.1.2", - "eslint": "^8.22.0", - "jest": "^28.1.3", - "prettier": "^2.7.1", - "sinon": "^14.0.0" - }, - "dependencies": { - "@friggframework/core": "^1.1.2", - "qs": "^6.11.1" - } -} diff --git a/packages/needs-updating/slack/test/api.test.js b/packages/needs-updating/slack/test/api.test.js deleted file mode 100644 index f637759..0000000 --- a/packages/needs-updating/slack/test/api.test.js +++ /dev/null @@ -1,97 +0,0 @@ -const {Authenticator} = require('@friggframework/devtools'); -const {Api} = require('../api'); -const config = require('../defaultConfig.json'); -require('dotenv').config(); - -describe(`Should fully test the ${config.label} API Class`, () => { - let api; - beforeAll(async () => { - api = new Api({access_token: process.env.TEST_ACCESS_TOKEN}); - }); - - afterAll(async () => { - }); - - describe('Authentication Tests', () => { - it('should return auth requirements', async () => { - const authUri = await api.getAuthUri(); - expect(authUri).exists; - }); - - it('should generate an access_token from a code', async () => { - const authUri = await api.getAuthUri(); - const response = await Authenticator.oauth2(authUri); - const baseArr = response.base.split('/'); - response.entityType = baseArr[baseArr.length - 1]; - delete response.base; - - const authRes = await api.getTokenFromCode(response.data.code); - expect(api.access_token).toBeTruthy(); - }); - - it('should test auth using access token', async () => { - const clientId = api.client_id; - const clientSecret = api.client_secret; - const redirectUri = api.redirect_uri; - - expect(clientId).exists; - expect(clientSecret).exists; - expect(redirectUri).exists; - - const response = await api.authTest(); - expect(response.ok).toBeTruthy(); - }); - it.skip('should refresh auth when token expires', async () => { - api.access_token = 'broken'; - await api.refreshToken(); - expect(api.access_token).to.not.equal('broken'); - }); - }); - - describe('Channel Tests', () => { - it('should return channels', async () => { - const channels = await api.listChannels(); - - expect(channels.ok).toBeTruthy(); - }); - describe('Direct Message Channel Tests', () => { - let messageChannel; - let messageResponse; - beforeEach(async () => { - const userEmail = process.env.TEST_USER_EMAIL; - const userDetails = await api.lookupUserByEmail(userEmail); - messageResponse = await api.postMessage({ - channel: userDetails.user.id, - text: 'Hello World!', - }); - expect(messageResponse.ok).toBeTruthy(); - messageChannel = messageResponse.channel; - }); - afterEach(async () => { - await api.deleteMessage({ - channel: messageChannel, - ts: messageResponse.ts, - asUser: true, - }); - }); - it('should create a direct message to a user', async () => { - expect(messageResponse.ok).toBeTruthy(); - }); - it('should return channel history', async () => { - const history = await api.getChannelHistory({ - channel: messageChannel, - latest: messageResponse.ts, - oldest: messageResponse.ts, - inclusive: true, - }); - expect(history.ok).toBeTruthy(); - }); - it('should return channel members', async () => { - const members = await api.getChannelMembers({ - channel: messageChannel - }); - expect(members.ok).toBeTruthy(); - }); - }); - }); -}); diff --git a/packages/needs-updating/slack/test/auther.test.js b/packages/needs-updating/slack/test/auther.test.js deleted file mode 100644 index 2f2383d..0000000 --- a/packages/needs-updating/slack/test/auther.test.js +++ /dev/null @@ -1,110 +0,0 @@ -const {connectToDatabase, disconnectFromDatabase, createObjectId, Auther} = require('@friggframework/core'); -const { - Authenticator, - testDefinitionRequiredAuthMethods, - testAutherDefinition -} = require("@friggframework/devtools"); -const {Definition} = require('../definition'); - - -const mocks = { - getUserDetails: {}, - authorizeResponse: {}, - getTokenFromCode: async function (code) { - const tokenResponse = { - "access_token": "foo", - "token_type": "Bearer", - "refresh_token": "bar", - "expires_in": 3600 - } - await this.setTokens(tokenResponse); - return tokenResponse - } -} -//testAutherDefinition(Definition, mocks) - - -describe(`${Definition.moduleName} Module Live Tests`, () => { - let module, authUrl; - beforeAll(async () => { - await connectToDatabase(); - module = await Auther.getInstance({ - definition: Definition, - userId: createObjectId(), - }); - }); - - afterAll(async () => { - await module.Credential.deleteMany(); - await module.Entity.deleteMany(); - await disconnectFromDatabase(); - }); - - describe('getAuthorizationRequirements() test', () => { - it('should return auth requirements', async () => { - const requirements = await module.getAuthorizationRequirements(); - expect(requirements).toBeDefined(); - expect(requirements.type).toEqual('oauth2'); - expect(requirements.url).toBeDefined(); - authUrl = requirements.url; - }); - }); - - describe('Authorization requests', () => { - let firstRes; - it('processAuthorizationCallback()', async () => { - const response = await Authenticator.oauth2(authUrl); - firstRes = await module.processAuthorizationCallback(response); - expect(firstRes).toBeDefined(); - expect(firstRes.entity_id).toBeDefined(); - expect(firstRes.credential_id).toBeDefined(); - }); - it.skip('retrieves existing entity on subsequent calls', async () => { - const response = await Authenticator.oauth2(authUrl); - const res = await module.processAuthorizationCallback(response); - expect(res).toEqual(firstRes); - }); - }); - describe('Test credential retrieval and module instantiation', () => { - it('retrieve by entity id', async () => { - const newModule = await Auther.getInstance({ - userId: module.userId, - entityId: module.entity.id, - definition: Definition, - }); - expect(newModule).toBeDefined(); - expect(newModule.entity).toBeDefined(); - expect(newModule.credential).toBeDefined(); - expect(await newModule.testAuth()).toBeTruthy(); - - }); - - it.skip('retrieve by credential id', async () => { - const newModule = await Auther.getInstance({ - userId: module.userId, - credentialId: module.credential.id, - definition: Definition, - }); - expect(newModule).toBeDefined(); - expect(newModule.credential).toBeDefined(); - expect(await newModule.testAuth()).toBeTruthy(); - - }); - }); - - describe.skip('Test team auth', () => { - it('processAuthorizationCallback()', async () => { - // const newModule = await Auther.getInstance({ - // userId: module.userId, - // entityId: module.entity.id, - // definition: Definition, - // }); - // await newModule.processAuthorizationCallback(); - // const res = await newModule.testAuth(); - // expect(res).toBeTruthy(); - // expect(newModule.api.graphApi.access_token).toBeTruthy(); - // expect(newModule.api.botFrameworkApi.access_token).toBeTruthy(); - }) - }) - -}); diff --git a/packages/needs-updating/slack/test/manager.test.js b/packages/needs-updating/slack/test/manager.test.js deleted file mode 100644 index fa89aca..0000000 --- a/packages/needs-updating/slack/test/manager.test.js +++ /dev/null @@ -1,119 +0,0 @@ -const {Authenticator} = require('@friggframework/devtools'); -const Manager = require('../manager'); -const mongoose = require('mongoose'); -const config = require('../defaultConfig.json'); -const {expect} = require('chai'); -require('dotenv').config(); -const nock = require('nock'); - -describe(`Should fully test the ${config.label} Manager`, () => { - let manager, authUrl; - - beforeAll(async () => { - await mongoose.connect(process.env.MONGO_URI); - manager = await Manager.getInstance({ - userId: new mongoose.Types.ObjectId(), - }); - }); - - afterAll(async () => { - await Manager.Credential.deleteMany(); - await Manager.Entity.deleteMany(); - await mongoose.disconnect(); - }); - - it('getAuthorizationRequirements() should return auth requirements', async () => { - const requirements = await manager.getAuthorizationRequirements(); - expect(requirements).exists; - expect(requirements.type).to.equal('oauth2'); - authUrl = requirements.url; - }); - describe('processAuthorizationCallback()', () => { - it('should return auth details', async () => { - const response = await Authenticator.oauth2(authUrl); - const baseArr = response.base.split('/'); - response.entityType = baseArr[baseArr.length - 1]; - delete response.base; - - const authRes = await manager.processAuthorizationCallback({ - data: { - code: response.data.code, - }, - }); - expect(authRes).to.exist; - expect(authRes).to.have.property('entity_id'); - expect(authRes).to.have.property('credential_id'); - expect(authRes).to.have.property('type'); - }); - it('should refresh token', async () => { - manager.api.access_token = 'nope'; - await manager.testAuth(); - expect(manager.api.access_token).to.not.equal('nope'); - expect(manager.api.access_token).to.exist; - }); - it('should refresh token after a fresh database retrieval', async () => { - const newManager = await Manager.getInstance({ - userId: manager.userId, - entityId: manager.entity.id, - }); - newManager.api.access_token = 'nope'; - await newManager.testAuth(); - expect(newManager.api.access_token).to.not.equal('nope'); - expect(newManager.api.access_token).to.exist; - }); - - it('should refresh token after it expires', async () => { - const newManager = await Manager.getInstance({ - userId: manager.userId, - entityId: manager.entity.id, - }); - const oldToken = `${newManager.api.access_token}`; - const testAuthNock = nock(newManager.api.baseUrl, { - allowUnmocked: true, - }) - .post(newManager.api.URLs.authTest) - .reply(200, { - ok: false, - error: 'token_expired', - }); - await newManager.testAuth(); - expect(testAuthNock.isDone()); - expect(newManager.api.access_token).to.not.equal(oldToken); - expect(newManager.api.access_token).to.exist; - }); - it('auth refresh should fail if redirect URI changes', async () => { - const newManager = await Manager.getInstance({ - userId: manager.userId, - entityId: manager.entity.id, - }); - const testAuthNock = nock(manager.api.baseUrl, { - allowUnmocked: true, - }) - .post(manager.api.URLs.authTest) - .reply(200, { - ok: false, - error: 'token_expired', - }); - newManager.api.redirect_uri = 'https://bogus.com'; - - try { - const authRes = await newManager.testAuth(); - expect(testAuthNock.isDone()); - expect(authRes).to.equal(false); - } catch (e) { - } - }); - it('should error if incorrect auth data', async () => { - try { - const authRes = await manager.processAuthorizationCallback({ - data: { - code: 'bad', - }, - }); - expect(authRes).to.not.exist; - } catch (e) { - expect(e.message).to.contain('Auth Error'); - } - }); - }); -}); diff --git a/packages/needs-updating/terminus/index.js b/packages/needs-updating/terminus/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/needs-updating/terminus/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/terminus/jest-setup.js b/packages/needs-updating/terminus/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/needs-updating/terminus/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/needs-updating/terminus/jest-teardown.js b/packages/needs-updating/terminus/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/terminus/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/terminus/manager.js b/packages/needs-updating/terminus/manager.js deleted file mode 100644 index b59d2b3..0000000 --- a/packages/needs-updating/terminus/manager.js +++ /dev/null @@ -1,197 +0,0 @@ -const { - ModuleManager, - ModuleConstants, - debug -} = require('@friggframework/core'); -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); - -Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - // initializes the Api - const terminusParams = {delegate: instance}; - if (params.entityId) { - try { - instance.entity = await Entity.findById(params.entityId); - let credential = await Credential.findById( - instance.entity.credential - ); - terminusParams.api_key = credential.apiKey; - } catch (e) { - debug( - `Error retrieving Salesforce credential for Entity ${instance.entity.id}` - ); - } - } - instance.api = await new Api(terminusParams); - - return instance; - } - - async testAuth() { - let validAuth = false; - if (await this.api.listFolders()) validAuth = true; - return validAuth; - } - - async getAuthorizationRequirements(params) { - return { - // url: await this.api.getAuthUri(), - type: ModuleConstants.authType.apiKey, - jsonSchema: { - // "title": "Authorization Credentials", - // "description": "A simple form example.", - type: 'object', - required: ['api_key'], - properties: { - api_key: { - type: 'string', - title: 'Username', - }, - }, - }, - uiSchema: { - api_key: { - 'ui:help': 'The API Key you use to access the Terminus API', - 'ui:placeholder': 'User API Key', - }, - }, - }; - } - - async processAuthorizationCallback(params) { - // Create credential and entity? - const api_key = get(params.data, 'apiKey'); - await this.api.setApiKey(api_key); - const isValid = await this.testAuth(); - if (isValid) { - let credential, entity; - const credentialSearch = await this.credentialMO.list({ - user: this.userId, - }); - // If found, then credential key should match - if (credentialSearch.length === 1) { - if (credentialSearch[0].apiKey !== api_key) { - credential = await this.credentialMO.update( - credentialSearch[0]._id, - {apiKey: api_key} - ); - } else { - credential = await this.credentialMO.get( - credentialSearch[0]._id - ); - } - const entityList = await this.entityMO.list({ - credential: credential._id, - }); - if (entityList.length === 1) { - entity = entityList[0]; - if (entity._id.toString() !== this.entity._id.toString()) { - throw new Error( - 'This credential is in use by another user' - ); - } - } - if (entityList.length === 0) { - entity = await this.entityMO.update(this.entity._id, { - credential, - }); - } - } - - // If not found, then create credential and entity - if (credentialSearch.length === 0) { - credential = await this.credentialMO.create({ - apiKey: api_key, - user: this.userId, - }); - entity = await this.entityMO.update(this.entity._id, { - credential, - }); - } - - //This shouldn't happen - if (credentialSearch.length > 1) { - throw new Error( - "Shouldn't have more than one credential per user ID" - ); - } - - return { - credential_id: credential._id, - entity_id: entity._id, - type: Manager.getName(), - }; - } - } - - //------------------------------------------------------------ - - async deauthorize() { - // wipe api connection - this.api = new Api({}); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - // Likely never invoked, as there isn't anything to invoke it yet - async receiveNotification(notifier, delegateString, object = null) { - // if (notifier instanceof Api) { - // if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - // console.log(`should update the api key: ${object}`); - // const updatedToken = { - // apiKey: this.api.apiKey - // }; - // let entity = await this.entityMO.getByUserId(this.userId); - // if (!entity) { - // entity = await this.entityMO.create({ - // user: this.userId, - // }); - // } - // let { credential } = entity; - // if (!credential) { - // credential = await this.credentialMO.create( - // updatedToken - // ); - // } else { - // credential = await this.credentialMO.update( - // credential, - // updatedToken - // ); - // } - // await this.entityMO.update(entity.id, { credential }); - // } - // if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - // await this.deauthorize(); - // console.log(this.checkUserAuthorized()); - // } - // } - } -} - -module.exports = Manager; diff --git a/packages/needs-updating/terminus/models/credential.js b/packages/needs-updating/terminus/models/credential.js deleted file mode 100644 index 44a62a0..0000000 --- a/packages/needs-updating/terminus/models/credential.js +++ /dev/null @@ -1,15 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); -const schema = new mongoose.Schema({ - apiKey: { - type: String, - trim: true, - unique: true, - lhEncrypt: true, - }, -}); - -const name = 'TerminusCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/needs-updating/terminus/models/entity.js b/packages/needs-updating/terminus/models/entity.js deleted file mode 100644 index 8fab94a..0000000 --- a/packages/needs-updating/terminus/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'TerminusEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/needs-updating/yotpo/index.js b/packages/needs-updating/yotpo/index.js deleted file mode 100644 index 4beb703..0000000 --- a/packages/needs-updating/yotpo/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api/api'); -const {Credential} = require('./credential'); -const {Entity} = require('./entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/needs-updating/yotpo/jest-setup.js b/packages/needs-updating/yotpo/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/needs-updating/yotpo/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/needs-updating/yotpo/jest-teardown.js b/packages/needs-updating/yotpo/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/needs-updating/yotpo/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/needs-updating/yotpo/manager.js b/packages/needs-updating/yotpo/manager.js deleted file mode 100644 index d52bd42..0000000 --- a/packages/needs-updating/yotpo/manager.js +++ /dev/null @@ -1,241 +0,0 @@ -const {get, debug, flushDebugLog} = require('@friggframework/core'); -const { - ModuleManager, - ModuleConstants, -} = require('@friggframework/module-plugin'); -const {Api} = require('./api/api'); -const {Entity} = require('./entity'); -const {Credential} = require('./credential'); - -const Config = require('./defaultConfig.json'); -const AuthFields = require('./authFields'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - let apiParams = { - client_id: process.env.YOTPO_CLIENT_ID, - client_secret: process.env.YOTPO_CLIENT_SECRET, - redirect_uri: `${process.env.REDIRECT_URI}/yotpo`, - delegate: instance, - }; - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - instance.credential = await Credential.findById( - instance.entity.credential - ); - apiParams = { - ...apiParams, - ...instance.credential.toObject(), - }; - apiParams.API_KEY_VALUE = apiParams.coreApiAccessToken; - } - instance.api = await new Api(apiParams); - if (apiParams.loyalty_api_key) { - instance.api.loyaltyApi.setApiKey(apiParams.loyalty_api_key); - instance.api.loyaltyApi.setGuid(apiParams.loyalty_guid); - } - - return instance; - } - - // Change to whatever your api uses to return identifying information - async testAuth() { - let validAuth = false; - const authRequests = [ - this.api.appDeveloperApi.listOrders(), - this.api.coreApi.listOrders(), - ]; - if ( - this.api.loyaltyApi.API_KEY_VALUE || - this.credential.loyalty_api_key - ) - authRequests.push(this.api.loyaltyApi.listActiveCampaigns()); - try { - await Promise.all(authRequests); - validAuth = true; - } catch (e) { - debug(e); - } - return validAuth; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.appDeveloperApi.authorizationUri, - type: ModuleConstants.authType.oauth2, - data: { - jsonSchema: AuthFields.jsonSchema, - uiSchema: AuthFields.uiSchema, - }, - }; - } - - async processAuthorizationCallback(params) { - const store_id = get(params.data, 'store_id', null); - const secret = get(params.data, 'secret', null); - const code = get(params.data, 'code', null); - const loyalty_api_key = get(params.data, 'loyalty_api_key', null); - const loyalty_guid = get(params.data, 'loyalty_guid', null); - // const appKey = get(params.data, 'app_key', null); - // vv TDOO temporary for specific implementation override. Don't do this at home. - const appKey = get(params.data, 'store_id', null); - this.api.coreApi.store_id = store_id; - this.api.coreApi.apiKeySecret = secret; - this.api.appDeveloperApi.appKey = appKey; - if (loyalty_api_key) this.api.loyaltyApi.setApiKey(loyalty_api_key); - if (loyalty_guid) this.api.loyaltyApi.setGuid(loyalty_guid); - await this.api.coreApi.getToken(); - await this.api.appDeveloperApi.getTokenFromCode(code); - const authRes = await this.testAuth(); - if (!authRes) throw new Error('Authentication failed'); - - await this.findOrCreateEntity({ - store_id, - secret, - }); - return { - credential_id: this.credential.id, - entity_id: this.entity.id, - type: Manager.getName(), - }; - } - - // Maybe need this if we want to offer JUST Core API - async findOrCreateCredential(params) { - const store_id = get(params.data, 'store_id', null); - const secret = get(params.data, 'secret', null); - - const search = await Credential.find({ - user: this.userId, - store_id, - secret, - }); - - if (search.length === 0) { - const createObj = { - user: this.userId, - store_id, - secret, - }; - this.credential = await Credential.create(createObj); - } else if (search.length === 1) { - this.credential = search[0]; - } else { - debug( - 'Multiple credentials found with the same Client ID', - store_id, - secret - ); - } - } - - async findOrCreateEntity(params) { - const store_id = get(params.data, 'store_id', null); - const name = get(params, 'name', null); - - const search = await Entity.find({ - user: this.userId, - externalId: store_id, - }); - if (search.length === 0) { - const createObj = { - credential: this.credential.id, - user: this.userId, - name, - externalId: store_id, - }; - this.entity = await Entity.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug( - 'Multiple entities found with the same external ID:', - store_id - ); - this.throwException(''); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (delegateString === this.api.appDeveloperApi.DLGT_TOKEN_UPDATE) { - const updatedToken = { - user: this.userId.toString(), - access_token: this.api.appDeveloperApi.access_token, - refresh_token: this.api.appDeveloperApi.refresh_token, - auth_is_valid: true, - store_id: this.api.coreApi.store_id, - secret: this.api.coreApi.secret, - coreApiAccessToken: this.api.coreApi.API_KEY_VALUE, - appKey: this.api.appDeveloperApi.appKey, - loyalty_api_key: this.api.loyaltyApi.API_KEY_VALUE, - loyalty_guid: this.api.loyaltyApi.GUID, - }; - - Object.keys(updatedToken).forEach( - (k) => updatedToken[k] == null && delete updatedToken[k] - ); - // TODO-new globally... multiple credentials should be allowed, this is 1:1 - if (!this.credential) { - let credentialSearch = await Credential.find({ - user: this.userId.toString(), - }); - if (credentialSearch.length === 0) { - this.credential = await Credential.create(updatedToken); - } else if (credentialSearch.length === 1) { - this.credential = await Credential.findOneAndUpdate( - {_id: credentialSearch[0]}, - {$set: updatedToken}, - {useFindAndModify: true, new: true} - ); - } else { - // Handling multiple credentials found with an error for the time being - debug( - 'Multiple credentials found with the same client ID:' - ); - } - } else { - this.credential = await Credential.findOneAndUpdate( - {_id: this.credential}, - {$set: updatedToken}, - {useFindAndModify: true, new: true} - ); - } - } - if ( - delegateString === this.api.appDeveloperApi.DLGT_TOKEN_DEAUTHORIZED - ) { - await this.deauthorize(); - } - if (delegateString === this.api.appDeveloperApi.DLGT_INVALID_AUTH) { - return this.markCredentialsInvalid(); - } - } - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/v1-ready/connectwise/.eslintrc.json b/packages/netx/.eslintrc.json similarity index 100% rename from packages/v1-ready/connectwise/.eslintrc.json rename to packages/netx/.eslintrc.json diff --git a/packages/needs-updating/outreach/CHANGELOG.md b/packages/netx/CHANGELOG.md similarity index 100% rename from packages/needs-updating/outreach/CHANGELOG.md rename to packages/netx/CHANGELOG.md diff --git a/packages/needs-updating/revio/LICENSE.md b/packages/netx/LICENSE.md similarity index 100% rename from packages/needs-updating/revio/LICENSE.md rename to packages/netx/LICENSE.md diff --git a/packages/needs-updating/netx/README.md b/packages/netx/README.md similarity index 100% rename from packages/needs-updating/netx/README.md rename to packages/netx/README.md diff --git a/packages/needs-updating/netx/api.js b/packages/netx/api.js similarity index 100% rename from packages/needs-updating/netx/api.js rename to packages/netx/api.js diff --git a/packages/needs-updating/netx/defaultConfig.json b/packages/netx/defaultConfig.json similarity index 100% rename from packages/needs-updating/netx/defaultConfig.json rename to packages/netx/defaultConfig.json diff --git a/packages/netx/definition.js b/packages/netx/definition.js new file mode 100644 index 0000000..24564a2 --- /dev/null +++ b/packages/netx/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Netx', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getTokenIdentity(); + return { + identifiers: {externalId: userDetails.companyId, user: userId}, + details: {name: userDetails.companyName}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'id_token', 'expires_in' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getTokenIdentity(); + return { + identifiers: {externalId: userDetails.companyId, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getTokenIdentity() + }, + }, + env: { + client_id: process.env.NETX_CLIENT_ID, + client_secret: process.env.NETX_CLIENT_SECRET, + scope: process.env.NETX_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/netx`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/netx/index.js b/packages/netx/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/netx/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/needs-updating/pipedrive/jest.config.js b/packages/netx/jest.config.js similarity index 100% rename from packages/needs-updating/pipedrive/jest.config.js rename to packages/netx/jest.config.js diff --git a/packages/needs-updating/monday/manager.test.js b/packages/netx/manager.test.js similarity index 100% rename from packages/needs-updating/monday/manager.test.js rename to packages/netx/manager.test.js diff --git a/packages/needs-updating/netx/test/Api.test.js b/packages/netx/test/Api.test.js similarity index 100% rename from packages/needs-updating/netx/test/Api.test.js rename to packages/netx/test/Api.test.js diff --git a/packages/netx/test/Manager.test.js b/packages/netx/test/Manager.test.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/needs-updating/netx/test/logotest.png b/packages/netx/test/logotest.png similarity index 100% rename from packages/needs-updating/netx/test/logotest.png rename to packages/netx/test/logotest.png diff --git a/packages/v1-ready/notion/README.md b/packages/notion/README.md similarity index 100% rename from packages/v1-ready/notion/README.md rename to packages/notion/README.md diff --git a/packages/v1-ready/notion/fenestra/platform.fenestra.yaml b/packages/notion/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/notion/fenestra/platform.fenestra.yaml rename to packages/notion/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/notion/fenestra/schemas/notion-validation.json b/packages/notion/fenestra/schemas/notion-validation.json similarity index 100% rename from packages/v1-ready/notion/fenestra/schemas/notion-validation.json rename to packages/notion/fenestra/schemas/notion-validation.json diff --git a/packages/v1-ready/notion/index.js b/packages/notion/index.js similarity index 100% rename from packages/v1-ready/notion/index.js rename to packages/notion/index.js diff --git a/packages/notion/openapi.yaml b/packages/notion/openapi.yaml new file mode 100644 index 0000000..22f347d --- /dev/null +++ b/packages/notion/openapi.yaml @@ -0,0 +1,342 @@ +openapi: 3.0.0 +info: + title: Notion API + description: API for interacting with Notion resources such as pages and databases. + version: 1.0.0 +servers: + - url: https://api.notion.com/v1 + description: Main API server +paths: + /pages/{page_id}: + get: + operationId: getPage + summary: Retrieve a page by its ID. + parameters: + - name: page_id + in: path + required: true + description: The ID of the page to retrieve. + schema: + type: string + format: uuid + - name: Notion-Version + in: header + required: true + description: Notion API version + schema: + type: string + default: 2022-06-28 + responses: + "200": + description: A JSON object representing the page. + content: + application/json: + schema: + $ref: "#/components/schemas/Page" + patch: + operationId: updatePage + summary: Update a page by its ID. + parameters: + - name: page_id + in: path + required: true + description: The ID of the page to update. + schema: + type: string + format: uuid + - name: Notion-Version + in: header + required: true + description: Notion API version + schema: + type: string + default: 2022-06-28 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PageUpdate" + responses: + "200": + description: The updated page. + content: + application/json: + schema: + $ref: "#/components/schemas/Page" + /databases/{database_id}: + get: + operationId: getDatabase + summary: Retrieve a database by its ID. + parameters: + - name: database_id + in: path + required: true + description: The ID of the database to retrieve. + schema: + type: string + format: uuid + - name: Notion-Version + in: header + required: true + description: Notion API version + schema: + type: string + default: 2022-06-28 + responses: + "200": + description: A JSON object representing the database. + content: + application/json: + schema: + $ref: "#/components/schemas/Database" +#error1 + /databases/{database_id}/query: + post: + operationId: queryDatabase + summary: Query a database. + parameters: + - name: database_id + in: path + required: true + description: The ID of the database to query. + schema: + type: string + format: uuid + - name: Notion-Version + in: header + required: true + description: Notion API version + schema: + type: string + default: 2022-06-28 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DatabaseQuery" + responses: + "200": + description: The query results. + content: + application/json: + schema: + $ref: "#/components/schemas/DatabaseRecord" + #error2 + /search: + post: + #error3 + operationId: search + summary: Search all pages and databases. + parameters: + - name: Notion-Version + in: header + required: true + description: Notion API version + schema: + type: string + default: 2022-06-28 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SearchRequest" + responses: + "200": + description: The search results. + content: + application/json: + schema: + $ref: "#/components/schemas/SearchResult" +components: + headers: + NotionVersion: + required: true + schema: + type: string + default: 2022-06-28 + description: Notion API version + schemas: + Page: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - page + id: + type: string + format: uuid + properties: + type: object + additionalProperties: true + PageUpdate: + type: object + properties: + properties: + type: object + additionalProperties: true + Database: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - database + id: + type: string + format: uuid + properties: + type: object + additionalProperties: true + User: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - user + id: + type: string + format: uuid + name: + type: string + avatar_url: + type: string + format: uri + BlockChildren: + type: array + items: + $ref: "#/components/schemas/Block" + Block: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - block + id: + type: string + format: uuid + type: + type: string + block_data: + type: object + additionalProperties: true + Comment: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - comment + id: + type: string + format: uuid + parent: + type: object + additionalProperties: true + content: + type: string + PagePropertyItem: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - property_item + id: + type: string + format: uuid + property_data: + type: object + additionalProperties: true + DatabaseQuery: + type: object + properties: + filter: + type: object + additionalProperties: true + sorts: + type: array + items: + type: object + additionalProperties: true + DatabaseRecord: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - database_record + id: + type: string + format: uuid + record_data: + type: object + additionalProperties: true + SearchRequest: + type: object + properties: + query: + type: string + sort: + type: object + additionalProperties: true + SearchResponse: + type: array + items: + $ref: "#/components/schemas/SearchResult" + SearchResult: + type: object + required: + - object + - id + properties: + object: + type: string + enum: + - search_result + id: + type: string + format: uuid + result_data: + type: object + additionalProperties: true + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT +security: + - BearerAuth: [] + + +#errors: +#In context=('paths', '/databases/{database_id}/query', '200', 'response', 'content', 'application/json', 'schema'), object schema missing properties +#In context=('paths', '/search', 'post', 'requestBody', 'content', 'application/json', 'schema'), reference to unknown component Search; using empty schema +#In path /search, method post, operationId search, request body schema is not an object schema; skipping +#In path /search, method post, operationId search, skipping function due to errors diff --git a/packages/v1-ready/notion/package.json b/packages/notion/package.json similarity index 100% rename from packages/v1-ready/notion/package.json rename to packages/notion/package.json diff --git a/packages/onesignal/README.md b/packages/onesignal/README.md new file mode 100644 index 0000000..7c7b38f --- /dev/null +++ b/packages/onesignal/README.md @@ -0,0 +1,202 @@ +# OneSignal API Module + +A comprehensive OneSignal API module for the Frigg framework, providing push notification capabilities for mobile, web, and email platforms. + +## Features + +- **Push Notifications**: Send notifications to mobile, web, and email +- **Segmentation**: Target specific user segments and tags +- **Scheduling**: Schedule notifications for future delivery +- **Templates**: Create and manage notification templates +- **Device Management**: Track and manage user devices +- **Analytics**: Detailed delivery and engagement analytics +- **Live Activities**: iOS Live Activities support +- **A/B Testing**: Test different notification variations +- **Rich Media**: Images, buttons, and interactive elements + +## Installation + +```bash +npm install @friggframework/api-module-onesignal +``` + +## Environment Variables + +```env +ONESIGNAL_APP_ID=your_app_id +ONESIGNAL_REST_API_KEY=your_rest_api_key +ONESIGNAL_USER_AUTH_KEY=your_user_auth_key +``` + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-onesignal'); + +const oneSignalApi = new Api({ + app_id: process.env.ONESIGNAL_APP_ID, + rest_api_key: process.env.ONESIGNAL_REST_API_KEY, + user_auth_key: process.env.ONESIGNAL_USER_AUTH_KEY +}); + +// Send a simple push notification +await oneSignalApi.sendPushNotification('Hello, World!', { + included_segments: ['All'] +}); + +// Send to specific users +await oneSignalApi.sendNotificationToUsers( + 'Personal message', + ['user-id-1', 'user-id-2'] +); + +// Send rich notification with image and buttons +await oneSignalApi.sendRichNotification( + 'Check out our new feature!', + { feature_id: 123 }, + { + big_picture: 'https://example.com/image.jpg', + buttons: [ + { id: 'view', text: 'View Feature' }, + { id: 'dismiss', text: 'Not Now' } + ] + } +); + +// Schedule a notification +const sendTime = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from now +await oneSignalApi.sendScheduledNotification( + 'Reminder: Check your dashboard', + sendTime.toISOString() +); +``` + +## Key Methods + +### Notifications +- `sendPushNotification(contents, options)` - Send basic push notification +- `sendNotificationToSegments(contents, segments, options)` - Target segments +- `sendNotificationToUsers(contents, userIds, options)` - Target specific users +- `sendNotificationToTags(contents, tags, options)` - Target by tags +- `sendRichNotification(contents, data, options)` - Send with rich media +- `sendScheduledNotification(contents, sendAfter, options)` - Schedule delivery +- `getNotification(notificationId)` - Get notification details +- `getNotifications(options)` - List notifications +- `cancelNotification(notificationId)` - Cancel scheduled notification + +### Device Management +- `getDevices(options)` - Get all devices +- `getDevice(deviceId)` - Get specific device +- `addDevice(deviceData)` - Add new device +- `updateDevice(deviceId, deviceData)` - Update device +- `deleteDevice(deviceId)` - Remove device +- `exportDevices(options)` - Export device list + +### Segmentation +- `getSegments(options)` - Get all segments +- `createSegment(segmentData)` - Create new segment +- `deleteSegment(segmentId)` - Delete segment + +### Templates +- `getTemplates(options)` - Get all templates +- `createTemplate(templateData)` - Create template +- `updateTemplate(templateId, templateData)` - Update template +- `deleteTemplate(templateId)` - Delete template + +### Analytics & Tracking +- `trackSession(deviceId, sessionData)` - Track app session +- `trackPurchase(deviceId, purchaseData)` - Track purchase +- `trackFocus(deviceId, focusData)` - Track app focus +- `getNotificationAnalytics(notificationId)` - Get notification stats +- `getAppAnalytics(days)` - Get app analytics + +### Live Activities (iOS) +- `createLiveActivity(liveActivityData)` - Start Live Activity +- `updateLiveActivity(liveActivityId, updateData)` - Update Live Activity +- `deleteLiveActivity(liveActivityId)` - End Live Activity + +### App Management +- `getApps()` - Get all apps (requires user auth) +- `getApp(appId)` - Get app details +- `createApp(appData)` - Create new app +- `updateApp(appId, appData)` - Update app settings + +## Targeting Options + +### Segments +Target predefined user segments: +```javascript +await oneSignalApi.sendNotificationToSegments( + 'Special offer!', + ['Active Users', 'Subscribed Users'] +); +``` + +### Tags +Target users by custom tags: +```javascript +await oneSignalApi.sendNotificationToTags( + 'Local event nearby!', + [ + { field: 'tag', key: 'location', relation: '=', value: 'San Francisco' }, + { field: 'tag', key: 'interests', relation: '=', value: 'events' } + ] +); +``` + +### Specific Users +Target individual users: +```javascript +await oneSignalApi.sendNotificationToUsers( + 'Your order is ready!', + ['player-id-1', 'player-id-2'] +); +``` + +## Rich Notifications + +Send notifications with images, buttons, and custom data: +```javascript +await oneSignalApi.sendRichNotification( + 'New message from John', + { + conversation_id: 'conv_123', + sender_id: 'user_456' + }, + { + big_picture: 'https://example.com/avatar.jpg', + buttons: [ + { id: 'reply', text: 'Reply', icon: 'ic_reply' }, + { id: 'mark_read', text: 'Mark as Read' } + ], + android_sound: 'notification_sound', + ios_sound: 'notification.wav' + } +); +``` + +## Authentication + +OneSignal uses two types of API keys: +- **REST API Key**: For sending notifications and basic operations +- **User Auth Key**: For app management and advanced operations + +## Platform Support + +- **Mobile**: iOS and Android push notifications +- **Web**: Chrome, Firefox, Safari web push +- **Email**: Email notifications +- **SMS**: SMS messaging (with additional setup) + +## Analytics + +Track notification performance: +- Delivery rates +- Open rates +- Click-through rates +- Conversion tracking +- Revenue attribution + +## Error Handling + +Comprehensive error handling with OneSignal API error codes and detailed error messages for debugging delivery issues. \ No newline at end of file diff --git a/packages/onesignal/api.js b/packages/onesignal/api.js new file mode 100644 index 0000000..bc17e7f --- /dev/null +++ b/packages/onesignal/api.js @@ -0,0 +1,507 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + this.app_id = get(params, 'app_id', null); + this.rest_api_key = get(params, 'rest_api_key', null); + this.user_auth_key = get(params, 'user_auth_key', null); + + this.baseUrl = 'https://onesignal.com/api/v1'; + + this.URLs = { + // Notifications + notifications: '/notifications', + notificationById: (id) => `/notifications/${id}`, + notificationHistory: (id) => `/notifications/${id}/history`, + + // Apps + apps: '/apps', + appById: (id) => `/apps/${id}`, + + // Devices/Players + players: '/players', + playerById: (id) => `/players/${id}`, + playersCSV: '/players/csv_export', + playersOnSession: '/players/on_session', + playersOnPurchase: '/players/on_purchase', + playersOnFocus: '/players/on_focus', + + // Segments + segments: '/segments', + segmentById: (id) => `/segments/${id}`, + + // Outcomes + outcomes: '/outcomes', + outcomesByNotification: (id) => `/notifications/${id}/outcomes`, + + // Templates + templates: '/templates', + templateById: (id) => `/templates/${id}`, + + // Live Activities (iOS) + liveActivities: '/live_activities', + liveActivityById: (id) => `/live_activities/${id}`, + + // Webhooks + webhooks: '/webhooks', + }; + } + + addAuthHeaders(headers = {}) { + if (this.rest_api_key) { + headers.Authorization = `Basic ${this.rest_api_key}`; + } + return headers; + } + + addUserAuthHeaders(headers = {}) { + if (this.user_auth_key) { + headers.Authorization = `Basic ${this.user_auth_key}`; + } + return headers; + } + + async _request(url, options = {}) { + options.headers = this.addAuthHeaders(options.headers); + return super._request(url, options); + } + + async _requestWithUserAuth(url, options = {}) { + options.headers = this.addUserAuthHeaders(options.headers); + return super._request(url, options); + } + + // ************************** Notification Methods ********************************** + + async sendNotification(message, params = {}) { + const notificationData = { + app_id: this.app_id, + ...message, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.notifications, + body: notificationData + }; + return this._post(options); + } + + async sendPushNotification(contents, params = {}) { + const message = { + contents: typeof contents === 'string' ? { en: contents } : contents, + ...params + }; + return this.sendNotification(message); + } + + async sendNotificationToSegments(contents, segments, params = {}) { + const message = { + contents: typeof contents === 'string' ? { en: contents } : contents, + included_segments: segments, + ...params + }; + return this.sendNotification(message); + } + + async sendNotificationToUsers(contents, userIds, params = {}) { + const message = { + contents: typeof contents === 'string' ? { en: contents } : contents, + include_player_ids: userIds, + ...params + }; + return this.sendNotification(message); + } + + async sendNotificationToTags(contents, tags, params = {}) { + const message = { + contents: typeof contents === 'string' ? { en: contents } : contents, + filters: tags, + ...params + }; + return this.sendNotification(message); + } + + async sendRichNotification(contents, data = {}, params = {}) { + const message = { + contents: typeof contents === 'string' ? { en: contents } : contents, + data, + ...params + }; + return this.sendNotification(message); + } + + async sendScheduledNotification(contents, sendAfter, params = {}) { + const message = { + contents: typeof contents === 'string' ? { en: contents } : contents, + send_after: sendAfter, + ...params + }; + return this.sendNotification(message); + } + + async getNotification(notificationId) { + const options = { + url: this.baseUrl + this.URLs.notificationById(notificationId) + `?app_id=${this.app_id}`, + }; + return this._get(options); + } + + async getNotifications(params = {}) { + const options = { + url: this.baseUrl + this.URLs.notifications, + query: { + app_id: this.app_id, + ...params + } + }; + return this._get(options); + } + + async getNotificationHistory(notificationId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.notificationHistory(notificationId), + query: { + app_id: this.app_id, + ...params + } + }; + return this._get(options); + } + + async cancelNotification(notificationId) { + const options = { + url: this.baseUrl + this.URLs.notificationById(notificationId) + `?app_id=${this.app_id}`, + }; + return this._delete(options); + } + + // ************************** Device/Player Methods ********************************** + + async getDevices(params = {}) { + const options = { + url: this.baseUrl + this.URLs.players, + query: { + app_id: this.app_id, + ...params + } + }; + return this._get(options); + } + + async getDevice(deviceId) { + const options = { + url: this.baseUrl + this.URLs.playerById(deviceId) + `?app_id=${this.app_id}`, + }; + return this._get(options); + } + + async addDevice(deviceData) { + const options = { + url: this.baseUrl + this.URLs.players, + body: { + app_id: this.app_id, + ...deviceData + } + }; + return this._post(options); + } + + async updateDevice(deviceId, deviceData) { + const options = { + url: this.baseUrl + this.URLs.playerById(deviceId), + body: deviceData + }; + return this._put(options); + } + + async deleteDevice(deviceId) { + const options = { + url: this.baseUrl + this.URLs.playerById(deviceId) + `?app_id=${this.app_id}`, + }; + return this._delete(options); + } + + async exportDevices(params = {}) { + const options = { + url: this.baseUrl + this.URLs.playersCSV, + query: { + app_id: this.app_id, + ...params + } + }; + return this._post(options); + } + + async trackSession(deviceId, sessionData) { + const options = { + url: this.baseUrl + this.URLs.playersOnSession, + body: { + id: deviceId, + app_id: this.app_id, + ...sessionData + } + }; + return this._post(options); + } + + async trackPurchase(deviceId, purchaseData) { + const options = { + url: this.baseUrl + this.URLs.playersOnPurchase, + body: { + id: deviceId, + app_id: this.app_id, + ...purchaseData + } + }; + return this._post(options); + } + + async trackFocus(deviceId, focusData) { + const options = { + url: this.baseUrl + this.URLs.playersOnFocus, + body: { + id: deviceId, + app_id: this.app_id, + ...focusData + } + }; + return this._post(options); + } + + // ************************** Segment Methods ********************************** + + async getSegments(params = {}) { + const options = { + url: this.baseUrl + this.URLs.segments + `?app_id=${this.app_id}`, + query: params + }; + return this._get(options); + } + + async createSegment(segmentData) { + const options = { + url: this.baseUrl + this.URLs.segments, + body: { + app_id: this.app_id, + ...segmentData + } + }; + return this._post(options); + } + + async deleteSegment(segmentId) { + const options = { + url: this.baseUrl + this.URLs.segmentById(segmentId) + `?app_id=${this.app_id}`, + }; + return this._delete(options); + } + + // ************************** App Methods ********************************** + + async getApps() { + const options = { + url: this.baseUrl + this.URLs.apps, + }; + return this._requestWithUserAuth(options.url, options); + } + + async getApp(appId = null) { + const id = appId || this.app_id; + const options = { + url: this.baseUrl + this.URLs.appById(id), + }; + return this._requestWithUserAuth(options.url, options); + } + + async createApp(appData) { + const options = { + url: this.baseUrl + this.URLs.apps, + body: appData + }; + return this._requestWithUserAuth(options.url, { ...options, method: 'POST' }); + } + + async updateApp(appId, appData) { + const options = { + url: this.baseUrl + this.URLs.appById(appId), + body: appData + }; + return this._requestWithUserAuth(options.url, { ...options, method: 'PUT' }); + } + + // ************************** Template Methods ********************************** + + async getTemplates(params = {}) { + const options = { + url: this.baseUrl + this.URLs.templates, + query: { + app_id: this.app_id, + ...params + } + }; + return this._get(options); + } + + async getTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId) + `?app_id=${this.app_id}`, + }; + return this._get(options); + } + + async createTemplate(templateData) { + const options = { + url: this.baseUrl + this.URLs.templates, + body: { + app_id: this.app_id, + ...templateData + } + }; + return this._post(options); + } + + async updateTemplate(templateId, templateData) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + body: templateData + }; + return this._put(options); + } + + async deleteTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId) + `?app_id=${this.app_id}`, + }; + return this._delete(options); + } + + // ************************** Outcome Methods ********************************** + + async getOutcomes(params = {}) { + const options = { + url: this.baseUrl + this.URLs.outcomes + `?app_id=${this.app_id}`, + query: params + }; + return this._get(options); + } + + async getNotificationOutcomes(notificationId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.outcomesByNotification(notificationId), + query: { + app_id: this.app_id, + ...params + } + }; + return this._get(options); + } + + // ************************** Live Activity Methods (iOS) ********************************** + + async createLiveActivity(liveActivityData) { + const options = { + url: this.baseUrl + this.URLs.liveActivities, + body: { + app_id: this.app_id, + ...liveActivityData + } + }; + return this._post(options); + } + + async updateLiveActivity(liveActivityId, updateData) { + const options = { + url: this.baseUrl + this.URLs.liveActivityById(liveActivityId), + body: updateData + }; + return this._put(options); + } + + async deleteLiveActivity(liveActivityId) { + const options = { + url: this.baseUrl + this.URLs.liveActivityById(liveActivityId) + `?app_id=${this.app_id}`, + }; + return this._delete(options); + } + + // ************************** Analytics Methods ********************************** + + async getNotificationAnalytics(notificationId) { + try { + const notification = await this.getNotification(notificationId); + const outcomes = await this.getNotificationOutcomes(notificationId); + + return { + notification: notification, + analytics: { + sent: notification.successful || 0, + failed: notification.failed || 0, + delivered: notification.delivered || 0, + opened: notification.opened || 0, + converted: notification.converted || 0 + }, + outcomes: outcomes + }; + } catch (error) { + throw new Error(`Failed to get notification analytics: ${error.message}`); + } + } + + async getAppAnalytics(days = 30) { + try { + const app = await this.getApp(); + const notifications = await this.getNotifications({ limit: 50 }); + + return { + app: app, + notifications: notifications, + summary: { + total_notifications: notifications.total_count || 0, + active_users: app.players || 0 + } + }; + } catch (error) { + throw new Error(`Failed to get app analytics: ${error.message}`); + } + } + + // ************************** Helper Methods ********************************** + + createFilter(key, relation, value) { + return { + field: key, + relation: relation, + value: value + }; + } + + createTagFilter(key, relation, value) { + return this.createFilter(`tag.${key}`, relation, value); + } + + createAudienceFilter(filters) { + return { + filters: filters + }; + } + + // ************************** Error Handling ********************************** + + async handleError(error) { + if (error.response && error.response.data) { + const errorData = error.response.data; + return { + message: errorData.errors?.[0] || errorData.error || 'Unknown error', + errors: errorData.errors || [] + }; + } + return { + message: error.message || 'Unknown error occurred' + }; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/onesignal/defaultConfig.json b/packages/onesignal/defaultConfig.json new file mode 100644 index 0000000..2a8945f --- /dev/null +++ b/packages/onesignal/defaultConfig.json @@ -0,0 +1,9 @@ +{ + "name": "onesignal", + "label": "OneSignal", + "productUrl": "https://onesignal.com", + "apiDocs": "https://documentation.onesignal.com/reference", + "logoUrl": "https://friggframework.org/assets/img/onesignal-icon.png", + "categories": ["Communication", "Push Notifications", "Marketing"], + "description": "OneSignal push notification platform for mobile, web, and email messaging." +} \ No newline at end of file diff --git a/packages/onesignal/definition.js b/packages/onesignal/definition.js new file mode 100644 index 0000000..374a89c --- /dev/null +++ b/packages/onesignal/definition.js @@ -0,0 +1,76 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'OneSignal', + requiredAuthMethods: { + getToken: async function (api, params) { + // OneSignal uses REST API key authentication + return { + access_token: api.rest_api_key, + token_type: 'Basic' + }; + }, + + getEntityDetails: async function (api, userId) { + const appInfo = await api.getApp(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: appInfo.id, + user: userId + }, + details: { + name: appInfo.name, + players: appInfo.players, + messageable_players: appInfo.messageable_players, + updated_at: appInfo.updated_at, + created_at: appInfo.created_at, + gcm_key: appInfo.gcm_key ? 'configured' : 'not configured', + chrome_web_origin: appInfo.chrome_web_origin, + chrome_web_default_notification_icon: appInfo.chrome_web_default_notification_icon, + chrome_web_sub_domain: appInfo.chrome_web_sub_domain, + apns_env: appInfo.apns_env, + apns_certificates: appInfo.apns_certificates ? 'configured' : 'not configured' + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['rest_api_key', 'user_auth_key'], + entity: ['app_id'], + }, + + getCredentialDetails: async function (api, userId) { + const appInfo = await api.getApp(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: appInfo.id, + user: userId + }, + details: { + rest_api_key: api.rest_api_key, + user_auth_key: api.user_auth_key + }, + }; + }, + + testAuthRequest: function (api) { + return api.getApp(); + }, + }, + env: { + app_id: process.env.ONESIGNAL_APP_ID, + rest_api_key: process.env.ONESIGNAL_REST_API_KEY, + user_auth_key: process.env.ONESIGNAL_USER_AUTH_KEY, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/onesignal/index.js b/packages/onesignal/index.js new file mode 100644 index 0000000..be08f56 --- /dev/null +++ b/packages/onesignal/index.js @@ -0,0 +1,5 @@ +const Config = require('./defaultConfig.json'); +const { Definition } = require('./definition.js'); +const { Api } = require('./api.js'); + +module.exports = { Config, Definition, Api }; \ No newline at end of file diff --git a/packages/onesignal/package.json b/packages/onesignal/package.json new file mode 100644 index 0000000..506b95e --- /dev/null +++ b/packages/onesignal/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-onesignal", + "version": "1.0.0", + "description": "OneSignal API module for the Frigg framework", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "frigg", + "onesignal", + "push", + "notifications", + "api" + ], + "author": "Frigg Framework", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/packages/openai/README.md b/packages/openai/README.md new file mode 100644 index 0000000..7b22593 --- /dev/null +++ b/packages/openai/README.md @@ -0,0 +1,229 @@ +# OpenAI API Module + +This module provides a complete interface to OpenAI's API services including GPT models, DALL-E, Whisper, and more. + +## Features + +- **Chat Completions**: Access GPT-4 and GPT-3.5 models for conversational AI +- **Embeddings**: Generate text embeddings for semantic search and similarity +- **Image Generation**: Create images with DALL-E 3 and DALL-E 2 +- **Audio**: Transcribe and translate audio with Whisper, generate speech +- **Fine-tuning**: Customize models with your own training data +- **Assistants API**: Build AI assistants with persistent threads +- **Content Moderation**: Check content for policy compliance +- **Streaming Support**: Real-time streaming for chat completions + +## Authentication + +OpenAI uses API key authentication. You'll need to: + +1. Sign up at [platform.openai.com](https://platform.openai.com) +2. Generate an API key from your account settings +3. Set the following environment variables: + +```bash +OPENAI_API_KEY=your_api_key_here +OPENAI_ORGANIZATION_ID=your_org_id_here # Optional +``` + +## Usage Examples + +### Chat Completions + +```javascript +const {Api} = require('./api'); + +const api = new Api({ + apiKey: process.env.OPENAI_API_KEY +}); + +// Simple chat completion +const response = await api.createChatCompletion({ + model: "gpt-4", + messages: [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello, how are you?"} + ], + temperature: 0.7, + max_tokens: 150 +}); + +// Streaming chat completion +const stream = await api.createChatCompletionStream({ + model: "gpt-3.5-turbo", + messages: [{"role": "user", "content": "Tell me a story"}], + stream: true +}); +``` + +### Embeddings + +```javascript +const embedding = await api.createEmbedding({ + model: "text-embedding-ada-002", + input: "The quick brown fox jumps over the lazy dog" +}); +``` + +### Image Generation + +```javascript +// Generate image from text +const image = await api.createImage({ + prompt: "A serene landscape with mountains and a lake", + n: 1, + size: "1024x1024" +}); + +// Create variations of an existing image +const variation = await api.createImageVariation({ + image: imageFile, + n: 2, + size: "512x512" +}); +``` + +### Audio Transcription + +```javascript +const transcription = await api.createTranscription({ + file: audioFile, + model: "whisper-1", + language: "en" +}); +``` + +### Fine-tuning + +```javascript +// Create fine-tuning job +const job = await api.createFineTuningJob({ + training_file: "file-abc123", + model: "gpt-3.5-turbo" +}); + +// Monitor progress +const events = await api.listFineTuningEvents(job.id); +``` + +### Assistants API + +```javascript +// Create an assistant +const assistant = await api.createAssistant({ + name: "Math Tutor", + instructions: "You are a personal math tutor.", + tools: [{"type": "code_interpreter"}], + model: "gpt-4" +}); + +// Create a thread +const thread = await api.createThread(); + +// Add a message +await api.createMessage(thread.id, { + role: "user", + content: "I need help with calculus" +}); + +// Run the assistant +const run = await api.createRun(thread.id, { + assistant_id: assistant.id +}); +``` + +### Content Moderation + +```javascript +const moderation = await api.createModeration({ + input: "Text to check for policy compliance" +}); +``` + +## Token Management + +The module includes utilities for token estimation: + +```javascript +// Estimate tokens in text +const tokenCount = api.estimateTokens("Your text here"); + +// Get model token limit +const limit = api.getTokenLimit("gpt-4"); +``` + +## Error Handling + +The module handles common OpenAI errors: + +- Rate limiting (429 errors) +- Invalid API key +- Model availability +- Token limits exceeded + +## API Methods + +### Chat & Completions +- `createChatCompletion(params)` - Generate chat responses +- `createChatCompletionStream(params)` - Stream chat responses + +### Embeddings +- `createEmbedding(params)` - Generate text embeddings + +### Images +- `createImage(params)` - Generate images from text +- `createImageEdit(params)` - Edit images with prompts +- `createImageVariation(params)` - Create image variations + +### Audio +- `createTranscription(params)` - Transcribe audio to text +- `createTranslation(params)` - Translate audio to English +- `createSpeech(params)` - Generate speech from text + +### Files +- `uploadFile(params)` - Upload training files +- `listFiles(params)` - List uploaded files +- `retrieveFile(fileId)` - Get file metadata +- `deleteFile(fileId)` - Delete a file +- `retrieveFileContent(fileId)` - Download file content + +### Fine-tuning +- `createFineTuningJob(params)` - Start fine-tuning +- `listFineTuningJobs(params)` - List fine-tuning jobs +- `retrieveFineTuningJob(jobId)` - Get job details +- `cancelFineTuningJob(jobId)` - Cancel a job +- `listFineTuningEvents(jobId, params)` - Get job events + +### Models +- `listModels()` - List available models +- `retrieveModel(modelId)` - Get model details +- `deleteModel(modelId)` - Delete fine-tuned model + +### Assistants +- `createAssistant(params)` - Create an assistant +- `listAssistants(params)` - List assistants +- `retrieveAssistant(assistantId)` - Get assistant details +- `modifyAssistant(assistantId, params)` - Update assistant +- `deleteAssistant(assistantId)` - Delete assistant + +### Threads & Messages +- `createThread(params)` - Create a conversation thread +- `retrieveThread(threadId)` - Get thread details +- `modifyThread(threadId, params)` - Update thread +- `deleteThread(threadId)` - Delete thread +- `createMessage(threadId, params)` - Add message to thread +- `listMessages(threadId, params)` - List thread messages +- `createRun(threadId, params)` - Run assistant on thread +- `listRuns(threadId, params)` - List thread runs + +### Moderation +- `createModeration(params)` - Check content compliance + +### Utilities +- `testAuth()` - Verify API key validity +- `estimateTokens(text)` - Estimate token count +- `getTokenLimit(model)` - Get model token limit + +## Support + +For more information, visit [OpenAI API Documentation](https://platform.openai.com/docs/api-reference). \ No newline at end of file diff --git a/packages/openai/api.js b/packages/openai/api.js new file mode 100644 index 0000000..9e29d14 --- /dev/null +++ b/packages/openai/api.js @@ -0,0 +1,463 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.openai.com/v1'; + this.organizationId = get(params, 'organizationId', null); + + this.URLs = { + // Chat Completions + chatCompletions: '/chat/completions', + + // Completions (Legacy) + completions: '/completions', + + // Embeddings + embeddings: '/embeddings', + + // Images + imagesGenerations: '/images/generations', + imagesEdits: '/images/edits', + imagesVariations: '/images/variations', + + // Audio + audioTranscriptions: '/audio/transcriptions', + audioTranslations: '/audio/translations', + audioSpeech: '/audio/speech', + + // Files + files: '/files', + fileById: (fileId) => `/files/${fileId}`, + fileContent: (fileId) => `/files/${fileId}/content`, + + // Fine-tuning + fineTuningJobs: '/fine_tuning/jobs', + fineTuningJobById: (jobId) => `/fine_tuning/jobs/${jobId}`, + fineTuningEvents: (jobId) => `/fine_tuning/jobs/${jobId}/events`, + fineTuningCancel: (jobId) => `/fine_tuning/jobs/${jobId}/cancel`, + + // Models + models: '/models', + modelById: (modelId) => `/models/${modelId}`, + + // Assistants + assistants: '/assistants', + assistantById: (assistantId) => `/assistants/${assistantId}`, + + // Threads + threads: '/threads', + threadById: (threadId) => `/threads/${threadId}`, + threadMessages: (threadId) => `/threads/${threadId}/messages`, + threadRuns: (threadId) => `/threads/${threadId}/runs`, + + // Moderations + moderations: '/moderations', + }; + + this.tokenLimits = { + 'gpt-4': 8192, + 'gpt-4-32k': 32768, + 'gpt-4-1106-preview': 128000, + 'gpt-3.5-turbo': 4096, + 'gpt-3.5-turbo-16k': 16385, + 'text-embedding-ada-002': 8191, + }; + } + + async addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${this.apiKey}`, + 'OpenAI-Beta': 'assistants=v1', + }; + + if (this.organizationId) { + options.headers['OpenAI-Organization'] = this.organizationId; + } + } + + async _get(options) { + await this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + await this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + await this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + await this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + await this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Chat Completions ********************************** + + async createChatCompletion(params) { + const options = { + url: this.baseUrl + this.URLs.chatCompletions, + body: params, + }; + + return this._post(options); + } + + async createChatCompletionStream(params) { + const options = { + url: this.baseUrl + this.URLs.chatCompletions, + body: { ...params, stream: true }, + headers: { + 'Accept': 'text/event-stream', + }, + }; + + // Return the raw response for streaming + const response = await this._post(options); + return response; + } + + // ************************** Embeddings ********************************** + + async createEmbedding(params) { + const options = { + url: this.baseUrl + this.URLs.embeddings, + body: params, + }; + + return this._post(options); + } + + // ************************** Images ********************************** + + async createImage(params) { + const options = { + url: this.baseUrl + this.URLs.imagesGenerations, + body: params, + }; + + return this._post(options); + } + + async createImageEdit(params) { + const options = { + url: this.baseUrl + this.URLs.imagesEdits, + body: params, + }; + + return this._post(options); + } + + async createImageVariation(params) { + const options = { + url: this.baseUrl + this.URLs.imagesVariations, + body: params, + }; + + return this._post(options); + } + + // ************************** Audio ********************************** + + async createTranscription(params) { + const options = { + url: this.baseUrl + this.URLs.audioTranscriptions, + body: params, + }; + + return this._post(options); + } + + async createTranslation(params) { + const options = { + url: this.baseUrl + this.URLs.audioTranslations, + body: params, + }; + + return this._post(options); + } + + async createSpeech(params) { + const options = { + url: this.baseUrl + this.URLs.audioSpeech, + body: params, + }; + + return this._post(options); + } + + // ************************** Files ********************************** + + async uploadFile(params) { + const options = { + url: this.baseUrl + this.URLs.files, + body: params, + }; + + return this._post(options); + } + + async listFiles(params = {}) { + const options = { + url: this.baseUrl + this.URLs.files, + query: params, + }; + + return this._get(options); + } + + async retrieveFile(fileId) { + const options = { + url: this.baseUrl + this.URLs.fileById(fileId), + }; + + return this._get(options); + } + + async deleteFile(fileId) { + const options = { + url: this.baseUrl + this.URLs.fileById(fileId), + }; + + return this._delete(options); + } + + async retrieveFileContent(fileId) { + const options = { + url: this.baseUrl + this.URLs.fileContent(fileId), + }; + + return this._get(options); + } + + // ************************** Fine-tuning ********************************** + + async createFineTuningJob(params) { + const options = { + url: this.baseUrl + this.URLs.fineTuningJobs, + body: params, + }; + + return this._post(options); + } + + async listFineTuningJobs(params = {}) { + const options = { + url: this.baseUrl + this.URLs.fineTuningJobs, + query: params, + }; + + return this._get(options); + } + + async retrieveFineTuningJob(jobId) { + const options = { + url: this.baseUrl + this.URLs.fineTuningJobById(jobId), + }; + + return this._get(options); + } + + async cancelFineTuningJob(jobId) { + const options = { + url: this.baseUrl + this.URLs.fineTuningCancel(jobId), + }; + + return this._post(options); + } + + async listFineTuningEvents(jobId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.fineTuningEvents(jobId), + query: params, + }; + + return this._get(options); + } + + // ************************** Models ********************************** + + async listModels() { + const options = { + url: this.baseUrl + this.URLs.models, + }; + + return this._get(options); + } + + async retrieveModel(modelId) { + const options = { + url: this.baseUrl + this.URLs.modelById(modelId), + }; + + return this._get(options); + } + + async deleteModel(modelId) { + const options = { + url: this.baseUrl + this.URLs.modelById(modelId), + }; + + return this._delete(options); + } + + // ************************** Assistants ********************************** + + async createAssistant(params) { + const options = { + url: this.baseUrl + this.URLs.assistants, + body: params, + }; + + return this._post(options); + } + + async listAssistants(params = {}) { + const options = { + url: this.baseUrl + this.URLs.assistants, + query: params, + }; + + return this._get(options); + } + + async retrieveAssistant(assistantId) { + const options = { + url: this.baseUrl + this.URLs.assistantById(assistantId), + }; + + return this._get(options); + } + + async modifyAssistant(assistantId, params) { + const options = { + url: this.baseUrl + this.URLs.assistantById(assistantId), + body: params, + }; + + return this._post(options); + } + + async deleteAssistant(assistantId) { + const options = { + url: this.baseUrl + this.URLs.assistantById(assistantId), + }; + + return this._delete(options); + } + + // ************************** Threads ********************************** + + async createThread(params = {}) { + const options = { + url: this.baseUrl + this.URLs.threads, + body: params, + }; + + return this._post(options); + } + + async retrieveThread(threadId) { + const options = { + url: this.baseUrl + this.URLs.threadById(threadId), + }; + + return this._get(options); + } + + async modifyThread(threadId, params) { + const options = { + url: this.baseUrl + this.URLs.threadById(threadId), + body: params, + }; + + return this._post(options); + } + + async deleteThread(threadId) { + const options = { + url: this.baseUrl + this.URLs.threadById(threadId), + }; + + return this._delete(options); + } + + async createMessage(threadId, params) { + const options = { + url: this.baseUrl + this.URLs.threadMessages(threadId), + body: params, + }; + + return this._post(options); + } + + async listMessages(threadId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.threadMessages(threadId), + query: params, + }; + + return this._get(options); + } + + async createRun(threadId, params) { + const options = { + url: this.baseUrl + this.URLs.threadRuns(threadId), + body: params, + }; + + return this._post(options); + } + + async listRuns(threadId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.threadRuns(threadId), + query: params, + }; + + return this._get(options); + } + + // ************************** Moderations ********************************** + + async createModeration(params) { + const options = { + url: this.baseUrl + this.URLs.moderations, + body: params, + }; + + return this._post(options); + } + + // ************************** Utility Methods ********************************** + + async testAuth() { + try { + await this.listModels(); + return true; + } catch (error) { + return false; + } + } + + estimateTokens(text) { + // Rough estimation: ~4 characters per token + return Math.ceil(text.length / 4); + } + + getTokenLimit(model) { + return this.tokenLimits[model] || 4096; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/openai/defaultConfig.json b/packages/openai/defaultConfig.json new file mode 100644 index 0000000..5064cbf --- /dev/null +++ b/packages/openai/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "openai", + "label": "OpenAI", + "productUrl": "https://openai.com", + "apiDocs": "https://platform.openai.com/docs/api-reference", + "logoUrl": "https://friggframework.org/assets/img/openai-icon.png", + "categories": [ + "AI/ML", + "Large Language Models", + "Text Generation", + "Image Generation" + ], + "description": "OpenAI provides API access to powerful AI models including GPT-4 for text generation, DALL-E for image generation, and Whisper for speech recognition." +} \ No newline at end of file diff --git a/packages/openai/definition.js b/packages/openai/definition.js new file mode 100644 index 0000000..f23b532 --- /dev/null +++ b/packages/openai/definition.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'OpenAI', + requiredAuthMethods: { + getToken: async function (api, params) { + // OpenAI uses API keys, not OAuth + const apiKey = get(params.data, 'apiKey'); + if (!apiKey) { + throw new Error('API Key is required for OpenAI authentication'); + } + return { apiKey }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + // OpenAI doesn't have user accounts via API, so we use a generic identifier + return { + identifiers: {externalId: 'openai-user', user: userId}, + details: {name: 'OpenAI API User', apiKey: tokenResponse.apiKey}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'apiKey', 'organizationId' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + // Test the API key by making a simple request + const models = await api.listModels(); + return { + identifiers: {externalId: 'openai-api', user: userId}, + details: { modelsAvailable: models.data.length } + }; + }, + testAuthRequest: async function (api) { + return api.testAuth() + }, + }, + env: { + apiKey: process.env.OPENAI_API_KEY, + organizationId: process.env.OPENAI_ORGANIZATION_ID, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/openai/index.js b/packages/openai/index.js new file mode 100644 index 0000000..18a6c30 --- /dev/null +++ b/packages/openai/index.js @@ -0,0 +1,3 @@ +const {Definition} = require('./definition'); + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/v1-ready/openphone/CHANGELOG.md b/packages/openphone/CHANGELOG.md similarity index 100% rename from packages/v1-ready/openphone/CHANGELOG.md rename to packages/openphone/CHANGELOG.md diff --git a/packages/v1-ready/openphone/README.md b/packages/openphone/README.md similarity index 100% rename from packages/v1-ready/openphone/README.md rename to packages/openphone/README.md diff --git a/packages/v1-ready/openphone/api.js b/packages/openphone/api.js similarity index 100% rename from packages/v1-ready/openphone/api.js rename to packages/openphone/api.js diff --git a/packages/v1-ready/openphone/defaultConfig.json b/packages/openphone/defaultConfig.json similarity index 100% rename from packages/v1-ready/openphone/defaultConfig.json rename to packages/openphone/defaultConfig.json diff --git a/packages/v1-ready/openphone/definition.js b/packages/openphone/definition.js similarity index 100% rename from packages/v1-ready/openphone/definition.js rename to packages/openphone/definition.js diff --git a/packages/v1-ready/openphone/index.js b/packages/openphone/index.js similarity index 100% rename from packages/v1-ready/openphone/index.js rename to packages/openphone/index.js diff --git a/packages/needs-updating/qbo/jest.config.js b/packages/openphone/jest.config.js similarity index 100% rename from packages/needs-updating/qbo/jest.config.js rename to packages/openphone/jest.config.js diff --git a/packages/v1-ready/openphone/package.json b/packages/openphone/package.json similarity index 100% rename from packages/v1-ready/openphone/package.json rename to packages/openphone/package.json diff --git a/packages/v1-ready/openphone/specs/openAPI.json b/packages/openphone/specs/openAPI.json similarity index 100% rename from packages/v1-ready/openphone/specs/openAPI.json rename to packages/openphone/specs/openAPI.json diff --git a/packages/v1-ready/openphone/tests/ManagerTest.js b/packages/openphone/tests/ManagerTest.js similarity index 100% rename from packages/v1-ready/openphone/tests/ManagerTest.js rename to packages/openphone/tests/ManagerTest.js diff --git a/packages/v1-ready/openphone/tests/api.test.js b/packages/openphone/tests/api.test.js similarity index 100% rename from packages/v1-ready/openphone/tests/api.test.js rename to packages/openphone/tests/api.test.js diff --git a/packages/v1-ready/openphone/tests/auther.test.js b/packages/openphone/tests/auther.test.js similarity index 100% rename from packages/v1-ready/openphone/tests/auther.test.js rename to packages/openphone/tests/auther.test.js diff --git a/packages/v1-ready/contentful/.eslintrc.json b/packages/outreach/.eslintrc.json similarity index 100% rename from packages/v1-ready/contentful/.eslintrc.json rename to packages/outreach/.eslintrc.json diff --git a/packages/needs-updating/pipedrive/CHANGELOG.md b/packages/outreach/CHANGELOG.md similarity index 100% rename from packages/needs-updating/pipedrive/CHANGELOG.md rename to packages/outreach/CHANGELOG.md diff --git a/packages/needs-updating/rollworks/LICENSE.md b/packages/outreach/LICENSE.md similarity index 100% rename from packages/needs-updating/rollworks/LICENSE.md rename to packages/outreach/LICENSE.md diff --git a/packages/needs-updating/outreach/README.md b/packages/outreach/README.md similarity index 100% rename from packages/needs-updating/outreach/README.md rename to packages/outreach/README.md diff --git a/packages/needs-updating/outreach/api.js b/packages/outreach/api.js similarity index 100% rename from packages/needs-updating/outreach/api.js rename to packages/outreach/api.js diff --git a/packages/needs-updating/outreach/defaultConfig.json b/packages/outreach/defaultConfig.json similarity index 100% rename from packages/needs-updating/outreach/defaultConfig.json rename to packages/outreach/defaultConfig.json diff --git a/packages/outreach/definition.js b/packages/outreach/definition.js new file mode 100644 index 0000000..1652c5d --- /dev/null +++ b/packages/outreach/definition.js @@ -0,0 +1,160 @@ +const { IntegrationBase, ModuleConstants, flushDebugLog, debug } = require('@friggframework/core'); +const _ = require('lodash'); +const { Api } = require('./api.js'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); +const Config = require('./defaultConfig.json'); + +class OutreachIntegration extends IntegrationBase { + static Definition = { + name: Config.name, + version: '1.0.0', + modules: { Api, Entity, Credential }, + display: { + label: Config.label, + description: Config.description, + category: Config.categories[0], + iconUrl: Config.logoUrl, + detailsUrl: Config.productUrl, + }, + }; + + static Entity = Entity; + static Credential = Credential; + + async getAuthorizationRequirements(params) { + return { + url: this.api.authorizationUri, + type: ModuleConstants.authType.oauth2, + }; + } + + async processAuthorizationCallback(params) { + const code = _.get(params.data, 'code'); + await this.api.getTokenFromCode(code); + await this.testAuth(); + + const userProfile = await this.api.getUser(); + await this.findOrCreateEntity({ + org_uuid: userProfile.org_uuid, + org_name: userProfile.org_name, + }); + + return { + credential_id: this.credential.id, + entity_id: this.entity.id, + type: Config.name, + }; + } + + async testAuth() { + let validAuth = false; + try { + if (await this.api.getUser()) validAuth = true; + } catch (e) { + await this.markCredentialsInvalid(); + flushDebugLog(e); + } + return validAuth; + } + + async findOrCreateEntity(params) { + const org_uuid = _.get(params, 'org_uuid'); + const org_name = _.get(params, 'org_name'); + + const search = await this.entityMO.list({ + user: this.userId, + externalId: org_uuid, + }); + if (search.length === 0) { + // validate choices!!! + // create entity + const createObj = { + credential: this.credential.id, + user: this.userId, + name: org_name, + externalId: org_uuid, + }; + this.entity = await this.entityMO.create(createObj); + } else if (search.length === 1) { + this.entity = search[0]; + } else { + debug('Multiple entities found with the same Org ID:', org_uuid); + } + + return { + entity_id: this.entity.id, + }; + } + + async deauthorize() { + this.api = new Api(); + + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async receiveNotification(notifier, delegateString, object = null) { + if (notifier instanceof Api) { + if (delegateString === this.api.DLGT_TOKEN_UPDATE) { + const userProfile = await this.api.getUser(); + const updatedToken = { + user: this.userId, + accessToken: this.api.access_token, + refreshToken: this.api.refresh_token, + accessTokenExpire: this.api.accessTokenExpire, + externalId: userProfile.user_id, + auth_is_valid: true, + }; + + if (!this.credential) { + const credentialSearch = await this.credentialMO.list({ + externalId: userProfile.user_id, + }); + if (credentialSearch.length === 0) { + this.credential = await this.credentialMO.create( + updatedToken + ); + } else if (credentialSearch.length === 1) { + if ( + credentialSearch[0].user.toString() === this.userId + ) { + this.credential = await this.credentialMO.update( + credentialSearch[0], + updatedToken + ); + } else { + debug( + 'Somebody else already created a credential with the same User ID:', + userProfile.user_id + ); + } + } else { + // Handling multiple credentials found with an error for the time being + debug( + 'Multiple credentials found with the same User ID:', + userProfile.user_id + ); + } + } else { + this.credential = await this.credentialMO.update( + this.credential, + updatedToken + ); + } + } + if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { + await this.deauthorize(); + } + if (delegateString === this.api.DLGT_INVALID_AUTH) { + await this.markCredentialsInvalid(); + } + } + } +} + +module.exports = OutreachIntegration; \ No newline at end of file diff --git a/packages/outreach/index.js b/packages/outreach/index.js new file mode 100644 index 0000000..3ca9218 --- /dev/null +++ b/packages/outreach/index.js @@ -0,0 +1,13 @@ +const { Api } = require('./api'); +const { Credential } = require('./models/credential'); +const { Entity } = require('./models/entity'); +const Definition = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/needs-updating/revio/jest.config.js b/packages/outreach/jest.config.js similarity index 100% rename from packages/needs-updating/revio/jest.config.js rename to packages/outreach/jest.config.js diff --git a/packages/needs-updating/netx/manager.test.js b/packages/outreach/manager.test.js similarity index 100% rename from packages/needs-updating/netx/manager.test.js rename to packages/outreach/manager.test.js diff --git a/packages/needs-updating/outreach/mocks/accounts/listAccounts.js b/packages/outreach/mocks/accounts/listAccounts.js similarity index 100% rename from packages/needs-updating/outreach/mocks/accounts/listAccounts.js rename to packages/outreach/mocks/accounts/listAccounts.js diff --git a/packages/needs-updating/outreach/mocks/apiMock.js b/packages/outreach/mocks/apiMock.js similarity index 100% rename from packages/needs-updating/outreach/mocks/apiMock.js rename to packages/outreach/mocks/apiMock.js diff --git a/packages/needs-updating/outreach/mocks/tasks/createTask.js b/packages/outreach/mocks/tasks/createTask.js similarity index 100% rename from packages/needs-updating/outreach/mocks/tasks/createTask.js rename to packages/outreach/mocks/tasks/createTask.js diff --git a/packages/needs-updating/outreach/mocks/tasks/deleteTask.js b/packages/outreach/mocks/tasks/deleteTask.js similarity index 100% rename from packages/needs-updating/outreach/mocks/tasks/deleteTask.js rename to packages/outreach/mocks/tasks/deleteTask.js diff --git a/packages/needs-updating/outreach/mocks/tasks/getTasks.js b/packages/outreach/mocks/tasks/getTasks.js similarity index 100% rename from packages/needs-updating/outreach/mocks/tasks/getTasks.js rename to packages/outreach/mocks/tasks/getTasks.js diff --git a/packages/needs-updating/outreach/mocks/tasks/updateTask.js b/packages/outreach/mocks/tasks/updateTask.js similarity index 100% rename from packages/needs-updating/outreach/mocks/tasks/updateTask.js rename to packages/outreach/mocks/tasks/updateTask.js diff --git a/packages/needs-updating/outreach/test/Api.test.js b/packages/outreach/test/Api.test.js similarity index 100% rename from packages/needs-updating/outreach/test/Api.test.js rename to packages/outreach/test/Api.test.js diff --git a/packages/needs-updating/outreach/test/Manager.test.js b/packages/outreach/test/Manager.test.js similarity index 100% rename from packages/needs-updating/outreach/test/Manager.test.js rename to packages/outreach/test/Manager.test.js diff --git a/packages/v1-ready/payjunction/README.md b/packages/payjunction/README.md similarity index 100% rename from packages/v1-ready/payjunction/README.md rename to packages/payjunction/README.md diff --git a/packages/v1-ready/payjunction/api.js b/packages/payjunction/api.js similarity index 100% rename from packages/v1-ready/payjunction/api.js rename to packages/payjunction/api.js diff --git a/packages/v1-ready/payjunction/defaultConfig.json b/packages/payjunction/defaultConfig.json similarity index 100% rename from packages/v1-ready/payjunction/defaultConfig.json rename to packages/payjunction/defaultConfig.json diff --git a/packages/v1-ready/payjunction/definition.js b/packages/payjunction/definition.js similarity index 100% rename from packages/v1-ready/payjunction/definition.js rename to packages/payjunction/definition.js diff --git a/packages/v1-ready/payjunction/index.js b/packages/payjunction/index.js similarity index 100% rename from packages/v1-ready/payjunction/index.js rename to packages/payjunction/index.js diff --git a/packages/v1-ready/payjunction/package.json b/packages/payjunction/package.json similarity index 100% rename from packages/v1-ready/payjunction/package.json rename to packages/payjunction/package.json diff --git a/packages/payjunction/specs/openAPI.yaml b/packages/payjunction/specs/openAPI.yaml new file mode 100644 index 0000000..d37d875 --- /dev/null +++ b/packages/payjunction/specs/openAPI.yaml @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a64498d623dba95970b4fc2f09d685358815bdca4a593a2380354d50af92928 +size 14034 diff --git a/packages/paypal/README.md b/packages/paypal/README.md new file mode 100644 index 0000000..243a681 --- /dev/null +++ b/packages/paypal/README.md @@ -0,0 +1,294 @@ +# PayPal API Module + +A comprehensive PayPal REST API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +PAYPAL_CLIENT_ID=your_paypal_client_id +PAYPAL_CLIENT_SECRET=your_paypal_client_secret +PAYPAL_SCOPE=openid profile email +PAYPAL_SANDBOX=true +REDIRECT_URI=your_redirect_uri_base +``` + +### Getting PayPal API Credentials + +1. Go to the [PayPal Developer Dashboard](https://developer.paypal.com/) +2. Sign in with your PayPal account +3. Create a new app or select an existing one +4. Get your Client ID and Client Secret +5. Set up your redirect URI (e.g., `https://yourdomain.com/paypal`) + +### Sandbox vs Production + +- Set `PAYPAL_SANDBOX=true` for testing with PayPal's sandbox environment +- Set `PAYPAL_SANDBOX=false` for production usage + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-paypal'); + +// Initialize with credentials +const paypalApi = new Api({ + client_id: process.env.PAYPAL_CLIENT_ID, + client_secret: process.env.PAYPAL_CLIENT_SECRET, + redirect_uri: process.env.REDIRECT_URI + '/paypal', + scope: 'openid profile email', + sandbox: process.env.PAYPAL_SANDBOX === 'true' +}); + +// Get authorization URL +const authUrl = paypalApi.getAuthUri(); + +// Exchange code for tokens +const tokens = await paypalApi.getTokenFromCode(authorizationCode); + +// Create a simple order +const order = await paypalApi.createSimpleOrder('10.00', 'USD', 'Test payment'); + +// Capture the order +const capture = await paypalApi.captureOrder(order.id); +``` + +## Available Methods + +### Identity Methods +- `getUserInfo()` - Get user profile information + +### Orders Methods +- `createOrder(orderData)` - Create a payment order +- `getOrder(orderId)` - Get order details +- `updateOrder(orderId, patchData)` - Update order +- `captureOrder(orderId, captureData)` - Capture authorized payment +- `authorizeOrder(orderId, authorizeData)` - Authorize payment +- `createSimpleOrder(amount, currency, description)` - Helper for simple orders + +### Payments Methods +- `getPayment(paymentId)` - Get payment details + +### Invoicing Methods +- `getInvoices(params)` - List invoices +- `createInvoice(invoiceData)` - Create new invoice +- `getInvoice(invoiceId)` - Get specific invoice +- `updateInvoice(invoiceId, invoiceData)` - Update invoice +- `deleteInvoice(invoiceId)` - Delete draft invoice +- `sendInvoice(invoiceId, sendData)` - Send invoice to recipient +- `cancelInvoice(invoiceId, cancelData)` - Cancel sent invoice + +### Subscriptions Methods +- `createSubscription(subscriptionData)` - Create subscription +- `getSubscription(subscriptionId)` - Get subscription details +- `updateSubscription(subscriptionId, patchData)` - Update subscription +- `activateSubscription(subscriptionId, reason)` - Activate subscription +- `cancelSubscription(subscriptionId, reason)` - Cancel subscription +- `suspendSubscription(subscriptionId, reason)` - Suspend subscription + +### Plans Methods +- `getPlans(params)` - List billing plans +- `createPlan(planData)` - Create billing plan +- `getPlan(planId)` - Get plan details +- `updatePlan(planId, patchData)` - Update plan +- `activatePlan(planId)` - Activate plan +- `deactivatePlan(planId)` - Deactivate plan + +### Products Methods +- `getProducts(params)` - List products +- `createProduct(productData)` - Create product +- `getProduct(productId)` - Get product details +- `updateProduct(productId, patchData)` - Update product + +### Webhooks Methods +- `getWebhooks()` - List webhooks +- `createWebhook(webhookData)` - Create webhook +- `getWebhook(webhookId)` - Get webhook details +- `updateWebhook(webhookId, patchData)` - Update webhook +- `deleteWebhook(webhookId)` - Delete webhook +- `getWebhookEventTypes()` - Get available event types + +### Disputes Methods +- `getDisputes(params)` - List disputes +- `getDispute(disputeId)` - Get dispute details + +### Payouts Methods +- `createPayout(payoutData)` - Create batch payout +- `getPayout(payoutBatchId)` - Get payout batch details +- `getPayoutItem(payoutItemId)` - Get payout item details + +## Usage Examples + +### Creating and Capturing an Order +```javascript +// Create order +const orderData = { + intent: 'CAPTURE', + purchase_units: [ + { + amount: { + currency_code: 'USD', + value: '100.00' + }, + description: 'Product purchase' + } + ], + application_context: { + return_url: 'https://yoursite.com/return', + cancel_url: 'https://yoursite.com/cancel' + } +}; + +const order = await paypalApi.createOrder(orderData); + +// Get approval URL from order.links +const approvalUrl = order.links.find(link => link.rel === 'approve').href; + +// After user approves, capture the order +const capture = await paypalApi.captureOrder(order.id); +``` + +### Creating an Invoice +```javascript +const invoiceData = { + detail: { + invoice_number: 'INV-001', + currency_code: 'USD' + }, + invoicer: { + name: { + given_name: 'John', + surname: 'Doe' + }, + email_address: 'seller@example.com' + }, + primary_recipients: [ + { + billing_info: { + email_address: 'buyer@example.com' + } + } + ], + items: [ + { + name: 'Product Name', + quantity: '1', + unit_amount: { + currency_code: 'USD', + value: '100.00' + } + } + ] +}; + +const invoice = await paypalApi.createInvoice(invoiceData); +await paypalApi.sendInvoice(invoice.id); +``` + +### Creating a Subscription Plan +```javascript +// First create a product +const productData = { + name: 'Monthly Service', + description: 'Monthly subscription service', + type: 'SERVICE', + category: 'SOFTWARE' +}; + +const product = await paypalApi.createProduct(productData); + +// Then create a plan +const planData = { + product_id: product.id, + name: 'Monthly Plan', + description: 'Monthly subscription plan', + billing_cycles: [ + { + frequency: { + interval_unit: 'MONTH', + interval_count: 1 + }, + tenure_type: 'REGULAR', + sequence: 1, + total_cycles: 0, + pricing_scheme: { + fixed_price: { + value: '29.99', + currency_code: 'USD' + } + } + } + ], + payment_preferences: { + auto_bill_outstanding: true, + payment_failure_threshold: 3 + } +}; + +const plan = await paypalApi.createPlan(planData); +``` + +### Setting up Webhooks +```javascript +const webhookData = { + url: 'https://yoursite.com/webhooks/paypal', + event_types: [ + { name: 'PAYMENT.CAPTURE.COMPLETED' }, + { name: 'PAYMENT.CAPTURE.DENIED' }, + { name: 'BILLING.SUBSCRIPTION.CREATED' }, + { name: 'BILLING.SUBSCRIPTION.CANCELLED' } + ] +}; + +const webhook = await paypalApi.createWebhook(webhookData); +``` + +## Authentication Flow + +PayPal uses OAuth2 with a unique authorization flow: + +1. Redirect users to PayPal's authorization URL +2. Handle the callback with the authorization code +3. Exchange the code for access and refresh tokens +4. Use tokens for API requests + +## Error Handling + +PayPal returns detailed error information. Always wrap API calls in try-catch blocks: + +```javascript +try { + const order = await paypalApi.createOrder(orderData); + console.log('Order created:', order.id); +} catch (error) { + console.error('PayPal error:', error.message); + // Handle specific error types + if (error.details) { + error.details.forEach(detail => { + console.error('Error detail:', detail.description); + }); + } +} +``` + +## Testing + +Use PayPal's sandbox environment for testing: +- Test credit card numbers are available in PayPal's documentation +- All transactions in sandbox are simulated +- Use sandbox.paypal.com for buyer accounts + +## Webhooks + +PayPal sends webhooks for various events. Important event types include: +- `PAYMENT.CAPTURE.COMPLETED` - Payment completed +- `PAYMENT.CAPTURE.DENIED` - Payment denied +- `BILLING.SUBSCRIPTION.CREATED` - Subscription created +- `BILLING.SUBSCRIPTION.CANCELLED` - Subscription cancelled + +## Documentation + +For detailed PayPal API documentation, visit: https://developer.paypal.com/docs/api/overview/ \ No newline at end of file diff --git a/packages/paypal/api.js b/packages/paypal/api.js new file mode 100644 index 0000000..78421ea --- /dev/null +++ b/packages/paypal/api.js @@ -0,0 +1,468 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.sandbox = get(params, 'sandbox', false); + this.baseUrl = this.sandbox + ? 'https://api-m.sandbox.paypal.com' + : 'https://api-m.paypal.com'; + + this.authBaseUrl = this.sandbox + ? 'https://www.sandbox.paypal.com' + : 'https://www.paypal.com'; + + this.URLs = { + authorization: '/connect', + access_token: '/v1/oauth2/token', + + // Identity + userInfo: '/v1/identity/oauth2/userinfo', + + // Payments + payments: '/v2/payments', + paymentById: (paymentId) => `/v2/payments/payment/${paymentId}`, + + // Orders + orders: '/v2/checkout/orders', + orderById: (orderId) => `/v2/checkout/orders/${orderId}`, + orderCapture: (orderId) => `/v2/checkout/orders/${orderId}/capture`, + orderAuthorize: (orderId) => `/v2/checkout/orders/${orderId}/authorize`, + + // Invoicing + invoices: '/v2/invoicing/invoices', + invoiceById: (invoiceId) => `/v2/invoicing/invoices/${invoiceId}`, + invoiceSend: (invoiceId) => `/v2/invoicing/invoices/${invoiceId}/send`, + invoiceCancel: (invoiceId) => `/v2/invoicing/invoices/${invoiceId}/cancel`, + + // Subscriptions + subscriptions: '/v1/billing/subscriptions', + subscriptionById: (subscriptionId) => `/v1/billing/subscriptions/${subscriptionId}`, + subscriptionActivate: (subscriptionId) => `/v1/billing/subscriptions/${subscriptionId}/activate`, + subscriptionCancel: (subscriptionId) => `/v1/billing/subscriptions/${subscriptionId}/cancel`, + subscriptionSuspend: (subscriptionId) => `/v1/billing/subscriptions/${subscriptionId}/suspend`, + + // Plans + plans: '/v1/billing/plans', + planById: (planId) => `/v1/billing/plans/${planId}`, + planActivate: (planId) => `/v1/billing/plans/${planId}/activate`, + planDeactivate: (planId) => `/v1/billing/plans/${planId}/deactivate`, + + // Products + products: '/v1/catalogs/products', + productById: (productId) => `/v1/catalogs/products/${productId}`, + + // Webhooks + webhooks: '/v1/notifications/webhooks', + webhookById: (webhookId) => `/v1/notifications/webhooks/${webhookId}`, + webhookEventTypes: '/v1/notifications/webhooks-event-types', + + // Disputes + disputes: '/v1/customer/disputes', + disputeById: (disputeId) => `/v1/customer/disputes/${disputeId}`, + + // Partner Referrals + partnerReferrals: '/v1/customer/partner-referrals', + + // Payouts + payouts: '/v1/payments/payouts', + payoutById: (payoutBatchId) => `/v1/payments/payouts/${payoutBatchId}`, + payoutItem: (payoutItemId) => `/v1/payments/payouts-item/${payoutItemId}`, + }; + + // OAuth2 URLs for PayPal are different + this.authorizationUri = encodeURI( + `${this.authBaseUrl}/connect?flowEntry=static&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${this.scope}&response_type=code&state=${this.state}` + ); + this.tokenUri = this.baseUrl + '/v1/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** Identity Methods ********************************** + + async getUserInfo() { + const options = { + url: this.baseUrl + this.URLs.userInfo, + query: { + schema: 'paypalv1.1' + } + }; + return this._get(options); + } + + // ************************** Orders Methods ********************************** + + async createOrder(orderData) { + const options = { + url: this.baseUrl + this.URLs.orders, + body: orderData, + }; + return this._post(options); + } + + async getOrder(orderId) { + const options = { + url: this.baseUrl + this.URLs.orderById(orderId), + }; + return this._get(options); + } + + async updateOrder(orderId, patchData) { + const options = { + url: this.baseUrl + this.URLs.orderById(orderId), + body: patchData, + }; + return this._patch(options); + } + + async captureOrder(orderId, captureData = {}) { + const options = { + url: this.baseUrl + this.URLs.orderCapture(orderId), + body: captureData, + }; + return this._post(options); + } + + async authorizeOrder(orderId, authorizeData = {}) { + const options = { + url: this.baseUrl + this.URLs.orderAuthorize(orderId), + body: authorizeData, + }; + return this._post(options); + } + + // ************************** Payments Methods ********************************** + + async getPayment(paymentId) { + const options = { + url: this.baseUrl + this.URLs.paymentById(paymentId), + }; + return this._get(options); + } + + // ************************** Invoicing Methods ********************************** + + async getInvoices(params = {}) { + const options = { + url: this.baseUrl + this.URLs.invoices, + query: params, + }; + return this._get(options); + } + + async createInvoice(invoiceData) { + const options = { + url: this.baseUrl + this.URLs.invoices, + body: invoiceData, + }; + return this._post(options); + } + + async getInvoice(invoiceId) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + }; + return this._get(options); + } + + async updateInvoice(invoiceId, invoiceData) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + body: invoiceData, + }; + return this._put(options); + } + + async deleteInvoice(invoiceId) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + }; + return this._delete(options); + } + + async sendInvoice(invoiceId, sendData = {}) { + const options = { + url: this.baseUrl + this.URLs.invoiceSend(invoiceId), + body: sendData, + }; + return this._post(options); + } + + async cancelInvoice(invoiceId, cancelData) { + const options = { + url: this.baseUrl + this.URLs.invoiceCancel(invoiceId), + body: cancelData, + }; + return this._post(options); + } + + // ************************** Subscriptions Methods ********************************** + + async createSubscription(subscriptionData) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + body: subscriptionData, + }; + return this._post(options); + } + + async getSubscription(subscriptionId) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId), + }; + return this._get(options); + } + + async updateSubscription(subscriptionId, patchData) { + const options = { + url: this.baseUrl + this.URLs.subscriptionById(subscriptionId), + body: patchData, + }; + return this._patch(options); + } + + async activateSubscription(subscriptionId, reason) { + const options = { + url: this.baseUrl + this.URLs.subscriptionActivate(subscriptionId), + body: { reason }, + }; + return this._post(options); + } + + async cancelSubscription(subscriptionId, reason) { + const options = { + url: this.baseUrl + this.URLs.subscriptionCancel(subscriptionId), + body: { reason }, + }; + return this._post(options); + } + + async suspendSubscription(subscriptionId, reason) { + const options = { + url: this.baseUrl + this.URLs.subscriptionSuspend(subscriptionId), + body: { reason }, + }; + return this._post(options); + } + + // ************************** Plans Methods ********************************** + + async getPlans(params = {}) { + const options = { + url: this.baseUrl + this.URLs.plans, + query: params, + }; + return this._get(options); + } + + async createPlan(planData) { + const options = { + url: this.baseUrl + this.URLs.plans, + body: planData, + }; + return this._post(options); + } + + async getPlan(planId) { + const options = { + url: this.baseUrl + this.URLs.planById(planId), + }; + return this._get(options); + } + + async updatePlan(planId, patchData) { + const options = { + url: this.baseUrl + this.URLs.planById(planId), + body: patchData, + }; + return this._patch(options); + } + + async activatePlan(planId) { + const options = { + url: this.baseUrl + this.URLs.planActivate(planId), + }; + return this._post(options); + } + + async deactivatePlan(planId) { + const options = { + url: this.baseUrl + this.URLs.planDeactivate(planId), + }; + return this._post(options); + } + + // ************************** Products Methods ********************************** + + async getProducts(params = {}) { + const options = { + url: this.baseUrl + this.URLs.products, + query: params, + }; + return this._get(options); + } + + async createProduct(productData) { + const options = { + url: this.baseUrl + this.URLs.products, + body: productData, + }; + return this._post(options); + } + + async getProduct(productId) { + const options = { + url: this.baseUrl + this.URLs.productById(productId), + }; + return this._get(options); + } + + async updateProduct(productId, patchData) { + const options = { + url: this.baseUrl + this.URLs.productById(productId), + body: patchData, + }; + return this._patch(options); + } + + // ************************** Webhooks Methods ********************************** + + async getWebhooks() { + const options = { + url: this.baseUrl + this.URLs.webhooks, + }; + return this._get(options); + } + + async createWebhook(webhookData) { + const options = { + url: this.baseUrl + this.URLs.webhooks, + body: webhookData, + }; + return this._post(options); + } + + async getWebhook(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + }; + return this._get(options); + } + + async updateWebhook(webhookId, patchData) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + body: patchData, + }; + return this._patch(options); + } + + async deleteWebhook(webhookId) { + const options = { + url: this.baseUrl + this.URLs.webhookById(webhookId), + }; + return this._delete(options); + } + + async getWebhookEventTypes() { + const options = { + url: this.baseUrl + this.URLs.webhookEventTypes, + }; + return this._get(options); + } + + // ************************** Disputes Methods ********************************** + + async getDisputes(params = {}) { + const options = { + url: this.baseUrl + this.URLs.disputes, + query: params, + }; + return this._get(options); + } + + async getDispute(disputeId) { + const options = { + url: this.baseUrl + this.URLs.disputeById(disputeId), + }; + return this._get(options); + } + + // ************************** Payouts Methods ********************************** + + async createPayout(payoutData) { + const options = { + url: this.baseUrl + this.URLs.payouts, + body: payoutData, + }; + return this._post(options); + } + + async getPayout(payoutBatchId) { + const options = { + url: this.baseUrl + this.URLs.payoutById(payoutBatchId), + }; + return this._get(options); + } + + async getPayoutItem(payoutItemId) { + const options = { + url: this.baseUrl + this.URLs.payoutItem(payoutItemId), + }; + return this._get(options); + } + + // ************************** Helper Methods ********************************** + + async createSimpleOrder(amount, currency = 'USD', description = '') { + const orderData = { + intent: 'CAPTURE', + purchase_units: [ + { + amount: { + currency_code: currency, + value: amount.toString() + }, + description: description + } + ] + }; + + return this.createOrder(orderData); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/paypal/defaultConfig.json b/packages/paypal/defaultConfig.json new file mode 100644 index 0000000..4c9ce55 --- /dev/null +++ b/packages/paypal/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "paypal", + "label": "PayPal", + "productUrl": "https://paypal.com", + "apiDocs": "https://developer.paypal.com/docs/api/overview/", + "logoUrl": "https://www.paypalobjects.com/webstatic/mktg/logo/pp_cc_mark_111x69.jpg", + "categories": [ + "Payments", + "E-commerce", + "Financial Services", + "Money Transfer" + ], + "description": "PayPal is a digital payment platform that allows users to make payments and money transfers online" +} \ No newline at end of file diff --git a/packages/paypal/definition.js b/packages/paypal/definition.js new file mode 100644 index 0000000..a786137 --- /dev/null +++ b/packages/paypal/definition.js @@ -0,0 +1,54 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'PayPal', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: { externalId: userInfo.user_id, user: userId }, + details: { + name: userInfo.name, + email: userInfo.email + } + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userInfo = await api.getUserInfo(); + return { + identifiers: { externalId: userInfo.user_id, user: userId }, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserInfo(); + }, + }, + env: { + client_id: process.env.PAYPAL_CLIENT_ID, + client_secret: process.env.PAYPAL_CLIENT_SECRET, + scope: process.env.PAYPAL_SCOPE || 'openid profile email', + redirect_uri: `${process.env.REDIRECT_URI}/paypal`, + sandbox: process.env.PAYPAL_SANDBOX === 'true', + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/paypal/index.js b/packages/paypal/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/paypal/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/paypal/openapi.json b/packages/paypal/openapi.json new file mode 100644 index 0000000..a90ca82 --- /dev/null +++ b/packages/paypal/openapi.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cc647244b3cc1e3244d26a685f3a94a64f53fa6828e533bdfde67f440b734c1 +size 564432 diff --git a/packages/v1-ready/contentstack/.eslintrc.json b/packages/personio/.eslintrc.json similarity index 100% rename from packages/v1-ready/contentstack/.eslintrc.json rename to packages/personio/.eslintrc.json diff --git a/packages/needs-updating/personio/CHANGELOG.md b/packages/personio/CHANGELOG.md similarity index 100% rename from packages/needs-updating/personio/CHANGELOG.md rename to packages/personio/CHANGELOG.md diff --git a/packages/needs-updating/salesloft/LICENSE.md b/packages/personio/LICENSE.md similarity index 100% rename from packages/needs-updating/salesloft/LICENSE.md rename to packages/personio/LICENSE.md diff --git a/packages/needs-updating/personio/README.md b/packages/personio/README.md similarity index 100% rename from packages/needs-updating/personio/README.md rename to packages/personio/README.md diff --git a/packages/needs-updating/personio/api.js b/packages/personio/api.js similarity index 100% rename from packages/needs-updating/personio/api.js rename to packages/personio/api.js diff --git a/packages/needs-updating/personio/authFields.js b/packages/personio/authFields.js similarity index 100% rename from packages/needs-updating/personio/authFields.js rename to packages/personio/authFields.js diff --git a/packages/needs-updating/personio/defaultConfig.json b/packages/personio/defaultConfig.json similarity index 100% rename from packages/needs-updating/personio/defaultConfig.json rename to packages/personio/defaultConfig.json diff --git a/packages/personio/definition.js b/packages/personio/definition.js new file mode 100644 index 0000000..e990ee5 --- /dev/null +++ b/packages/personio/definition.js @@ -0,0 +1,123 @@ +const { IntegrationBase, get, ModuleConstants } = require('@friggframework/core'); +const { Api } = require('./api'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); +const AuthFields = require('./authFields'); + +class PersonioIntegration extends IntegrationBase { + static Definition = { + name: 'personio', + version: '1.0.0', + display: { + label: 'Personio', + description: 'Personio', + imageURL: 'https://friggframework.org/assets/img/personio-icon.png', + icon: '', + category: 'HR', + }, + modules: { + api: Api, + credential: Credential, + entity: Entity, + }, + }; + + static AuthFields = AuthFields; + + async getAuthorizationRequirements() { + // see parent docs. only use these three top level keys + return { + url: null, + type: ModuleConstants.authType.apiKey, + data: { + jsonSchema: AuthFields.jsonSchema, + uiSchema: AuthFields.uiSchema, + }, + }; + } + + async processAuthorizationCallback(params) { + const clientId = get(params.data, 'clientId'); + const clientSecret = get(params.data, 'clientSecret'); + const companyId = get(params.data, 'companyId'); + const accessToken = get(params.data, 'accessToken'); + const subdomain = get(params.data, 'subdomain'); + this.api = new Api({ + clientId, + clientSecret, + companyId, + accessToken, + subdomain, + }); + const userDetails = await this.api.getUserDetails(); + + const byUserId = {user: this.userId}; + const credentials = await this.credentialMO.list(byUserId); + + if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } + + const credential = await this.credentialMO.upsert(byUserId, { + user: this.userId, + client_id: clientId, + client_secret: clientSecret, + company_id: companyId, + access_token: accessToken, + subdomain: subdomain, + }); + + const byUserIdAndCredential = { + ...byUserId, + credential: credential.id, + }; + const entity = await this.entityMO.upsert(byUserIdAndCredential, { + user: this.userId, + credential: credential.id, + name: userDetails.user.username, + externalId: userDetails.user.id, + }); + + return { + entity_id: entity.id, + credential_id: credential.id, + type: PersonioIntegration.Definition.name, + }; + } + + async testAuth() { + // TODO - this method doesn't exist in API + await this.api.getUserDetails(); + } + + async deauthorize() { + // wipe api connection + this.api = new Api(); + + // delete credentials from the database + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async getApiObject() { + let personioParams = {}; + + if (this.credential) { + personioParams = { + clientId: this.credential.clientId, + clientSecret: this.credential.clientSecret, + companyId: this.credential.companyId, + accessToken: this.credential.accessToken, + subdomain: this.credential.subdomain, + }; + } + + return new Api(personioParams); + } +} + +module.exports = PersonioIntegration; \ No newline at end of file diff --git a/packages/personio/index.js b/packages/personio/index.js new file mode 100644 index 0000000..a0eac7a --- /dev/null +++ b/packages/personio/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; diff --git a/packages/needs-updating/rollworks/jest.config.js b/packages/personio/jest.config.js similarity index 100% rename from packages/needs-updating/rollworks/jest.config.js rename to packages/personio/jest.config.js diff --git a/packages/needs-updating/personio/manager.test.js b/packages/personio/manager.test.js similarity index 100% rename from packages/needs-updating/personio/manager.test.js rename to packages/personio/manager.test.js diff --git a/packages/needs-updating/personio/test/Api.test.js b/packages/personio/test/Api.test.js similarity index 100% rename from packages/needs-updating/personio/test/Api.test.js rename to packages/personio/test/Api.test.js diff --git a/packages/v1-ready/crossbeam/.eslintrc.json b/packages/pipedrive/.eslintrc.json similarity index 100% rename from packages/v1-ready/crossbeam/.eslintrc.json rename to packages/pipedrive/.eslintrc.json diff --git a/packages/needs-updating/qbo/CHANGELOG.md b/packages/pipedrive/CHANGELOG.md similarity index 100% rename from packages/needs-updating/qbo/CHANGELOG.md rename to packages/pipedrive/CHANGELOG.md diff --git a/packages/needs-updating/slack/LICENSE.md b/packages/pipedrive/LICENSE.md similarity index 100% rename from packages/needs-updating/slack/LICENSE.md rename to packages/pipedrive/LICENSE.md diff --git a/packages/v1-ready/pipedrive/README.md b/packages/pipedrive/README.md similarity index 100% rename from packages/v1-ready/pipedrive/README.md rename to packages/pipedrive/README.md diff --git a/packages/v1-ready/pipedrive/api.js b/packages/pipedrive/api.js similarity index 100% rename from packages/v1-ready/pipedrive/api.js rename to packages/pipedrive/api.js diff --git a/packages/needs-updating/pipedrive/defaultConfig.json b/packages/pipedrive/defaultConfig.json similarity index 100% rename from packages/needs-updating/pipedrive/defaultConfig.json rename to packages/pipedrive/defaultConfig.json diff --git a/packages/v1-ready/pipedrive/definition.js b/packages/pipedrive/definition.js similarity index 100% rename from packages/v1-ready/pipedrive/definition.js rename to packages/pipedrive/definition.js diff --git a/packages/v1-ready/pipedrive/fenestra/platform.fenestra.yaml b/packages/pipedrive/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/pipedrive/fenestra/platform.fenestra.yaml rename to packages/pipedrive/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/pipedrive/fenestra/schemas/pipedrive-validation.json b/packages/pipedrive/fenestra/schemas/pipedrive-validation.json similarity index 100% rename from packages/v1-ready/pipedrive/fenestra/schemas/pipedrive-validation.json rename to packages/pipedrive/fenestra/schemas/pipedrive-validation.json diff --git a/packages/pipedrive/index.js b/packages/pipedrive/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/pipedrive/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/needs-updating/salesloft/jest.config.js b/packages/pipedrive/jest.config.js similarity index 100% rename from packages/needs-updating/salesloft/jest.config.js rename to packages/pipedrive/jest.config.js diff --git a/packages/needs-updating/outreach/manager.test.js b/packages/pipedrive/manager.test.js similarity index 100% rename from packages/needs-updating/outreach/manager.test.js rename to packages/pipedrive/manager.test.js diff --git a/packages/needs-updating/pipedrive/mocks/activities/createActivity.js b/packages/pipedrive/mocks/activities/createActivity.js similarity index 100% rename from packages/needs-updating/pipedrive/mocks/activities/createActivity.js rename to packages/pipedrive/mocks/activities/createActivity.js diff --git a/packages/needs-updating/pipedrive/mocks/activities/deleteActivity.js b/packages/pipedrive/mocks/activities/deleteActivity.js similarity index 100% rename from packages/needs-updating/pipedrive/mocks/activities/deleteActivity.js rename to packages/pipedrive/mocks/activities/deleteActivity.js diff --git a/packages/needs-updating/pipedrive/mocks/activities/listActivities.js b/packages/pipedrive/mocks/activities/listActivities.js similarity index 100% rename from packages/needs-updating/pipedrive/mocks/activities/listActivities.js rename to packages/pipedrive/mocks/activities/listActivities.js diff --git a/packages/needs-updating/pipedrive/mocks/activities/updateActivity.js b/packages/pipedrive/mocks/activities/updateActivity.js similarity index 100% rename from packages/needs-updating/pipedrive/mocks/activities/updateActivity.js rename to packages/pipedrive/mocks/activities/updateActivity.js diff --git a/packages/needs-updating/pipedrive/mocks/apiMock.js b/packages/pipedrive/mocks/apiMock.js similarity index 100% rename from packages/needs-updating/pipedrive/mocks/apiMock.js rename to packages/pipedrive/mocks/apiMock.js diff --git a/packages/needs-updating/pipedrive/mocks/deals/listDeals.js b/packages/pipedrive/mocks/deals/listDeals.js similarity index 100% rename from packages/needs-updating/pipedrive/mocks/deals/listDeals.js rename to packages/pipedrive/mocks/deals/listDeals.js diff --git a/packages/v1-ready/pipedrive/package.json b/packages/pipedrive/package.json similarity index 100% rename from packages/v1-ready/pipedrive/package.json rename to packages/pipedrive/package.json diff --git a/packages/pipedrive/specs/openAPI.yaml b/packages/pipedrive/specs/openAPI.yaml new file mode 100644 index 0000000..1f6f633 --- /dev/null +++ b/packages/pipedrive/specs/openAPI.yaml @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e58d0ccbe0deaa88095942a6443bb128be9e0323b92442169594a8d1ae59e698 +size 481523 diff --git a/packages/needs-updating/pipedrive/test/Api.test.js b/packages/pipedrive/test/Api.test.js similarity index 100% rename from packages/needs-updating/pipedrive/test/Api.test.js rename to packages/pipedrive/test/Api.test.js diff --git a/packages/needs-updating/pipedrive/test/Manager.test.js b/packages/pipedrive/test/Manager.test.js similarity index 100% rename from packages/needs-updating/pipedrive/test/Manager.test.js rename to packages/pipedrive/test/Manager.test.js diff --git a/packages/plaid/api.js b/packages/plaid/api.js new file mode 100644 index 0000000..e602ea0 --- /dev/null +++ b/packages/plaid/api.js @@ -0,0 +1,295 @@ +const { Requester, get } = require('@friggframework/core'); +const { Configuration, PlaidApi, PlaidEnvironments } = require('plaid'); + +class Api extends Requester { + constructor(params = {}) { + super(params); + + this.clientId = get(params, 'clientId', null); + this.secret = get(params, 'secret', null); + this.environment = get(params, 'environment', 'sandbox'); + this.redirectUri = get(params, 'redirectUri', null); + this.accessToken = get(params, 'accessToken', null); + + // Initialize Plaid configuration + const plaidEnv = this.environment === 'production' + ? PlaidEnvironments.production + : this.environment === 'development' + ? PlaidEnvironments.development + : PlaidEnvironments.sandbox; + + const configuration = new Configuration({ + basePath: plaidEnv, + baseOptions: { + headers: { + 'PLAID-CLIENT-ID': this.clientId, + 'PLAID-SECRET': this.secret, + }, + }, + }); + + this.client = new PlaidApi(configuration); + } + + // Link token creation for Plaid Link + async createLinkToken(params = {}) { + const request = { + client_id: this.clientId, + secret: this.secret, + user: { + client_user_id: params.userId || 'user-' + Date.now(), + }, + client_name: params.clientName || 'Frigg Framework App', + products: params.products || ['transactions', 'accounts', 'balances'], + country_codes: params.countryCodes || ['US'], + language: params.language || 'en', + redirect_uri: this.redirectUri, + webhook: params.webhook, + }; + + if (params.accessToken) { + request.access_token = params.accessToken; + } + + const response = await this.client.linkTokenCreate(request); + return response.data; + } + + // Exchange public token for access token + async exchangePublicToken(publicToken) { + const request = { + client_id: this.clientId, + secret: this.secret, + public_token: publicToken, + }; + + const response = await this.client.itemPublicTokenExchange(request); + this.accessToken = response.data.access_token; + return response.data; + } + + // Get accounts + async getAccounts(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.accountsGet(request); + return response.data; + } + + // Get account balances + async getBalances(accessToken = null, accountIds = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + if (accountIds) { + request.options = { account_ids: accountIds }; + } + + const response = await this.client.accountsBalanceGet(request); + return response.data; + } + + // Get transactions + async getTransactions(startDate, endDate, params = {}) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: params.accessToken || this.accessToken, + start_date: startDate, + end_date: endDate, + options: { + count: params.count || 100, + offset: params.offset || 0, + account_ids: params.accountIds, + include_personal_finance_category: params.includeCategories || false, + }, + }; + + const response = await this.client.transactionsGet(request); + return response.data; + } + + // Sync transactions (for incremental updates) + async syncTransactions(cursor = null, params = {}) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: params.accessToken || this.accessToken, + }; + + if (cursor) { + request.cursor = cursor; + } + + if (params.count) { + request.count = params.count; + } + + const response = await this.client.transactionsSync(request); + return response.data; + } + + // Get item (institution) details + async getItem(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.itemGet(request); + return response.data; + } + + // Get institution by ID + async getInstitutionById(institutionId, countryCodes = ['US']) { + const request = { + client_id: this.clientId, + secret: this.secret, + institution_id: institutionId, + country_codes: countryCodes, + }; + + const response = await this.client.institutionsGetById(request); + return response.data; + } + + // Search institutions + async searchInstitutions(query, countryCodes = ['US'], products = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + query: query, + country_codes: countryCodes, + }; + + if (products) { + request.products = products; + } + + const response = await this.client.institutionsSearch(request); + return response.data; + } + + // Get identity information + async getIdentity(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.identityGet(request); + return response.data; + } + + // Get investment holdings + async getInvestmentHoldings(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.investmentsHoldingsGet(request); + return response.data; + } + + // Get investment transactions + async getInvestmentTransactions(startDate, endDate, params = {}) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: params.accessToken || this.accessToken, + start_date: startDate, + end_date: endDate, + options: { + count: params.count || 100, + offset: params.offset || 0, + account_ids: params.accountIds, + }, + }; + + const response = await this.client.investmentsTransactionsGet(request); + return response.data; + } + + // Get liabilities + async getLiabilities(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.liabilitiesGet(request); + return response.data; + } + + // Create processor token for integrations (e.g., Stripe, Dwolla) + async createProcessorToken(accountId, processor, accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + account_id: accountId, + processor: processor, + }; + + const response = await this.client.processorTokenCreate(request); + return response.data; + } + + // Remove item (unlink bank account) + async removeItem(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.itemRemove(request); + return response.data; + } + + // Update webhook URL + async updateWebhook(webhook, accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + webhook: webhook, + }; + + const response = await this.client.itemWebhookUpdate(request); + return response.data; + } + + // Force refresh transactions + async refreshTransactions(accessToken = null) { + const request = { + client_id: this.clientId, + secret: this.secret, + access_token: accessToken || this.accessToken, + }; + + const response = await this.client.transactionsRefresh(request); + return response.data; + } + + // Get categories + async getCategories() { + const request = {}; + const response = await this.client.categoriesGet(request); + return response.data; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/plaid/defaultConfig.json b/packages/plaid/defaultConfig.json new file mode 100644 index 0000000..ed046b8 --- /dev/null +++ b/packages/plaid/defaultConfig.json @@ -0,0 +1,15 @@ +{ + "name": "plaid", + "displayName": "Plaid", + "description": "Financial data aggregation platform", + "version": "1.0.0", + "categories": ["finance", "banking", "data-aggregation"], + "scopes": [ + "accounts", + "transactions", + "balances", + "identity", + "investments", + "liabilities" + ] +} \ No newline at end of file diff --git a/packages/plaid/definition.js b/packages/plaid/definition.js new file mode 100644 index 0000000..0233141 --- /dev/null +++ b/packages/plaid/definition.js @@ -0,0 +1,75 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Plaid', + requiredAuthMethods: { + getToken: async function (api, params) { + // Plaid uses public token exchange instead of OAuth + const publicToken = get(params.data, 'public_token'); + const result = await api.exchangePublicToken(publicToken); + return { + access_token: result.access_token, + item_id: result.item_id, + }; + }, + + getEntityDetails: async function (api, userId) { + const item = await api.getItem(); + const institution = await api.getInstitutionById(item.item.institution_id); + + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: item.item.item_id, + user: userId + }, + details: { + name: institution.institution.name, + institutionId: item.item.institution_id, + availableProducts: item.item.available_products, + billedProducts: item.item.billed_products, + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['access_token', 'item_id'], + entity: ['institution_id', 'available_products'], + }, + + getCredentialDetails: async function (api, userId) { + const item = await api.getItem(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: item.item.item_id, + user: userId + }, + details: { + webhook: item.item.webhook, + consentExpirationTime: item.item.consent_expiration_time, + }, + }; + }, + + testAuthRequest: function (api) { + return api.getAccounts(); + }, + }, + env: { + clientId: process.env.PLAID_CLIENT_ID, + secret: process.env.PLAID_SECRET, + environment: process.env.PLAID_ENV || 'sandbox', + redirectUri: `${process.env.REDIRECT_URI}/plaid`, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/plaid/index.js b/packages/plaid/index.js new file mode 100644 index 0000000..5a1a5bd --- /dev/null +++ b/packages/plaid/index.js @@ -0,0 +1,7 @@ +const { Definition } = require('./definition'); +const { Api } = require('./api'); + +module.exports = { + Definition, + Api, +}; \ No newline at end of file diff --git a/packages/plaid/package.json b/packages/plaid/package.json new file mode 100644 index 0000000..14f770e --- /dev/null +++ b/packages/plaid/package.json @@ -0,0 +1,25 @@ +{ + "name": "@friggframework/plaid", + "version": "1.0.0", + "description": "Plaid financial data aggregation API module for Frigg Framework", + "main": "index.js", + "scripts": { + "test": "jest" + }, + "dependencies": { + "@friggframework/core": "^1.0.0", + "plaid": "^21.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "keywords": [ + "plaid", + "financial", + "banking", + "api", + "frigg" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/plaid/readme.md b/packages/plaid/readme.md new file mode 100644 index 0000000..c8a1193 --- /dev/null +++ b/packages/plaid/readme.md @@ -0,0 +1,27 @@ +# Plaid API Module + +This module provides integration with the Plaid financial data aggregation API. + +## Features + +- Link token creation for Plaid Link +- Public token exchange +- Account and balance retrieval +- Transaction fetching and syncing +- Investment holdings and transactions +- Identity verification +- Liability information +- Institution search + +## Environment Variables + +``` +PLAID_CLIENT_ID=your_client_id +PLAID_SECRET=your_secret +PLAID_ENV=sandbox|development|production +REDIRECT_URI=your_app_redirect_uri +``` + +## Usage + +See the [Frigg Framework documentation](https://docs.friggframework.org) for usage details. \ No newline at end of file diff --git a/packages/plaid/tests/api.test.js b/packages/plaid/tests/api.test.js new file mode 100644 index 0000000..4ce80c8 --- /dev/null +++ b/packages/plaid/tests/api.test.js @@ -0,0 +1,101 @@ +const { Api } = require('../api'); + +describe('Plaid API', () => { + let api; + + beforeEach(() => { + api = new Api({ + clientId: 'test_client_id', + secret: 'test_secret', + environment: 'sandbox', + }); + }); + + test('should initialize with proper configuration', () => { + expect(api.clientId).toBe('test_client_id'); + expect(api.secret).toBe('test_secret'); + expect(api.environment).toBe('sandbox'); + expect(api.client).toBeDefined(); + }); + + test('should create link token request with proper parameters', async () => { + const params = { + userId: 'test-user-123', + clientName: 'Test App', + products: ['transactions'], + countryCodes: ['US'], + }; + + // Mock the client method + api.client.linkTokenCreate = jest.fn().mockResolvedValue({ + data: { + link_token: 'link-sandbox-test-token', + expiration: '2024-01-01T00:00:00Z', + }, + }); + + const result = await api.createLinkToken(params); + + expect(api.client.linkTokenCreate).toHaveBeenCalledWith( + expect.objectContaining({ + client_id: 'test_client_id', + secret: 'test_secret', + user: { client_user_id: 'test-user-123' }, + client_name: 'Test App', + products: ['transactions'], + country_codes: ['US'], + language: 'en', + }) + ); + expect(result.link_token).toBe('link-sandbox-test-token'); + }); + + test('should exchange public token for access token', async () => { + const publicToken = 'public-sandbox-test-token'; + + api.client.itemPublicTokenExchange = jest.fn().mockResolvedValue({ + data: { + access_token: 'access-sandbox-test-token', + item_id: 'item-123', + }, + }); + + const result = await api.exchangePublicToken(publicToken); + + expect(api.client.itemPublicTokenExchange).toHaveBeenCalledWith({ + client_id: 'test_client_id', + secret: 'test_secret', + public_token: publicToken, + }); + expect(result.access_token).toBe('access-sandbox-test-token'); + expect(api.accessToken).toBe('access-sandbox-test-token'); + }); + + test('should fetch accounts', async () => { + api.accessToken = 'access-sandbox-test-token'; + + api.client.accountsGet = jest.fn().mockResolvedValue({ + data: { + accounts: [ + { + account_id: 'acc-123', + name: 'Checking Account', + type: 'depository', + subtype: 'checking', + }, + ], + item: { item_id: 'item-123' }, + }, + }); + + const result = await api.getAccounts(); + + expect(api.client.accountsGet).toHaveBeenCalledWith({ + client_id: 'test_client_id', + secret: 'test_secret', + access_token: 'access-sandbox-test-token', + }); + expect(result.accounts).toHaveLength(1); + expect(result.accounts[0].name).toBe('Checking Account'); + }); +}); \ No newline at end of file diff --git a/packages/posthog/README.md b/packages/posthog/README.md new file mode 100644 index 0000000..b3148c2 --- /dev/null +++ b/packages/posthog/README.md @@ -0,0 +1,458 @@ +# PostHog API Module + +This module provides a v1-ready integration with PostHog's open source product analytics platform using Personal API Key and Project API Key authentication. + +## Installation + +```bash +npm install @friggframework/api-module-posthog +``` + +## Features + +- Personal API Key authentication for full API access +- Project API Key for event tracking +- Event capture and batch operations +- Person identification and management +- Feature flags +- Session recordings +- Insights and analytics +- Cohort management +- A/B testing experiments +- Open source with self-hosting option + +## Authentication + +PostHog uses two types of API keys: +- **Personal API Key**: Full API access with customizable scopes +- **Project API Key**: Write-only key for event tracking + +### Finding Your Keys +1. **Personal API Key**: Go to Account settings → Personal API Keys +2. **Project API Key**: Go to Project settings → API Keys + +## Quick Start + +### Initialize the Integration + +```javascript +const { Definition } = require('@friggframework/api-module-posthog'); + +const posthog = new Definition({ + personalApiKey: 'phx_your-personal-api-key', + projectApiKey: 'phc_your-project-api-key', + host: 'https://app.posthog.com' // Or your self-hosted URL +}); +``` + +## API Methods + +### Event Tracking + +#### Capture Single Event +```javascript +await posthog.capture({ + distinctId: 'user-123', + event: 'Button Clicked', + properties: { + button_name: 'signup', + page: '/homepage', + $browser: 'Chrome', + $current_url: 'https://example.com/homepage' + } +}); +``` + +#### Batch Events +```javascript +await posthog.batch([ + { + distinctId: 'user-123', + event: 'Page Viewed', + properties: { page: '/home' } + }, + { + distinctId: 'user-123', + event: 'Video Played', + properties: { video_id: 'intro-video' } + } +]); +``` + +### Person Management + +#### Identify Person +```javascript +await posthog.identify({ + distinctId: 'user-123', + properties: { + email: 'john@example.com', + name: 'John Doe', + plan: 'premium', + company: 'Acme Corp' + }, + setOnce: { + created_at: '2024-01-01' + } +}); +``` + +#### Alias Person +```javascript +await posthog.alias({ + distinctId: 'user-123', + alias: 'john@example.com' +}); +``` + +#### Get Person +```javascript +const person = await posthog.getPerson('user-123'); +``` + +#### Get Persons List +```javascript +const persons = await posthog.getPersons({ + search: 'john', + properties: { plan: 'premium' }, + limit: 100 +}); +``` + +#### Update Person +```javascript +await posthog.updatePerson('user-123', { + last_login: new Date().toISOString(), + total_purchases: 5 +}); +``` + +#### Delete Person (GDPR) +```javascript +await posthog.deletePerson('user-123'); +``` + +### Feature Flags + +#### Get All Feature Flags +```javascript +const flags = await posthog.getFeatureFlags(); +``` + +#### Get Specific Feature Flag +```javascript +const flag = await posthog.getFeatureFlag(123); +``` + +#### Create Feature Flag +```javascript +const flag = await posthog.createFeatureFlag({ + name: 'new-dashboard', + key: 'new-dashboard-enabled', + filters: { + groups: [{ + properties: [{ + key: 'plan', + type: 'person', + value: ['premium'], + operator: 'exact' + }], + rollout_percentage: 100 + }] + }, + active: true +}); +``` + +#### Evaluate Feature Flags for Person +```javascript +const flags = await posthog.evaluateFeatureFlags('user-123', { + personProperties: { + plan: 'premium' + } +}); +``` + +### Insights and Analytics + +#### Get Insights +```javascript +const insights = await posthog.getInsights({ + saved: true, + user: true, + limit: 20 +}); +``` + +#### Get Specific Insight +```javascript +const insight = await posthog.getInsight(456); +``` + +#### Create Insight +```javascript +const insight = await posthog.createInsight({ + name: 'Daily Active Users', + description: 'Track DAU over time', + filters: { + events: [{ id: '$pageview' }], + display: 'ActionsLineGraph', + interval: 'day', + date_from: '-30d' + }, + saved: true +}); +``` + +### Cohorts + +#### Get Cohorts +```javascript +const cohorts = await posthog.getCohorts(); +``` + +#### Create Cohort +```javascript +const cohort = await posthog.createCohort({ + name: 'Power Users', + description: 'Users with high engagement', + filters: { + properties: { + type: 'AND', + values: [{ + key: 'total_events', + type: 'person', + value: 100, + operator: 'gt' + }] + } + } +}); +``` + +### Session Recordings + +#### Get Session Recordings +```javascript +const recordings = await posthog.getSessionRecordings({ + date_from: '2024-01-01', + date_to: '2024-01-31', + person_id: 'user-123', + limit: 20 +}); +``` + +#### Get Specific Recording +```javascript +const recording = await posthog.getSessionRecording('session-abc-123'); +``` + +### Events and Properties + +#### Get Events +```javascript +const events = await posthog.getEvents({ + event: '$pageview', + person_id: 'user-123', + after: '2024-01-01T00:00:00Z', + limit: 100 +}); +``` + +#### Get Event Definitions +```javascript +const eventDefs = await posthog.getEventDefinitions(); +``` + +#### Get Property Definitions +```javascript +const propDefs = await posthog.getPropertyDefinitions(); +``` + +### Dashboards + +#### Get Dashboards +```javascript +const dashboards = await posthog.getDashboards(); +``` + +#### Get Specific Dashboard +```javascript +const dashboard = await posthog.getDashboard(789); +``` + +### Annotations + +#### Get Annotations +```javascript +const annotations = await posthog.getAnnotations({ + after: '2024-01-01', + before: '2024-01-31' +}); +``` + +#### Create Annotation +```javascript +const annotation = await posthog.createAnnotation({ + content: 'Product launch', + date_marker: '2024-01-15T00:00:00Z', + scope: 'project' +}); +``` + +### Experiments (A/B Testing) + +#### Get Experiments +```javascript +const experiments = await posthog.api.getExperiments(); +``` + +#### Create Experiment +```javascript +const experiment = await posthog.api.createExperiment({ + name: 'Homepage CTA Test', + description: 'Testing different CTA buttons', + feature_flag_key: 'homepage-cta-variant', + start_date: '2024-02-01', + end_date: '2024-02-28', + variants: [ + { key: 'control', name: 'Control', rollout_percentage: 50 }, + { key: 'variant-a', name: 'Variant A', rollout_percentage: 50 } + ] +}); +``` + +### Actions + +#### Get Actions +```javascript +const actions = await posthog.api.getActions(); +``` + +#### Create Action +```javascript +const action = await posthog.api.createAction({ + name: 'Completed Signup', + description: 'User completed the signup process', + steps: [{ + event: '$pageview', + properties: [{ + key: '$current_url', + type: 'event', + value: 'signup/complete', + operator: 'contains' + }] + }] +}); +``` + +## Advanced Event Properties + +### Standard Properties +```javascript +await posthog.capture({ + distinctId: 'user-123', + event: 'Purchase Completed', + properties: { + // PostHog standard properties + $browser: 'Chrome', + $browser_version: '96', + $current_url: 'https://shop.com/checkout', + $host: 'shop.com', + $pathname: '/checkout', + $screen_height: 1080, + $screen_width: 1920, + $referrer: 'https://google.com', + $referring_domain: 'google.com', + $device_type: 'Desktop', + $ip: '192.168.1.1', + + // Custom properties + order_id: 'ORD-123', + total_amount: 299.99, + items_count: 3 + } +}); +``` + +### Super Properties (set once, sent with all events) +```javascript +await posthog.identify({ + distinctId: 'user-123', + properties: { + $set: { + plan: 'premium', + company_id: 'comp-456' + } + } +}); +``` + +## Error Handling + +```javascript +try { + await posthog.capture({ + distinctId: 'user-123', + event: 'Test Event' + }); +} catch (error) { + if (error.status === 401) { + console.error('Invalid API key'); + } else if (error.status === 429) { + console.error('Rate limit exceeded'); + } else { + console.error('PostHog API Error:', error); + } +} +``` + +## Testing Authentication + +```javascript +const testResult = await posthog.testAuth(); +if (testResult.success) { + console.log('Authentication successful!'); +} else { + console.error('Authentication failed:', testResult.message); +} +``` + +## Self-Hosting Configuration + +If you're using self-hosted PostHog: +```javascript +const posthog = new Definition({ + personalApiKey: 'your-personal-key', + projectApiKey: 'your-project-key', + host: 'https://posthog.yourcompany.com' +}); +``` + +## Best Practices + +1. **Use distinct IDs consistently**: Ensure the same user has the same distinct ID across sessions +2. **Include standard properties**: PostHog can extract more insights with browser and device info +3. **Use feature flags**: Control feature rollouts without deploying code +4. **Set up actions**: Define key events as actions for easier analysis +5. **Use cohorts**: Segment users for targeted analysis and feature flags +6. **Enable session recording**: Understand user behavior with visual recordings + +## Rate Limits + +- **Event ingestion**: No hard limits, reasonable usage expected +- **API requests**: 600 requests per minute for Personal API Keys +- **Batch size**: Maximum 1000 events per batch +- **Event size**: Maximum 1MB per event + +## Autocapture + +PostHog supports autocapture when using their JavaScript library. This API module focuses on server-side tracking where you have full control over what events are sent. + +## Resources + +- [PostHog Documentation](https://posthog.com/docs) +- [API Reference](https://posthog.com/docs/api) +- [Event Tracking Guide](https://posthog.com/docs/getting-started/send-events) +- [Feature Flags Guide](https://posthog.com/docs/feature-flags) +- [Self-Hosting Guide](https://posthog.com/docs/self-host) \ No newline at end of file diff --git a/packages/posthog/api.js b/packages/posthog/api.js new file mode 100644 index 0000000..b40d978 --- /dev/null +++ b/packages/posthog/api.js @@ -0,0 +1,429 @@ +const { ApiClass } = require('@friggframework/core'); + +class PostHogApi extends ApiClass { + constructor(params) { + super(params); + this.baseUrl = params.host || 'https://app.posthog.com'; + this.projectApiKey = params.projectApiKey; + this.personalApiKey = params.personalApiKey; + } + + /** + * Get authorization headers based on request type + * @param {boolean} usePersonalKey - Whether to use personal API key + * @returns {Object} Headers with authorization + */ + _getAuthHeaders(usePersonalKey = true) { + const headers = { + 'Content-Type': 'application/json' + }; + + if (usePersonalKey && this.personalApiKey) { + headers['Authorization'] = `Bearer ${this.personalApiKey}`; + } + + return headers; + } + + /** + * Capture an event + * @param {Object} event - Event data + * @returns {Promise} Response + */ + async capture(event) { + const eventData = { + api_key: this.projectApiKey, + event: event.event, + properties: { + distinct_id: event.distinctId || event.distinct_id, + ...event.properties + }, + timestamp: event.timestamp || new Date().toISOString() + }; + + const url = `${this.baseUrl}/i/v0/e/`; + return this._post(url, eventData); + } + + /** + * Capture multiple events in batch + * @param {Array} events - Array of events + * @returns {Promise} Response + */ + async batch(events) { + const batch = { + api_key: this.projectApiKey, + batch: events.map(event => ({ + event: event.event, + properties: { + distinct_id: event.distinctId || event.distinct_id, + ...event.properties + }, + timestamp: event.timestamp || new Date().toISOString() + })) + }; + + const url = `${this.baseUrl}/batch/`; + return this._post(url, batch); + } + + /** + * Identify a person + * @param {Object} identify - Identify data + * @returns {Promise} Response + */ + async identify(identify) { + const identifyData = { + api_key: this.projectApiKey, + event: '$identify', + properties: { + distinct_id: identify.distinctId || identify.distinct_id, + $set: identify.properties || {}, + $set_once: identify.setOnce || {} + }, + timestamp: identify.timestamp || new Date().toISOString() + }; + + const url = `${this.baseUrl}/i/v0/e/`; + return this._post(url, identifyData); + } + + /** + * Alias person IDs + * @param {Object} alias - Alias data + * @returns {Promise} Response + */ + async alias(alias) { + const aliasData = { + api_key: this.projectApiKey, + event: '$create_alias', + properties: { + distinct_id: alias.distinctId || alias.distinct_id, + alias: alias.alias + }, + timestamp: alias.timestamp || new Date().toISOString() + }; + + const url = `${this.baseUrl}/i/v0/e/`; + return this._post(url, aliasData); + } + + /** + * Get person by distinct ID + * @param {string} distinctId - Person's distinct ID + * @returns {Promise} Person data + */ + async getPerson(distinctId) { + const url = `${this.baseUrl}/api/projects/@current/persons/${distinctId}/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get persons list with filters + * @param {Object} params - Query parameters + * @returns {Promise} Persons list + */ + async getPersons(params = {}) { + const url = `${this.baseUrl}/api/projects/@current/persons/`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Update person properties + * @param {string} distinctId - Person's distinct ID + * @param {Object} properties - Properties to update + * @returns {Promise} Updated person + */ + async updatePerson(distinctId, properties) { + const url = `${this.baseUrl}/api/projects/@current/persons/${distinctId}/`; + return this._patch(url, { properties }, { headers: this._getAuthHeaders() }); + } + + /** + * Delete person + * @param {string} distinctId - Person's distinct ID + * @returns {Promise} Deletion response + */ + async deletePerson(distinctId) { + const url = `${this.baseUrl}/api/projects/@current/persons/${distinctId}/`; + return this._delete(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get insights (saved queries) + * @param {Object} params - Query parameters + * @returns {Promise} List of insights + */ + async getInsights(params = {}) { + const url = `${this.baseUrl}/api/projects/@current/insights/`; + const queryString = new URLSearchParams(params).toString(); + const response = await this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get specific insight + * @param {number} insightId - Insight ID + * @returns {Promise} Insight data + */ + async getInsight(insightId) { + const url = `${this.baseUrl}/api/projects/@current/insights/${insightId}/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Create an insight + * @param {Object} insight - Insight configuration + * @returns {Promise} Created insight + */ + async createInsight(insight) { + const url = `${this.baseUrl}/api/projects/@current/insights/`; + return this._post(url, insight, { headers: this._getAuthHeaders() }); + } + + /** + * Get feature flags + * @returns {Promise} List of feature flags + */ + async getFeatureFlags() { + const url = `${this.baseUrl}/api/projects/@current/feature_flags/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get specific feature flag + * @param {number} flagId - Feature flag ID + * @returns {Promise} Feature flag data + */ + async getFeatureFlag(flagId) { + const url = `${this.baseUrl}/api/projects/@current/feature_flags/${flagId}/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Create feature flag + * @param {Object} flag - Feature flag configuration + * @returns {Promise} Created feature flag + */ + async createFeatureFlag(flag) { + const url = `${this.baseUrl}/api/projects/@current/feature_flags/`; + return this._post(url, flag, { headers: this._getAuthHeaders() }); + } + + /** + * Update feature flag + * @param {number} flagId - Feature flag ID + * @param {Object} updates - Updates to apply + * @returns {Promise} Updated feature flag + */ + async updateFeatureFlag(flagId, updates) { + const url = `${this.baseUrl}/api/projects/@current/feature_flags/${flagId}/`; + return this._patch(url, updates, { headers: this._getAuthHeaders() }); + } + + /** + * Evaluate feature flags for a person + * @param {string} distinctId - Person's distinct ID + * @param {Object} options - Evaluation options + * @returns {Promise} Feature flag values + */ + async evaluateFeatureFlags(distinctId, options = {}) { + const url = `${this.baseUrl}/decide/`; + const data = { + api_key: this.projectApiKey, + distinct_id: distinctId, + groups: options.groups || {}, + person_properties: options.personProperties || {}, + group_properties: options.groupProperties || {} + }; + + return this._post(url, data); + } + + /** + * Get cohorts + * @returns {Promise} List of cohorts + */ + async getCohorts() { + const url = `${this.baseUrl}/api/projects/@current/cohorts/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get specific cohort + * @param {number} cohortId - Cohort ID + * @returns {Promise} Cohort data + */ + async getCohort(cohortId) { + const url = `${this.baseUrl}/api/projects/@current/cohorts/${cohortId}/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Create cohort + * @param {Object} cohort - Cohort configuration + * @returns {Promise} Created cohort + */ + async createCohort(cohort) { + const url = `${this.baseUrl}/api/projects/@current/cohorts/`; + return this._post(url, cohort, { headers: this._getAuthHeaders() }); + } + + /** + * Get annotations + * @param {Object} params - Query parameters + * @returns {Promise} List of annotations + */ + async getAnnotations(params = {}) { + const url = `${this.baseUrl}/api/projects/@current/annotations/`; + const queryString = new URLSearchParams(params).toString(); + const response = await this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Create annotation + * @param {Object} annotation - Annotation data + * @returns {Promise} Created annotation + */ + async createAnnotation(annotation) { + const url = `${this.baseUrl}/api/projects/@current/annotations/`; + return this._post(url, annotation, { headers: this._getAuthHeaders() }); + } + + /** + * Get dashboards + * @returns {Promise} List of dashboards + */ + async getDashboards() { + const url = `${this.baseUrl}/api/projects/@current/dashboards/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get specific dashboard + * @param {number} dashboardId - Dashboard ID + * @returns {Promise} Dashboard data + */ + async getDashboard(dashboardId) { + const url = `${this.baseUrl}/api/projects/@current/dashboards/${dashboardId}/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get session recordings + * @param {Object} params - Query parameters + * @returns {Promise} Session recordings + */ + async getSessionRecordings(params = {}) { + const url = `${this.baseUrl}/api/projects/@current/session_recordings/`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get specific session recording + * @param {string} sessionId - Session ID + * @returns {Promise} Session recording data + */ + async getSessionRecording(sessionId) { + const url = `${this.baseUrl}/api/projects/@current/session_recordings/${sessionId}/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Get events + * @param {Object} params - Query parameters + * @returns {Promise} Events list + */ + async getEvents(params = {}) { + const url = `${this.baseUrl}/api/projects/@current/events/`; + const queryString = new URLSearchParams(params).toString(); + return this._get(`${url}?${queryString}`, { headers: this._getAuthHeaders() }); + } + + /** + * Get event definitions + * @returns {Promise} Event definitions + */ + async getEventDefinitions() { + const url = `${this.baseUrl}/api/projects/@current/event_definitions/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get property definitions + * @returns {Promise} Property definitions + */ + async getPropertyDefinitions() { + const url = `${this.baseUrl}/api/projects/@current/property_definitions/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Get actions + * @returns {Promise} List of actions + */ + async getActions() { + const url = `${this.baseUrl}/api/projects/@current/actions/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Create action + * @param {Object} action - Action configuration + * @returns {Promise} Created action + */ + async createAction(action) { + const url = `${this.baseUrl}/api/projects/@current/actions/`; + return this._post(url, action, { headers: this._getAuthHeaders() }); + } + + /** + * Get experiments + * @returns {Promise} List of experiments + */ + async getExperiments() { + const url = `${this.baseUrl}/api/projects/@current/experiments/`; + const response = await this._get(url, { headers: this._getAuthHeaders() }); + return response.results || []; + } + + /** + * Create experiment + * @param {Object} experiment - Experiment configuration + * @returns {Promise} Created experiment + */ + async createExperiment(experiment) { + const url = `${this.baseUrl}/api/projects/@current/experiments/`; + return this._post(url, experiment, { headers: this._getAuthHeaders() }); + } + + /** + * Get project details + * @returns {Promise} Project details + */ + async getProject() { + const url = `${this.baseUrl}/api/projects/@current/`; + return this._get(url, { headers: this._getAuthHeaders() }); + } + + /** + * Update project settings + * @param {Object} settings - Project settings to update + * @returns {Promise} Updated project + */ + async updateProject(settings) { + const url = `${this.baseUrl}/api/projects/@current/`; + return this._patch(url, settings, { headers: this._getAuthHeaders() }); + } +} + +module.exports = PostHogApi; \ No newline at end of file diff --git a/packages/posthog/defaultConfig.json b/packages/posthog/defaultConfig.json new file mode 100644 index 0000000..380720a --- /dev/null +++ b/packages/posthog/defaultConfig.json @@ -0,0 +1,129 @@ +{ + "name": "PostHog", + "version": "1.0.0", + "category": "Analytics", + "type": "posthog", + "description": "Open source product analytics platform with feature flags and session recording", + "documentation": "https://posthog.com/docs", + "apiDocs": "https://posthog.com/docs/api", + "authentication": { + "types": [ + { + "type": "personalApiKey", + "name": "Personal API Key", + "description": "Full API access with customizable scopes", + "fields": ["personalApiKey"] + }, + { + "type": "projectApiKey", + "name": "Project API Key", + "description": "Write-only key for event tracking", + "fields": ["projectApiKey"] + } + ] + }, + "endpoints": { + "cloud": "https://app.posthog.com", + "euCloud": "https://eu.posthog.com", + "selfHosted": "Variable based on deployment" + }, + "features": [ + "Event tracking", + "User identification", + "Feature flags", + "Session recordings", + "Insights and analytics", + "Cohort analysis", + "A/B testing experiments", + "Actions and annotations", + "Dashboards", + "Data export", + "Autocapture (JS SDK)", + "Heatmaps", + "Paths analysis", + "Retention analysis", + "Funnel analysis", + "Group analytics", + "SQL insights" + ], + "limitations": [ + "1MB maximum event size", + "1000 events per batch", + "90 days data retention on free plan", + "Session recording limits based on plan" + ], + "pricing": "Free tier with 1M events/month, paid plans for higher volume", + "rateLimits": { + "personalApiKey": "600 requests/minute", + "eventIngestion": "No hard limits", + "batchSize": "1000 events", + "eventSize": "1MB" + }, + "supportedRegions": [ + "US", + "EU", + "Self-hosted anywhere" + ], + "dataRetention": { + "free": "90 days", + "paid": "1 year standard, custom available", + "selfHosted": "Unlimited" + }, + "webhooks": true, + "integrations": [ + "Slack", + "Microsoft Teams", + "Discord", + "Zapier", + "Segment", + "Sentry", + "Datadog", + "Customer.io", + "Intercom", + "HubSpot", + "Salesforce" + ], + "sdks": [ + "JavaScript", + "React", + "React Native", + "Node.js", + "Python", + "Ruby", + "PHP", + "Go", + "Java", + "iOS", + "Android", + "Flutter", + "Elixir" + ], + "openSource": { + "repository": "https://github.com/PostHog/posthog", + "license": "MIT", + "selfHostingOptions": [ + "Docker", + "Kubernetes", + "AWS", + "Google Cloud", + "Azure", + "DigitalOcean" + ] + }, + "compliance": [ + "GDPR", + "CCPA", + "SOC 2 Type II", + "HIPAA (self-hosted)", + "Privacy Shield" + ], + "standardEvents": [ + "$pageview", + "$pageleave", + "$autocapture", + "$identify", + "$create_alias", + "$feature_flag_called", + "$groupidentify" + ] +} \ No newline at end of file diff --git a/packages/posthog/definition.js b/packages/posthog/definition.js new file mode 100644 index 0000000..b76c9d3 --- /dev/null +++ b/packages/posthog/definition.js @@ -0,0 +1,284 @@ +const { Integration } = require('@friggframework/module-plugin'); +const ApiClass = require('./api'); + +class PostHogIntegration extends Integration { + static name = 'PostHog'; + static category = 'Analytics'; + static catalogDescription = 'Open source product analytics platform with feature flags and session recording'; + static version = '1.0.0'; + static referenceUrl = 'https://posthog.com'; + static apiDocs = 'https://posthog.com/docs/api'; + + /** + * Constructor for PostHogIntegration + * @param {Object} params - Should include personalApiKey and/or projectApiKey + */ + constructor(params) { + super(params); + this.api = new ApiClass(params); + } + + /** + * Capture an event + * @param {Object} event - Event data + * @returns {Promise} Response + */ + async capture(event) { + return this.api.capture(event); + } + + /** + * Capture multiple events in batch + * @param {Array} events - Array of events + * @returns {Promise} Response + */ + async batch(events) { + return this.api.batch(events); + } + + /** + * Identify a person + * @param {Object} identify - Identify data + * @returns {Promise} Response + */ + async identify(identify) { + return this.api.identify(identify); + } + + /** + * Alias person IDs + * @param {Object} alias - Alias data + * @returns {Promise} Response + */ + async alias(alias) { + return this.api.alias(alias); + } + + /** + * Get person by distinct ID + * @param {string} distinctId - Person's distinct ID + * @returns {Promise} Person data + */ + async getPerson(distinctId) { + return this.api.getPerson(distinctId); + } + + /** + * Get persons list with filters + * @param {Object} params - Query parameters + * @returns {Promise} Persons list + */ + async getPersons(params = {}) { + return this.api.getPersons(params); + } + + /** + * Update person properties + * @param {string} distinctId - Person's distinct ID + * @param {Object} properties - Properties to update + * @returns {Promise} Updated person + */ + async updatePerson(distinctId, properties) { + return this.api.updatePerson(distinctId, properties); + } + + /** + * Delete person + * @param {string} distinctId - Person's distinct ID + * @returns {Promise} Deletion response + */ + async deletePerson(distinctId) { + return this.api.deletePerson(distinctId); + } + + /** + * Get insights (saved queries) + * @param {Object} params - Query parameters + * @returns {Promise} List of insights + */ + async getInsights(params = {}) { + return this.api.getInsights(params); + } + + /** + * Get specific insight + * @param {number} insightId - Insight ID + * @returns {Promise} Insight data + */ + async getInsight(insightId) { + return this.api.getInsight(insightId); + } + + /** + * Create an insight + * @param {Object} insight - Insight configuration + * @returns {Promise} Created insight + */ + async createInsight(insight) { + return this.api.createInsight(insight); + } + + /** + * Get feature flags + * @returns {Promise} List of feature flags + */ + async getFeatureFlags() { + return this.api.getFeatureFlags(); + } + + /** + * Get specific feature flag + * @param {number} flagId - Feature flag ID + * @returns {Promise} Feature flag data + */ + async getFeatureFlag(flagId) { + return this.api.getFeatureFlag(flagId); + } + + /** + * Create feature flag + * @param {Object} flag - Feature flag configuration + * @returns {Promise} Created feature flag + */ + async createFeatureFlag(flag) { + return this.api.createFeatureFlag(flag); + } + + /** + * Evaluate feature flags for a person + * @param {string} distinctId - Person's distinct ID + * @param {Object} options - Evaluation options + * @returns {Promise} Feature flag values + */ + async evaluateFeatureFlags(distinctId, options = {}) { + return this.api.evaluateFeatureFlags(distinctId, options); + } + + /** + * Get cohorts + * @returns {Promise} List of cohorts + */ + async getCohorts() { + return this.api.getCohorts(); + } + + /** + * Get specific cohort + * @param {number} cohortId - Cohort ID + * @returns {Promise} Cohort data + */ + async getCohort(cohortId) { + return this.api.getCohort(cohortId); + } + + /** + * Create cohort + * @param {Object} cohort - Cohort configuration + * @returns {Promise} Created cohort + */ + async createCohort(cohort) { + return this.api.createCohort(cohort); + } + + /** + * Get annotations + * @param {Object} params - Query parameters + * @returns {Promise} List of annotations + */ + async getAnnotations(params = {}) { + return this.api.getAnnotations(params); + } + + /** + * Create annotation + * @param {Object} annotation - Annotation data + * @returns {Promise} Created annotation + */ + async createAnnotation(annotation) { + return this.api.createAnnotation(annotation); + } + + /** + * Get dashboards + * @returns {Promise} List of dashboards + */ + async getDashboards() { + return this.api.getDashboards(); + } + + /** + * Get specific dashboard + * @param {number} dashboardId - Dashboard ID + * @returns {Promise} Dashboard data + */ + async getDashboard(dashboardId) { + return this.api.getDashboard(dashboardId); + } + + /** + * Get session recordings + * @param {Object} params - Query parameters + * @returns {Promise} Session recordings + */ + async getSessionRecordings(params = {}) { + return this.api.getSessionRecordings(params); + } + + /** + * Get specific session recording + * @param {string} sessionId - Session ID + * @returns {Promise} Session recording data + */ + async getSessionRecording(sessionId) { + return this.api.getSessionRecording(sessionId); + } + + /** + * Get events + * @param {Object} params - Query parameters + * @returns {Promise} Events list + */ + async getEvents(params = {}) { + return this.api.getEvents(params); + } + + /** + * Get event definitions + * @returns {Promise} Event definitions + */ + async getEventDefinitions() { + return this.api.getEventDefinitions(); + } + + /** + * Get property definitions + * @returns {Promise} Property definitions + */ + async getPropertyDefinitions() { + return this.api.getPropertyDefinitions(); + } + + /** + * Test authentication + * @returns {Promise} Test result + */ + async testAuth() { + try { + // Try to get insights as a simple auth test + await this.getInsights({ limit: 1 }); + + return { + success: true, + message: 'Authentication successful' + }; + } catch (error) { + return { + success: false, + message: `Authentication failed: ${error.message}`, + error: error + }; + } + } +} + +module.exports = PostHogIntegration; \ No newline at end of file diff --git a/packages/posthog/index.js b/packages/posthog/index.js new file mode 100644 index 0000000..2f5c456 --- /dev/null +++ b/packages/posthog/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/pusher/README.md b/packages/pusher/README.md new file mode 100644 index 0000000..d2cb71c --- /dev/null +++ b/packages/pusher/README.md @@ -0,0 +1,157 @@ +# Pusher API Module + +A comprehensive Pusher API module for the Frigg framework, providing real-time channels, presence features, and event broadcasting capabilities. + +## Features + +- **Real-time Events**: Trigger events on channels with instant delivery +- **Channel Management**: Public, private, and presence channels +- **Batch Operations**: Send multiple events efficiently +- **Authentication**: Channel authorization for private and presence channels +- **Presence**: Track users joining/leaving presence channels +- **Webhooks**: Handle channel lifecycle events +- **Statistics**: Monitor channel usage and connection stats +- **Security**: HMAC signature verification for webhooks + +## Installation + +```bash +npm install @friggframework/api-module-pusher +``` + +## Environment Variables + +```env +PUSHER_APP_ID=your_app_id +PUSHER_KEY=your_app_key +PUSHER_SECRET=your_app_secret +PUSHER_CLUSTER=us2 +PUSHER_USE_TLS=true +``` + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-pusher'); + +const pusherApi = new Api({ + app_id: process.env.PUSHER_APP_ID, + key: process.env.PUSHER_KEY, + secret: process.env.PUSHER_SECRET, + cluster: process.env.PUSHER_CLUSTER +}); + +// Trigger an event on a channel +await pusherApi.triggerEvent('my-channel', 'my-event', { + message: 'Hello World!' +}); + +// Trigger event on multiple channels +await pusherApi.triggerEventToMultipleChannels( + ['channel-1', 'channel-2'], + 'notification', + { alert: 'New update available!' } +); + +// Get channel information +const channelInfo = await pusherApi.getChannelInfo('presence-chat', { + info: 'user_count,subscription_count' +}); + +// Batch trigger multiple events +await pusherApi.triggerMultipleEvents([ + { + channel: 'channel-1', + event: 'update', + data: { status: 'online' } + }, + { + channel: 'channel-2', + event: 'alert', + data: { message: 'System maintenance' } + } +]); +``` + +## Key Methods + +### Event Broadcasting +- `triggerEvent(channel, event, data, options)` - Trigger single event +- `triggerEventToMultipleChannels(channels, event, data)` - Broadcast to multiple channels +- `triggerMultipleEvents(events)` - Batch trigger events +- `sendBatchNotifications(notifications)` - Send multiple notifications + +### Channel Management +- `getChannels(options)` - Get all channels +- `getChannelInfo(channel, options)` - Get channel details +- `getChannelUsers(channel)` - Get users in presence channel +- `isPrivateChannel(channel)` - Check if channel is private +- `isPresenceChannel(channel)` - Check if channel is presence +- `isValidChannelName(channel)` - Validate channel name + +### Authentication +- `generateChannelAuth(channel, socketId, customData)` - Generate channel auth +- `generatePresenceChannelAuth(channel, socketId, userData)` - Generate presence auth +- `generateUserAuth(socketId, userData)` - Generate user authentication + +### Webhooks +- `validateWebhook(body, signature)` - Validate webhook signature +- `handleWebhook(body, headers)` - Process webhook events + +### Statistics +- `getApplicationStats()` - Get app statistics +- `testConnection()` - Test API connection + +### Presence +- `notifyUserAdded(channel, userId, userInfo)` - Notify user joined +- `notifyUserRemoved(channel, userId)` - Notify user left + +## Channel Types + +### Public Channels +- No authentication required +- Anyone can subscribe +- Events visible to all subscribers + +### Private Channels +- Require authentication +- Channel names start with `private-` +- Server must authorize subscriptions + +### Presence Channels +- Include user presence information +- Channel names start with `presence-` +- Track who's online in real-time +- Provide member lists and user info + +## Authentication Flow + +For private and presence channels, implement server-side authentication: + +```javascript +// In your authentication endpoint +app.post('/pusher/auth', (req, res) => { + const socketId = req.body.socket_id; + const channel = req.body.channel_name; + + // Verify user can access this channel + if (userCanAccessChannel(user, channel)) { + const auth = pusherApi.generateChannelAuth(channel, socketId); + res.send(auth); + } else { + res.status(403).send('Forbidden'); + } +}); +``` + +## Webhook Events + +Pusher can send webhooks for: +- Channel occupied (first subscriber) +- Channel vacated (last subscriber left) +- Member added (presence channels) +- Member removed (presence channels) + +## Error Handling + +All methods include proper error handling and will throw descriptive errors for authentication issues, invalid channels, or API limits. \ No newline at end of file diff --git a/packages/pusher/api.js b/packages/pusher/api.js new file mode 100644 index 0000000..c6361e3 --- /dev/null +++ b/packages/pusher/api.js @@ -0,0 +1,311 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); +const crypto = require('crypto'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + this.app_id = get(params, 'app_id', null); + this.key = get(params, 'key', null); + this.secret = get(params, 'secret', null); + this.cluster = get(params, 'cluster', 'us2'); + this.useTLS = get(params, 'useTLS', true); + + const protocol = this.useTLS ? 'https' : 'http'; + this.baseUrl = `${protocol}://api-${this.cluster}.pusherapp.com/apps/${this.app_id}`; + + this.URLs = { + // Events + events: '/events', + batchEvents: '/batch_events', + + // Channels + channels: '/channels', + channelInfo: (channel) => `/channels/${encodeURIComponent(channel)}`, + channelUsers: (channel) => `/channels/${encodeURIComponent(channel)}/users`, + + // Authentication + userAuth: '/user-auth', + + // Webhooks + webhooks: '/webhooks', + }; + } + + // Generate authentication signature for Pusher API + generateAuthSignature(method, path, query = '', body = '') { + const timestamp = Math.floor(Date.now() / 1000); + const bodyMd5 = crypto.createHash('md5').update(body).digest('hex'); + + const queryString = new URLSearchParams({ + auth_key: this.key, + auth_timestamp: timestamp, + auth_version: '1.0', + body_md5: bodyMd5, + ...query + }).toString(); + + const stringToSign = [method, path, queryString].join('\n'); + const signature = crypto.createHmac('sha256', this.secret).update(stringToSign).digest('hex'); + + return { + auth_key: this.key, + auth_timestamp: timestamp, + auth_version: '1.0', + auth_signature: signature, + body_md5: bodyMd5 + }; + } + + async _request(url, options = {}) { + const method = options.method || 'GET'; + const path = url.replace(this.baseUrl, ''); + const body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body || {}); + const query = options.query || {}; + + const authParams = this.generateAuthSignature(method.toUpperCase(), path, query, body); + + // Merge auth params with existing query params + const finalQuery = { ...query, ...authParams }; + + return super._request(url, { + ...options, + query: finalQuery, + body: body !== '{}' ? body : undefined, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } + + // ************************** Event Methods ********************************** + + async triggerEvent(channel, event, data, params = {}) { + const eventData = { + name: event, + channel: channel, + data: typeof data === 'string' ? data : JSON.stringify(data), + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.events, + method: 'POST', + body: eventData + }; + return this._request(options.url, options); + } + + async triggerMultipleEvents(events, params = {}) { + const eventsData = { + batch: events.map(event => ({ + name: event.event, + channel: event.channel, + data: typeof event.data === 'string' ? event.data : JSON.stringify(event.data), + socket_id: event.socket_id + })), + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.batchEvents, + method: 'POST', + body: eventsData + }; + return this._request(options.url, options); + } + + async triggerEventToMultipleChannels(channels, event, data, params = {}) { + const eventData = { + name: event, + channels: channels, + data: typeof data === 'string' ? data : JSON.stringify(data), + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.events, + method: 'POST', + body: eventData + }; + return this._request(options.url, options); + } + + // ************************** Channel Methods ********************************** + + async getChannels(params = {}) { + const options = { + url: this.baseUrl + this.URLs.channels, + query: params + }; + return this._request(options.url, options); + } + + async getChannelInfo(channel, params = {}) { + const options = { + url: this.baseUrl + this.URLs.channelInfo(channel), + query: params + }; + return this._request(options.url, options); + } + + async getChannelUsers(channel) { + const options = { + url: this.baseUrl + this.URLs.channelUsers(channel), + }; + return this._request(options.url, options); + } + + // ************************** Authentication Methods ********************************** + + generateChannelAuth(channel, socketId, customData = null) { + const stringToSign = `${socketId}:${channel}`; + let authString = stringToSign; + + if (customData) { + const customDataString = JSON.stringify(customData); + authString += `:${customDataString}`; + } + + const signature = crypto.createHmac('sha256', this.secret).update(authString).digest('hex'); + const auth = `${this.key}:${signature}`; + + const response = { auth }; + if (customData) { + response.channel_data = JSON.stringify(customData); + } + + return response; + } + + generatePresenceChannelAuth(channel, socketId, userData) { + return this.generateChannelAuth(channel, socketId, userData); + } + + generateUserAuth(socketId, userData) { + const userDataString = JSON.stringify(userData); + const stringToSign = `${socketId}::user::${userDataString}`; + const signature = crypto.createHmac('sha256', this.secret).update(stringToSign).digest('hex'); + + return { + auth: `${this.key}:${signature}`, + user_data: userDataString + }; + } + + // ************************** Webhook Methods ********************************** + + async validateWebhook(body, signature) { + const expectedSignature = crypto.createHmac('sha256', this.secret) + .update(body) + .digest('hex'); + + return signature === expectedSignature; + } + + async handleWebhook(body, headers) { + const signature = headers['x-pusher-signature']; + const keyHeader = headers['x-pusher-key']; + + if (keyHeader !== this.key) { + throw new Error('Invalid webhook key'); + } + + const bodyString = typeof body === 'string' ? body : JSON.stringify(body); + const isValid = await this.validateWebhook(bodyString, signature); + + if (!isValid) { + throw new Error('Invalid webhook signature'); + } + + const webhookData = typeof body === 'string' ? JSON.parse(body) : body; + + return { + type: 'webhook', + data: { + time_ms: webhookData.time_ms, + events: webhookData.events + } + }; + } + + // ************************** Presence Methods ********************************** + + async notifyUserAdded(channel, userId, userInfo = {}) { + return this.triggerEvent(channel, 'pusher:member_added', { + user_id: userId, + user_info: userInfo + }); + } + + async notifyUserRemoved(channel, userId) { + return this.triggerEvent(channel, 'pusher:member_removed', { + user_id: userId + }); + } + + // ************************** Statistics Methods ********************************** + + async getApplicationStats() { + try { + const channels = await this.getChannels(); + return { + channel_count: channels.channels ? Object.keys(channels.channels).length : 0, + channels: channels.channels || {} + }; + } catch (error) { + throw new Error(`Failed to get application stats: ${error.message}`); + } + } + + // ************************** Connection Testing ********************************** + + async testConnection() { + try { + const result = await this.getChannels(); + return { + success: true, + message: 'Connection successful', + data: result + }; + } catch (error) { + return { + success: false, + message: `Connection failed: ${error.message}`, + error: error + }; + } + } + + // ************************** Utility Methods ********************************** + + isPrivateChannel(channel) { + return channel.startsWith('private-'); + } + + isPresenceChannel(channel) { + return channel.startsWith('presence-'); + } + + isValidChannelName(channel) { + // Channel name must be 1-200 characters, letters, numbers, hyphens, underscores, equals, dots + const channelRegex = /^[a-zA-Z0-9_\-=.]+$/; + return channel.length >= 1 && channel.length <= 200 && channelRegex.test(channel); + } + + // ************************** Batch Operations ********************************** + + async sendBatchNotifications(notifications) { + const events = notifications.map(notification => ({ + channel: notification.channel, + event: notification.event || 'notification', + data: notification.data, + socket_id: notification.socket_id + })); + + return this.triggerMultipleEvents(events); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/pusher/defaultConfig.json b/packages/pusher/defaultConfig.json new file mode 100644 index 0000000..8a3a3e7 --- /dev/null +++ b/packages/pusher/defaultConfig.json @@ -0,0 +1,9 @@ +{ + "name": "pusher", + "label": "Pusher", + "productUrl": "https://pusher.com", + "apiDocs": "https://pusher.com/docs", + "logoUrl": "https://friggframework.org/assets/img/pusher-icon.png", + "categories": ["Communication", "Real-time", "Notifications"], + "description": "Pusher real-time channels for live notifications, messaging, and presence features." +} \ No newline at end of file diff --git a/packages/pusher/definition.js b/packages/pusher/definition.js new file mode 100644 index 0000000..c026298 --- /dev/null +++ b/packages/pusher/definition.js @@ -0,0 +1,70 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Pusher', + requiredAuthMethods: { + getToken: async function (api, params) { + // Pusher uses API key/secret authentication + return { + access_token: api.key, + token_type: 'Key' + }; + }, + + getEntityDetails: async function (api, userId) { + const stats = await api.getApplicationStats(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: api.app_id, + user: userId + }, + details: { + app_id: api.app_id, + cluster: api.cluster, + channel_count: stats.channel_count, + useTLS: api.useTLS + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['key', 'secret'], + entity: ['app_id', 'cluster'], + }, + + getCredentialDetails: async function (api, userId) { + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: api.app_id, + user: userId + }, + details: { + key: api.key, + secret: api.secret + }, + }; + }, + + testAuthRequest: function (api) { + return api.testConnection(); + }, + }, + env: { + app_id: process.env.PUSHER_APP_ID, + key: process.env.PUSHER_KEY, + secret: process.env.PUSHER_SECRET, + cluster: process.env.PUSHER_CLUSTER || 'us2', + useTLS: process.env.PUSHER_USE_TLS !== 'false', + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/pusher/index.js b/packages/pusher/index.js new file mode 100644 index 0000000..be08f56 --- /dev/null +++ b/packages/pusher/index.js @@ -0,0 +1,5 @@ +const Config = require('./defaultConfig.json'); +const { Definition } = require('./definition.js'); +const { Api } = require('./api.js'); + +module.exports = { Config, Definition, Api }; \ No newline at end of file diff --git a/packages/pusher/package.json b/packages/pusher/package.json new file mode 100644 index 0000000..da78336 --- /dev/null +++ b/packages/pusher/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-pusher", + "version": "1.0.0", + "description": "Pusher API module for the Frigg framework", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "frigg", + "pusher", + "realtime", + "channels", + "api" + ], + "author": "Frigg Framework", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/packages/v1-ready/deel/.eslintrc.json b/packages/qbo/.eslintrc.json similarity index 100% rename from packages/v1-ready/deel/.eslintrc.json rename to packages/qbo/.eslintrc.json diff --git a/packages/needs-updating/revio/CHANGELOG.md b/packages/qbo/CHANGELOG.md similarity index 100% rename from packages/needs-updating/revio/CHANGELOG.md rename to packages/qbo/CHANGELOG.md diff --git a/packages/needs-updating/terminus/LICENSE.md b/packages/qbo/LICENSE.md similarity index 100% rename from packages/needs-updating/terminus/LICENSE.md rename to packages/qbo/LICENSE.md diff --git a/packages/needs-updating/qbo/README.md b/packages/qbo/README.md similarity index 100% rename from packages/needs-updating/qbo/README.md rename to packages/qbo/README.md diff --git a/packages/needs-updating/qbo/api.js b/packages/qbo/api.js similarity index 100% rename from packages/needs-updating/qbo/api.js rename to packages/qbo/api.js diff --git a/packages/needs-updating/qbo/defaultConfig.json b/packages/qbo/defaultConfig.json similarity index 100% rename from packages/needs-updating/qbo/defaultConfig.json rename to packages/qbo/defaultConfig.json diff --git a/packages/qbo/definition.js b/packages/qbo/definition.js new file mode 100644 index 0000000..597663f --- /dev/null +++ b/packages/qbo/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Qbo', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id || userDetails.sub, user: userId}, + details: {name: userDetails.name || userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id || userDetails.sub, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.QBO_CLIENT_ID, + client_secret: process.env.QBO_CLIENT_SECRET, + scope: process.env.QBO_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/qbo`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/qbo/index.js b/packages/qbo/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/qbo/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/needs-updating/terminus/jest.config.js b/packages/qbo/jest.config.js similarity index 100% rename from packages/needs-updating/terminus/jest.config.js rename to packages/qbo/jest.config.js diff --git a/packages/needs-updating/pipedrive/manager.test.js b/packages/qbo/manager.test.js similarity index 100% rename from packages/needs-updating/pipedrive/manager.test.js rename to packages/qbo/manager.test.js diff --git a/packages/v1-ready/recharge/.env.example b/packages/recharge/.env.example similarity index 100% rename from packages/v1-ready/recharge/.env.example rename to packages/recharge/.env.example diff --git a/packages/v1-ready/recharge/.eslintrc.js b/packages/recharge/.eslintrc.js similarity index 100% rename from packages/v1-ready/recharge/.eslintrc.js rename to packages/recharge/.eslintrc.js diff --git a/packages/v1-ready/recharge/LICENSE.md b/packages/recharge/LICENSE.md similarity index 100% rename from packages/v1-ready/recharge/LICENSE.md rename to packages/recharge/LICENSE.md diff --git a/packages/v1-ready/recharge/README.md b/packages/recharge/README.md similarity index 100% rename from packages/v1-ready/recharge/README.md rename to packages/recharge/README.md diff --git a/packages/v1-ready/recharge/api.js b/packages/recharge/api.js similarity index 100% rename from packages/v1-ready/recharge/api.js rename to packages/recharge/api.js diff --git a/packages/v1-ready/recharge/api.ts b/packages/recharge/api.ts similarity index 100% rename from packages/v1-ready/recharge/api.ts rename to packages/recharge/api.ts diff --git a/packages/v1-ready/recharge/defaultConfig.json b/packages/recharge/defaultConfig.json similarity index 100% rename from packages/v1-ready/recharge/defaultConfig.json rename to packages/recharge/defaultConfig.json diff --git a/packages/v1-ready/recharge/definition.ts b/packages/recharge/definition.ts similarity index 100% rename from packages/v1-ready/recharge/definition.ts rename to packages/recharge/definition.ts diff --git a/packages/v1-ready/recharge/dist/api.d.ts b/packages/recharge/dist/api.d.ts similarity index 100% rename from packages/v1-ready/recharge/dist/api.d.ts rename to packages/recharge/dist/api.d.ts diff --git a/packages/v1-ready/recharge/dist/api.d.ts.map b/packages/recharge/dist/api.d.ts.map similarity index 100% rename from packages/v1-ready/recharge/dist/api.d.ts.map rename to packages/recharge/dist/api.d.ts.map diff --git a/packages/v1-ready/recharge/dist/api.js b/packages/recharge/dist/api.js similarity index 100% rename from packages/v1-ready/recharge/dist/api.js rename to packages/recharge/dist/api.js diff --git a/packages/v1-ready/recharge/dist/api.js.map b/packages/recharge/dist/api.js.map similarity index 100% rename from packages/v1-ready/recharge/dist/api.js.map rename to packages/recharge/dist/api.js.map diff --git a/packages/v1-ready/recharge/dist/defaultConfig.json b/packages/recharge/dist/defaultConfig.json similarity index 100% rename from packages/v1-ready/recharge/dist/defaultConfig.json rename to packages/recharge/dist/defaultConfig.json diff --git a/packages/v1-ready/recharge/dist/definition.d.ts b/packages/recharge/dist/definition.d.ts similarity index 100% rename from packages/v1-ready/recharge/dist/definition.d.ts rename to packages/recharge/dist/definition.d.ts diff --git a/packages/v1-ready/recharge/dist/definition.d.ts.map b/packages/recharge/dist/definition.d.ts.map similarity index 100% rename from packages/v1-ready/recharge/dist/definition.d.ts.map rename to packages/recharge/dist/definition.d.ts.map diff --git a/packages/v1-ready/recharge/dist/definition.js b/packages/recharge/dist/definition.js similarity index 100% rename from packages/v1-ready/recharge/dist/definition.js rename to packages/recharge/dist/definition.js diff --git a/packages/v1-ready/recharge/dist/definition.js.map b/packages/recharge/dist/definition.js.map similarity index 100% rename from packages/v1-ready/recharge/dist/definition.js.map rename to packages/recharge/dist/definition.js.map diff --git a/packages/v1-ready/recharge/dist/index.d.ts b/packages/recharge/dist/index.d.ts similarity index 100% rename from packages/v1-ready/recharge/dist/index.d.ts rename to packages/recharge/dist/index.d.ts diff --git a/packages/v1-ready/recharge/dist/index.d.ts.map b/packages/recharge/dist/index.d.ts.map similarity index 100% rename from packages/v1-ready/recharge/dist/index.d.ts.map rename to packages/recharge/dist/index.d.ts.map diff --git a/packages/v1-ready/recharge/dist/index.js b/packages/recharge/dist/index.js similarity index 100% rename from packages/v1-ready/recharge/dist/index.js rename to packages/recharge/dist/index.js diff --git a/packages/v1-ready/recharge/dist/index.js.map b/packages/recharge/dist/index.js.map similarity index 100% rename from packages/v1-ready/recharge/dist/index.js.map rename to packages/recharge/dist/index.js.map diff --git a/packages/v1-ready/recharge/dist/jest-setup.d.ts b/packages/recharge/dist/jest-setup.d.ts similarity index 100% rename from packages/v1-ready/recharge/dist/jest-setup.d.ts rename to packages/recharge/dist/jest-setup.d.ts diff --git a/packages/v1-ready/recharge/dist/jest-setup.d.ts.map b/packages/recharge/dist/jest-setup.d.ts.map similarity index 100% rename from packages/v1-ready/recharge/dist/jest-setup.d.ts.map rename to packages/recharge/dist/jest-setup.d.ts.map diff --git a/packages/v1-ready/recharge/dist/jest-setup.js.map b/packages/recharge/dist/jest-setup.js.map similarity index 100% rename from packages/v1-ready/recharge/dist/jest-setup.js.map rename to packages/recharge/dist/jest-setup.js.map diff --git a/packages/v1-ready/recharge/dist/jest-teardown.d.ts b/packages/recharge/dist/jest-teardown.d.ts similarity index 100% rename from packages/v1-ready/recharge/dist/jest-teardown.d.ts rename to packages/recharge/dist/jest-teardown.d.ts diff --git a/packages/v1-ready/recharge/dist/jest-teardown.d.ts.map b/packages/recharge/dist/jest-teardown.d.ts.map similarity index 100% rename from packages/v1-ready/recharge/dist/jest-teardown.d.ts.map rename to packages/recharge/dist/jest-teardown.d.ts.map diff --git a/packages/v1-ready/recharge/dist/jest-teardown.js.map b/packages/recharge/dist/jest-teardown.js.map similarity index 100% rename from packages/v1-ready/recharge/dist/jest-teardown.js.map rename to packages/recharge/dist/jest-teardown.js.map diff --git a/packages/v1-ready/recharge/dist/jest.config.d.ts b/packages/recharge/dist/jest.config.d.ts similarity index 100% rename from packages/v1-ready/recharge/dist/jest.config.d.ts rename to packages/recharge/dist/jest.config.d.ts diff --git a/packages/v1-ready/recharge/dist/jest.config.d.ts.map b/packages/recharge/dist/jest.config.d.ts.map similarity index 100% rename from packages/v1-ready/recharge/dist/jest.config.d.ts.map rename to packages/recharge/dist/jest.config.d.ts.map diff --git a/packages/v1-ready/recharge/dist/jest.config.js b/packages/recharge/dist/jest.config.js similarity index 100% rename from packages/v1-ready/recharge/dist/jest.config.js rename to packages/recharge/dist/jest.config.js diff --git a/packages/v1-ready/recharge/dist/jest.config.js.map b/packages/recharge/dist/jest.config.js.map similarity index 100% rename from packages/v1-ready/recharge/dist/jest.config.js.map rename to packages/recharge/dist/jest.config.js.map diff --git a/packages/v1-ready/recharge/frigg.d.ts b/packages/recharge/frigg.d.ts similarity index 100% rename from packages/v1-ready/recharge/frigg.d.ts rename to packages/recharge/frigg.d.ts diff --git a/packages/v1-ready/recharge/index.js b/packages/recharge/index.js similarity index 100% rename from packages/v1-ready/recharge/index.js rename to packages/recharge/index.js diff --git a/packages/v1-ready/recharge/index.ts b/packages/recharge/index.ts similarity index 100% rename from packages/v1-ready/recharge/index.ts rename to packages/recharge/index.ts diff --git a/packages/v1-ready/recharge/jest.config.js b/packages/recharge/jest.config.js similarity index 100% rename from packages/v1-ready/recharge/jest.config.js rename to packages/recharge/jest.config.js diff --git a/packages/v1-ready/recharge/package.json b/packages/recharge/package.json similarity index 100% rename from packages/v1-ready/recharge/package.json rename to packages/recharge/package.json diff --git a/packages/v1-ready/recharge/tests/README.md b/packages/recharge/tests/README.md similarity index 100% rename from packages/v1-ready/recharge/tests/README.md rename to packages/recharge/tests/README.md diff --git a/packages/v1-ready/recharge/tests/api.test.ts b/packages/recharge/tests/api.test.ts similarity index 100% rename from packages/v1-ready/recharge/tests/api.test.ts rename to packages/recharge/tests/api.test.ts diff --git a/packages/v1-ready/recharge/tests/fixtures/mockData.ts b/packages/recharge/tests/fixtures/mockData.ts similarity index 100% rename from packages/v1-ready/recharge/tests/fixtures/mockData.ts rename to packages/recharge/tests/fixtures/mockData.ts diff --git a/packages/v1-ready/recharge/tests/helpers/testUtils.ts b/packages/recharge/tests/helpers/testUtils.ts similarity index 100% rename from packages/v1-ready/recharge/tests/helpers/testUtils.ts rename to packages/recharge/tests/helpers/testUtils.ts diff --git a/packages/v1-ready/recharge/tests/integration.test.ts b/packages/recharge/tests/integration.test.ts similarity index 100% rename from packages/v1-ready/recharge/tests/integration.test.ts rename to packages/recharge/tests/integration.test.ts diff --git a/packages/v1-ready/recharge/tests/jest.config.js b/packages/recharge/tests/jest.config.js similarity index 100% rename from packages/v1-ready/recharge/tests/jest.config.js rename to packages/recharge/tests/jest.config.js diff --git a/packages/v1-ready/recharge/tests/package.json.example b/packages/recharge/tests/package.json.example similarity index 100% rename from packages/v1-ready/recharge/tests/package.json.example rename to packages/recharge/tests/package.json.example diff --git a/packages/v1-ready/recharge/tests/runTests.sh b/packages/recharge/tests/runTests.sh similarity index 100% rename from packages/v1-ready/recharge/tests/runTests.sh rename to packages/recharge/tests/runTests.sh diff --git a/packages/v1-ready/recharge/tests/setup.ts b/packages/recharge/tests/setup.ts similarity index 100% rename from packages/v1-ready/recharge/tests/setup.ts rename to packages/recharge/tests/setup.ts diff --git a/packages/v1-ready/recharge/tsconfig.build.json b/packages/recharge/tsconfig.build.json similarity index 100% rename from packages/v1-ready/recharge/tsconfig.build.json rename to packages/recharge/tsconfig.build.json diff --git a/packages/v1-ready/recharge/tsconfig.json b/packages/recharge/tsconfig.json similarity index 100% rename from packages/v1-ready/recharge/tsconfig.json rename to packages/recharge/tsconfig.json diff --git a/packages/reddit/api.js b/packages/reddit/api.js new file mode 100644 index 0000000..0631e69 --- /dev/null +++ b/packages/reddit/api.js @@ -0,0 +1,101 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// Reddit API +// https://www.reddit.com/dev/api/ +// Core resources: subreddits, posts, comments, users + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://oauth.reddit.com/api/v1'; + + this.URLs = { + me: '/me', + subreddits: '/subreddits', + submit: '/submit', + comment: '/comment', + userPosts: '/user/{username}/submitted', + }; + + this.authorizationUri = encodeURI( + `https://www.reddit.com/api/v1/authorize?client_id=${this.client_id}&response_type=code&state=${this.state}&redirect_uri=${this.redirect_uri}&duration=permanent&scope=${this.scope}` + ); + this.tokenUri = 'https://www.reddit.com/api/v1/access_token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + const auth = Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64'); + const options = { + url: this.tokenUri, + headers: { + 'Authorization': `Basic ${auth}`, + 'User-Agent': 'FriggFramework/1.0', + }, + form: { + grant_type: 'authorization_code', + code: code, + redirect_uri: this.redirect_uri, + }, + }; + + const response = await this._post(options); + await this.setTokens(response); + return response; + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + this.refresh_token = get(params, 'refresh_token'); + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addAuthHeaders(options) { + const authHeaders = { + 'Authorization': `Bearer ${this.access_token}`, + 'User-Agent': 'FriggFramework/1.0', + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + async getUserDetails() { + const options = { + url: this.baseUrl + this.URLs.me, + }; + return this._get(options); + } + + async getSubreddits(params = {}) { + const options = { + url: this.baseUrl + this.URLs.subreddits, + query: params, + }; + return this._get(options); + } + + async submitPost(body) { + const options = { + url: this.baseUrl + this.URLs.submit, + body: body, + }; + return this._post(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/reddit/defaultConfig.json b/packages/reddit/defaultConfig.json new file mode 100644 index 0000000..8b3ba03 --- /dev/null +++ b/packages/reddit/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "reddit", + "label": "Reddit", + "productUrl": "https://reddit.com", + "apiDocs": "https://www.reddit.com/dev/api/", + "logoUrl": "https://logoeps.com/wp-content/uploads/2013/03/reddit-vector-logo.png", + "categories": [ + "Social News", + "Community Platform", + "Content Aggregation", + "Discussion Forums" + ], + "description": "Reddit is a social news aggregation and discussion platform where users submit content and vote on submissions." +} \ No newline at end of file diff --git a/packages/reddit/definition.js b/packages/reddit/definition.js new file mode 100644 index 0000000..c67cc37 --- /dev/null +++ b/packages/reddit/definition.js @@ -0,0 +1,44 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Reddit', + requiredAuthMethods: { + getToken: async (api, params) => { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.id, user: userId }, + details: { name: userDetails.name }, + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token'], + entity: [], + }, + getCredentialDetails: async (api, userId) => { + const userDetails = await api.getUserDetails(); + return { + identifiers: { externalId: userDetails.id, user: userId }, + details: {}, + }; + }, + testAuthRequest: async (api) => api.getUserDetails(), + }, + env: { + client_id: process.env.REDDIT_CLIENT_ID, + client_secret: process.env.REDDIT_CLIENT_SECRET, + scope: process.env.REDDIT_SCOPE || 'identity read submit', + redirect_uri: `${process.env.REDIRECT_URI}/reddit`, + }, +}; + +module.exports = { Definition }; diff --git a/packages/reddit/index.js b/packages/reddit/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/reddit/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/replicate/README.md b/packages/replicate/README.md new file mode 100644 index 0000000..e7e490c --- /dev/null +++ b/packages/replicate/README.md @@ -0,0 +1,384 @@ +# Replicate API Module + +This module provides comprehensive access to Replicate's cloud-based machine learning model hosting platform, allowing you to run thousands of open-source models with a simple API. + +## Features + +- **Model Execution**: Run any public model on Replicate +- **Async Predictions**: Create and monitor long-running predictions +- **Streaming Support**: Stream output from compatible models +- **Deployment Management**: Use dedicated deployments for consistent performance +- **Webhook Integration**: Get notified when predictions complete +- **Progress Tracking**: Monitor prediction progress in real-time +- **High-Level Helpers**: Simplified methods for common model types + +## Authentication + +Replicate uses API tokens for authentication. You'll need to: + +1. Sign up at [replicate.com](https://replicate.com) +2. Get your API token from [replicate.com/account/api-tokens](https://replicate.com/account/api-tokens) +3. Set the following environment variable: + +```bash +REPLICATE_API_TOKEN=your_token_here +# or +REPLICATE_API_KEY=your_token_here +``` + +## Usage Examples + +### Basic Model Execution + +```javascript +const {Api} = require('./api'); + +const api = new Api({ + apiKey: process.env.REPLICATE_API_TOKEN +}); + +// Run a model and wait for results +const output = await api.run( + "stability-ai/stable-diffusion:db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf", + { + prompt: "a vision of paradise, unreal engine" + } +); + +// Run without waiting (get prediction object immediately) +const prediction = await api.run( + "meta/llama-2-70b-chat:02e509c789964a7ea8736978a43525956ef40397be9033abf9fd2badfe68c9e3", + { + prompt: "What is machine learning?", + max_new_tokens: 500 + }, + { wait: false } +); + +// Check status later +const result = await api.getPrediction(prediction.id); +``` + +### Text Generation + +```javascript +// Using high-level helper +const response = await api.generateText( + "Write a haiku about artificial intelligence", + { + temperature: 0.8, + maxTokens: 100, + model: "meta/llama-2-70b-chat:latest" // optional, defaults to Llama 2 + } +); + +// Using specific model +const output = await api.run( + "meta/llama-2-13b-chat:f4e2de70d66816a838a89eeeb621910adffb0dd0baba3976c96980970978018d", + { + prompt: "Explain quantum computing in simple terms", + system_prompt: "You are a helpful, respectful and honest assistant.", + max_new_tokens: 500, + temperature: 0.7, + top_p: 0.9, + repetition_penalty: 1.1 + } +); +``` + +### Image Generation + +```javascript +// Using high-level helper +const images = await api.generateImage( + "An astronaut riding a horse on Mars, photorealistic", + { + negativePrompt: "blurry, bad quality", + width: 1024, + height: 1024, + numOutputs: 2 + } +); + +// Using SDXL directly +const output = await api.run( + "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", + { + prompt: "A serene Japanese garden in autumn", + negative_prompt: "worst quality, low quality", + width: 1024, + height: 1024, + scheduler: "K_EULER", + num_inference_steps: 25, + guidance_scale: 7.5, + num_outputs: 1 + } +); +``` + +### Audio Transcription + +```javascript +// Using high-level helper +const transcription = await api.transcribeAudio( + "https://example.com/audio.mp3", + { + whisperModel: "large-v2", + transcription: "plain text" + } +); + +// Using Whisper directly +const output = await api.run( + "openai/whisper:4d50797290df275329f202e48c76360b3f22b08d28c196cbc54600319435f8d2", + { + audio: "https://example.com/speech.wav", + model: "base", + transcription: "srt", + translate: false, + language: "en" + } +); +``` + +### Progress Tracking + +```javascript +// Run with progress callback +const output = await api.runWithProgress( + "stability-ai/stable-diffusion:db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf", + { + prompt: "A futuristic city at night" + }, + (prediction) => { + console.log(`Status: ${prediction.status}`); + if (prediction.logs) { + console.log(`Logs: ${prediction.logs}`); + } + if (prediction.metrics?.predict_time) { + console.log(`Time: ${prediction.metrics.predict_time}s`); + } + } +); +``` + +### Streaming Output + +```javascript +// For models that support streaming +const prediction = await api.run( + "meta/llama-2-70b-chat:latest", + { + prompt: "Tell me a story", + max_new_tokens: 1000, + stream: true + }, + { wait: false } +); + +// Stream the output +for await (const token of api.streamPrediction(prediction.id)) { + process.stdout.write(token); +} +``` + +### Model Information + +```javascript +// Get model details +const model = await api.getModel("stability-ai", "stable-diffusion"); + +// List model versions +const versions = await api.listModelVersions("stability-ai", "stable-diffusion"); + +// Get specific version +const version = await api.getModelVersion( + "stability-ai", + "stable-diffusion", + "db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf" +); +``` + +### Collections + +```javascript +// Browse curated collections +const collections = await api.listCollections(); + +// Get models in a collection +const textToImage = await api.getCollection("text-to-image"); +console.log(textToImage.models); // Array of models in collection +``` + +### Deployments + +```javascript +// List your deployments +const deployments = await api.listDeployments(); + +// Get deployment details +const deployment = await api.getDeployment("my-username", "my-deployment"); + +// Run prediction on deployment +const output = await api.createDeploymentPredictionAndWait( + "my-username", + "my-deployment", + { + input: { + prompt: "Hello world" + } + } +); +``` + +### Webhooks + +```javascript +// Create prediction with webhook +const prediction = await api.createPrediction({ + version: "db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf", + input: { + prompt: "A beautiful landscape" + }, + webhook: "https://example.com/webhook", + webhook_events_filter: ["start", "completed"] +}); + +// Get webhook signing secret +const secret = await api.getWebhookSecret(); +``` + +### Batch Predictions + +```javascript +// Process multiple inputs +const prompts = [ + "A cat in space", + "A dog underwater", + "A bird in a library" +]; + +const predictions = await Promise.all( + prompts.map(prompt => + api.run( + "stability-ai/stable-diffusion:latest", + { prompt }, + { wait: false } + ) + ) +); + +// Wait for all to complete +const results = await Promise.all( + predictions.map(p => api.waitForPrediction(p.id)) +); +``` + +### Error Handling + +```javascript +try { + const output = await api.run("invalid/model", { input: "test" }); +} catch (error) { + if (error.status === 404) { + console.error("Model not found"); + } else if (error.message.includes("timeout")) { + console.error("Prediction timed out"); + } else { + console.error("Error:", error.message); + } +} + +// Cancel a long-running prediction +const prediction = await api.run("some/model", { input }, { wait: false }); +// ... later +await api.cancelPrediction(prediction.id); +``` + +## API Methods + +### Predictions +- `createPrediction(params)` - Create a new prediction +- `createPredictionAndWait(params, options)` - Create and wait for completion +- `getPrediction(predictionId)` - Get prediction details +- `cancelPrediction(predictionId)` - Cancel a running prediction +- `listPredictions(params)` - List your predictions +- `waitForPrediction(predictionId, options)` - Wait for prediction to complete + +### Models +- `getModel(owner, name)` - Get model information +- `listModelVersions(owner, name)` - List model versions +- `getModelVersion(owner, name, versionId)` - Get specific version +- `searchModels(query)` - Search for models (via collections) + +### Collections +- `listCollections()` - List model collections +- `getCollection(slug)` - Get collection details + +### Deployments +- `listDeployments()` - List your deployments +- `getDeployment(owner, name)` - Get deployment details +- `createDeploymentPrediction(owner, name, params)` - Run on deployment +- `createDeploymentPredictionAndWait(owner, name, params, options)` - Run and wait + +### Account & Hardware +- `getAccount()` - Get account information +- `listHardware()` - List available hardware +- `getWebhookSecret()` - Get webhook signing secret + +### High-Level Helpers +- `run(model, input, options)` - Run any model easily +- `runWithProgress(model, input, progressCallback, options)` - Run with progress +- `streamPrediction(predictionId, options)` - Stream prediction output +- `generateText(prompt, options)` - Generate text easily +- `generateImage(prompt, options)` - Generate images easily +- `transcribeAudio(audioUrl, options)` - Transcribe audio easily + +### Utilities +- `testAuth()` - Verify API token +- `formatModelId(owner, name, version)` - Format model identifier +- `parseModelId(modelId)` - Parse model identifier + +## Model Identifiers + +Models are identified in the format: +- `owner/name` - Uses latest version +- `owner/name:version` - Uses specific version +- `owner/name:sha256hash` - Uses exact version by hash + +Examples: +- `stability-ai/stable-diffusion` +- `meta/llama-2-70b-chat:latest` +- `openai/whisper:4d50797290df275329f202e48c76360b3f22b08d28c196cbc54600319435f8d2` + +## Options + +### Prediction Options +- `wait`: Whether to wait for completion (default: true) +- `webhook`: Webhook URL for notifications +- `webhook_events_filter`: Events to send (start, output, logs, completed) + +### Wait Options +- `maxWait`: Maximum time to wait in ms (default: 60000) +- `interval`: Polling interval in ms (default: 500) + +## Best Practices + +1. **Use Specific Versions**: Pin model versions for consistent results +2. **Handle Timeouts**: Set appropriate `maxWait` for long-running models +3. **Use Webhooks**: For production, use webhooks instead of polling +4. **Batch Wisely**: Run predictions in parallel but respect rate limits +5. **Monitor Costs**: Check prediction metrics and costs regularly + +## Error Handling + +Common errors: +- `401`: Invalid API token +- `404`: Model not found +- `422`: Invalid input parameters +- `429`: Rate limit exceeded +- `503`: Model is loading + +## Support + +For more information, visit [Replicate Documentation](https://replicate.com/docs). \ No newline at end of file diff --git a/packages/replicate/api.js b/packages/replicate/api.js new file mode 100644 index 0000000..710ef77 --- /dev/null +++ b/packages/replicate/api.js @@ -0,0 +1,429 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.replicate.com/v1'; + + this.URLs = { + // Predictions + predictions: '/predictions', + predictionById: (predictionId) => `/predictions/${predictionId}`, + predictionCancel: (predictionId) => `/predictions/${predictionId}/cancel`, + + // Models + models: '/models', + modelVersions: (owner, name) => `/models/${owner}/${name}/versions`, + modelVersion: (owner, name, versionId) => `/models/${owner}/${name}/versions/${versionId}`, + + // Collections + collections: '/collections', + collectionBySlug: (slug) => `/collections/${slug}`, + + // Deployments + deployments: '/deployments', + deploymentById: (owner, name) => `/deployments/${owner}/${name}`, + deploymentPredictions: (owner, name) => `/deployments/${owner}/${name}/predictions`, + + // Hardware + hardware: '/hardware', + + // Account + account: '/account', + + // Webhooks + webhookSecret: '/webhooks/default/secret', + }; + + this.predictionStatus = { + STARTING: 'starting', + PROCESSING: 'processing', + SUCCEEDED: 'succeeded', + FAILED: 'failed', + CANCELED: 'canceled', + }; + } + + async addAuthHeaders(options) { + options.headers = { + ...options.headers, + 'Authorization': `Token ${this.apiKey}`, + 'Content-Type': 'application/json', + }; + } + + async _get(options) { + await this.addAuthHeaders(options); + return super._get(options); + } + + async _post(options, stringify = true) { + await this.addAuthHeaders(options); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + await this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + await this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + await this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Predictions ********************************** + + async createPrediction(params) { + const options = { + url: this.baseUrl + this.URLs.predictions, + body: params, + }; + + return this._post(options); + } + + async createPredictionAndWait(params, options = {}) { + const prediction = await this.createPrediction(params); + return this.waitForPrediction(prediction.id, options); + } + + async getPrediction(predictionId) { + const options = { + url: this.baseUrl + this.URLs.predictionById(predictionId), + }; + + return this._get(options); + } + + async cancelPrediction(predictionId) { + const options = { + url: this.baseUrl + this.URLs.predictionCancel(predictionId), + }; + + return this._post(options); + } + + async listPredictions(params = {}) { + const options = { + url: this.baseUrl + this.URLs.predictions, + query: params, + }; + + return this._get(options); + } + + async waitForPrediction(predictionId, options = {}) { + const maxWait = options.maxWait || 60000; // 60 seconds default + const interval = options.interval || 500; // 500ms default + const startTime = Date.now(); + + while (Date.now() - startTime < maxWait) { + const prediction = await this.getPrediction(predictionId); + + if (prediction.status === this.predictionStatus.SUCCEEDED) { + return prediction; + } + + if (prediction.status === this.predictionStatus.FAILED || + prediction.status === this.predictionStatus.CANCELED) { + throw new Error(`Prediction ${prediction.status}: ${prediction.error || 'Unknown error'}`); + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } + + throw new Error(`Prediction timeout after ${maxWait}ms`); + } + + // ************************** Models ********************************** + + async getModel(owner, name) { + const versions = await this.listModelVersions(owner, name); + // The first version contains model metadata + return versions.results[0] || null; + } + + async listModelVersions(owner, name) { + const options = { + url: this.baseUrl + this.URLs.modelVersions(owner, name), + }; + + return this._get(options); + } + + async getModelVersion(owner, name, versionId) { + const options = { + url: this.baseUrl + this.URLs.modelVersion(owner, name, versionId), + }; + + return this._get(options); + } + + async searchModels(query) { + // Note: Replicate doesn't have a direct model search endpoint + // This uses collections as a workaround + const collections = await this.listCollections(); + const models = []; + + for (const collection of collections.results) { + if (collection.name.toLowerCase().includes(query.toLowerCase()) || + collection.description.toLowerCase().includes(query.toLowerCase())) { + const collectionModels = await this.getCollection(collection.slug); + models.push(...collectionModels.models); + } + } + + return models; + } + + // ************************** Collections ********************************** + + async listCollections() { + const options = { + url: this.baseUrl + this.URLs.collections, + }; + + return this._get(options); + } + + async getCollection(slug) { + const options = { + url: this.baseUrl + this.URLs.collectionBySlug(slug), + }; + + return this._get(options); + } + + // ************************** Deployments ********************************** + + async listDeployments() { + const options = { + url: this.baseUrl + this.URLs.deployments, + }; + + return this._get(options); + } + + async getDeployment(owner, name) { + const options = { + url: this.baseUrl + this.URLs.deploymentById(owner, name), + }; + + return this._get(options); + } + + async createDeploymentPrediction(owner, name, params) { + const options = { + url: this.baseUrl + this.URLs.deploymentPredictions(owner, name), + body: params, + }; + + return this._post(options); + } + + async createDeploymentPredictionAndWait(owner, name, params, options = {}) { + const prediction = await this.createDeploymentPrediction(owner, name, params); + return this.waitForPrediction(prediction.id, options); + } + + // ************************** Hardware ********************************** + + async listHardware() { + const options = { + url: this.baseUrl + this.URLs.hardware, + }; + + return this._get(options); + } + + // ************************** Account ********************************** + + async getAccount() { + const options = { + url: this.baseUrl + this.URLs.account, + }; + + return this._get(options); + } + + // ************************** Webhooks ********************************** + + async getWebhookSecret() { + const options = { + url: this.baseUrl + this.URLs.webhookSecret, + }; + + return this._get(options); + } + + // ************************** High-Level Helpers ********************************** + + async run(model, input, options = {}) { + // Parse model string (format: owner/name:version or owner/name) + const parts = model.split(':'); + const [owner, name] = parts[0].split('/'); + const version = parts[1] || 'latest'; + + let versionId = version; + + // If version is 'latest', get the latest version + if (version === 'latest') { + const versions = await this.listModelVersions(owner, name); + if (versions.results && versions.results.length > 0) { + versionId = versions.results[0].id; + } else { + throw new Error(`No versions found for model ${owner}/${name}`); + } + } + + // Create prediction + const predictionParams = { + version: versionId, + input: input, + }; + + if (options.webhook) { + predictionParams.webhook = options.webhook; + predictionParams.webhook_events_filter = options.webhook_events_filter || ['completed']; + } + + if (options.wait !== false) { + return this.createPredictionAndWait(predictionParams, options); + } else { + return this.createPrediction(predictionParams); + } + } + + async runWithProgress(model, input, progressCallback, options = {}) { + // Create initial prediction + const prediction = await this.run(model, input, { ...options, wait: false }); + + // Poll for updates + const maxWait = options.maxWait || 60000; + const interval = options.interval || 500; + const startTime = Date.now(); + + while (Date.now() - startTime < maxWait) { + const current = await this.getPrediction(prediction.id); + + if (progressCallback) { + progressCallback(current); + } + + if (current.status === this.predictionStatus.SUCCEEDED) { + return current; + } + + if (current.status === this.predictionStatus.FAILED || + current.status === this.predictionStatus.CANCELED) { + throw new Error(`Prediction ${current.status}: ${current.error || 'Unknown error'}`); + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } + + throw new Error(`Prediction timeout after ${maxWait}ms`); + } + + // ************************** Utility Methods ********************************** + + async testAuth() { + try { + await this.getAccount(); + return true; + } catch (error) { + if (error.status === 401) { + return false; + } + throw error; + } + } + + // Helper to format model identifier + formatModelId(owner, name, version = null) { + const base = `${owner}/${name}`; + return version ? `${base}:${version}` : base; + } + + // Helper to parse model identifier + parseModelId(modelId) { + const parts = modelId.split(':'); + const [owner, name] = parts[0].split('/'); + const version = parts[1] || null; + + return { owner, name, version }; + } + + // Helper to stream output from models that support it + async* streamPrediction(predictionId, options = {}) { + const interval = options.interval || 100; + let lastOutputLength = 0; + + while (true) { + const prediction = await this.getPrediction(predictionId); + + // Stream new output + if (prediction.output && Array.isArray(prediction.output)) { + const newOutput = prediction.output.slice(lastOutputLength); + for (const item of newOutput) { + yield item; + } + lastOutputLength = prediction.output.length; + } + + // Check if completed + if (prediction.status === this.predictionStatus.SUCCEEDED) { + break; + } + + if (prediction.status === this.predictionStatus.FAILED || + prediction.status === this.predictionStatus.CANCELED) { + throw new Error(`Prediction ${prediction.status}: ${prediction.error || 'Unknown error'}`); + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } + } + + // Helper for common model types + async generateText(prompt, options = {}) { + const model = options.model || 'meta/llama-2-70b-chat:02e509c789964a7ea8736978a43525956ef40397be9033abf9fd2badfe68c9e3'; + return this.run(model, { + prompt: prompt, + max_new_tokens: options.maxTokens || 500, + temperature: options.temperature || 0.75, + top_p: options.topP || 0.9, + ...options.input + }, options); + } + + async generateImage(prompt, options = {}) { + const model = options.model || 'stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b'; + return this.run(model, { + prompt: prompt, + negative_prompt: options.negativePrompt || '', + width: options.width || 1024, + height: options.height || 1024, + num_outputs: options.numOutputs || 1, + ...options.input + }, options); + } + + async transcribeAudio(audioUrl, options = {}) { + const model = options.model || 'openai/whisper:4d50797290df275329f202e48c76360b3f22b08d28c196cbc54600319435f8d2'; + return this.run(model, { + audio: audioUrl, + model: options.whisperModel || 'base', + transcription: options.transcription || 'plain text', + ...options.input + }, options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/replicate/defaultConfig.json b/packages/replicate/defaultConfig.json new file mode 100644 index 0000000..7417aae --- /dev/null +++ b/packages/replicate/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "replicate", + "label": "Replicate", + "productUrl": "https://replicate.com", + "apiDocs": "https://replicate.com/docs/reference/http", + "logoUrl": "https://friggframework.org/assets/img/replicate-icon.png", + "categories": [ + "AI/ML", + "Model Hosting", + "Machine Learning", + "Cloud ML" + ], + "description": "Replicate lets you run machine learning models in the cloud with a simple API, supporting thousands of open-source models." +} \ No newline at end of file diff --git a/packages/replicate/definition.js b/packages/replicate/definition.js new file mode 100644 index 0000000..48cff1a --- /dev/null +++ b/packages/replicate/definition.js @@ -0,0 +1,60 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Replicate', + requiredAuthMethods: { + getToken: async function (api, params) { + // Replicate uses API tokens, not OAuth + const apiKey = get(params.data, 'apiToken') || get(params.data, 'apiKey'); + if (!apiKey) { + throw new Error('API Token is required for Replicate authentication'); + } + return { apiKey }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + // Get account info from Replicate + const account = await api.getAccount(); + return { + identifiers: {externalId: account.username || 'replicate-user', user: userId}, + details: { + username: account.username, + type: account.type, + githubUrl: account.github_url + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'apiKey' + ], + entity: ['username', 'type'], + }, + getCredentialDetails: async function (api, userId) { + // Get account details + const account = await api.getAccount(); + return { + identifiers: {externalId: account.username || 'replicate-api', user: userId}, + details: { + username: account.username, + accountType: account.type + } + }; + }, + testAuthRequest: async function (api) { + return api.testAuth() + }, + }, + env: { + apiKey: process.env.REPLICATE_API_TOKEN || process.env.REPLICATE_API_KEY, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/replicate/index.js b/packages/replicate/index.js new file mode 100644 index 0000000..18a6c30 --- /dev/null +++ b/packages/replicate/index.js @@ -0,0 +1,3 @@ +const {Definition} = require('./definition'); + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/v1-ready/google-calendar/.eslintrc.json b/packages/revio/.eslintrc.json similarity index 100% rename from packages/v1-ready/google-calendar/.eslintrc.json rename to packages/revio/.eslintrc.json diff --git a/packages/v1-ready/crossbeam/CHANGELOG.md b/packages/revio/CHANGELOG.md similarity index 100% rename from packages/v1-ready/crossbeam/CHANGELOG.md rename to packages/revio/CHANGELOG.md diff --git a/packages/needs-updating/yotpo/LICENSE.md b/packages/revio/LICENSE.md similarity index 100% rename from packages/needs-updating/yotpo/LICENSE.md rename to packages/revio/LICENSE.md diff --git a/packages/needs-updating/revio/README.md b/packages/revio/README.md similarity index 100% rename from packages/needs-updating/revio/README.md rename to packages/revio/README.md diff --git a/packages/needs-updating/revio/api.js b/packages/revio/api.js similarity index 100% rename from packages/needs-updating/revio/api.js rename to packages/revio/api.js diff --git a/packages/needs-updating/revio/authFields.js b/packages/revio/authFields.js similarity index 100% rename from packages/needs-updating/revio/authFields.js rename to packages/revio/authFields.js diff --git a/packages/needs-updating/revio/defaultConfig.json b/packages/revio/defaultConfig.json similarity index 100% rename from packages/needs-updating/revio/defaultConfig.json rename to packages/revio/defaultConfig.json diff --git a/packages/revio/definition.js b/packages/revio/definition.js new file mode 100644 index 0000000..c033fbd --- /dev/null +++ b/packages/revio/definition.js @@ -0,0 +1,39 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Revio', + requiredAuthMethods: { + getToken: async function(api, params) { + return api.getTokenFromApiKey(params.data.apiKey); + }, + getEntityDetails: async function(api, callbackParams, tokenResponse, userId) { + return { + identifiers: {externalId: params.data.apiKey || 'default', user: userId}, + details: {name: params.data.apiKey || 'Default'} + }; + }, + getCredentialDetails: async function(api, userId) { + return { + identifiers: {externalId: 'default', user: userId}, + details: {} + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'apiKey'], + entity: [] + }, + testAuthRequest: async function(api) { + return await api.testAuth(); + } + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/v1-ready/connectwise/formatPatchBody.js b/packages/revio/formatPatchBody.js similarity index 100% rename from packages/v1-ready/connectwise/formatPatchBody.js rename to packages/revio/formatPatchBody.js diff --git a/packages/revio/index.js b/packages/revio/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/revio/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/asana/jest.config.js b/packages/revio/jest.config.js similarity index 100% rename from packages/v1-ready/asana/jest.config.js rename to packages/revio/jest.config.js diff --git a/packages/needs-updating/revio/manager.test.js b/packages/revio/manager.test.js similarity index 100% rename from packages/needs-updating/revio/manager.test.js rename to packages/revio/manager.test.js diff --git a/packages/v1-ready/google-drive/.eslintrc.json b/packages/rollworks/.eslintrc.json similarity index 100% rename from packages/v1-ready/google-drive/.eslintrc.json rename to packages/rollworks/.eslintrc.json diff --git a/packages/needs-updating/rollworks/CHANGELOG.md b/packages/rollworks/CHANGELOG.md similarity index 100% rename from packages/needs-updating/rollworks/CHANGELOG.md rename to packages/rollworks/CHANGELOG.md diff --git a/packages/v1-ready/asana/LICENSE.md b/packages/rollworks/LICENSE.md similarity index 100% rename from packages/v1-ready/asana/LICENSE.md rename to packages/rollworks/LICENSE.md diff --git a/packages/needs-updating/rollworks/README.md b/packages/rollworks/README.md similarity index 100% rename from packages/needs-updating/rollworks/README.md rename to packages/rollworks/README.md diff --git a/packages/needs-updating/rollworks/api.js b/packages/rollworks/api.js similarity index 100% rename from packages/needs-updating/rollworks/api.js rename to packages/rollworks/api.js diff --git a/packages/needs-updating/rollworks/defaultConfig.json b/packages/rollworks/defaultConfig.json similarity index 100% rename from packages/needs-updating/rollworks/defaultConfig.json rename to packages/rollworks/defaultConfig.json diff --git a/packages/rollworks/definition.js b/packages/rollworks/definition.js new file mode 100644 index 0000000..4069b0a --- /dev/null +++ b/packages/rollworks/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Rollworks', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id || userDetails.sub, user: userId}, + details: {name: userDetails.name || userDetails.email}, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const userDetails = await api.getUserDetails(); + return { + identifiers: {externalId: userDetails.id || userDetails.sub, user: userId}, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getUserDetails() + }, + }, + env: { + client_id: process.env.ROLLWORKS_CLIENT_ID, + client_secret: process.env.ROLLWORKS_CLIENT_SECRET, + scope: process.env.ROLLWORKS_SCOPE, + redirect_uri: `${process.env.REDIRECT_URI}/rollworks`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/rollworks/index.js b/packages/rollworks/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/rollworks/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/connectwise/jest.config.js b/packages/rollworks/jest.config.js similarity index 100% rename from packages/v1-ready/connectwise/jest.config.js rename to packages/rollworks/jest.config.js diff --git a/packages/needs-updating/qbo/manager.test.js b/packages/rollworks/manager.test.js similarity index 100% rename from packages/needs-updating/qbo/manager.test.js rename to packages/rollworks/manager.test.js diff --git a/packages/needs-updating/rollworks/test/Api.test.js b/packages/rollworks/test/Api.test.js similarity index 100% rename from packages/needs-updating/rollworks/test/Api.test.js rename to packages/rollworks/test/Api.test.js diff --git a/packages/needs-updating/rollworks/test/Manager.test.js b/packages/rollworks/test/Manager.test.js similarity index 100% rename from packages/needs-updating/rollworks/test/Manager.test.js rename to packages/rollworks/test/Manager.test.js diff --git a/packages/v1-ready/salesforce/.env.example b/packages/salesforce/.env.example similarity index 100% rename from packages/v1-ready/salesforce/.env.example rename to packages/salesforce/.env.example diff --git a/packages/v1-ready/helpscout/.eslintrc.json b/packages/salesforce/.eslintrc.json similarity index 100% rename from packages/v1-ready/helpscout/.eslintrc.json rename to packages/salesforce/.eslintrc.json diff --git a/packages/v1-ready/salesforce/CHANGELOG.md b/packages/salesforce/CHANGELOG.md similarity index 100% rename from packages/v1-ready/salesforce/CHANGELOG.md rename to packages/salesforce/CHANGELOG.md diff --git a/packages/v1-ready/connectwise/LICENSE.md b/packages/salesforce/LICENSE.md similarity index 100% rename from packages/v1-ready/connectwise/LICENSE.md rename to packages/salesforce/LICENSE.md diff --git a/packages/v1-ready/salesforce/README.md b/packages/salesforce/README.md similarity index 100% rename from packages/v1-ready/salesforce/README.md rename to packages/salesforce/README.md diff --git a/packages/v1-ready/salesforce/api.js b/packages/salesforce/api.js similarity index 100% rename from packages/v1-ready/salesforce/api.js rename to packages/salesforce/api.js diff --git a/packages/v1-ready/salesforce/defaultConfig.json b/packages/salesforce/defaultConfig.json similarity index 100% rename from packages/v1-ready/salesforce/defaultConfig.json rename to packages/salesforce/defaultConfig.json diff --git a/packages/v1-ready/salesforce/definition.js b/packages/salesforce/definition.js similarity index 100% rename from packages/v1-ready/salesforce/definition.js rename to packages/salesforce/definition.js diff --git a/packages/v1-ready/salesforce/fenestra/examples/salesforce-extension.json b/packages/salesforce/fenestra/examples/salesforce-extension.json similarity index 100% rename from packages/v1-ready/salesforce/fenestra/examples/salesforce-extension.json rename to packages/salesforce/fenestra/examples/salesforce-extension.json diff --git a/packages/v1-ready/salesforce/fenestra/examples/salesforce-lwc.fenestra.yaml b/packages/salesforce/fenestra/examples/salesforce-lwc.fenestra.yaml similarity index 100% rename from packages/v1-ready/salesforce/fenestra/examples/salesforce-lwc.fenestra.yaml rename to packages/salesforce/fenestra/examples/salesforce-lwc.fenestra.yaml diff --git a/packages/v1-ready/salesforce/fenestra/platform.fenestra.yaml b/packages/salesforce/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/salesforce/fenestra/platform.fenestra.yaml rename to packages/salesforce/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/salesforce/fenestra/schemas/salesforce-validation.json b/packages/salesforce/fenestra/schemas/salesforce-validation.json similarity index 100% rename from packages/v1-ready/salesforce/fenestra/schemas/salesforce-validation.json rename to packages/salesforce/fenestra/schemas/salesforce-validation.json diff --git a/packages/salesforce/index.js b/packages/salesforce/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/salesforce/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/crossbeam/jest.config.js b/packages/salesforce/jest.config.js similarity index 100% rename from packages/v1-ready/crossbeam/jest.config.js rename to packages/salesforce/jest.config.js diff --git a/packages/v1-ready/salesforce/package.json b/packages/salesforce/package.json similarity index 100% rename from packages/v1-ready/salesforce/package.json rename to packages/salesforce/package.json diff --git a/packages/salesforce/specs/arazzo.yaml b/packages/salesforce/specs/arazzo.yaml new file mode 100644 index 0000000..8ab6312 --- /dev/null +++ b/packages/salesforce/specs/arazzo.yaml @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:367bcbe43a6d385c5acc1c7ef593cb230eefe261364bdcb6b4afa7e668d9f271 +size 7761 diff --git a/packages/salesforce/streamHandler.js b/packages/salesforce/streamHandler.js new file mode 100644 index 0000000..1775029 --- /dev/null +++ b/packages/salesforce/streamHandler.js @@ -0,0 +1,54 @@ +const nforce = require('nforce'); +const {opportunityPushTopicName} = require('../../constants/StringConstants'); +// All the authenication is part of the configuration for a Connected App in Salesforce + +// consumerKey and consumer secret should be provided via environment variables +const org = nforce.createConnection({ + clientId: process.env.SALESFORCE_CLIENT_ID || 'YOUR_CLIENT_ID', + clientSecret: process.env.SALESFORCE_CLIENT_SECRET || 'YOUR_CLIENT_SECRET', + redirectUri: 'http://localhost:3000/oauth/callback', + // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + // Licensed under the Amazon Software License + // http://aws.amazon.com/asl/ + // environment:'sandbox', + apiVersion: 'v44.0', + mode: 'multi', // was single +}); +// const TOPIC = '/event/Raz_Test_Event__e';// 'OppCRUD__e'; +// const REPLAY_ID = -1; +// const USERNAME = 'ryan@coderden.com.salesrightappdev'; +// const PASSWORD = '5688razy'; +// SNS TOPIC +// const TOPIC_ARN = 'Opportunity'; +// exports.handler = function(event, context, callback) {/**/ +// authenticate via oauth process to SFDC +const oauth = { + access_token: process.env.SALESFORCE_ACCESS_TOKEN || 'YOUR_ACCESS_TOKEN', + instance_url: process.env.SALESFORCE_INSTANCE_URL || 'https://your-instance.salesforce.com', +}; +const client = org.createStreamClient({oauth}); +const accs = client.subscribe({ + topic: opportunityPushTopicName, + replayId: -1, + retry: -1, + oauth, +}); +console.log( + `Subscription to ${opportunityPushTopicName} supposedly successful for thing` +); +accs.on('error', (err) => { + console.log(`Error occurred, ${err}`); + client.disconnect(); +}); + +accs.on('data', (data) => { + console.log( + `PushTopic, ${opportunityPushTopicName} detected\nEvent:${JSON.stringify( + data + )}` + ); +}); +const exiting = () => { + console.log('Exiting'); +}; +setTimeout(exiting, 90000); diff --git a/packages/v1-ready/salesforce/test/auther.test.js b/packages/salesforce/test/auther.test.js similarity index 100% rename from packages/v1-ready/salesforce/test/auther.test.js rename to packages/salesforce/test/auther.test.js diff --git a/packages/v1-ready/salesforce/test/manager.test.js b/packages/salesforce/test/manager.test.js similarity index 100% rename from packages/v1-ready/salesforce/test/manager.test.js rename to packages/salesforce/test/manager.test.js diff --git a/packages/v1-ready/hubspot/.eslintrc.json b/packages/salesloft/.eslintrc.json similarity index 100% rename from packages/v1-ready/hubspot/.eslintrc.json rename to packages/salesloft/.eslintrc.json diff --git a/packages/needs-updating/salesloft/CHANGELOG.md b/packages/salesloft/CHANGELOG.md similarity index 100% rename from packages/needs-updating/salesloft/CHANGELOG.md rename to packages/salesloft/CHANGELOG.md diff --git a/packages/v1-ready/crossbeam/LICENSE.md b/packages/salesloft/LICENSE.md similarity index 100% rename from packages/v1-ready/crossbeam/LICENSE.md rename to packages/salesloft/LICENSE.md diff --git a/packages/needs-updating/salesloft/README.md b/packages/salesloft/README.md similarity index 100% rename from packages/needs-updating/salesloft/README.md rename to packages/salesloft/README.md diff --git a/packages/needs-updating/salesloft/api.js b/packages/salesloft/api.js similarity index 100% rename from packages/needs-updating/salesloft/api.js rename to packages/salesloft/api.js diff --git a/packages/needs-updating/salesloft/defaultConfig.json b/packages/salesloft/defaultConfig.json similarity index 100% rename from packages/needs-updating/salesloft/defaultConfig.json rename to packages/salesloft/defaultConfig.json diff --git a/packages/salesloft/definition.js b/packages/salesloft/definition.js new file mode 100644 index 0000000..dc8dac9 --- /dev/null +++ b/packages/salesloft/definition.js @@ -0,0 +1,139 @@ +const { IntegrationBase, ModuleConstants } = require('@friggframework/core'); +const _ = require('lodash'); +const { Api } = require('./api.js'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); +const Config = require('./defaultConfig.json'); + +class SalesloftIntegration extends IntegrationBase { + static Definition = { + name: Config.name, + version: '1.0.0', + modules: { Api, Entity, Credential }, + display: { + label: Config.label, + description: Config.description, + category: Config.categories[0], + iconUrl: Config.logoUrl, + detailsUrl: Config.productUrl, + }, + }; + + static Entity = Entity; + static Credential = Credential; + + async getAuthorizationRequirements(params) { + return { + url: await this.api.authorizationUri, + type: ModuleConstants.authType.oauth2, + }; + } + + async processAuthorizationCallback(params) { + const code = _.get(params.data, 'code'); + const response = await this.api.getTokenFromCode(code); + + const credentials = await this.credentialMO.list({ user: this.userId }); + const entitySearch = await this.entityMO.list({ user: this.userId }); + let entity; + + await this.testAuth(); + + const teamDetails = await this.api.getTeam(); + + if (entitySearch.length === 0) { + const createObj = { + credential: credentials[0]._id, + user: this.userId, + name: teamDetails.data.name, + externalId: teamDetails.data.id, + }; + entity = await this.entityMO.create(createObj); + } else { + entity = entitySearch[0]; + } + + if (credentials.length === 0) { + throw new Error('Credential failed to create'); + } + if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } + + return { + credential_id: credentials[0].id, + entity_id: entity.id, + type: Config.name, + }; + } + + async testAuth() { + await this.api.getTeam(); + } + + async deauthorize() { + this.api = new Api(); + + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async mark_credentials_invalid() { + const credentials = await this.credentialMO.list({ user: this.userId }); + if (credentials.length === 1) { + return await this.credentialMO.update(credentials[0]._id, { + auth_is_valid: false, + }); + } + if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } else if (credentials.length === 0) { + throw new Error( + 'How are we marking noexistant credentials invalid???' + ); + } + } + + async receiveNotification(notifier, delegateString, object = null) { + if (notifier instanceof Api) { + if (delegateString === this.api.DLGT_TOKEN_UPDATE) { + const updatedToken = { + user: this.userId, + access_token: this.api.access_token, + refresh_token: this.api.refresh_token, + expires_at: this.api.accessTokenExpire, + }; + + Object.keys(updatedToken).forEach( + (k) => updatedToken[k] === null && delete updatedToken[k] + ); + const credentials = await this.credentialMO.list({ + user: this.userId, + }); + let credential; + if (credentials.length === 1) { + credential = credentials[0]; + } else if (credentials.length > 1) { + throw new Error('User has multiple credentials???'); + } + if (!credential) { + credential = await this.credentialMO.create(updatedToken); + } else { + credential = await this.credentialMO.update( + credential._id, + updatedToken + ); + } + } + if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { + await this.deauthorize(); + } + } + } +} + +module.exports = SalesloftIntegration; \ No newline at end of file diff --git a/packages/salesloft/index.js b/packages/salesloft/index.js new file mode 100644 index 0000000..3ca9218 --- /dev/null +++ b/packages/salesloft/index.js @@ -0,0 +1,13 @@ +const { Api } = require('./api'); +const { Credential } = require('./models/credential'); +const { Entity } = require('./models/entity'); +const Definition = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/hubspot/jest.config.js b/packages/salesloft/jest.config.js similarity index 100% rename from packages/v1-ready/hubspot/jest.config.js rename to packages/salesloft/jest.config.js diff --git a/packages/needs-updating/rollworks/manager.test.js b/packages/salesloft/manager.test.js similarity index 100% rename from packages/needs-updating/rollworks/manager.test.js rename to packages/salesloft/manager.test.js diff --git a/packages/needs-updating/salesloft/test/Api.test.js b/packages/salesloft/test/Api.test.js similarity index 100% rename from packages/needs-updating/salesloft/test/Api.test.js rename to packages/salesloft/test/Api.test.js diff --git a/packages/segment/README.md b/packages/segment/README.md new file mode 100644 index 0000000..607a05c --- /dev/null +++ b/packages/segment/README.md @@ -0,0 +1,384 @@ +# Segment API Module + +This module provides a v1-ready integration with Segment's customer data platform using Write Key authentication for server-side tracking. + +## Installation + +```bash +npm install @friggframework/api-module-segment +``` + +## Features + +- Write Key authentication for server-side tracking +- All core tracking methods (identify, track, page, screen, group, alias) +- Batch operations for high-volume data +- Historical data import +- GDPR compliance with user deletion +- E-commerce event helpers +- Workspace management (with workspace token) + +## Authentication + +Segment uses Write Key authentication for the tracking API. The Write Key is specific to each source in your Segment workspace. + +### Finding Your Write Key +1. Log in to your Segment workspace +2. Navigate to Sources +3. Select your source +4. Go to Settings → API Keys +5. Copy the Write Key + +## Quick Start + +### Initialize the Integration + +```javascript +const { Definition } = require('@friggframework/api-module-segment'); + +const segment = new Definition({ + writeKey: 'your-write-key' +}); +``` + +## API Methods + +### Core Tracking Methods + +#### Identify User +```javascript +await segment.identify({ + userId: 'user-123', + traits: { + name: 'John Doe', + email: 'john@example.com', + plan: 'premium', + createdAt: '2024-01-01T00:00:00Z' + } +}); +``` + +#### Track Event +```javascript +await segment.track({ + userId: 'user-123', + event: 'Item Purchased', + properties: { + item_id: 'SKU-123', + price: 29.99, + quantity: 2 + } +}); +``` + +#### Page View +```javascript +await segment.page({ + userId: 'user-123', + name: 'Product Page', + category: 'Ecommerce', + properties: { + path: '/products/shoes', + referrer: 'https://google.com', + search: 'running shoes', + title: 'Running Shoes - Store' + } +}); +``` + +#### Screen View (Mobile) +```javascript +await segment.screen({ + userId: 'user-123', + name: 'Home Screen', + properties: { + variation: 'A' + } +}); +``` + +#### Group Association +```javascript +await segment.group({ + userId: 'user-123', + groupId: 'company-456', + traits: { + name: 'Acme Corp', + industry: 'Technology', + employees: 500 + } +}); +``` + +#### Alias User IDs +```javascript +await segment.alias({ + previousId: 'anonymous-789', + userId: 'user-123' +}); +``` + +### Batch Operations + +#### Send Multiple Events +```javascript +await segment.batch([ + { + type: 'identify', + userId: 'user-123', + traits: { plan: 'premium' } + }, + { + type: 'track', + userId: 'user-123', + event: 'Subscription Started', + properties: { plan: 'premium', value: 99 } + } +]); +``` + +### Historical Data Import + +#### Import Past Events +```javascript +await segment.import([ + { + type: 'track', + userId: 'user-123', + event: 'Sign Up', + timestamp: '2023-01-01T00:00:00Z', + properties: { source: 'organic' } + }, + { + type: 'track', + userId: 'user-123', + event: 'First Purchase', + timestamp: '2023-01-15T00:00:00Z', + properties: { value: 49.99 } + } +]); +``` + +### Helper Methods + +#### Identify User (Simplified) +```javascript +await segment.identifyUser('user-123', { + name: 'John Doe', + email: 'john@example.com' +}); +``` + +#### Track Event (Simplified) +```javascript +await segment.trackEvent('user-123', 'Button Clicked', { + button: 'signup', + location: 'header' +}); +``` + +#### Track Page View (Simplified) +```javascript +await segment.trackPage('user-123', 'Home Page', { + path: '/', + referrer: 'https://google.com' +}); +``` + +### E-commerce Events + +#### Order Completed +```javascript +await segment.trackOrderCompleted('user-123', { + orderId: 'ORDER-456', + total: 299.99, + shipping: 10, + tax: 25.50, + discount: 20, + coupon: 'SAVE20', + products: [ + { + product_id: 'SKU-123', + name: 'Running Shoes', + price: 149.99, + quantity: 2 + } + ] +}); +``` + +#### Product Viewed +```javascript +await segment.trackProductViewed('user-123', { + productId: 'SKU-123', + name: 'Running Shoes', + category: 'Footwear', + brand: 'Nike', + price: 149.99, + currency: 'USD', + url: 'https://store.com/products/running-shoes', + imageUrl: 'https://store.com/images/shoes.jpg' +}); +``` + +### Workspace Management (Requires Workspace Token) + +#### Get Sources +```javascript +const sources = await segment.getSources('workspace-token'); +``` + +#### Get Destinations +```javascript +const destinations = await segment.getDestinations('workspace-token'); +``` + +#### Get Tracking Plan +```javascript +const trackingPlan = await segment.getTrackingPlan('workspace-token'); +``` + +### GDPR Compliance + +#### Delete User Data +```javascript +await segment.deleteUser({ + userId: 'user-123', + regulation: 'GDPR' +}, 'workspace-token'); +``` + +## Working with Context + +Add context to any tracking call: +```javascript +await segment.track({ + userId: 'user-123', + event: 'Purchase', + properties: { value: 99.99 }, + context: { + ip: '192.168.1.1', + userAgent: 'Mozilla/5.0...', + locale: 'en-US', + timezone: 'America/New_York', + app: { + name: 'MyApp', + version: '1.0.0' + }, + device: { + type: 'mobile', + manufacturer: 'Apple', + model: 'iPhone 13' + } + } +}); +``` + +## Using Anonymous IDs + +Track users before they sign up: +```javascript +// Before sign up +await segment.track({ + anonymousId: 'anonymous-abc-123', + event: 'Product Viewed', + properties: { product_id: 'SKU-123' } +}); + +// After sign up, link the IDs +await segment.alias({ + previousId: 'anonymous-abc-123', + userId: 'user-123' +}); +``` + +## Integrations Control + +Control which destinations receive data: +```javascript +await segment.track({ + userId: 'user-123', + event: 'Test Event', + integrations: { + 'Google Analytics': false, // Disable GA + 'Mixpanel': true, // Ensure Mixpanel gets it + 'All': false, // Disable all except specified + 'Amplitude': true // Enable Amplitude + } +}); +``` + +## Error Handling + +```javascript +try { + await segment.track({ + userId: 'user-123', + event: 'Purchase' + }); +} catch (error) { + if (error.status === 400) { + console.error('Invalid request:', error.message); + } else if (error.status === 401) { + console.error('Invalid write key'); + } else if (error.status === 429) { + console.error('Rate limit exceeded'); + } else { + console.error('Segment API Error:', error); + } +} +``` + +## Batch Validation + +```javascript +const batch = [ + { type: 'track', userId: 'user-1', event: 'Test' }, + { type: 'identify', userId: 'user-2', traits: { name: 'Jane' } } +]; + +const validation = segment.api.validateBatch(batch); +if (!validation.valid) { + console.error('Batch validation errors:', validation.errors); +} +``` + +## Testing Authentication + +```javascript +const testResult = await segment.testAuth(); +if (testResult.success) { + console.log('Authentication successful!'); +} else { + console.error('Authentication failed:', testResult.message); +} +``` + +## Best Practices + +1. **Use userId when available**: Always prefer userId over anonymousId for logged-in users +2. **Batch for performance**: Use batch endpoint for multiple events (max 500 per batch) +3. **Include timestamps**: Especially important for historical imports +4. **Set context**: Include device, location, and app information when relevant +5. **Use semantic events**: Follow Segment's spec for e-commerce and standard events + +## Rate Limits + +- **Tracking API**: No hard rate limits, but respect reasonable usage +- **Batch size**: Maximum 500 messages per batch +- **Message size**: Maximum 32KB per message +- **Request size**: Maximum 500KB per request + +## Segment Spec + +This module follows the Segment Spec. Key specifications: +- **Common Fields**: userId, anonymousId, context, timestamp, integrations +- **E-commerce Events**: Order Completed, Product Added, Cart Viewed, etc. +- **B2B Events**: Account Created, Trial Started, Feature Used, etc. + +## Resources + +- [Segment Documentation](https://segment.com/docs/) +- [HTTP Tracking API](https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/) +- [Segment Spec](https://segment.com/docs/connections/spec/) +- [Best Practices](https://segment.com/docs/protocols/tracking-plan/best-practices/) \ No newline at end of file diff --git a/packages/segment/api.js b/packages/segment/api.js new file mode 100644 index 0000000..371fb7a --- /dev/null +++ b/packages/segment/api.js @@ -0,0 +1,380 @@ +const { ApiClass } = require('@friggframework/core'); + +class SegmentApi extends ApiClass { + constructor(params) { + super(params); + this.baseUrl = 'https://api.segment.io/v1'; + this.publicApiUrl = 'https://api.segmentapis.com/v1beta'; + this.writeKey = params.writeKey; + + // Set up basic auth with write key + if (this.writeKey) { + this.authHeader = 'Basic ' + Buffer.from(this.writeKey + ':').toString('base64'); + } + } + + /** + * Get authorization headers + * @param {boolean} useBearer - Use bearer token instead of basic auth + * @param {string} token - Bearer token if using bearer auth + * @returns {Object} Headers with authorization + */ + _getAuthHeaders(useBearer = false, token = null) { + const headers = { + 'Content-Type': 'application/json' + }; + + if (useBearer && token) { + headers['Authorization'] = `Bearer ${token}`; + } else { + headers['Authorization'] = this.authHeader; + } + + return headers; + } + + /** + * Identify a user + * @param {Object} identify - Identify payload + * @returns {Promise} Response + */ + async identify(identify) { + const payload = { + ...identify, + type: 'identify', + writeKey: this.writeKey, + timestamp: identify.timestamp || new Date().toISOString() + }; + + return this._post(`${this.baseUrl}/identify`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Track an event + * @param {Object} track - Track payload + * @returns {Promise} Response + */ + async track(track) { + const payload = { + ...track, + type: 'track', + writeKey: this.writeKey, + timestamp: track.timestamp || new Date().toISOString() + }; + + return this._post(`${this.baseUrl}/track`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Record page view + * @param {Object} page - Page payload + * @returns {Promise} Response + */ + async page(page) { + const payload = { + ...page, + type: 'page', + writeKey: this.writeKey, + timestamp: page.timestamp || new Date().toISOString() + }; + + return this._post(`${this.baseUrl}/page`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Record screen view (mobile) + * @param {Object} screen - Screen payload + * @returns {Promise} Response + */ + async screen(screen) { + const payload = { + ...screen, + type: 'screen', + writeKey: this.writeKey, + timestamp: screen.timestamp || new Date().toISOString() + }; + + return this._post(`${this.baseUrl}/screen`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Associate user with a group + * @param {Object} group - Group payload + * @returns {Promise} Response + */ + async group(group) { + const payload = { + ...group, + type: 'group', + writeKey: this.writeKey, + timestamp: group.timestamp || new Date().toISOString() + }; + + return this._post(`${this.baseUrl}/group`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Alias one user ID to another + * @param {Object} alias - Alias payload + * @returns {Promise} Response + */ + async alias(alias) { + const payload = { + ...alias, + type: 'alias', + writeKey: this.writeKey, + timestamp: alias.timestamp || new Date().toISOString() + }; + + return this._post(`${this.baseUrl}/alias`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Send batch of messages + * @param {Array} batch - Array of messages + * @returns {Promise} Response + */ + async batch(batch) { + const messages = batch.map(msg => ({ + ...msg, + timestamp: msg.timestamp || new Date().toISOString() + })); + + const payload = { + batch: messages, + writeKey: this.writeKey + }; + + return this._post(`${this.baseUrl}/batch`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Import historical data + * @param {Array} events - Array of historical events + * @returns {Promise} Response + */ + async import(events) { + const messages = events.map(event => ({ + ...event, + timestamp: event.timestamp || new Date().toISOString() + })); + + const payload = { + batch: messages, + writeKey: this.writeKey + }; + + return this._post(`${this.baseUrl}/import`, payload, { + headers: this._getAuthHeaders() + }); + } + + /** + * Get tracking plan (requires workspace token) + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} Tracking plan + */ + async getTrackingPlan(workspaceToken) { + const url = `${this.publicApiUrl}/tracking-plans`; + return this._get(url, { + headers: this._getAuthHeaders(true, workspaceToken) + }); + } + + /** + * Get sources (requires workspace token) + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} List of sources + */ + async getSources(workspaceToken) { + const url = `${this.publicApiUrl}/sources`; + return this._get(url, { + headers: this._getAuthHeaders(true, workspaceToken) + }); + } + + /** + * Get destinations (requires workspace token) + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} List of destinations + */ + async getDestinations(workspaceToken) { + const url = `${this.publicApiUrl}/destinations`; + return this._get(url, { + headers: this._getAuthHeaders(true, workspaceToken) + }); + } + + /** + * Delete user data (GDPR) + * @param {Object} params - Deletion parameters + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} Deletion job + */ + async deleteUser(params, workspaceToken) { + const url = `${this.publicApiUrl}/deletion-requests`; + return this._post(url, params, { + headers: this._getAuthHeaders(true, workspaceToken) + }); + } + + /** + * Validate batch payload + * @param {Array} batch - Batch of messages + * @returns {Object} Validation result + */ + validateBatch(batch) { + const errors = []; + const maxBatchSize = 500; + const maxMessageSize = 32 * 1024; // 32KB + + if (!Array.isArray(batch)) { + errors.push('Batch must be an array'); + } else if (batch.length > maxBatchSize) { + errors.push(`Batch size exceeds maximum of ${maxBatchSize}`); + } + + batch.forEach((msg, index) => { + const msgSize = JSON.stringify(msg).length; + if (msgSize > maxMessageSize) { + errors.push(`Message ${index} exceeds maximum size of ${maxMessageSize} bytes`); + } + + if (!msg.type) { + errors.push(`Message ${index} missing required field: type`); + } + + if (!msg.userId && !msg.anonymousId) { + errors.push(`Message ${index} must have either userId or anonymousId`); + } + }); + + return { + valid: errors.length === 0, + errors + }; + } + + /** + * Format context object with common fields + * @param {Object} context - Context override + * @returns {Object} Formatted context + */ + formatContext(context = {}) { + return { + library: { + name: '@friggframework/api-module-segment', + version: '1.0.0' + }, + ...context + }; + } + + /** + * Create identify payload with validation + * @param {Object} params - Identify parameters + * @returns {Object} Formatted identify payload + */ + createIdentifyPayload(params) { + if (!params.userId && !params.anonymousId) { + throw new Error('Either userId or anonymousId is required'); + } + + return { + userId: params.userId, + anonymousId: params.anonymousId, + traits: params.traits || {}, + context: this.formatContext(params.context), + timestamp: params.timestamp || new Date().toISOString(), + integrations: params.integrations || {} + }; + } + + /** + * Create track payload with validation + * @param {Object} params - Track parameters + * @returns {Object} Formatted track payload + */ + createTrackPayload(params) { + if (!params.event) { + throw new Error('Event name is required'); + } + + if (!params.userId && !params.anonymousId) { + throw new Error('Either userId or anonymousId is required'); + } + + return { + userId: params.userId, + anonymousId: params.anonymousId, + event: params.event, + properties: params.properties || {}, + context: this.formatContext(params.context), + timestamp: params.timestamp || new Date().toISOString(), + integrations: params.integrations || {} + }; + } + + /** + * Create page payload with validation + * @param {Object} params - Page parameters + * @returns {Object} Formatted page payload + */ + createPagePayload(params) { + if (!params.userId && !params.anonymousId) { + throw new Error('Either userId or anonymousId is required'); + } + + return { + userId: params.userId, + anonymousId: params.anonymousId, + name: params.name, + category: params.category, + properties: params.properties || {}, + context: this.formatContext(params.context), + timestamp: params.timestamp || new Date().toISOString(), + integrations: params.integrations || {} + }; + } + + /** + * Create group payload with validation + * @param {Object} params - Group parameters + * @returns {Object} Formatted group payload + */ + createGroupPayload(params) { + if (!params.groupId) { + throw new Error('Group ID is required'); + } + + if (!params.userId && !params.anonymousId) { + throw new Error('Either userId or anonymousId is required'); + } + + return { + userId: params.userId, + anonymousId: params.anonymousId, + groupId: params.groupId, + traits: params.traits || {}, + context: this.formatContext(params.context), + timestamp: params.timestamp || new Date().toISOString(), + integrations: params.integrations || {} + }; + } +} + +module.exports = SegmentApi; \ No newline at end of file diff --git a/packages/segment/defaultConfig.json b/packages/segment/defaultConfig.json new file mode 100644 index 0000000..3d9cdfb --- /dev/null +++ b/packages/segment/defaultConfig.json @@ -0,0 +1,102 @@ +{ + "name": "Segment", + "version": "1.0.0", + "category": "Analytics", + "type": "segment", + "description": "Customer data platform for collecting, cleaning, and routing analytics data", + "documentation": "https://segment.com/docs/", + "apiDocs": "https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/", + "authentication": { + "types": [ + { + "type": "writeKey", + "name": "Write Key", + "description": "For server-side event tracking", + "fields": ["writeKey"] + }, + { + "type": "bearerToken", + "name": "Workspace Token", + "description": "For workspace management APIs", + "fields": ["workspaceToken"] + } + ] + }, + "endpoints": { + "tracking": "https://api.segment.io/v1", + "public": "https://api.segmentapis.com/v1beta", + "eu": "https://events.eu1.segmentapis.com/v1" + }, + "features": [ + "Event tracking", + "User identification", + "Page and screen tracking", + "Group analytics", + "User aliasing", + "Batch operations", + "Historical data import", + "GDPR compliance", + "Real-time data routing", + "Over 300 integrations", + "Data warehouses support", + "Tracking plans" + ], + "limitations": [ + "500 messages per batch", + "32KB per message", + "500KB per request", + "No hard rate limits on tracking API" + ], + "pricing": "Usage-based pricing starting with free tier", + "rateLimits": { + "tracking": "No hard limits, reasonable usage expected", + "batch": "500 messages per batch", + "messageSize": "32KB", + "requestSize": "500KB" + }, + "supportedRegions": [ + "US", + "EU" + ], + "dataRetention": "Based on plan, typically 1 year for free tier", + "webhooks": false, + "directIntegrations": [ + "Google Analytics", + "Mixpanel", + "Amplitude", + "Braze", + "Salesforce", + "HubSpot", + "Intercom", + "Facebook Pixel", + "Google Ads", + "Slack", + "Webhooks", + "Data Warehouses" + ], + "sdks": [ + "JavaScript", + "Node.js", + "Python", + "Ruby", + "PHP", + "Java", + "Go", + ".NET", + "iOS", + "Android", + "React Native" + ], + "compliance": [ + "GDPR", + "CCPA", + "SOC 2 Type II", + "ISO 27001", + "HIPAA eligible" + ], + "protocols": { + "spec": "https://segment.com/docs/connections/spec/", + "ecommerce": "https://segment.com/docs/connections/spec/ecommerce/v2/", + "b2b": "https://segment.com/docs/connections/spec/b2b-saas/" + } +} \ No newline at end of file diff --git a/packages/segment/definition.js b/packages/segment/definition.js new file mode 100644 index 0000000..5684d8e --- /dev/null +++ b/packages/segment/definition.js @@ -0,0 +1,264 @@ +const { Integration } = require('@friggframework/module-plugin'); +const ApiClass = require('./api'); + +class SegmentIntegration extends Integration { + static name = 'Segment'; + static category = 'Analytics'; + static catalogDescription = 'Customer data platform for collecting, cleaning, and routing analytics data'; + static version = '1.0.0'; + static referenceUrl = 'https://segment.com'; + static apiDocs = 'https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/'; + + /** + * Constructor for SegmentIntegration + * @param {Object} params - Should include writeKey for authentication + */ + constructor(params) { + super(params); + this.api = new ApiClass(params); + } + + /** + * Identify a user + * @param {Object} identify - Identify payload + * @returns {Promise} Response + */ + async identify(identify) { + return this.api.identify(identify); + } + + /** + * Track an event + * @param {Object} track - Track payload + * @returns {Promise} Response + */ + async track(track) { + return this.api.track(track); + } + + /** + * Record page view + * @param {Object} page - Page payload + * @returns {Promise} Response + */ + async page(page) { + return this.api.page(page); + } + + /** + * Record screen view (mobile) + * @param {Object} screen - Screen payload + * @returns {Promise} Response + */ + async screen(screen) { + return this.api.screen(screen); + } + + /** + * Associate user with a group + * @param {Object} group - Group payload + * @returns {Promise} Response + */ + async group(group) { + return this.api.group(group); + } + + /** + * Alias one user ID to another + * @param {Object} alias - Alias payload + * @returns {Promise} Response + */ + async alias(alias) { + return this.api.alias(alias); + } + + /** + * Send batch of messages + * @param {Array} batch - Array of messages + * @returns {Promise} Response + */ + async batch(batch) { + return this.api.batch(batch); + } + + /** + * Import historical data + * @param {Array} events - Array of historical events + * @returns {Promise} Response + */ + async import(events) { + return this.api.import(events); + } + + /** + * Get tracking plan (requires workspace token) + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} Tracking plan + */ + async getTrackingPlan(workspaceToken) { + return this.api.getTrackingPlan(workspaceToken); + } + + /** + * Get sources (requires workspace token) + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} List of sources + */ + async getSources(workspaceToken) { + return this.api.getSources(workspaceToken); + } + + /** + * Get destinations (requires workspace token) + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} List of destinations + */ + async getDestinations(workspaceToken) { + return this.api.getDestinations(workspaceToken); + } + + /** + * Delete user data (GDPR) + * @param {Object} params - Deletion parameters + * @param {string} workspaceToken - Workspace access token + * @returns {Promise} Deletion job + */ + async deleteUser(params, workspaceToken) { + return this.api.deleteUser(params, workspaceToken); + } + + /** + * Test authentication by sending a test event + * @returns {Promise} Test result + */ + async testAuth() { + try { + await this.track({ + userId: 'test-user', + event: 'Test Event', + properties: { + test: true, + timestamp: new Date().toISOString() + } + }); + + return { + success: true, + message: 'Authentication successful - test event sent' + }; + } catch (error) { + return { + success: false, + message: `Authentication failed: ${error.message}`, + error: error + }; + } + } + + /** + * Helper to create identify call with common patterns + * @param {string} userId - User ID + * @param {Object} traits - User traits + * @param {Object} options - Additional options + * @returns {Promise} Response + */ + async identifyUser(userId, traits, options = {}) { + return this.identify({ + userId, + traits, + timestamp: new Date().toISOString(), + ...options + }); + } + + /** + * Helper to track event with common patterns + * @param {string} userId - User ID + * @param {string} event - Event name + * @param {Object} properties - Event properties + * @param {Object} options - Additional options + * @returns {Promise} Response + */ + async trackEvent(userId, event, properties = {}, options = {}) { + return this.track({ + userId, + event, + properties, + timestamp: new Date().toISOString(), + ...options + }); + } + + /** + * Helper to track page view + * @param {string} userId - User ID + * @param {string} name - Page name + * @param {Object} properties - Page properties + * @param {Object} options - Additional options + * @returns {Promise} Response + */ + async trackPage(userId, name, properties = {}, options = {}) { + return this.page({ + userId, + name, + properties, + timestamp: new Date().toISOString(), + ...options + }); + } + + /** + * Helper for e-commerce order completed event + * @param {string} userId - User ID + * @param {Object} order - Order details + * @returns {Promise} Response + */ + async trackOrderCompleted(userId, order) { + return this.track({ + userId, + event: 'Order Completed', + properties: { + order_id: order.orderId, + total: order.total, + revenue: order.revenue || order.total, + shipping: order.shipping || 0, + tax: order.tax || 0, + discount: order.discount || 0, + coupon: order.coupon, + currency: order.currency || 'USD', + products: order.products || [] + }, + timestamp: new Date().toISOString() + }); + } + + /** + * Helper for product viewed event + * @param {string} userId - User ID + * @param {Object} product - Product details + * @returns {Promise} Response + */ + async trackProductViewed(userId, product) { + return this.track({ + userId, + event: 'Product Viewed', + properties: { + product_id: product.productId, + sku: product.sku, + category: product.category, + name: product.name, + brand: product.brand, + variant: product.variant, + price: product.price, + quantity: product.quantity || 1, + currency: product.currency || 'USD', + position: product.position, + url: product.url, + image_url: product.imageUrl + }, + timestamp: new Date().toISOString() + }); + } +} + +module.exports = SegmentIntegration; \ No newline at end of file diff --git a/packages/segment/index.js b/packages/segment/index.js new file mode 100644 index 0000000..2f5c456 --- /dev/null +++ b/packages/segment/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/sendgrid/README.md b/packages/sendgrid/README.md new file mode 100644 index 0000000..35fc188 --- /dev/null +++ b/packages/sendgrid/README.md @@ -0,0 +1,209 @@ +# SendGrid API Module + +A comprehensive SendGrid API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +SENDGRID_API_KEY=your_sendgrid_api_key +``` + +### Getting SendGrid API Key + +1. Sign up for a SendGrid account at [https://sendgrid.com/](https://sendgrid.com/) +2. Navigate to Settings > API Keys in your SendGrid dashboard +3. Click "Create API Key" +4. Choose the appropriate permissions (Full Access for all features, or Restricted Access for specific features) +5. Copy the generated API key + +### Authentication + +This module uses API Key authentication via the `Authorization: Bearer {api_key}` header. + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-sendgrid'); + +// Initialize with API key +const sendGridApi = new Api({ + api_key: process.env.SENDGRID_API_KEY +}); + +// Send a simple email +const result = await sendGridApi.sendSimpleEmail( + 'recipient@example.com', + 'sender@example.com', + 'Hello from SendGrid!', + 'This is a test email sent via SendGrid API.', + 'text/plain' +); + +// Send email with template +const templateResult = await sendGridApi.sendEmailWithTemplate( + 'recipient@example.com', + 'sender@example.com', + 'template-id-here', + { + first_name: 'John', + last_name: 'Doe' + } +); +``` + +## Available Methods + +### Mail Send Methods +- `sendMail(mailData)` - Send email with full SendGrid mail object +- `sendSimpleEmail(to, from, subject, content, contentType)` - Send simple text/HTML email +- `sendEmailWithTemplate(to, from, templateId, dynamicTemplateData)` - Send email using template + +### User Profile Methods +- `getCurrentUser()` - Get current user profile +- `updateUserProfile(profileData)` - Update user profile +- `getUserAccount()` - Get account information + +### Templates Methods +- `getTemplates(params)` - List email templates +- `getTemplate(templateId)` - Get specific template +- `createTemplate(templateData)` - Create new template +- `updateTemplate(templateId, templateData)` - Update template +- `deleteTemplate(templateId)` - Delete template +- `getTemplateVersions(templateId)` - Get template versions +- `createTemplateVersion(templateId, versionData)` - Create template version + +### Contacts Methods +- `getContacts(params)` - List contacts with pagination +- `addContacts(contacts)` - Add or update contacts +- `searchContacts(query)` - Search contacts +- `getContactById(contactId)` - Get specific contact +- `deleteContacts(contactIds)` - Delete contacts + +### Lists Methods +- `getLists()` - Get marketing lists +- `createList(name, contactCount)` - Create new list +- `getList(listId)` - Get specific list +- `updateList(listId, name)` - Update list +- `deleteList(listId)` - Delete list +- `addContactsToList(listId, contactIds)` - Add contacts to list +- `removeContactsFromList(listId, contactIds)` - Remove contacts from list + +### Campaigns Methods +- `getCampaigns(params)` - List campaigns +- `createCampaign(campaignData)` - Create new campaign +- `getCampaign(campaignId)` - Get specific campaign +- `updateCampaign(campaignId, campaignData)` - Update campaign +- `deleteCampaign(campaignId)` - Delete campaign +- `scheduleCampaign(campaignId, sendAt)` - Schedule campaign + +### Suppressions Methods +- `getGlobalSuppressions()` - Get globally suppressed emails +- `addGlobalSuppression(email)` - Add email to global suppressions +- `removeGlobalSuppression(email)` - Remove email from global suppressions +- `getBounces(params)` - Get bounced emails +- `deleteBounces(emails)` - Delete bounce records + +### Stats Methods +- `getGlobalStats(params)` - Get global email statistics +- `getCategoryStats(params)` - Get category-specific statistics + +### Sender Identity Methods +- `getSenderIdentities()` - Get verified sender identities +- `createSenderIdentity(senderData)` - Create new sender identity +- `getSenderIdentity(senderId)` - Get specific sender identity +- `updateSenderIdentity(senderId, senderData)` - Update sender identity +- `deleteSenderIdentity(senderId)` - Delete sender identity + +## Email Sending Examples + +### Simple Text Email +```javascript +await sendGridApi.sendSimpleEmail( + 'user@example.com', + 'noreply@yoursite.com', + 'Welcome!', + 'Welcome to our service!', + 'text/plain' +); +``` + +### HTML Email +```javascript +await sendGridApi.sendSimpleEmail( + 'user@example.com', + 'noreply@yoursite.com', + 'Welcome!', + '

Welcome!

Welcome to our service!

', + 'text/html' +); +``` + +### Template Email with Personalization +```javascript +await sendGridApi.sendEmailWithTemplate( + 'user@example.com', + 'noreply@yoursite.com', + 'welcome-template-id', + { + username: 'john_doe', + verification_url: 'https://yoursite.com/verify/token' + } +); +``` + +### Advanced Email with Attachments +```javascript +const mailData = { + personalizations: [ + { + to: [{ email: 'user@example.com', name: 'John Doe' }], + subject: 'Document Attached' + } + ], + from: { email: 'noreply@yoursite.com', name: 'Your Service' }, + content: [ + { + type: 'text/html', + value: '

Please find the attached document.

' + } + ], + attachments: [ + { + content: 'base64-encoded-content', + filename: 'document.pdf', + type: 'application/pdf' + } + ] +}; + +await sendGridApi.sendMail(mailData); +``` + +## Error Handling + +SendGrid returns detailed error information. Always wrap API calls in try-catch blocks: + +```javascript +try { + const result = await sendGridApi.sendSimpleEmail(to, from, subject, content); + console.log('Email sent successfully'); +} catch (error) { + console.error('SendGrid error:', error.message); +} +``` + +## Rate Limiting + +SendGrid enforces rate limits based on your plan. The module does not implement automatic retry logic - you should handle rate limiting in your application. + +## Webhooks + +SendGrid can send webhooks for email events (delivered, opened, clicked, etc.). Configure webhook endpoints in your SendGrid dashboard. + +## Documentation + +For detailed SendGrid API documentation, visit: https://docs.sendgrid.com/api-reference \ No newline at end of file diff --git a/packages/sendgrid/api.js b/packages/sendgrid/api.js new file mode 100644 index 0000000..52f7042 --- /dev/null +++ b/packages/sendgrid/api.js @@ -0,0 +1,473 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + this.api_key = get(params, 'api_key', null); + this.baseUrl = 'https://api.sendgrid.com/v3'; + + this.URLs = { + // Mail Send + mail: '/mail/send', + + // User Profile + user: '/user/profile', + account: '/user/account', + + // Templates + templates: '/templates', + templateById: (templateId) => `/templates/${templateId}`, + templateVersions: (templateId) => `/templates/${templateId}/versions`, + templateVersionById: (templateId, versionId) => `/templates/${templateId}/versions/${versionId}`, + + // Sender Authentication + senderIdentities: '/verified_senders', + senderIdentityById: (senderId) => `/verified_senders/${senderId}`, + + // Lists + lists: '/marketing/lists', + listById: (listId) => `/marketing/lists/${listId}`, + listContacts: (listId) => `/marketing/lists/${listId}/contacts`, + + // Contacts + contacts: '/marketing/contacts', + contactsSearch: '/marketing/contacts/search', + contactById: (contactId) => `/marketing/contacts/${contactId}`, + + // Campaigns + campaigns: '/marketing/campaigns', + campaignById: (campaignId) => `/marketing/campaigns/${campaignId}`, + campaignSchedule: (campaignId) => `/marketing/campaigns/${campaignId}/schedules`, + + // Suppressions + suppressions: '/asm/suppressions', + globalSuppressions: '/asm/suppressions/global', + bounces: '/suppression/bounces', + blocks: '/suppression/blocks', + spam: '/suppression/spam_reports', + invalid: '/suppression/invalid_emails', + + // Stats + stats: '/stats', + globalStats: '/stats/global', + categoryStats: '/categories/stats', + + // Subusers + subusers: '/subusers', + subuserById: (username) => `/subusers/${username}`, + + // API Keys + apiKeys: '/api_keys', + apiKeyById: (keyId) => `/api_keys/${keyId}`, + + // Webhooks + webhookStats: '/user/webhooks/event/settings', + webhookParse: '/user/webhooks/parse/settings', + }; + } + + addAuthHeaders(headers = {}) { + if (this.api_key) { + headers.Authorization = `Bearer ${this.api_key}`; + } + return headers; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** Mail Send Methods ********************************** + + async sendMail(mailData) { + const options = { + url: this.baseUrl + this.URLs.mail, + body: mailData, + }; + return this._post(options); + } + + async sendSimpleEmail(to, from, subject, content, contentType = 'text/plain') { + const mailData = { + personalizations: [ + { + to: [{ email: to }], + subject: subject + } + ], + from: { email: from }, + content: [ + { + type: contentType, + value: content + } + ] + }; + + return this.sendMail(mailData); + } + + async sendEmailWithTemplate(to, from, templateId, dynamicTemplateData = {}) { + const mailData = { + personalizations: [ + { + to: [{ email: to }], + dynamic_template_data: dynamicTemplateData + } + ], + from: { email: from }, + template_id: templateId + }; + + return this.sendMail(mailData); + } + + // ************************** User Profile Methods ********************************** + + async getCurrentUser() { + const options = { + url: this.baseUrl + this.URLs.user, + }; + return this._get(options); + } + + async updateUserProfile(profileData) { + const options = { + url: this.baseUrl + this.URLs.user, + body: profileData, + }; + return this._patch(options); + } + + async getUserAccount() { + const options = { + url: this.baseUrl + this.URLs.account, + }; + return this._get(options); + } + + // ************************** Templates Methods ********************************** + + async getTemplates(params = {}) { + const options = { + url: this.baseUrl + this.URLs.templates, + query: params, + }; + return this._get(options); + } + + async getTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + }; + return this._get(options); + } + + async createTemplate(templateData) { + const options = { + url: this.baseUrl + this.URLs.templates, + body: templateData, + }; + return this._post(options); + } + + async updateTemplate(templateId, templateData) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + body: templateData, + }; + return this._patch(options); + } + + async deleteTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateById(templateId), + }; + return this._delete(options); + } + + async getTemplateVersions(templateId) { + const options = { + url: this.baseUrl + this.URLs.templateVersions(templateId), + }; + return this._get(options); + } + + async createTemplateVersion(templateId, versionData) { + const options = { + url: this.baseUrl + this.URLs.templateVersions(templateId), + body: versionData, + }; + return this._post(options); + } + + // ************************** Contacts Methods ********************************** + + async getContacts(params = {}) { + const options = { + url: this.baseUrl + this.URLs.contacts, + query: params, + }; + return this._get(options); + } + + async addContacts(contacts) { + const options = { + url: this.baseUrl + this.URLs.contacts, + body: { contacts }, + }; + return this._put(options); + } + + async searchContacts(query) { + const options = { + url: this.baseUrl + this.URLs.contactsSearch, + body: { query }, + }; + return this._post(options); + } + + async getContactById(contactId) { + const options = { + url: this.baseUrl + this.URLs.contactById(contactId), + }; + return this._get(options); + } + + async deleteContacts(contactIds) { + const options = { + url: this.baseUrl + this.URLs.contacts, + query: { ids: contactIds.join(',') }, + }; + return this._delete(options); + } + + // ************************** Lists Methods ********************************** + + async getLists() { + const options = { + url: this.baseUrl + this.URLs.lists, + }; + return this._get(options); + } + + async createList(name, contactCount = 0) { + const options = { + url: this.baseUrl + this.URLs.lists, + body: { + name, + contact_count: contactCount + }, + }; + return this._post(options); + } + + async getList(listId) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + }; + return this._get(options); + } + + async updateList(listId, name) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + body: { name }, + }; + return this._patch(options); + } + + async deleteList(listId) { + const options = { + url: this.baseUrl + this.URLs.listById(listId), + query: { delete_contacts: false }, + }; + return this._delete(options); + } + + async addContactsToList(listId, contactIds) { + const options = { + url: this.baseUrl + this.URLs.listContacts(listId), + body: { contact_ids: contactIds }, + }; + return this._post(options); + } + + async removeContactsFromList(listId, contactIds) { + const options = { + url: this.baseUrl + this.URLs.listContacts(listId), + query: { contact_ids: contactIds.join(',') }, + }; + return this._delete(options); + } + + // ************************** Campaigns Methods ********************************** + + async getCampaigns(params = {}) { + const options = { + url: this.baseUrl + this.URLs.campaigns, + query: params, + }; + return this._get(options); + } + + async createCampaign(campaignData) { + const options = { + url: this.baseUrl + this.URLs.campaigns, + body: campaignData, + }; + return this._post(options); + } + + async getCampaign(campaignId) { + const options = { + url: this.baseUrl + this.URLs.campaignById(campaignId), + }; + return this._get(options); + } + + async updateCampaign(campaignId, campaignData) { + const options = { + url: this.baseUrl + this.URLs.campaignById(campaignId), + body: campaignData, + }; + return this._patch(options); + } + + async deleteCampaign(campaignId) { + const options = { + url: this.baseUrl + this.URLs.campaignById(campaignId), + }; + return this._delete(options); + } + + async scheduleCampaign(campaignId, sendAt) { + const options = { + url: this.baseUrl + this.URLs.campaignSchedule(campaignId), + body: { send_at: sendAt }, + }; + return this._post(options); + } + + // ************************** Suppressions Methods ********************************** + + async getGlobalSuppressions() { + const options = { + url: this.baseUrl + this.URLs.globalSuppressions, + }; + return this._get(options); + } + + async addGlobalSuppression(email) { + const options = { + url: this.baseUrl + this.URLs.globalSuppressions, + body: { recipient_emails: [email] }, + }; + return this._post(options); + } + + async removeGlobalSuppression(email) { + const options = { + url: this.baseUrl + this.URLs.globalSuppressions + `/${email}`, + }; + return this._delete(options); + } + + async getBounces(params = {}) { + const options = { + url: this.baseUrl + this.URLs.bounces, + query: params, + }; + return this._get(options); + } + + async deleteBounces(emails) { + const options = { + url: this.baseUrl + this.URLs.bounces, + body: { emails }, + }; + return this._delete(options); + } + + // ************************** Stats Methods ********************************** + + async getGlobalStats(params = {}) { + const options = { + url: this.baseUrl + this.URLs.globalStats, + query: params, + }; + return this._get(options); + } + + async getCategoryStats(params = {}) { + const options = { + url: this.baseUrl + this.URLs.categoryStats, + query: params, + }; + return this._get(options); + } + + // ************************** Sender Identity Methods ********************************** + + async getSenderIdentities() { + const options = { + url: this.baseUrl + this.URLs.senderIdentities, + }; + return this._get(options); + } + + async createSenderIdentity(senderData) { + const options = { + url: this.baseUrl + this.URLs.senderIdentities, + body: senderData, + }; + return this._post(options); + } + + async getSenderIdentity(senderId) { + const options = { + url: this.baseUrl + this.URLs.senderIdentityById(senderId), + }; + return this._get(options); + } + + async updateSenderIdentity(senderId, senderData) { + const options = { + url: this.baseUrl + this.URLs.senderIdentityById(senderId), + body: senderData, + }; + return this._patch(options); + } + + async deleteSenderIdentity(senderId) { + const options = { + url: this.baseUrl + this.URLs.senderIdentityById(senderId), + }; + return this._delete(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/sendgrid/defaultConfig.json b/packages/sendgrid/defaultConfig.json new file mode 100644 index 0000000..fd9eb21 --- /dev/null +++ b/packages/sendgrid/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "sendgrid", + "label": "SendGrid", + "productUrl": "https://sendgrid.com", + "apiDocs": "https://docs.sendgrid.com/api-reference", + "logoUrl": "https://sendgrid.com/wp-content/themes/sgdotcom/pages/resource/brand/2016/SendGrid-Logomark.png", + "categories": [ + "Email", + "Marketing", + "Transactional Email", + "Email Delivery" + ], + "description": "SendGrid is a cloud-based SMTP provider that allows you to send email without having to maintain email servers" +} \ No newline at end of file diff --git a/packages/sendgrid/definition.js b/packages/sendgrid/definition.js new file mode 100644 index 0000000..224870b --- /dev/null +++ b/packages/sendgrid/definition.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'SendGrid', + requiredAuthMethods: { + getAuthorizationRequirements: async function (params) { + return { + type: 'api_key', + url: 'https://app.sendgrid.com/settings/api_keys', + description: 'Generate an API key from your SendGrid account settings. Make sure to give it appropriate permissions.' + }; + }, + getCredentialDetails: async function (api, userId) { + const userProfile = await api.getCurrentUser(); + return { + identifiers: { externalId: userProfile.username, user: userId }, + details: {} + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const userProfile = await api.getCurrentUser(); + return { + identifiers: { externalId: userProfile.username, user: userId }, + details: { + name: userProfile.first_name && userProfile.last_name + ? `${userProfile.first_name} ${userProfile.last_name}` + : userProfile.username, + email: userProfile.email + } + }; + }, + testAuthRequest: async function (api) { + return api.getCurrentUser(); + }, + apiPropertiesToPersist: { + credential: ['api_key'], + entity: [] + } + }, + env: { + api_key: process.env.SENDGRID_API_KEY + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/sendgrid/index.js b/packages/sendgrid/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/sendgrid/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/sendgrid/package.json b/packages/sendgrid/package.json new file mode 100644 index 0000000..490f160 --- /dev/null +++ b/packages/sendgrid/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/sendgrid", + "version": "0.0.1", + "description": "SendGrid API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "sendgrid", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0" + } +} \ No newline at end of file diff --git a/packages/sentry/.env.example b/packages/sentry/.env.example new file mode 100644 index 0000000..db0316c --- /dev/null +++ b/packages/sentry/.env.example @@ -0,0 +1,2 @@ +# SENTRY API Configuration +SENTRY_API_KEY=your_api_key_here diff --git a/packages/sentry/README.md b/packages/sentry/README.md new file mode 100644 index 0000000..658f2f6 --- /dev/null +++ b/packages/sentry/README.md @@ -0,0 +1,35 @@ +# Sentry API Module + +Error tracking and performance monitoring + +## Installation + +```bash +npm install @friggframework/sentry +``` + +## Configuration + +See `.env.example` for required environment variables. + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/sentry'); + +// Initialize API client +const api = new Api({ + // Add required credentials +}); + +// Test the connection +const result = await api.getCurrentUser(); +``` + +## Category + +Monitoring + +## License + +MIT diff --git a/packages/sentry/defaultConfig.json b/packages/sentry/defaultConfig.json new file mode 100644 index 0000000..121af07 --- /dev/null +++ b/packages/sentry/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "Sentry", + "moduleName": "sentry", + "version": "0.0.1", + "supportedAuthTypes": [ + "apiKey" + ], + "docs": { + "description": "Error tracking and performance monitoring", + "category": "Monitoring", + "apiDocUrl": "https://docs.sentry.com/api", + "icon": "" + } +} \ No newline at end of file diff --git a/packages/sentry/index.js b/packages/sentry/index.js new file mode 100644 index 0000000..aaada0e --- /dev/null +++ b/packages/sentry/index.js @@ -0,0 +1,5 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { Api, Config, Definition }; diff --git a/packages/sentry/package.json b/packages/sentry/package.json new file mode 100644 index 0000000..b2dd096 --- /dev/null +++ b/packages/sentry/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/sentry", + "version": "0.0.1", + "description": "Sentry API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "sentry", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0" + } +} \ No newline at end of file diff --git a/packages/v1-ready/linear/.eslintrc.json b/packages/sharepoint/.eslintrc.json similarity index 100% rename from packages/v1-ready/linear/.eslintrc.json rename to packages/sharepoint/.eslintrc.json diff --git a/packages/needs-updating/sharepoint/CHANGELOG.md b/packages/sharepoint/CHANGELOG.md similarity index 100% rename from packages/needs-updating/sharepoint/CHANGELOG.md rename to packages/sharepoint/CHANGELOG.md diff --git a/packages/v1-ready/linear/LICENSE.md b/packages/sharepoint/LICENSE.md similarity index 100% rename from packages/v1-ready/linear/LICENSE.md rename to packages/sharepoint/LICENSE.md diff --git a/packages/needs-updating/sharepoint/README.md b/packages/sharepoint/README.md similarity index 100% rename from packages/needs-updating/sharepoint/README.md rename to packages/sharepoint/README.md diff --git a/packages/needs-updating/sharepoint/api.js b/packages/sharepoint/api.js similarity index 100% rename from packages/needs-updating/sharepoint/api.js rename to packages/sharepoint/api.js diff --git a/packages/needs-updating/sharepoint/api.test.js b/packages/sharepoint/api.test.js similarity index 100% rename from packages/needs-updating/sharepoint/api.test.js rename to packages/sharepoint/api.test.js diff --git a/packages/needs-updating/sharepoint/defaultConfig.json b/packages/sharepoint/defaultConfig.json similarity index 100% rename from packages/needs-updating/sharepoint/defaultConfig.json rename to packages/sharepoint/defaultConfig.json diff --git a/packages/sharepoint/definition.js b/packages/sharepoint/definition.js new file mode 100644 index 0000000..40b4319 --- /dev/null +++ b/packages/sharepoint/definition.js @@ -0,0 +1,165 @@ +const { IntegrationBase, ModuleConstants, get, debug, flushDebugLog } = require('@friggframework/core'); +const { Api } = require('./api'); +const { Entity } = require('./models/entity'); +const { Credential } = require('./models/credential'); +const Config = require('./defaultConfig'); + +class SharePointIntegration extends IntegrationBase { + static Definition = { + name: Config.name, + version: '1.0.0', + modules: { Api, Entity, Credential }, + display: { + label: Config.label, + description: Config.description, + category: Config.categories[0], + iconUrl: Config.logoUrl, + detailsUrl: Config.productUrl, + }, + }; + + static Entity = Entity; + static Credential = Credential; + + async getAuthorizationRequirements() { + return { + url: this.api.getAuthUri(), + type: ModuleConstants.authType.oauth2, + }; + } + + async processAuthorizationCallback(params) { + const code = get(params.data, 'code', 'test'); + await this.api.getTokenFromCode(code); + const authCheck = await this.testAuth(); + if (!authCheck) throw new Error('Authentication failed'); + + const userDetails = await this.api.getUser(); + // TODO determine if there's a good flag to make for this, where we have individual tokens vs. org/tenant tokens + // The issue here is that the Entity should reflect "on whose behalf are we making api requests", and in the + // individual user case, it's a user. In the org/tenant case, it's a tenant. + // The catch is that personal microsoft users do not have an org. So the graph API throws a 500 error. + // const orgDetails = await this.api.getOrganization(); + + await this.findOrCreateEntity({ + externalId: userDetails.id, + name: `${userDetails.displayName} (${userDetails.userPrincipalName})` + }); + return { + entity_id: this.entity.id, + credential_id: this.credential.id, + type: Config.name, + }; + } + + async testAuth() { + let validAuth = false; + try { + if (await this.api.listSites()) validAuth = true; + } catch (e) { + flushDebugLog(e); + } + return validAuth; + } + + async findOrCreateEntity(params) { + const externalId = get(params, 'externalId'); + const name = get(params, 'name'); + + // TODO-new... this doesn't allow for multiple entities for a specific User. + const search = await Entity.find({ + user: this.userId, + externalId, + }); + if (search.length === 0) { + // validate choices!!! + // create entity + const createObj = { + credential: this.credential.id, + user: this.userId, + name, + externalId, + }; + this.entity = await Entity.create(createObj); + } else if (search.length === 1) { + this.entity = await Entity.findOneAndUpdate( + { _id: search[0] }, + { + $set: { + credential: this.credential.id + } + }, + { useFindAndModify: true, new: true } + ); + } else { + const message = 'Multiple entities found with the same external ID: ' + externalId; + debug(message); + throw new Error(message); + } + } + + async deauthorize() { + // wipe api connection + this.api = new Api(); + + // delete credentials from the database + const entity = await Entity.findByUserId(this.userId); + if (entity.credential) { + await Credential.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + this.credential = undefined; + } + + async receiveNotification(notifier, delegateString, object = null) { + if (notifier instanceof Api) { + if (delegateString === this.api.DLGT_TOKEN_UPDATE) { + const updatedToken = { + user: this.userId.toString(), + accessToken: this.api.access_token, + refreshToken: this.api.refresh_token, + auth_is_valid: true, + }; + + Object.keys(updatedToken).forEach( + (k) => updatedToken[k] == null && delete updatedToken[k] + ); + // TODO-new globally... multiple credentials should be allowed, this is 1:1 + if (!this.credential) { + let credentialSearch = await Credential.find({ + user: this.userId.toString(), + }); + if (credentialSearch.length === 0) { + this.credential = await Credential.create(updatedToken); + } else if (credentialSearch.length === 1) { + this.credential = await Credential.findOneAndUpdate( + { _id: credentialSearch[0] }, + { $set: updatedToken }, + { useFindAndModify: true, new: true } + ); + } else { + // Handling multiple credentials found with an error for the time being + debug( + 'Multiple credentials found with the same user ID: ' + this.userId + ); + } + } else { + this.credential = await Credential.findOneAndUpdate( + { _id: this.credential }, + { $set: updatedToken }, + { useFindAndModify: true, new: true } + ); + } + } + if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { + await this.deauthorize(); + } + if (delegateString === this.api.DLGT_INVALID_AUTH) { + return this.markCredentialsInvalid(); + } + } + } +} + +module.exports = SharePointIntegration; \ No newline at end of file diff --git a/packages/sharepoint/index.js b/packages/sharepoint/index.js new file mode 100644 index 0000000..3ca9218 --- /dev/null +++ b/packages/sharepoint/index.js @@ -0,0 +1,13 @@ +const { Api } = require('./api'); +const { Credential } = require('./models/credential'); +const { Entity } = require('./models/entity'); +const Definition = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/microsoft-teams/jest.config.js b/packages/sharepoint/jest.config.js similarity index 100% rename from packages/v1-ready/microsoft-teams/jest.config.js rename to packages/sharepoint/jest.config.js diff --git a/packages/needs-updating/sharepoint/manager.test.js b/packages/sharepoint/manager.test.js similarity index 100% rename from packages/needs-updating/sharepoint/manager.test.js rename to packages/sharepoint/manager.test.js diff --git a/packages/shopify/README.md b/packages/shopify/README.md new file mode 100644 index 0000000..4e45ce8 --- /dev/null +++ b/packages/shopify/README.md @@ -0,0 +1,167 @@ +# Shopify API Module + +This is the API Module for Shopify that allows the [Frigg Framework](https://friggframework.org) to interact with the Shopify API. + +## Description + +Shopify is a complete commerce platform that lets you start, grow, and manage a business. This module provides OAuth2 authentication and access to Shopify's REST and GraphQL APIs for building custom storefronts, apps, and integrations. + +## Developer Resources + +- **Official API Documentation**: [https://shopify.dev/docs/api](https://shopify.dev/docs/api) +- **Developer Portal**: [https://partners.shopify.com](https://partners.shopify.com) +- **REST API Reference**: [https://shopify.dev/docs/api/admin-rest](https://shopify.dev/docs/api/admin-rest) +- **GraphQL API Reference**: [https://shopify.dev/docs/api/admin-graphql](https://shopify.dev/docs/api/admin-graphql) +- **Product Website**: [https://shopify.com](https://shopify.com) + +## Authentication + +This module uses **OAuth2** authentication for public apps and API key authentication for private apps. + +### Required Environment Variables + +```bash +# OAuth2 Credentials (Public Apps) +SHOPIFY_CLIENT_ID=your_app_api_key +SHOPIFY_CLIENT_SECRET=your_app_api_secret_key +SHOPIFY_SCOPE=read_products,write_orders,read_customers + +# For Private Apps +SHOPIFY_API_KEY=your_private_app_api_key +SHOPIFY_PASSWORD=your_private_app_password +SHOPIFY_SHOP_DOMAIN=yourstore.myshopify.com + +# Redirect URI +REDIRECT_URI=https://your-app.com/auth/callback +``` + +## API Rate Limits + +Shopify uses different rate limiting strategies: + +### REST API +- **Standard**: 2 requests/second (with bursting) +- **Shopify Plus**: 4 requests/second +- **Rate limit header**: `X-Shopify-Shop-Api-Call-Limit` + +### GraphQL API +- **Cost-based system**: Each query has a calculated cost +- **Standard**: 50 cost points/second +- **Restored rate**: 50 points/second + +## Setup Instructions + +1. Install the module: + ```bash + npm install @friggframework/api-module-shopify + ``` + +2. Create a Shopify app: + - Sign up for a [Shopify Partner account](https://partners.shopify.com) + - Create a new app in your Partner Dashboard + - Configure app settings and permissions + +3. Set up environment variables as shown above + +4. Initialize the module: + ```javascript + const { Api, Definition } = require('@friggframework/api-module-shopify'); + + // Initialize with OAuth2 + const api = new Api({ + access_token: 'your_access_token', + shop: 'yourstore.myshopify.com' + }); + ``` + +## Common Use Cases + +- Product catalog management +- Order processing and fulfillment +- Customer relationship management +- Inventory tracking +- Custom checkout experiences +- Discount and pricing automation +- Multi-channel selling +- Analytics and reporting + +## Available API Methods + +Key endpoints available through this module: +- **Products**: Create, read, update, delete products +- **Orders**: Manage orders and fulfillment +- **Customers**: Customer data and segmentation +- **Inventory**: Track inventory levels +- **Collections**: Organize products +- **Webhooks**: Real-time event notifications +- **Themes**: Customize store appearance +- **Metafields**: Store custom data + +## SDK and Integration Notes + +- Official SDKs: `@shopify/shopify-api`, `@shopify/admin-api-client` +- Webhook verification for secure event handling +- Both REST and GraphQL APIs supported +- Storefront API for custom storefronts +- App Bridge for embedded app experiences + +## Known Issues and Limitations + +- API version deprecation (versions supported for 12 months) +- Some features require Shopify Plus +- App installation requires merchant approval +- Rate limits shared across all apps for a shop +- Maximum API call execution time: 60 seconds + +## Troubleshooting + +### Common Issues + +1. **401 Unauthorized**: Check access token and shop domain +2. **429 Too Many Requests**: Implement rate limit handling +3. **Invalid API version**: Use a supported API version +4. **Scope errors**: Request necessary access scopes during OAuth + +### Debug Mode + +Enable debug logging: +```javascript +api.setDebug(true); +``` + +### API Versioning + +Specify API version: +```javascript +api.setVersion('2024-01'); +``` + +## Fenestra UI Extensions + +This module includes comprehensive Fenestra specifications for Shopify UI extensibility. + +### Available Extension Types +See `fenestra/platform.fenestra.yaml` for complete specification. + +### Examples +Check `fenestra/examples/` directory for implementation examples. + +### Fenestra Specifications + +- **Platform Spec**: `fenestra/platform.fenestra.yaml` +- **Examples**: `fenestra/examples/` +- **Schemas**: `fenestra/schemas/` + +## Support + +For Shopify API issues: +- [Shopify Developer Documentation](https://shopify.dev) +- [Partner Support](https://help.shopify.com/en/partners) +- [Community Forums](https://community.shopify.com/c/shopify-apis-and-sdks/bd-p/shopify-apis-and-sdks) + +For Frigg Framework issues: +- [Frigg Documentation](https://docs.friggframework.org) + +## Categories + +E-commerce, Retail, Payment Processing, Inventory Management diff --git a/packages/v1-ready/shopify/fenestra/platform.fenestra.yaml b/packages/shopify/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/shopify/fenestra/platform.fenestra.yaml rename to packages/shopify/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/shopify/fenestra/schemas/shopify-validation.json b/packages/shopify/fenestra/schemas/shopify-validation.json similarity index 100% rename from packages/v1-ready/shopify/fenestra/schemas/shopify-validation.json rename to packages/shopify/fenestra/schemas/shopify-validation.json diff --git a/packages/v1-ready/shopify/index.js b/packages/shopify/index.js similarity index 100% rename from packages/v1-ready/shopify/index.js rename to packages/shopify/index.js diff --git a/packages/shopify/openapi.json b/packages/shopify/openapi.json new file mode 100644 index 0000000..d990453 --- /dev/null +++ b/packages/shopify/openapi.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf89d87833a1c3e1dca35059adcb1b25959258daeea402d0ad4681d6257d841 +size 3122904 diff --git a/packages/v1-ready/shopify/package.json b/packages/shopify/package.json similarity index 100% rename from packages/v1-ready/shopify/package.json rename to packages/shopify/package.json diff --git a/packages/square/README.md b/packages/square/README.md new file mode 100644 index 0000000..1ccece9 --- /dev/null +++ b/packages/square/README.md @@ -0,0 +1,284 @@ +# Square API Module + +A comprehensive Square API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +SQUARE_CLIENT_ID=your_square_client_id +SQUARE_CLIENT_SECRET=your_square_client_secret +SQUARE_SCOPE=MERCHANT_PROFILE_READ PAYMENTS_READ PAYMENTS_WRITE +SQUARE_SANDBOX=true +REDIRECT_URI=your_redirect_uri_base +``` + +### Getting Square API Credentials + +1. Go to the [Square Developer Dashboard](https://developer.squareup.com/) +2. Sign in with your Square account +3. Create a new application or select an existing one +4. Get your Application ID (Client ID) and Application Secret (Client Secret) +5. Set up your redirect URI (e.g., `https://yourdomain.com/square`) + +### Sandbox vs Production + +- Set `SQUARE_SANDBOX=true` for testing with Square's sandbox environment +- Set `SQUARE_SANDBOX=false` for production usage + +## Available Scopes + +- `MERCHANT_PROFILE_READ` - Read merchant profile information +- `MERCHANT_PROFILE_WRITE` - Modify merchant profile information +- `PAYMENTS_READ` - Read payment information +- `PAYMENTS_WRITE` - Process payments +- `CUSTOMERS_READ` - Read customer information +- `CUSTOMERS_WRITE` - Manage customers +- `ORDERS_READ` - Read order information +- `ORDERS_WRITE` - Manage orders +- `INVENTORY_READ` - Read inventory information +- `INVENTORY_WRITE` - Manage inventory + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-square'); + +// Initialize with credentials +const squareApi = new Api({ + client_id: process.env.SQUARE_CLIENT_ID, + client_secret: process.env.SQUARE_CLIENT_SECRET, + redirect_uri: process.env.REDIRECT_URI + '/square', + scope: 'MERCHANT_PROFILE_READ PAYMENTS_READ PAYMENTS_WRITE', + sandbox: process.env.SQUARE_SANDBOX === 'true' +}); + +// Get authorization URL +const authUrl = squareApi.getAuthUri(); + +// Exchange code for tokens +const tokens = await squareApi.getTokenFromCode(authorizationCode); + +// Create a payment +const payment = await squareApi.createSimplePayment( + 1000, // $10.00 in cents + 'USD', + 'card-nonce-from-frontend', + 'location-id' +); +``` + +## Available Methods + +### Merchants Methods +- `listMerchants()` - List merchant accounts + +### Locations Methods +- `listLocations()` - List business locations +- `getLocation(locationId)` - Get specific location +- `updateLocation(locationId, locationData)` - Update location information + +### Payments Methods +- `listPayments(params)` - List payments with filters +- `createPayment(paymentData)` - Process a payment +- `getPayment(paymentId)` - Get payment details +- `cancelPayment(paymentId)` - Cancel payment +- `completePayment(paymentId)` - Complete payment +- `createSimplePayment(amount, currency, sourceId, locationId)` - Helper for simple payments + +### Orders Methods +- `createOrder(orderData)` - Create new order +- `searchOrders(searchQuery)` - Search orders +- `batchRetrieveOrders(orderIds, locationId)` - Get multiple orders +- `updateOrder(orderId, orderData)` - Update order +- `payOrder(orderId, paymentData)` - Pay for order + +### Catalog Methods +- `listCatalog(params)` - List catalog items +- `searchCatalogObjects(searchQuery)` - Search catalog +- `getCatalogObject(objectId, includeRelatedObjects)` - Get catalog item +- `batchUpsertCatalogObjects(objects)` - Create/update catalog items +- `batchDeleteCatalogObjects(objectIds)` - Delete catalog items + +### Inventory Methods +- `adjustInventory(adjustmentData)` - Adjust inventory count +- `batchChangeInventory(changes)` - Batch inventory changes +- `batchRetrieveInventoryCount(catalogObjectIds, locationIds)` - Get inventory counts + +### Customers Methods +- `listCustomers(params)` - List customers +- `createCustomer(customerData)` - Create new customer +- `getCustomer(customerId)` - Get customer details +- `updateCustomer(customerId, customerData)` - Update customer +- `deleteCustomer(customerId)` - Delete customer +- `searchCustomers(searchQuery)` - Search customers + +### Invoices Methods +- `listInvoices(params)` - List invoices +- `createInvoice(invoiceData)` - Create new invoice +- `getInvoice(invoiceId)` - Get invoice details +- `updateInvoice(invoiceId, invoiceData)` - Update invoice +- `deleteInvoice(invoiceId, version)` - Delete invoice +- `sendInvoice(invoiceId, requestData)` - Send invoice to customer +- `cancelInvoice(invoiceId, version)` - Cancel invoice + +### Refunds Methods +- `listRefunds(params)` - List refunds +- `createRefund(refundData)` - Process refund +- `getRefund(refundId)` - Get refund details + +### Webhooks Methods +- `listWebhookSubscriptions()` - List webhook subscriptions +- `createWebhookSubscription(subscriptionData)` - Create webhook +- `getWebhookSubscription(subscriptionId)` - Get webhook details +- `updateWebhookSubscription(subscriptionId, subscriptionData)` - Update webhook +- `deleteWebhookSubscription(subscriptionId)` - Delete webhook + +## Usage Examples + +### Processing a Payment +```javascript +// Create a payment with card nonce from Square's frontend SDK +const paymentData = { + idempotency_key: 'unique-key-' + Date.now(), + amount_money: { + amount: 1000, // $10.00 in cents + currency: 'USD' + }, + source_id: 'card-nonce-from-frontend', + location_id: 'location-id', + buyer_email_address: 'customer@example.com' +}; + +const payment = await squareApi.createPayment(paymentData); +``` + +### Creating an Order +```javascript +const orderData = { + idempotency_key: 'order-key-' + Date.now(), + order: { + location_id: 'location-id', + line_items: [ + { + quantity: '1', + catalog_object_id: 'item-catalog-id', + modifiers: [] + } + ] + } +}; + +const order = await squareApi.createOrder(orderData); +``` + +### Creating a Customer +```javascript +const customerData = { + given_name: 'John', + family_name: 'Doe', + email_address: 'john.doe@example.com', + phone_number: '+15551234567' +}; + +const customer = await squareApi.createCustomer(customerData); +``` + +### Setting up Webhooks +```javascript +const subscriptionData = { + idempotency_key: 'webhook-key-' + Date.now(), + subscription: { + name: 'Payment Notifications', + event_types: [ + 'payment.created', + 'payment.updated' + ], + notification_url: 'https://yoursite.com/webhooks/square', + api_version: '2023-10-18' + } +}; + +const webhook = await squareApi.createWebhookSubscription(subscriptionData); +``` + +### Adding Catalog Items +```javascript +const catalogItems = [ + { + type: 'ITEM', + id: '#item-1', + item_data: { + name: 'Coffee', + description: 'Freshly brewed coffee', + variations: [ + { + type: 'ITEM_VARIATION', + id: '#variation-1', + item_variation_data: { + item_id: '#item-1', + name: 'Regular', + pricing_type: 'FIXED_PRICING', + price_money: { + amount: 300, // $3.00 + currency: 'USD' + } + } + } + ] + } + } +]; + +const result = await squareApi.batchUpsertCatalogObjects(catalogItems); +``` + +## Authentication Flow + +Square uses OAuth2: + +1. Redirect users to Square's authorization URL +2. Handle the callback with the authorization code +3. Exchange the code for access and refresh tokens +4. Use tokens for API requests + +## Error Handling + +Square returns detailed error information. Always wrap API calls in try-catch blocks: + +```javascript +try { + const payment = await squareApi.createPayment(paymentData); + console.log('Payment processed:', payment.payment.id); +} catch (error) { + console.error('Square error:', error.message); + if (error.errors) { + error.errors.forEach(err => { + console.error('Error detail:', err.detail); + }); + } +} +``` + +## Testing + +Use Square's sandbox environment for testing: +- Test credit card numbers are available in Square's documentation +- All transactions in sandbox are simulated +- Use Square Sandbox dashboard to view test data + +## Webhooks + +Square sends webhooks for various events. Important event types include: +- `payment.created` - Payment created +- `payment.updated` - Payment status changed +- `order.created` - Order created +- `order.updated` - Order modified +- `invoice.payment_made` - Invoice paid + +## Documentation + +For detailed Square API documentation, visit: https://developer.squareup.com/docs \ No newline at end of file diff --git a/packages/square/api.js b/packages/square/api.js new file mode 100644 index 0000000..96ce614 --- /dev/null +++ b/packages/square/api.js @@ -0,0 +1,518 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.sandbox = get(params, 'sandbox', false); + this.baseUrl = this.sandbox + ? 'https://connect.squareupsandbox.com' + : 'https://connect.squareup.com'; + + this.URLs = { + authorization: '/oauth2/authorize', + access_token: '/oauth2/token', + revoke_token: '/oauth2/revoke', + + // Merchants + merchants: '/v2/merchants', + + // Locations + locations: '/v2/locations', + locationById: (locationId) => `/v2/locations/${locationId}`, + + // Payments + payments: '/v2/payments', + paymentById: (paymentId) => `/v2/payments/${paymentId}`, + + // Orders + orders: '/v2/orders', + createOrder: '/v2/orders', + batchRetrieveOrders: '/v2/orders/batch-retrieve', + searchOrders: '/v2/orders/search', + updateOrder: (orderId) => `/v2/orders/${orderId}`, + payOrder: (orderId) => `/v2/orders/${orderId}/pay`, + + // Catalog + catalog: '/v2/catalog', + catalogList: '/v2/catalog/list', + catalogSearch: '/v2/catalog/search', + catalogObject: (objectId) => `/v2/catalog/object/${objectId}`, + catalogBatchUpsert: '/v2/catalog/batch-upsert', + catalogBatchDelete: '/v2/catalog/batch-delete', + catalogBatchRetrieve: '/v2/catalog/batch-retrieve', + + // Inventory + inventory: '/v2/inventory', + inventoryAdjustment: '/v2/inventory/adjustment', + inventoryCount: '/v2/inventory/count', + inventoryBatchChange: '/v2/inventory/batch-change', + inventoryBatchRetrieveCount: '/v2/inventory/batch-retrieve-count', + + // Customers + customers: '/v2/customers', + customerById: (customerId) => `/v2/customers/${customerId}`, + customerSearch: '/v2/customers/search', + + // Invoices + invoices: '/v2/invoices', + invoiceById: (invoiceId) => `/v2/invoices/${invoiceId}`, + invoiceSearch: '/v2/invoices/search', + invoiceSend: (invoiceId) => `/v2/invoices/${invoiceId}/send-invoice`, + invoiceCancel: (invoiceId) => `/v2/invoices/${invoiceId}/cancel-invoice`, + + // Subscriptions + subscriptions: '/v2/subscriptions', + subscriptionById: (subscriptionId) => `/v2/subscriptions/${subscriptionId}`, + subscriptionSearch: '/v2/subscriptions/search', + subscriptionCancel: (subscriptionId) => `/v2/subscriptions/${subscriptionId}/cancel`, + subscriptionPause: (subscriptionId) => `/v2/subscriptions/${subscriptionId}/pause`, + subscriptionResume: (subscriptionId) => `/v2/subscriptions/${subscriptionId}/resume`, + + // Refunds + refunds: '/v2/refunds', + refundById: (refundId) => `/v2/refunds/${refundId}`, + + // Disputes + disputes: '/v2/disputes', + disputeById: (disputeId) => `/v2/disputes/${disputeId}`, + + // Webhooks + webhookSubscriptions: '/v2/webhooks/subscriptions', + webhookSubscriptionById: (subscriptionId) => `/v2/webhooks/subscriptions/${subscriptionId}`, + }; + + this.authorizationUri = encodeURI( + `${this.baseUrl}/oauth2/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&scope=${this.scope}&state=${this.state}` + ); + this.tokenUri = this.baseUrl + '/oauth2/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** Merchants Methods ********************************** + + async listMerchants() { + const options = { + url: this.baseUrl + this.URLs.merchants, + }; + return this._get(options); + } + + // ************************** Locations Methods ********************************** + + async listLocations() { + const options = { + url: this.baseUrl + this.URLs.locations, + }; + return this._get(options); + } + + async getLocation(locationId) { + const options = { + url: this.baseUrl + this.URLs.locationById(locationId), + }; + return this._get(options); + } + + async updateLocation(locationId, locationData) { + const options = { + url: this.baseUrl + this.URLs.locationById(locationId), + body: locationData, + }; + return this._put(options); + } + + // ************************** Payments Methods ********************************** + + async listPayments(params = {}) { + const options = { + url: this.baseUrl + this.URLs.payments, + query: params, + }; + return this._get(options); + } + + async createPayment(paymentData) { + const options = { + url: this.baseUrl + this.URLs.payments, + body: paymentData, + }; + return this._post(options); + } + + async getPayment(paymentId) { + const options = { + url: this.baseUrl + this.URLs.paymentById(paymentId), + }; + return this._get(options); + } + + async cancelPayment(paymentId) { + const options = { + url: this.baseUrl + this.URLs.paymentById(paymentId) + '/cancel', + body: {}, + }; + return this._post(options); + } + + async completePayment(paymentId) { + const options = { + url: this.baseUrl + this.URLs.paymentById(paymentId) + '/complete', + body: {}, + }; + return this._post(options); + } + + // ************************** Orders Methods ********************************** + + async createOrder(orderData) { + const options = { + url: this.baseUrl + this.URLs.createOrder, + body: orderData, + }; + return this._post(options); + } + + async searchOrders(searchQuery) { + const options = { + url: this.baseUrl + this.URLs.searchOrders, + body: searchQuery, + }; + return this._post(options); + } + + async batchRetrieveOrders(orderIds, locationId) { + const options = { + url: this.baseUrl + this.URLs.batchRetrieveOrders, + body: { + order_ids: orderIds, + location_id: locationId, + }, + }; + return this._post(options); + } + + async updateOrder(orderId, orderData) { + const options = { + url: this.baseUrl + this.URLs.updateOrder(orderId), + body: orderData, + }; + return this._put(options); + } + + async payOrder(orderId, paymentData) { + const options = { + url: this.baseUrl + this.URLs.payOrder(orderId), + body: paymentData, + }; + return this._post(options); + } + + // ************************** Catalog Methods ********************************** + + async listCatalog(params = {}) { + const options = { + url: this.baseUrl + this.URLs.catalogList, + query: params, + }; + return this._get(options); + } + + async searchCatalogObjects(searchQuery) { + const options = { + url: this.baseUrl + this.URLs.catalogSearch, + body: searchQuery, + }; + return this._post(options); + } + + async getCatalogObject(objectId, includeRelatedObjects = false) { + const options = { + url: this.baseUrl + this.URLs.catalogObject(objectId), + query: { + include_related_objects: includeRelatedObjects, + }, + }; + return this._get(options); + } + + async batchUpsertCatalogObjects(objects) { + const options = { + url: this.baseUrl + this.URLs.catalogBatchUpsert, + body: { + idempotency_key: this._generateIdempotencyKey(), + batches: [ + { + objects: objects, + }, + ], + }, + }; + return this._post(options); + } + + async batchDeleteCatalogObjects(objectIds) { + const options = { + url: this.baseUrl + this.URLs.catalogBatchDelete, + body: { + object_ids: objectIds, + }, + }; + return this._post(options); + } + + // ************************** Inventory Methods ********************************** + + async adjustInventory(adjustmentData) { + const options = { + url: this.baseUrl + this.URLs.inventoryAdjustment, + body: adjustmentData, + }; + return this._post(options); + } + + async batchChangeInventory(changes) { + const options = { + url: this.baseUrl + this.URLs.inventoryBatchChange, + body: { + idempotency_key: this._generateIdempotencyKey(), + changes: changes, + }, + }; + return this._post(options); + } + + async batchRetrieveInventoryCount(catalogObjectIds, locationIds) { + const options = { + url: this.baseUrl + this.URLs.inventoryBatchRetrieveCount, + body: { + catalog_object_ids: catalogObjectIds, + location_ids: locationIds, + }, + }; + return this._post(options); + } + + // ************************** Customers Methods ********************************** + + async listCustomers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.customers, + query: params, + }; + return this._get(options); + } + + async createCustomer(customerData) { + const options = { + url: this.baseUrl + this.URLs.customers, + body: customerData, + }; + return this._post(options); + } + + async getCustomer(customerId) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + }; + return this._get(options); + } + + async updateCustomer(customerId, customerData) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + body: customerData, + }; + return this._put(options); + } + + async deleteCustomer(customerId) { + const options = { + url: this.baseUrl + this.URLs.customerById(customerId), + }; + return this._delete(options); + } + + async searchCustomers(searchQuery) { + const options = { + url: this.baseUrl + this.URLs.customerSearch, + body: searchQuery, + }; + return this._post(options); + } + + // ************************** Invoices Methods ********************************** + + async listInvoices(params = {}) { + const options = { + url: this.baseUrl + this.URLs.invoices, + query: params, + }; + return this._get(options); + } + + async createInvoice(invoiceData) { + const options = { + url: this.baseUrl + this.URLs.invoices, + body: invoiceData, + }; + return this._post(options); + } + + async getInvoice(invoiceId) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + }; + return this._get(options); + } + + async updateInvoice(invoiceId, invoiceData) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + body: invoiceData, + }; + return this._put(options); + } + + async deleteInvoice(invoiceId, version) { + const options = { + url: this.baseUrl + this.URLs.invoiceById(invoiceId), + body: { version }, + }; + return this._delete(options); + } + + async sendInvoice(invoiceId, requestData) { + const options = { + url: this.baseUrl + this.URLs.invoiceSend(invoiceId), + body: requestData, + }; + return this._post(options); + } + + async cancelInvoice(invoiceId, version) { + const options = { + url: this.baseUrl + this.URLs.invoiceCancel(invoiceId), + body: { version }, + }; + return this._post(options); + } + + // ************************** Refunds Methods ********************************** + + async listRefunds(params = {}) { + const options = { + url: this.baseUrl + this.URLs.refunds, + query: params, + }; + return this._get(options); + } + + async createRefund(refundData) { + const options = { + url: this.baseUrl + this.URLs.refunds, + body: refundData, + }; + return this._post(options); + } + + async getRefund(refundId) { + const options = { + url: this.baseUrl + this.URLs.refundById(refundId), + }; + return this._get(options); + } + + // ************************** Webhooks Methods ********************************** + + async listWebhookSubscriptions() { + const options = { + url: this.baseUrl + this.URLs.webhookSubscriptions, + }; + return this._get(options); + } + + async createWebhookSubscription(subscriptionData) { + const options = { + url: this.baseUrl + this.URLs.webhookSubscriptions, + body: subscriptionData, + }; + return this._post(options); + } + + async getWebhookSubscription(subscriptionId) { + const options = { + url: this.baseUrl + this.URLs.webhookSubscriptionById(subscriptionId), + }; + return this._get(options); + } + + async updateWebhookSubscription(subscriptionId, subscriptionData) { + const options = { + url: this.baseUrl + this.URLs.webhookSubscriptionById(subscriptionId), + body: subscriptionData, + }; + return this._put(options); + } + + async deleteWebhookSubscription(subscriptionId) { + const options = { + url: this.baseUrl + this.URLs.webhookSubscriptionById(subscriptionId), + }; + return this._delete(options); + } + + // ************************** Helper Methods ********************************** + + _generateIdempotencyKey() { + return Date.now().toString() + Math.random().toString(36).substr(2, 9); + } + + async createSimplePayment(amount, currency, sourceId, locationId) { + const paymentData = { + idempotency_key: this._generateIdempotencyKey(), + amount_money: { + amount: amount, + currency: currency, + }, + source_id: sourceId, + location_id: locationId, + }; + + return this.createPayment(paymentData); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/square/defaultConfig.json b/packages/square/defaultConfig.json new file mode 100644 index 0000000..829f74a --- /dev/null +++ b/packages/square/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "square", + "label": "Square", + "productUrl": "https://squareup.com", + "apiDocs": "https://developer.squareup.com/docs", + "logoUrl": "https://images.squareup.com/favicon.ico", + "categories": [ + "Payments", + "Point of Sale", + "E-commerce", + "Business Management" + ], + "description": "Square is a financial services platform that provides payment processing, point-of-sale solutions, and business management tools" +} \ No newline at end of file diff --git a/packages/square/definition.js b/packages/square/definition.js new file mode 100644 index 0000000..f71441a --- /dev/null +++ b/packages/square/definition.js @@ -0,0 +1,56 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Square', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const merchants = await api.listMerchants(); + const merchant = merchants.merchants[0]; + return { + identifiers: { externalId: merchant.id, user: userId }, + details: { + name: merchant.business_name || merchant.country, + status: merchant.status + } + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const merchants = await api.listMerchants(); + const merchant = merchants.merchants[0]; + return { + identifiers: { externalId: merchant.id, user: userId }, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.listMerchants(); + }, + }, + env: { + client_id: process.env.SQUARE_CLIENT_ID, + client_secret: process.env.SQUARE_CLIENT_SECRET, + scope: process.env.SQUARE_SCOPE || 'MERCHANT_PROFILE_READ PAYMENTS_READ PAYMENTS_WRITE', + redirect_uri: `${process.env.REDIRECT_URI}/square`, + sandbox: process.env.SQUARE_SANDBOX === 'true', + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/square/index.js b/packages/square/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/square/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/square/openapi.json b/packages/square/openapi.json new file mode 100644 index 0000000..41361b5 --- /dev/null +++ b/packages/square/openapi.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bdd72cb76e267fa96df6a75bd24b01f7c2d28890903652c5973f4860a9472b2 +size 3113770 diff --git a/packages/v1-ready/stripe/api.js b/packages/stripe/api.js similarity index 100% rename from packages/v1-ready/stripe/api.js rename to packages/stripe/api.js diff --git a/packages/v1-ready/stripe/defaultConfig.json b/packages/stripe/defaultConfig.json similarity index 100% rename from packages/v1-ready/stripe/defaultConfig.json rename to packages/stripe/defaultConfig.json diff --git a/packages/v1-ready/stripe/definition.js b/packages/stripe/definition.js similarity index 100% rename from packages/v1-ready/stripe/definition.js rename to packages/stripe/definition.js diff --git a/packages/v1-ready/stripe/index.js b/packages/stripe/index.js similarity index 100% rename from packages/v1-ready/stripe/index.js rename to packages/stripe/index.js diff --git a/packages/v1-ready/stripe/package.json b/packages/stripe/package.json similarity index 100% rename from packages/v1-ready/stripe/package.json rename to packages/stripe/package.json diff --git a/packages/v1-ready/stripe/readme.md b/packages/stripe/readme.md similarity index 100% rename from packages/v1-ready/stripe/readme.md rename to packages/stripe/readme.md diff --git a/packages/stripe/specs/openapi.yaml b/packages/stripe/specs/openapi.yaml new file mode 100644 index 0000000..f7740a6 --- /dev/null +++ b/packages/stripe/specs/openapi.yaml @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a12423bb2cc4c573d5157d563b6b4497f5c24dcce5ff5c736a26788b877f3145 +size 5587720 diff --git a/packages/v1-ready/stripe/tests/api.test.js b/packages/stripe/tests/api.test.js similarity index 100% rename from packages/v1-ready/stripe/tests/api.test.js rename to packages/stripe/tests/api.test.js diff --git a/packages/telegram/README.md b/packages/telegram/README.md new file mode 100644 index 0000000..46c8774 --- /dev/null +++ b/packages/telegram/README.md @@ -0,0 +1,93 @@ +# Telegram Bot API Module + +A comprehensive Telegram Bot API module for the Frigg framework, providing full access to Telegram's Bot API for messaging, file handling, inline queries, and webhook management. + +## Features + +- **Bot Management**: Get bot info, set commands, manage bot settings +- **Messaging**: Send text, photos, documents, videos, audio, locations, contacts +- **File Handling**: Upload and download files, handle media groups +- **Interactive Elements**: Inline keyboards, callback queries, inline mode +- **Chat Management**: Get chat info, administrators, member count +- **Message Editing**: Edit text, captions, media, reply markup +- **Webhook Support**: Set up webhooks, handle incoming updates +- **Error Handling**: Comprehensive error handling and validation + +## Installation + +```bash +npm install @friggframework/api-module-telegram +``` + +## Environment Variables + +```env +TELEGRAM_BOT_TOKEN=your_bot_token_here +``` + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-telegram'); + +const telegramApi = new Api({ + bot_token: process.env.TELEGRAM_BOT_TOKEN +}); + +// Send a simple text message +await telegramApi.sendMessage(chatId, 'Hello, World!'); + +// Send a photo with caption +await telegramApi.sendPhoto(chatId, photoUrl, { + caption: 'Check out this image!' +}); + +// Set up webhook +await telegramApi.setWebhook('https://your-domain.com/webhook'); + +// Handle webhook data +const update = telegramApi.handleWebhook(webhookBody); +``` + +## Key Methods + +### Bot Management +- `getMe()` - Get bot information +- `setMyCommands(commands)` - Set bot commands +- `getMyCommands()` - Get current bot commands + +### Messaging +- `sendMessage(chatId, text, options)` - Send text message +- `sendPhoto(chatId, photo, options)` - Send photo +- `sendDocument(chatId, document, options)` - Send document +- `sendVideo(chatId, video, options)` - Send video +- `sendLocation(chatId, latitude, longitude, options)` - Send location + +### Webhook Management +- `setWebhook(url, options)` - Set webhook URL +- `getWebhookInfo()` - Get webhook information +- `deleteWebhook()` - Delete webhook +- `handleWebhook(body)` - Process webhook updates + +### Message Management +- `editMessageText(text, options)` - Edit message text +- `deleteMessage(chatId, messageId)` - Delete message +- `forwardMessage(chatId, fromChatId, messageId)` - Forward message + +## Authentication + +Telegram Bot API uses bot tokens for authentication. Get your bot token by creating a bot with [@BotFather](https://t.me/botfather) on Telegram. + +## Webhook Handling + +The module includes comprehensive webhook handling for all Telegram update types: +- Messages (text, media, location, contact, etc.) +- Edited messages +- Channel posts +- Inline queries +- Callback queries +- And more... + +## Error Handling + +All methods include proper error handling and will throw descriptive errors for invalid requests or authentication issues. \ No newline at end of file diff --git a/packages/telegram/api.js b/packages/telegram/api.js new file mode 100644 index 0000000..3165c27 --- /dev/null +++ b/packages/telegram/api.js @@ -0,0 +1,455 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); +const FormData = require('form-data'); +const fs = require('fs'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + this.bot_token = get(params, 'bot_token', null); + this.baseUrl = `https://api.telegram.org/bot${this.bot_token}`; + + this.URLs = { + // Bot info + getMe: '/getMe', + + // Messages + sendMessage: '/sendMessage', + forwardMessage: '/forwardMessage', + sendPhoto: '/sendPhoto', + sendAudio: '/sendAudio', + sendDocument: '/sendDocument', + sendVideo: '/sendVideo', + sendAnimation: '/sendAnimation', + sendVoice: '/sendVoice', + sendVideoNote: '/sendVideoNote', + sendMediaGroup: '/sendMediaGroup', + sendLocation: '/sendLocation', + sendVenue: '/sendVenue', + sendContact: '/sendContact', + sendPoll: '/sendPoll', + sendDice: '/sendDice', + sendChatAction: '/sendChatAction', + + // Updates + getUpdates: '/getUpdates', + setWebhook: '/setWebhook', + deleteWebhook: '/deleteWebhook', + getWebhookInfo: '/getWebhookInfo', + + // Chat management + getChat: '/getChat', + getChatAdministrators: '/getChatAdministrators', + getChatMemberCount: '/getChatMemberCount', + getChatMember: '/getChatMember', + setChatStickerSet: '/setChatStickerSet', + deleteChatStickerSet: '/deleteChatStickerSet', + + // Message editing + editMessageText: '/editMessageText', + editMessageCaption: '/editMessageCaption', + editMessageMedia: '/editMessageMedia', + editMessageReplyMarkup: '/editMessageReplyMarkup', + stopPoll: '/stopPoll', + deleteMessage: '/deleteMessage', + + // Inline mode + answerInlineQuery: '/answerInlineQuery', + answerCallbackQuery: '/answerCallbackQuery', + + // Files + getFile: '/getFile', + + // Commands + setMyCommands: '/setMyCommands', + deleteMyCommands: '/deleteMyCommands', + getMyCommands: '/getMyCommands', + }; + } + + async _request(url, options = {}) { + // Telegram API doesn't use standard auth headers + return super._request(url, options); + } + + // ************************** Bot Methods ********************************** + + async getMe() { + const options = { + url: this.baseUrl + this.URLs.getMe, + }; + return this._get(options); + } + + async setMyCommands(commands, params = {}) { + const options = { + url: this.baseUrl + this.URLs.setMyCommands, + body: { + commands, + ...params + } + }; + return this._post(options); + } + + async getMyCommands(params = {}) { + const options = { + url: this.baseUrl + this.URLs.getMyCommands, + query: params + }; + return this._get(options); + } + + async deleteMyCommands(params = {}) { + const options = { + url: this.baseUrl + this.URLs.deleteMyCommands, + body: params + }; + return this._post(options); + } + + // ************************** Message Methods ********************************** + + async sendMessage(chat_id, text, params = {}) { + const options = { + url: this.baseUrl + this.URLs.sendMessage, + body: { + chat_id, + text, + ...params + } + }; + return this._post(options); + } + + async forwardMessage(chat_id, from_chat_id, message_id, params = {}) { + const options = { + url: this.baseUrl + this.URLs.forwardMessage, + body: { + chat_id, + from_chat_id, + message_id, + ...params + } + }; + return this._post(options); + } + + async sendPhoto(chat_id, photo, params = {}) { + if (typeof photo === 'string' && !photo.startsWith('http')) { + // File upload + const form = new FormData(); + form.append('chat_id', chat_id); + form.append('photo', fs.createReadStream(photo)); + Object.keys(params).forEach(key => { + form.append(key, params[key]); + }); + + const options = { + url: this.baseUrl + this.URLs.sendPhoto, + body: form, + headers: form.getHeaders() + }; + return this._post(options, false); + } else { + const options = { + url: this.baseUrl + this.URLs.sendPhoto, + body: { + chat_id, + photo, + ...params + } + }; + return this._post(options); + } + } + + async sendDocument(chat_id, document, params = {}) { + if (typeof document === 'string' && !document.startsWith('http')) { + // File upload + const form = new FormData(); + form.append('chat_id', chat_id); + form.append('document', fs.createReadStream(document)); + Object.keys(params).forEach(key => { + form.append(key, params[key]); + }); + + const options = { + url: this.baseUrl + this.URLs.sendDocument, + body: form, + headers: form.getHeaders() + }; + return this._post(options, false); + } else { + const options = { + url: this.baseUrl + this.URLs.sendDocument, + body: { + chat_id, + document, + ...params + } + }; + return this._post(options); + } + } + + async sendVideo(chat_id, video, params = {}) { + const options = { + url: this.baseUrl + this.URLs.sendVideo, + body: { + chat_id, + video, + ...params + } + }; + return this._post(options); + } + + async sendLocation(chat_id, latitude, longitude, params = {}) { + const options = { + url: this.baseUrl + this.URLs.sendLocation, + body: { + chat_id, + latitude, + longitude, + ...params + } + }; + return this._post(options); + } + + async sendChatAction(chat_id, action) { + const options = { + url: this.baseUrl + this.URLs.sendChatAction, + body: { + chat_id, + action + } + }; + return this._post(options); + } + + async deleteMessage(chat_id, message_id) { + const options = { + url: this.baseUrl + this.URLs.deleteMessage, + body: { + chat_id, + message_id + } + }; + return this._post(options); + } + + // ************************** Edit Methods ********************************** + + async editMessageText(text, params = {}) { + const options = { + url: this.baseUrl + this.URLs.editMessageText, + body: { + text, + ...params + } + }; + return this._post(options); + } + + async editMessageCaption(params = {}) { + const options = { + url: this.baseUrl + this.URLs.editMessageCaption, + body: params + }; + return this._post(options); + } + + async editMessageReplyMarkup(params = {}) { + const options = { + url: this.baseUrl + this.URLs.editMessageReplyMarkup, + body: params + }; + return this._post(options); + } + + // ************************** Update Methods ********************************** + + async getUpdates(params = {}) { + const options = { + url: this.baseUrl + this.URLs.getUpdates, + query: params + }; + return this._get(options); + } + + async setWebhook(url, params = {}) { + const options = { + url: this.baseUrl + this.URLs.setWebhook, + body: { + url, + ...params + } + }; + return this._post(options); + } + + async deleteWebhook(params = {}) { + const options = { + url: this.baseUrl + this.URLs.deleteWebhook, + body: params + }; + return this._post(options); + } + + async getWebhookInfo() { + const options = { + url: this.baseUrl + this.URLs.getWebhookInfo, + }; + return this._get(options); + } + + // ************************** Chat Methods ********************************** + + async getChat(chat_id) { + const options = { + url: this.baseUrl + this.URLs.getChat, + query: { chat_id } + }; + return this._get(options); + } + + async getChatAdministrators(chat_id) { + const options = { + url: this.baseUrl + this.URLs.getChatAdministrators, + query: { chat_id } + }; + return this._get(options); + } + + async getChatMemberCount(chat_id) { + const options = { + url: this.baseUrl + this.URLs.getChatMemberCount, + query: { chat_id } + }; + return this._get(options); + } + + async getChatMember(chat_id, user_id) { + const options = { + url: this.baseUrl + this.URLs.getChatMember, + query: { chat_id, user_id } + }; + return this._get(options); + } + + // ************************** Inline Methods ********************************** + + async answerInlineQuery(inline_query_id, results, params = {}) { + const options = { + url: this.baseUrl + this.URLs.answerInlineQuery, + body: { + inline_query_id, + results, + ...params + } + }; + return this._post(options); + } + + async answerCallbackQuery(callback_query_id, params = {}) { + const options = { + url: this.baseUrl + this.URLs.answerCallbackQuery, + body: { + callback_query_id, + ...params + } + }; + return this._post(options); + } + + // ************************** File Methods ********************************** + + async getFile(file_id) { + const options = { + url: this.baseUrl + this.URLs.getFile, + query: { file_id } + }; + return this._get(options); + } + + async downloadFile(file_path) { + const fileUrl = `https://api.telegram.org/file/bot${this.bot_token}/${file_path}`; + const options = { + url: fileUrl, + }; + return this._get(options); + } + + // ************************** Webhook Handling ********************************** + + async handleWebhook(body) { + // Process incoming webhook data + const update = body; + + if (update.message) { + return { + type: 'message', + data: update.message + }; + } else if (update.edited_message) { + return { + type: 'edited_message', + data: update.edited_message + }; + } else if (update.channel_post) { + return { + type: 'channel_post', + data: update.channel_post + }; + } else if (update.edited_channel_post) { + return { + type: 'edited_channel_post', + data: update.edited_channel_post + }; + } else if (update.inline_query) { + return { + type: 'inline_query', + data: update.inline_query + }; + } else if (update.chosen_inline_result) { + return { + type: 'chosen_inline_result', + data: update.chosen_inline_result + }; + } else if (update.callback_query) { + return { + type: 'callback_query', + data: update.callback_query + }; + } else if (update.shipping_query) { + return { + type: 'shipping_query', + data: update.shipping_query + }; + } else if (update.pre_checkout_query) { + return { + type: 'pre_checkout_query', + data: update.pre_checkout_query + }; + } else if (update.poll) { + return { + type: 'poll', + data: update.poll + }; + } else if (update.poll_answer) { + return { + type: 'poll_answer', + data: update.poll_answer + }; + } + + return { + type: 'unknown', + data: update + }; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/telegram/defaultConfig.json b/packages/telegram/defaultConfig.json new file mode 100644 index 0000000..b5bfc69 --- /dev/null +++ b/packages/telegram/defaultConfig.json @@ -0,0 +1,9 @@ +{ + "name": "telegram", + "label": "Telegram", + "productUrl": "https://telegram.org", + "apiDocs": "https://core.telegram.org/bots/api", + "logoUrl": "https://friggframework.org/assets/img/telegram-icon.png", + "categories": ["Communication", "Messaging"], + "description": "Telegram Bot API for messaging, notifications, and bot development." +} \ No newline at end of file diff --git a/packages/telegram/definition.js b/packages/telegram/definition.js new file mode 100644 index 0000000..94660cd --- /dev/null +++ b/packages/telegram/definition.js @@ -0,0 +1,68 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Telegram', + requiredAuthMethods: { + getToken: async function (api, params) { + // Telegram uses bot tokens directly, no OAuth flow + return { + access_token: api.bot_token, + token_type: 'Bot' + }; + }, + + getEntityDetails: async function (api, userId) { + const botInfo = await api.getMe(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: botInfo.result.id.toString(), + user: userId + }, + details: { + username: botInfo.result.username, + first_name: botInfo.result.first_name, + is_bot: botInfo.result.is_bot, + can_join_groups: botInfo.result.can_join_groups, + can_read_all_group_messages: botInfo.result.can_read_all_group_messages, + supports_inline_queries: botInfo.result.supports_inline_queries + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['bot_token'], + entity: [], + }, + + getCredentialDetails: async function (api, userId) { + const botInfo = await api.getMe(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: botInfo.result.id.toString(), + user: userId + }, + details: { + bot_token: api.bot_token + }, + }; + }, + + testAuthRequest: function (api) { + return api.getMe(); + }, + }, + env: { + bot_token: process.env.TELEGRAM_BOT_TOKEN, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/telegram/index.js b/packages/telegram/index.js new file mode 100644 index 0000000..be08f56 --- /dev/null +++ b/packages/telegram/index.js @@ -0,0 +1,5 @@ +const Config = require('./defaultConfig.json'); +const { Definition } = require('./definition.js'); +const { Api } = require('./api.js'); + +module.exports = { Config, Definition, Api }; \ No newline at end of file diff --git a/packages/telegram/package.json b/packages/telegram/package.json new file mode 100644 index 0000000..c82feef --- /dev/null +++ b/packages/telegram/package.json @@ -0,0 +1,29 @@ +{ + "name": "@friggframework/api-module-telegram", + "version": "1.0.0", + "description": "Telegram Bot API module for the Frigg framework", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "frigg", + "telegram", + "bot", + "messaging", + "api" + ], + "author": "Frigg Framework", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0", + "form-data": "^4.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/packages/v1-ready/microsoft-teams/.eslintrc.json b/packages/terminus/.eslintrc.json similarity index 100% rename from packages/v1-ready/microsoft-teams/.eslintrc.json rename to packages/terminus/.eslintrc.json diff --git a/packages/needs-updating/terminus/CHANGELOG.md b/packages/terminus/CHANGELOG.md similarity index 100% rename from packages/needs-updating/terminus/CHANGELOG.md rename to packages/terminus/CHANGELOG.md diff --git a/packages/v1-ready/hubspot/LICENSE.md b/packages/terminus/LICENSE.md similarity index 100% rename from packages/v1-ready/hubspot/LICENSE.md rename to packages/terminus/LICENSE.md diff --git a/packages/needs-updating/terminus/README.md b/packages/terminus/README.md similarity index 100% rename from packages/needs-updating/terminus/README.md rename to packages/terminus/README.md diff --git a/packages/needs-updating/terminus/api.js b/packages/terminus/api.js similarity index 100% rename from packages/needs-updating/terminus/api.js rename to packages/terminus/api.js diff --git a/packages/needs-updating/terminus/defaultConfig.json b/packages/terminus/defaultConfig.json similarity index 100% rename from packages/needs-updating/terminus/defaultConfig.json rename to packages/terminus/defaultConfig.json diff --git a/packages/terminus/definition.js b/packages/terminus/definition.js new file mode 100644 index 0000000..5445752 --- /dev/null +++ b/packages/terminus/definition.js @@ -0,0 +1,39 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Terminus', + requiredAuthMethods: { + getToken: async function(api, params) { + return api.getTokenFromApiKey(params.data.apiKey); + }, + getEntityDetails: async function(api, callbackParams, tokenResponse, userId) { + return { + identifiers: {externalId: params.data.apiKey || 'default', user: userId}, + details: {name: params.data.apiKey || 'Default'} + }; + }, + getCredentialDetails: async function(api, userId) { + return { + identifiers: {externalId: 'default', user: userId}, + details: {} + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'apiKey'], + entity: [] + }, + testAuthRequest: async function(api) { + return await api.testAuth(); + } + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/terminus/index.js b/packages/terminus/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/terminus/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/openphone/jest.config.js b/packages/terminus/jest.config.js similarity index 100% rename from packages/v1-ready/openphone/jest.config.js rename to packages/terminus/jest.config.js diff --git a/packages/needs-updating/terminus/manager.test.js b/packages/terminus/manager.test.js similarity index 100% rename from packages/needs-updating/terminus/manager.test.js rename to packages/terminus/manager.test.js diff --git a/packages/needs-updating/terminus/mocks/accountLists/addAccountsToList.js b/packages/terminus/mocks/accountLists/addAccountsToList.js similarity index 100% rename from packages/needs-updating/terminus/mocks/accountLists/addAccountsToList.js rename to packages/terminus/mocks/accountLists/addAccountsToList.js diff --git a/packages/needs-updating/terminus/mocks/accountLists/createAccountList.js b/packages/terminus/mocks/accountLists/createAccountList.js similarity index 100% rename from packages/needs-updating/terminus/mocks/accountLists/createAccountList.js rename to packages/terminus/mocks/accountLists/createAccountList.js diff --git a/packages/needs-updating/terminus/mocks/accountLists/listAccountLists.js b/packages/terminus/mocks/accountLists/listAccountLists.js similarity index 100% rename from packages/needs-updating/terminus/mocks/accountLists/listAccountLists.js rename to packages/terminus/mocks/accountLists/listAccountLists.js diff --git a/packages/needs-updating/terminus/mocks/accountLists/removeAccountsFromList.js b/packages/terminus/mocks/accountLists/removeAccountsFromList.js similarity index 100% rename from packages/needs-updating/terminus/mocks/accountLists/removeAccountsFromList.js rename to packages/terminus/mocks/accountLists/removeAccountsFromList.js diff --git a/packages/needs-updating/terminus/mocks/apiMock.js b/packages/terminus/mocks/apiMock.js similarity index 100% rename from packages/needs-updating/terminus/mocks/apiMock.js rename to packages/terminus/mocks/apiMock.js diff --git a/packages/needs-updating/terminus/mocks/folders/createFolder.js b/packages/terminus/mocks/folders/createFolder.js similarity index 100% rename from packages/needs-updating/terminus/mocks/folders/createFolder.js rename to packages/terminus/mocks/folders/createFolder.js diff --git a/packages/needs-updating/terminus/mocks/folders/listFolders.js b/packages/terminus/mocks/folders/listFolders.js similarity index 100% rename from packages/needs-updating/terminus/mocks/folders/listFolders.js rename to packages/terminus/mocks/folders/listFolders.js diff --git a/packages/needs-updating/terminus/test/Api.test.js b/packages/terminus/test/Api.test.js similarity index 100% rename from packages/needs-updating/terminus/test/Api.test.js rename to packages/terminus/test/Api.test.js diff --git a/packages/needs-updating/terminus/test/Manager.test.js b/packages/terminus/test/Manager.test.js similarity index 100% rename from packages/needs-updating/terminus/test/Manager.test.js rename to packages/terminus/test/Manager.test.js diff --git a/packages/todoist/api.js b/packages/todoist/api.js new file mode 100644 index 0000000..e6b82d6 --- /dev/null +++ b/packages/todoist/api.js @@ -0,0 +1,692 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); +const { v4: uuidv4 } = require('uuid'); + +// Todoist REST API client +// Supports API Token and OAuth2 authentication +// Documentation: https://developer.todoist.com/rest/v2/ + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.todoist.com/rest/v2'; + this.syncUrl = 'https://api.todoist.com/sync/v9'; + + // API Token authentication (preferred for most use cases) + this.apiToken = get(params, 'apiToken', null); + + // OAuth2 credentials + this.client_id = get(params, 'client_id', process.env.TODOIST_CLIENT_ID); + this.client_secret = get(params, 'client_secret', process.env.TODOIST_CLIENT_SECRET); + this.access_token = get(params, 'access_token', null); + + // OAuth endpoints + this.authorizationUri = 'https://todoist.com/oauth/authorize'; + this.tokenUri = 'https://todoist.com/oauth/access_token'; + + this.URLs = { + // Projects + projects: '/projects', + projectById: (projectId) => `/projects/${projectId}`, + + // Sections + sections: '/sections', + sectionById: (sectionId) => `/sections/${sectionId}`, + sectionsByProject: (projectId) => `/sections?project_id=${projectId}`, + + // Tasks + tasks: '/tasks', + taskById: (taskId) => `/tasks/${taskId}`, + tasksByProject: (projectId) => `/tasks?project_id=${projectId}`, + tasksBySection: (sectionId) => `/tasks?section_id=${sectionId}`, + tasksByLabel: (labelId) => `/tasks?label_id=${labelId}`, + tasksByFilter: (filter) => `/tasks?filter=${encodeURIComponent(filter)}`, + taskComments: (taskId) => `/comments?task_id=${taskId}`, + taskClose: (taskId) => `/tasks/${taskId}/close`, + taskReopen: (taskId) => `/tasks/${taskId}/reopen`, + + // Labels + labels: '/labels', + labelById: (labelId) => `/labels/${labelId}`, + personalLabels: '/labels?is_shared=false', + sharedLabels: '/labels?is_shared=true', + + // Comments + comments: '/comments', + commentById: (commentId) => `/comments/${commentId}`, + commentsByProject: (projectId) => `/comments?project_id=${projectId}`, + + // Collaborators + collaborators: (projectId) => `/projects/${projectId}/collaborators`, + + // Sync API endpoints (for advanced features) + sync: '/sync', + syncCompleted: '/completed/get_all', + syncActivity: '/activity/get', + syncStats: '/completed/get_stats', + syncBackups: '/backups/get', + syncQuickAdd: '/quick/add', + + // User info + syncUser: '/user', + + // Webhooks (through sync API) + webhooks: '/webhooks', + }; + + // Request ID for sync API (prevents duplicate requests) + this.generateRequestId = () => uuidv4(); + } + + // Generate OAuth authorization URL + getAuthUri(scopes = ['data:read_write']) { + const params = new URLSearchParams({ + client_id: this.client_id, + scope: scopes.join(','), + state: this.state || this.generateRequestId(), + }); + + return `${this.authorizationUri}?${params.toString()}`; + } + + // Exchange authorization code for access token + async getTokenFromCode(code) { + const tokenData = { + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri, + }; + + const options = { + url: this.tokenUri, + body: tokenData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }; + + const response = await this._post(options, false); + await this.setTokens(response); + return response; + } + + // Set access token + async setTokens(tokenResponse) { + this.access_token = tokenResponse.access_token; + + if (tokenResponse.token_type) { + this.token_type = tokenResponse.token_type; + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + // Add authentication headers + addAuthHeaders(options) { + let authHeader; + + if (this.apiToken) { + // Use API Token authentication + authHeader = `Bearer ${this.apiToken}`; + } else if (this.access_token) { + // Use OAuth2 access token + authHeader = `Bearer ${this.access_token}`; + } else { + throw new Error('No authentication token available'); + } + + options.headers = { + ...options.headers, + 'Authorization': authHeader, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + } + + // Add sync API headers (for sync endpoints) + addSyncHeaders(options) { + let authHeader; + + if (this.apiToken) { + authHeader = `Bearer ${this.apiToken}`; + } else if (this.access_token) { + authHeader = `Bearer ${this.access_token}`; + } else { + throw new Error('No authentication token available'); + } + + options.headers = { + ...options.headers, + 'Authorization': authHeader, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + } + + async _get(options, useSync = false) { + options.url = (useSync ? this.syncUrl : this.baseUrl) + options.url; + if (useSync) { + this.addSyncHeaders(options); + } else { + this.addAuthHeaders(options); + } + return super._get(options); + } + + async _post(options, stringify = true, useSync = false) { + options.url = (useSync ? this.syncUrl : this.baseUrl) + options.url; + if (useSync) { + this.addSyncHeaders(options); + } else { + this.addAuthHeaders(options); + } + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._patch(options, stringify); + } + + async _delete(options) { + options.url = this.baseUrl + options.url; + this.addAuthHeaders(options); + return super._delete(options); + } + + // ************************** Projects ********************************** + + async createProject(projectData) { + const options = { + url: this.URLs.projects, + body: projectData, + }; + return this._post(options); + } + + async getProjects() { + const options = { + url: this.URLs.projects, + }; + return this._get(options); + } + + async getProjectById(projectId) { + const options = { + url: this.URLs.projectById(projectId), + }; + return this._get(options); + } + + async updateProject(projectId, projectData) { + const options = { + url: this.URLs.projectById(projectId), + body: projectData, + }; + return this._post(options); + } + + async deleteProject(projectId) { + const options = { + url: this.URLs.projectById(projectId), + }; + return this._delete(options); + } + + async getProjectCollaborators(projectId) { + const options = { + url: this.URLs.collaborators(projectId), + }; + return this._get(options); + } + + // ************************** Sections ********************************** + + async createSection(sectionData) { + const options = { + url: this.URLs.sections, + body: sectionData, + }; + return this._post(options); + } + + async getSections(projectId = null) { + const url = projectId ? this.URLs.sectionsByProject(projectId) : this.URLs.sections; + const options = { + url: url, + }; + return this._get(options); + } + + async getSectionById(sectionId) { + const options = { + url: this.URLs.sectionById(sectionId), + }; + return this._get(options); + } + + async updateSection(sectionId, sectionData) { + const options = { + url: this.URLs.sectionById(sectionId), + body: sectionData, + }; + return this._post(options); + } + + async deleteSection(sectionId) { + const options = { + url: this.URLs.sectionById(sectionId), + }; + return this._delete(options); + } + + // ************************** Tasks ********************************** + + async createTask(taskData) { + const options = { + url: this.URLs.tasks, + body: taskData, + }; + return this._post(options); + } + + async getTasks(params = {}) { + let url = this.URLs.tasks; + + // Handle different filtering options + if (params.project_id) { + url = this.URLs.tasksByProject(params.project_id); + delete params.project_id; + } else if (params.section_id) { + url = this.URLs.tasksBySection(params.section_id); + delete params.section_id; + } else if (params.label_id) { + url = this.URLs.tasksByLabel(params.label_id); + delete params.label_id; + } else if (params.filter) { + url = this.URLs.tasksByFilter(params.filter); + delete params.filter; + } + + const options = { + url: url, + query: params + }; + return this._get(options); + } + + async getTaskById(taskId) { + const options = { + url: this.URLs.taskById(taskId), + }; + return this._get(options); + } + + async updateTask(taskId, taskData) { + const options = { + url: this.URLs.taskById(taskId), + body: taskData, + }; + return this._post(options); + } + + async deleteTask(taskId) { + const options = { + url: this.URLs.taskById(taskId), + }; + return this._delete(options); + } + + async closeTask(taskId) { + const options = { + url: this.URLs.taskClose(taskId), + body: {}, + }; + return this._post(options); + } + + async reopenTask(taskId) { + const options = { + url: this.URLs.taskReopen(taskId), + body: {}, + }; + return this._post(options); + } + + // ************************** Labels ********************************** + + async createLabel(labelData) { + const options = { + url: this.URLs.labels, + body: labelData, + }; + return this._post(options); + } + + async getLabels(isShared = null) { + let url = this.URLs.labels; + + if (isShared === true) { + url = this.URLs.sharedLabels; + } else if (isShared === false) { + url = this.URLs.personalLabels; + } + + const options = { + url: url, + }; + return this._get(options); + } + + async getLabelById(labelId) { + const options = { + url: this.URLs.labelById(labelId), + }; + return this._get(options); + } + + async updateLabel(labelId, labelData) { + const options = { + url: this.URLs.labelById(labelId), + body: labelData, + }; + return this._post(options); + } + + async deleteLabel(labelId) { + const options = { + url: this.URLs.labelById(labelId), + }; + return this._delete(options); + } + + // ************************** Comments ********************************** + + async createComment(commentData) { + const options = { + url: this.URLs.comments, + body: commentData, + }; + return this._post(options); + } + + async getComments(params = {}) { + let url = this.URLs.comments; + + if (params.task_id) { + url = this.URLs.taskComments(params.task_id); + delete params.task_id; + } else if (params.project_id) { + url = this.URLs.commentsByProject(params.project_id); + delete params.project_id; + } + + const options = { + url: url, + query: params + }; + return this._get(options); + } + + async getCommentById(commentId) { + const options = { + url: this.URLs.commentById(commentId), + }; + return this._get(options); + } + + async updateComment(commentId, commentData) { + const options = { + url: this.URLs.commentById(commentId), + body: commentData, + }; + return this._post(options); + } + + async deleteComment(commentId) { + const options = { + url: this.URLs.commentById(commentId), + }; + return this._delete(options); + } + + // ************************** Sync API Methods ********************************** + + async getUser() { + const options = { + url: this.URLs.syncUser, + }; + return this._get(options, true); + } + + async syncData(commands = [], resourceTypes = ['all']) { + const options = { + url: this.URLs.sync, + body: new URLSearchParams({ + token: this.apiToken || this.access_token, + sync_token: '*', + resource_types: JSON.stringify(resourceTypes), + commands: JSON.stringify(commands) + }), + }; + return this._post(options, false, true); + } + + async quickAdd(text) { + const options = { + url: this.URLs.syncQuickAdd, + body: new URLSearchParams({ + token: this.apiToken || this.access_token, + text: text + }), + }; + return this._post(options, false, true); + } + + async getCompletedTasks(params = {}) { + const defaultParams = { + token: this.apiToken || this.access_token, + ...params + }; + + const options = { + url: this.URLs.syncCompleted, + body: new URLSearchParams(defaultParams), + }; + return this._post(options, false, true); + } + + async getProductivityStats() { + const options = { + url: this.URLs.syncStats, + body: new URLSearchParams({ + token: this.apiToken || this.access_token + }), + }; + return this._post(options, false, true); + } + + async getActivity(params = {}) { + const defaultParams = { + token: this.apiToken || this.access_token, + ...params + }; + + const options = { + url: this.URLs.syncActivity, + body: new URLSearchParams(defaultParams), + }; + return this._post(options, false, true); + } + + async getBackups() { + const options = { + url: this.URLs.syncBackups, + body: new URLSearchParams({ + token: this.apiToken || this.access_token + }), + }; + return this._post(options, false, true); + } + + // ************************** Advanced Features ********************************** + + async moveTaskToProject(taskId, projectId, sectionId = null) { + const updateData = { + project_id: projectId + }; + + if (sectionId) { + updateData.section_id = sectionId; + } + + return this.updateTask(taskId, updateData); + } + + async duplicateTask(taskId, projectId = null) { + // First get the original task + const originalTask = await this.getTaskById(taskId); + + // Create a new task with similar data + const duplicateData = { + content: originalTask.content, + description: originalTask.description, + project_id: projectId || originalTask.project_id, + section_id: originalTask.section_id, + parent_id: originalTask.parent_id, + order: originalTask.order, + label_ids: originalTask.label_ids, + priority: originalTask.priority, + due_string: originalTask.due?.string, + due_date: originalTask.due?.date, + due_datetime: originalTask.due?.datetime, + due_lang: originalTask.due?.lang, + assignee_id: originalTask.assignee_id, + }; + + return this.createTask(duplicateData); + } + + async bulkCreateTasks(tasksData) { + const commands = tasksData.map((taskData, index) => ({ + type: 'item_add', + uuid: this.generateRequestId(), + temp_id: `temp_${index}`, + args: taskData + })); + + return this.syncData(commands); + } + + async bulkUpdateTasks(updates) { + const commands = updates.map(update => ({ + type: 'item_update', + uuid: this.generateRequestId(), + args: { + id: update.id, + ...update.data + } + })); + + return this.syncData(commands); + } + + async bulkDeleteTasks(taskIds) { + const commands = taskIds.map(taskId => ({ + type: 'item_delete', + uuid: this.generateRequestId(), + args: { + id: taskId + } + })); + + return this.syncData(commands); + } + + // ************************** Filters and Search ********************************** + + async getTasksByFilter(filter) { + const options = { + url: this.URLs.tasksByFilter(filter), + }; + return this._get(options); + } + + async searchTasks(query) { + // Use filter syntax for searching + return this.getTasksByFilter(`search: ${query}`); + } + + async getOverdueTasks() { + return this.getTasksByFilter('overdue'); + } + + async getTodayTasks() { + return this.getTasksByFilter('today'); + } + + async getThisWeekTasks() { + return this.getTasksByFilter('7 days'); + } + + async getTasksByPriority(priority) { + return this.getTasksByFilter(`p${priority}`); + } + + async getTasksByAssignee(assigneeId) { + return this.getTasksByFilter(`assigned by: ${assigneeId}`); + } + + // ************************** Sharing and Collaboration ********************************** + + async shareProject(projectId, email, messageType = 'invitation') { + // This would typically be done through the web interface + // but can be implemented using sync API commands + const command = { + type: 'share_project', + uuid: this.generateRequestId(), + args: { + project_id: projectId, + email: email, + message_type: messageType + } + }; + + return this.syncData([command]); + } + + async acceptInvitation(invitationId, invitationSecret) { + const command = { + type: 'accept_invitation', + uuid: this.generateRequestId(), + args: { + invitation_id: invitationId, + invitation_secret: invitationSecret + } + }; + + return this.syncData([command]); + } + + async rejectInvitation(invitationId, invitationSecret) { + const command = { + type: 'reject_invitation', + uuid: this.generateRequestId(), + args: { + invitation_id: invitationId, + invitation_secret: invitationSecret + } + }; + + return this.syncData([command]); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/todoist/defaultConfig.json b/packages/todoist/defaultConfig.json new file mode 100644 index 0000000..9e242fa --- /dev/null +++ b/packages/todoist/defaultConfig.json @@ -0,0 +1,12 @@ +{ + "name": "todoist", + "label": "Todoist", + "productUrl": "https://todoist.com", + "apiDocs": "https://developer.todoist.com/", + "logoUrl": "https://friggframework.org/assets/img/todoist-icon.png", + "categories": [ + "Productivity", + "Task Management" + ], + "description": "Todoist is a task management application that helps individuals and teams organize their tasks and projects with powerful features for collaboration and productivity." +} \ No newline at end of file diff --git a/packages/todoist/definition.js b/packages/todoist/definition.js new file mode 100644 index 0000000..1e5444b --- /dev/null +++ b/packages/todoist/definition.js @@ -0,0 +1,93 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'Todoist', + requiredAuthMethods: { + getToken: async function (api, params) { + // Check for OAuth code + const code = get(params.data, 'code'); + if (code) { + return api.getTokenFromCode(code); + } + + // Check for API token (direct authentication) + const apiToken = get(params.data, 'apiToken') || get(params.data, 'api_token'); + if (apiToken) { + return { + api_token: apiToken, + access_token: apiToken, + token_type: 'Bearer' + }; + } + + // Check for OAuth access token + const access_token = get(params.data, 'access_token'); + if (access_token) { + return { + access_token: access_token, + token_type: 'Bearer' + }; + } + + throw new Error('Missing required Todoist credentials: code, apiToken, or access_token'); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const user = await api.getUser(); + + return { + identifiers: {externalId: user.id.toString(), user: userId}, + details: { + name: user.full_name, + email: user.email, + avatar: user.avatar_big || user.avatar_medium || user.avatar_small, + timezone: user.timezone, + language: user.lang, + premium: user.is_premium, + karma: user.karma, + karma_trend: user.karma_trend, + date_format: user.date_format, + time_format: user.time_format, + sort_order: user.sort_order, + week_start: user.start_day + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'api_token', 'token_type' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const user = await api.getUser(); + + return { + identifiers: {externalId: user.id.toString(), user: userId}, + details: { + name: user.full_name, + email: user.email, + timezone: user.timezone + } + }; + }, + testAuthRequest: async function (api) { + return api.getUser() + }, + }, + env: { + client_id: process.env.TODOIST_CLIENT_ID, + client_secret: process.env.TODOIST_CLIENT_SECRET, + api_token: process.env.TODOIST_API_TOKEN, + redirect_uri: `${process.env.REDIRECT_URI}/todoist`, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/todoist/index.js b/packages/todoist/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/todoist/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/todoist/jest.config.js b/packages/todoist/jest.config.js new file mode 100644 index 0000000..fa8c051 --- /dev/null +++ b/packages/todoist/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEnv: ['/tests/setup.js'] +}; \ No newline at end of file diff --git a/packages/todoist/package.json b/packages/todoist/package.json new file mode 100644 index 0000000..49dd6c7 --- /dev/null +++ b/packages/todoist/package.json @@ -0,0 +1,29 @@ +{ + "name": "@friggframework/api-module-todoist", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Todoist API module that lets the Frigg Framework interact with Todoist", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16", + "uuid": "^9.0.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/todoist/tests/api.test.js b/packages/todoist/tests/api.test.js new file mode 100644 index 0000000..dd59491 --- /dev/null +++ b/packages/todoist/tests/api.test.js @@ -0,0 +1,134 @@ +const { Api } = require('../api'); + +// Mock uuid to avoid dependency issues in tests +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mocked-uuid-12345') +})); + +describe('Todoist API', () => { + let api; + + beforeEach(() => { + api = new Api({ + apiToken: 'test_api_token', + client_id: 'test_client_id', + client_secret: 'test_client_secret' + }); + }); + + describe('Constructor', () => { + test('should initialize with API token', () => { + expect(api.apiToken).toBe('test_api_token'); + expect(api.baseUrl).toBe('https://api.todoist.com/rest/v2'); + expect(api.syncUrl).toBe('https://api.todoist.com/sync/v9'); + }); + + test('should initialize with OAuth credentials', () => { + const oauthApi = new Api({ + client_id: 'test_client_id', + client_secret: 'test_client_secret', + access_token: 'test_access_token' + }); + + expect(oauthApi.access_token).toBe('test_access_token'); + expect(oauthApi.client_id).toBe('test_client_id'); + }); + + test('should set OAuth endpoints correctly', () => { + expect(api.authorizationUri).toBe('https://todoist.com/oauth/authorize'); + expect(api.tokenUri).toBe('https://todoist.com/oauth/access_token'); + }); + }); + + describe('Authentication', () => { + test('should generate correct auth URI', () => { + const authUri = api.getAuthUri(['data:read_write']); + + expect(authUri).toContain('https://todoist.com/oauth/authorize'); + expect(authUri).toContain('client_id=test_client_id'); + expect(authUri).toContain('scope=data%3Aread_write'); + }); + + test('should add API token auth headers', () => { + const options = { headers: {} }; + api.addAuthHeaders(options); + + expect(options.headers.Authorization).toBe('Bearer test_api_token'); + expect(options.headers['Content-Type']).toBe('application/json'); + }); + + test('should add OAuth token auth headers', () => { + api.apiToken = null; + api.access_token = 'oauth_token'; + + const options = { headers: {} }; + api.addAuthHeaders(options); + + expect(options.headers.Authorization).toBe('Bearer oauth_token'); + }); + + test('should throw error when no token available', () => { + api.apiToken = null; + api.access_token = null; + + const options = { headers: {} }; + expect(() => api.addAuthHeaders(options)).toThrow('No authentication token available'); + }); + }); + + describe('URL Construction', () => { + test('should construct project URLs correctly', () => { + expect(api.URLs.projects).toBe('/projects'); + expect(api.URLs.projectById(123)).toBe('/projects/123'); + }); + + test('should construct task URLs correctly', () => { + expect(api.URLs.tasks).toBe('/tasks'); + expect(api.URLs.taskById(456)).toBe('/tasks/456'); + expect(api.URLs.tasksByProject(123)).toBe('/tasks?project_id=123'); + expect(api.URLs.tasksBySection(789)).toBe('/tasks?section_id=789'); + expect(api.URLs.taskClose(456)).toBe('/tasks/456/close'); + }); + + test('should construct label URLs correctly', () => { + expect(api.URLs.labels).toBe('/labels'); + expect(api.URLs.labelById(101)).toBe('/labels/101'); + expect(api.URLs.personalLabels).toBe('/labels?is_shared=false'); + expect(api.URLs.sharedLabels).toBe('/labels?is_shared=true'); + }); + + test('should construct comment URLs correctly', () => { + expect(api.URLs.comments).toBe('/comments'); + expect(api.URLs.commentById(202)).toBe('/comments/202'); + expect(api.URLs.taskComments(456)).toBe('/comments?task_id=456'); + }); + }); + + describe('Request ID Generation', () => { + test('should generate unique request IDs', () => { + const id1 = api.generateRequestId(); + const id2 = api.generateRequestId(); + + expect(id1).toBe('mocked-uuid-12345'); + expect(id2).toBe('mocked-uuid-12345'); + expect(typeof id1).toBe('string'); + }); + }); + + describe('Sync Headers', () => { + test('should add sync API headers', () => { + const options = { headers: {} }; + api.addSyncHeaders(options); + + expect(options.headers.Authorization).toBe('Bearer test_api_token'); + expect(options.headers['Content-Type']).toBe('application/x-www-form-urlencoded'); + }); + }); + + describe('Task Filtering', () => { + test('should construct filter URLs correctly', () => { + const filterUrl = api.URLs.tasksByFilter('today & p1'); + expect(filterUrl).toBe('/tasks?filter=today%20%26%20p1'); + }); + }); +}); \ No newline at end of file diff --git a/packages/todoist/tests/setup.js b/packages/todoist/tests/setup.js new file mode 100644 index 0000000..bd23b73 --- /dev/null +++ b/packages/todoist/tests/setup.js @@ -0,0 +1,12 @@ +// Test setup file for Todoist API module +require('dotenv').config(); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; \ No newline at end of file diff --git a/packages/v1-ready/trello/README.md b/packages/trello/README.md similarity index 100% rename from packages/v1-ready/trello/README.md rename to packages/trello/README.md diff --git a/packages/v1-ready/trello/fenestra/platform.fenestra.yaml b/packages/trello/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/trello/fenestra/platform.fenestra.yaml rename to packages/trello/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/trello/fenestra/schemas/trello-validation.json b/packages/trello/fenestra/schemas/trello-validation.json similarity index 100% rename from packages/v1-ready/trello/fenestra/schemas/trello-validation.json rename to packages/trello/fenestra/schemas/trello-validation.json diff --git a/packages/v1-ready/trello/index.js b/packages/trello/index.js similarity index 100% rename from packages/v1-ready/trello/index.js rename to packages/trello/index.js diff --git a/packages/trello/openapi.yaml b/packages/trello/openapi.yaml new file mode 100644 index 0000000..d86b0bf --- /dev/null +++ b/packages/trello/openapi.yaml @@ -0,0 +1,15037 @@ +swagger: '2.0' +schemes: + - https +host: trello.com +basePath: /1 +info: + contact: + name: Trello + url: 'https://trello.com/home' + description: |- + This document describes the REST API of Trello as published by Trello.com. + - Official Documentation + - The HTML pages that were scraped in order to generate this specification. + license: + name: 'Trello : Terms of Service' + url: 'https://trello.com/legal' + termsOfService: 'https://trello.com/legal' + title: Trello + version: '1.0' +externalDocs: + url: 'https://developers.trello.com' +securityDefinitions: + api_key: + in: query + name: key + type: apiKey + api_token: + in: query + name: token + type: apiKey +tags: + - description: 'https://trello.com/docs/api/action/index.html' + name: action + - description: 'https://trello.com/docs/api/batch/index.html' + name: batch + - description: 'https://trello.com/docs/api/board/index.html' + name: board + - description: 'https://trello.com/docs/api/card/index.html' + name: card + - description: 'https://trello.com/docs/api/checklist/index.html' + name: checklist + - description: 'https://trello.com/docs/api/label/index.html' + name: label + - description: 'https://trello.com/docs/api/list/index.html' + name: list + - description: 'https://trello.com/docs/api/member/index.html' + name: member + - description: 'https://trello.com/docs/api/notification/index.html' + name: notification + - description: 'https://trello.com/docs/api/organization/index.html' + name: organization + - description: 'https://trello.com/docs/api/search/index.html' + name: search + - description: 'https://trello.com/docs/api/session/index.html' + name: session + - description: 'https://trello.com/docs/api/token/index.html' + name: token + - description: 'https://trello.com/docs/api/type/index.html' + name: type + - description: 'https://trello.com/docs/api/webhook/index.html' + name: webhook +paths: + '/actions/{idAction}': + delete: + operationId: deleteActionsByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteActionsByIdAction() + tags: + - action + get: + operationId: getActionsByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: fields + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsByIdAction() + tags: + - action + put: + operationId: updateActionsByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: Attributes of "Actions" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/actions' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateActionsByIdAction() + tags: + - action + '/actions/{idAction}/board': + get: + operationId: getActionsBoardByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsBoardByIdAction() + tags: + - action + '/actions/{idAction}/board/{field}': + get: + operationId: getActionsBoardByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsBoardByIdActionByField() + tags: + - action + '/actions/{idAction}/card': + get: + operationId: getActionsCardByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsCardByIdAction() + tags: + - action + '/actions/{idAction}/card/{field}': + get: + operationId: getActionsCardByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsCardByIdActionByField() + tags: + - action + '/actions/{idAction}/display': + get: + operationId: getActionsDisplayByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsDisplayByIdAction() + tags: + - action + '/actions/{idAction}/entities': + get: + operationId: getActionsEntitiesByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsEntitiesByIdAction() + tags: + - action + '/actions/{idAction}/list': + get: + operationId: getActionsListByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsListByIdAction() + tags: + - action + '/actions/{idAction}/list/{field}': + get: + operationId: getActionsListByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsListByIdActionByField() + tags: + - action + '/actions/{idAction}/member': + get: + operationId: getActionsMemberByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsMemberByIdAction() + tags: + - action + '/actions/{idAction}/member/{field}': + get: + operationId: getActionsMemberByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsMemberByIdActionByField() + tags: + - action + '/actions/{idAction}/memberCreator': + get: + operationId: getActionsMemberCreatorByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsMemberCreatorByIdAction() + tags: + - action + '/actions/{idAction}/memberCreator/{field}': + get: + operationId: getActionsMemberCreatorByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsMemberCreatorByIdActionByField() + tags: + - action + '/actions/{idAction}/organization': + get: + operationId: getActionsOrganizationByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsOrganizationByIdAction() + tags: + - action + '/actions/{idAction}/organization/{field}': + get: + operationId: getActionsOrganizationByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getActionsOrganizationByIdActionByField() + tags: + - action + '/actions/{idAction}/text': + put: + operationId: updateActionsTextByIdAction + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: Attributes of "Actions Text" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/actions_text' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateActionsTextByIdAction() + tags: + - action + '/actions/{idAction}/{field}': + get: + operationId: getActionsByIdActionByField + parameters: + - description: idAction + in: path + name: idAction + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getActionsByIdActionByField() + tags: + - action + /batch: + get: + operationId: getBatch + parameters: + - description: 'list of API v1 GET routes, not including the version prefix' + in: query + name: urls + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getBatch() + tags: + - batch + /boards: + post: + operationId: addBoards + parameters: + - description: Attributes of "Boards" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoards() + tags: + - board + '/boards/{idBoard}': + get: + operationId: getBoardsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: actions_display + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: actions_format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: actions_since + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - description: ' true or false' + in: query + name: action_member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: action_member_fields + required: false + type: string + - description: ' true or false' + in: query + name: action_memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: action_memberCreator_fields + required: false + type: string + - default: none + description: 'One of: all, closed, none, open or visible' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: card_attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: card_attachment_fields + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: card_checklists + required: false + type: string + - description: ' true or false' + in: query + name: card_stickers + required: false + type: string + - default: none + description: 'One of: mine or none' + in: query + name: boardStars + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: labels + required: false + type: string + - default: all + description: 'all or a comma-separated list of: color, idBoard, name or uses' + in: query + name: label_fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: labels_limit + required: false + type: string + - default: none + description: 'One of: all, closed, none or open' + in: query + name: lists + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: list_fields + required: false + type: string + - default: none + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: memberships + required: false + type: string + - description: ' true or false' + in: query + name: memberships_member + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberships_member_fields + required: false + type: string + - default: none + description: 'One of: admins, all, none, normal or owners' + in: query + name: members + required: false + type: string + - default: 'avatarHash, initials, fullName, username and confirmed' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - default: none + description: 'One of: admins, all, none, normal or owners' + in: query + name: membersInvited + required: false + type: string + - default: 'avatarHash, initials, fullName and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: membersInvited_fields + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idBoard, idCard, name or pos' + in: query + name: checklist_fields + required: false + type: string + - description: ' true or false' + in: query + name: organization + required: false + type: string + - default: name and displayName + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organization_fields + required: false + type: string + - default: none + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: organization_memberships + required: false + type: string + - description: ' true or false' + in: query + name: myPrefs + required: false + type: string + - default: 'name, desc, descData, closed, idOrganization, pinned, url, shortUrl, prefs and labelNames' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsByIdBoard() + tags: + - board + put: + operationId: updateBoardsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsByIdBoard() + tags: + - board + '/boards/{idBoard}/actions': + get: + operationId: getBoardsActionsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - default: all + description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: '0' + description: Page * limit must be less than 1000 + in: query + name: page + required: false + type: string + - description: Only return actions related to these model ids + in: query + name: idModels + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsActionsByIdBoard() + tags: + - board + '/boards/{idBoard}/boardStars': + get: + operationId: getBoardsBoardStarsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: mine + description: 'One of: mine or none' + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsBoardStarsByIdBoard() + tags: + - board + '/boards/{idBoard}/calendarKey/generate': + post: + operationId: addBoardsCalendarKeyGenerateByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsCalendarKeyGenerateByIdBoard() + tags: + - board + '/boards/{idBoard}/cards': + get: + operationId: getBoardsCardsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: stickers + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - description: a number from 1 to 1000 + in: query + name: limit + required: false + type: string + - description: 'A date, or null' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: visible + description: 'One of: all, closed, none, open or visible' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsCardsByIdBoard() + tags: + - board + '/boards/{idBoard}/cards/{filter}': + get: + operationId: getBoardsCardsByIdBoardByFilter + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getBoardsCardsByIdBoardByFilter() + tags: + - board + '/boards/{idBoard}/cards/{idCard}': + get: + operationId: getBoardsCardsByIdBoardByIdCard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idCard + in: path + name: idCard + required: true + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: actions_display + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: action_memberCreator_fields + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, initials, fullName and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idCheckItem or state' + in: query + name: checkItemState_fields + required: false + type: string + - description: ' true or false' + in: query + name: labels + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idBoard, idCard, name or pos' + in: query + name: checklist_fields + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsCardsByIdBoardByIdCard() + tags: + - board + '/boards/{idBoard}/checklists': + get: + operationId: getBoardsChecklistsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: none + description: 'One of: all, closed, none, open or visible' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - default: all + description: 'One of: all or none' + in: query + name: checkItems + required: false + type: string + - default: 'name, nameData, pos and state' + description: 'all or a comma-separated list of: name, nameData, pos, state or type' + in: query + name: checkItem_fields + required: false + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idBoard, idCard, name or pos' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsChecklistsByIdBoard() + tags: + - board + post: + operationId: addBoardsChecklistsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Checklists" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_checklists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsChecklistsByIdBoard() + tags: + - board + '/boards/{idBoard}/closed': + put: + operationId: updateBoardsClosedByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Closed" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_closed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsClosedByIdBoard() + tags: + - board + '/boards/{idBoard}/deltas': + get: + operationId: getBoardsDeltasByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: A valid tag for subscribing + in: query + name: tags + required: true + type: string + - description: a number from -1 to Infinity + in: query + name: ixLastUpdate + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsDeltasByIdBoard() + tags: + - board + '/boards/{idBoard}/desc': + put: + operationId: updateBoardsDescByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Desc" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_desc' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsDescByIdBoard() + tags: + - board + '/boards/{idBoard}/emailKey/generate': + post: + operationId: addBoardsEmailKeyGenerateByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsEmailKeyGenerateByIdBoard() + tags: + - board + '/boards/{idBoard}/idOrganization': + put: + operationId: updateBoardsIdOrganizationByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Id Organization" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_idOrganization' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsIdOrganizationByIdBoard() + tags: + - board + '/boards/{idBoard}/labelNames/blue': + put: + operationId: updateBoardsLabelNamesBlueByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Label Names Blue" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labelNames_blue' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsLabelNamesBlueByIdBoard() + tags: + - board + '/boards/{idBoard}/labelNames/green': + put: + operationId: updateBoardsLabelNamesGreenByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Label Names Green" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labelNames_green' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsLabelNamesGreenByIdBoard() + tags: + - board + '/boards/{idBoard}/labelNames/orange': + put: + operationId: updateBoardsLabelNamesOrangeByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Label Names Orange" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labelNames_orange' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsLabelNamesOrangeByIdBoard() + tags: + - board + '/boards/{idBoard}/labelNames/purple': + put: + operationId: updateBoardsLabelNamesPurpleByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Label Names Purple" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labelNames_purple' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsLabelNamesPurpleByIdBoard() + tags: + - board + '/boards/{idBoard}/labelNames/red': + put: + operationId: updateBoardsLabelNamesRedByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Label Names Red" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labelNames_red' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsLabelNamesRedByIdBoard() + tags: + - board + '/boards/{idBoard}/labelNames/yellow': + put: + operationId: updateBoardsLabelNamesYellowByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Label Names Yellow" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labelNames_yellow' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsLabelNamesYellowByIdBoard() + tags: + - board + '/boards/{idBoard}/labels': + get: + operationId: getBoardsLabelsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: color, idBoard, name or uses' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: limit + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsLabelsByIdBoard() + tags: + - board + post: + operationId: addBoardsLabelsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Labels" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_labels' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsLabelsByIdBoard() + tags: + - board + '/boards/{idBoard}/labels/{idLabel}': + get: + operationId: getBoardsLabelsByIdBoardByIdLabel + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idLabel + in: path + name: idLabel + required: true + type: string + - default: all + description: 'all or a comma-separated list of: color, idBoard, name or uses' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsLabelsByIdBoardByIdLabel() + tags: + - board + '/boards/{idBoard}/lists': + get: + operationId: getBoardsListsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: none + description: 'One of: all, closed, none, open or visible' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - default: open + description: 'One of: all, closed, none or open' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsListsByIdBoard() + tags: + - board + post: + operationId: addBoardsListsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Lists" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_lists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsListsByIdBoard() + tags: + - board + '/boards/{idBoard}/lists/{filter}': + get: + operationId: getBoardsListsByIdBoardByFilter + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getBoardsListsByIdBoardByFilter() + tags: + - board + '/boards/{idBoard}/markAsViewed': + post: + operationId: addBoardsMarkAsViewedByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsMarkAsViewedByIdBoard() + tags: + - board + '/boards/{idBoard}/members': + get: + operationId: getBoardsMembersByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: all + description: 'One of: admins, all, none, normal or owners' + in: query + name: filter + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: fields + required: false + type: string + - description: true or false ; works for premium organizations only. + in: query + name: activity + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMembersByIdBoard() + tags: + - board + put: + operationId: updateBoardsMembersByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_members' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMembersByIdBoard() + tags: + - board + '/boards/{idBoard}/members/{filter}': + get: + operationId: getBoardsMembersByIdBoardByFilter + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getBoardsMembersByIdBoardByFilter() + tags: + - board + '/boards/{idBoard}/members/{idMember}': + delete: + operationId: deleteBoardsMembersByIdBoardByIdMember + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteBoardsMembersByIdBoardByIdMember() + tags: + - board + put: + operationId: updateBoardsMembersByIdBoardByIdMember + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: Attributes of "Boards Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_members' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMembersByIdBoardByIdMember() + tags: + - board + '/boards/{idBoard}/members/{idMember}/cards': + get: + operationId: getBoardsMembersCardsByIdBoardByIdMember + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - description: ' true or false' + in: query + name: board + required: false + type: string + - default: 'name, desc, closed, idOrganization, pinned, url and prefs' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - description: ' true or false' + in: query + name: list + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: list_fields + required: false + type: string + - default: visible + description: 'One of: all, closed, none, open or visible' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMembersCardsByIdBoardByIdMember() + tags: + - board + '/boards/{idBoard}/membersInvited': + get: + operationId: getBoardsMembersInvitedByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMembersInvitedByIdBoard() + tags: + - board + '/boards/{idBoard}/membersInvited/{field}': + get: + operationId: getBoardsMembersInvitedByIdBoardByField + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMembersInvitedByIdBoardByField() + tags: + - board + '/boards/{idBoard}/memberships': + get: + operationId: getBoardsMembershipsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: filter + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMembershipsByIdBoard() + tags: + - board + '/boards/{idBoard}/memberships/{idMembership}': + get: + operationId: getBoardsMembershipsByIdBoardByIdMembership + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idMembership + in: path + name: idMembership + required: true + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMembershipsByIdBoardByIdMembership() + tags: + - board + put: + operationId: updateBoardsMembershipsByIdBoardByIdMembership + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: idMembership + in: path + name: idMembership + required: true + type: string + - description: Attributes of "Boards Memberships" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_memberships' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMembershipsByIdBoardByIdMembership() + tags: + - board + '/boards/{idBoard}/myPrefs': + get: + operationId: getBoardsMyPrefsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsMyPrefsByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/emailPosition': + put: + operationId: updateBoardsMyPrefsEmailPositionByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Email Position" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_emailPosition' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsEmailPositionByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/idEmailList': + put: + operationId: updateBoardsMyPrefsIdEmailListByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Id Email List" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_idEmailList' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsIdEmailListByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/showListGuide': + put: + operationId: updateBoardsMyPrefsShowListGuideByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Show List Guide" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_showListGuide' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsShowListGuideByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/showSidebar': + put: + operationId: updateBoardsMyPrefsShowSidebarByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Show Sidebar" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_showSidebar' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsShowSidebarByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/showSidebarActivity': + put: + operationId: updateBoardsMyPrefsShowSidebarActivityByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Show Sidebar Activity" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_showSidebarActivity' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsShowSidebarActivityByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/showSidebarBoardActions': + put: + operationId: updateBoardsMyPrefsShowSidebarBoardActionsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Show Sidebar Board Actions" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_showSidebarBoardActions' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsShowSidebarBoardActionsByIdBoard() + tags: + - board + '/boards/{idBoard}/myPrefs/showSidebarMembers': + put: + operationId: updateBoardsMyPrefsShowSidebarMembersByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "My Prefs Show Sidebar Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/myPrefs_showSidebarMembers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsMyPrefsShowSidebarMembersByIdBoard() + tags: + - board + '/boards/{idBoard}/name': + put: + operationId: updateBoardsNameByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsNameByIdBoard() + tags: + - board + '/boards/{idBoard}/organization': + get: + operationId: getBoardsOrganizationByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsOrganizationByIdBoard() + tags: + - board + '/boards/{idBoard}/organization/{field}': + get: + operationId: getBoardsOrganizationByIdBoardByField + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getBoardsOrganizationByIdBoardByField() + tags: + - board + '/boards/{idBoard}/powerUps': + post: + operationId: addBoardsPowerUpsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Power Ups" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_powerUps' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addBoardsPowerUpsByIdBoard() + tags: + - board + '/boards/{idBoard}/powerUps/{powerUp}': + delete: + operationId: deleteBoardsPowerUpsByIdBoardByPowerUp + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: powerUp + in: path + name: powerUp + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteBoardsPowerUpsByIdBoardByPowerUp() + tags: + - board + '/boards/{idBoard}/prefs/background': + put: + operationId: updateBoardsPrefsBackgroundByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Background" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_background' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsBackgroundByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/calendarFeedEnabled': + put: + operationId: updateBoardsPrefsCalendarFeedEnabledByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Calendar Feed Enabled" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_calendarFeedEnabled' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsCalendarFeedEnabledByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/cardAging': + put: + operationId: updateBoardsPrefsCardAgingByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Card Aging" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_cardAging' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsCardAgingByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/cardCovers': + put: + operationId: updateBoardsPrefsCardCoversByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Card Covers" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_cardCovers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsCardCoversByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/comments': + put: + operationId: updateBoardsPrefsCommentsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Comments" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_comments' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsCommentsByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/invitations': + put: + operationId: updateBoardsPrefsInvitationsByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Invitations" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_invitations' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsInvitationsByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/permissionLevel': + put: + operationId: updateBoardsPrefsPermissionLevelByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Permission Level" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_permissionLevel' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsPermissionLevelByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/selfJoin': + put: + operationId: updateBoardsPrefsSelfJoinByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Self Join" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_selfJoin' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsSelfJoinByIdBoard() + tags: + - board + '/boards/{idBoard}/prefs/voting': + put: + operationId: updateBoardsPrefsVotingByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Prefs Voting" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_voting' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsPrefsVotingByIdBoard() + tags: + - board + '/boards/{idBoard}/subscribed': + put: + operationId: updateBoardsSubscribedByIdBoard + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: Attributes of "Boards Subscribed" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/boards_subscribed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateBoardsSubscribedByIdBoard() + tags: + - board + '/boards/{idBoard}/{field}': + get: + operationId: getBoardsByIdBoardByField + parameters: + - description: board_id + in: path + name: idBoard + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getBoardsByIdBoardByField() + tags: + - board + /cards: + post: + operationId: addCards + parameters: + - description: Attributes of "Cards" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCards() + tags: + - card + '/cards/{idCard}': + delete: + operationId: deleteCardsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsByIdCard() + tags: + - card + get: + operationId: getCardsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: actions_display + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: action_memberCreator_fields + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: membersVoted + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberVoted_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idCheckItem or state' + in: query + name: checkItemState_fields + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idBoard, idCard, name or pos' + in: query + name: checklist_fields + required: false + type: string + - description: ' true or false' + in: query + name: board + required: false + type: string + - default: 'name, desc, descData, closed, idOrganization, pinned, url and prefs' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - description: ' true or false' + in: query + name: list + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: list_fields + required: false + type: string + - description: ' true or false' + in: query + name: stickers + required: false + type: string + - default: all + description: 'all or a comma-separated list of: image, imageScaled, imageUrl, left, rotate, top or zIndex' + in: query + name: sticker_fields + required: false + type: string + - default: 'badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idBoard, idChecklists, idLabels, idList, idMembers, idShort, idAttachmentCover, manualCoverAttachment, labels, name, pos, shortUrl and url' + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsByIdCard() + tags: + - card + put: + operationId: updateCardsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsByIdCard() + tags: + - card + '/cards/{idCard}/actions': + get: + operationId: getCardsActionsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - default: 'commentCard and updateCard:idList' + description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: '0' + description: Page * limit must be less than 1000 + in: query + name: page + required: false + type: string + - description: Only return actions related to these model ids + in: query + name: idModels + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsActionsByIdCard() + tags: + - card + '/cards/{idCard}/actions/comments': + post: + operationId: addCardsActionsCommentsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Actions Comments" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/actions_comments' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsActionsCommentsByIdCard() + tags: + - card + '/cards/{idCard}/actions/{idAction}/comments': + delete: + description: 'This can only be done by the original author of the comment, or someone with higher permissions than the original author.' + operationId: deleteCardsActionsCommentsByIdCardByIdAction + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idAction + in: path + name: idAction + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsActionsCommentsByIdCardByIdAction() + tags: + - card + put: + description: This can only be done by the original author of the comment. + operationId: updateCardsActionsCommentsByIdCardByIdAction + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idAction + in: path + name: idAction + required: true + type: string + - description: Attributes of "Cards Actions Comments" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_actions_comments' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsActionsCommentsByIdCardByIdAction() + tags: + - card + '/cards/{idCard}/attachments': + get: + operationId: getCardsAttachmentsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: fields + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsAttachmentsByIdCard() + tags: + - card + post: + operationId: addCardsAttachmentsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Attachments" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_attachments' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsAttachmentsByIdCard() + tags: + - card + '/cards/{idCard}/attachments/{idAttachment}': + delete: + operationId: deleteCardsAttachmentsByIdCardByIdAttachment + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idAttachment + in: path + name: idAttachment + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsAttachmentsByIdCardByIdAttachment() + tags: + - card + get: + operationId: getCardsAttachmentsByIdCardByIdAttachment + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idAttachment + in: path + name: idAttachment + required: true + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsAttachmentsByIdCardByIdAttachment() + tags: + - card + '/cards/{idCard}/board': + get: + operationId: getCardsBoardByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsBoardByIdCard() + tags: + - card + '/cards/{idCard}/board/{field}': + get: + operationId: getCardsBoardByIdCardByField + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsBoardByIdCardByField() + tags: + - card + '/cards/{idCard}/checkItemStates': + get: + operationId: getCardsCheckItemStatesByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: idCheckItem or state' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsCheckItemStatesByIdCard() + tags: + - card + '/cards/{idCard}/checklist/{idChecklistCurrent}/checkItem/{idCheckItem}': + put: + operationId: updateCardsChecklistCheckItemByIdCardByIdChecklistCurrentByIdCheckItem + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklistCurrent + in: path + name: idChecklistCurrent + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: Attributes of "Cards Checklist Id Checklist Current Check Item" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_checklist_idChecklistCurrent_checkItem' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsChecklistCheckItemByIdCardByIdChecklistCurrentByIdCheckItem() + tags: + - card + '/cards/{idCard}/checklist/{idChecklist}/checkItem': + post: + operationId: addCardsChecklistCheckItemByIdCardByIdChecklist + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: Attributes of "Cards Checklist Check Item" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_checklist_checkItem' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsChecklistCheckItemByIdCardByIdChecklist() + tags: + - card + '/cards/{idCard}/checklist/{idChecklist}/checkItem/{idCheckItem}': + delete: + operationId: deleteCardsChecklistCheckItemByIdCardByIdChecklistByIdCheckItem + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsChecklistCheckItemByIdCardByIdChecklistByIdCheckItem() + tags: + - card + '/cards/{idCard}/checklist/{idChecklist}/checkItem/{idCheckItem}/convertToCard': + post: + operationId: addCardsChecklistCheckItemConvertToCardByIdCardByIdChecklistByIdCheckItem + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsChecklistCheckItemConvertToCardByIdCardByIdChecklistByIdCheckItem() + tags: + - card + '/cards/{idCard}/checklist/{idChecklist}/checkItem/{idCheckItem}/name': + put: + operationId: updateCardsChecklistCheckItemNameByIdCardByIdChecklistByIdCheckItem + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: Attributes of "Cards Checklist Check Item Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_checklist_checkItem_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsChecklistCheckItemNameByIdCardByIdChecklistByIdCheckItem() + tags: + - card + '/cards/{idCard}/checklist/{idChecklist}/checkItem/{idCheckItem}/pos': + put: + operationId: updateCardsChecklistCheckItemPosByIdCardByIdChecklistByIdCheckItem + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: Attributes of "Cards Checklist Check Item Pos" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_checklist_checkItem_pos' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsChecklistCheckItemPosByIdCardByIdChecklistByIdCheckItem() + tags: + - card + '/cards/{idCard}/checklist/{idChecklist}/checkItem/{idCheckItem}/state': + put: + operationId: updateCardsChecklistCheckItemStateByIdCardByIdChecklistByIdCheckItem + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: Attributes of "Cards Checklist Check Item State" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_checklist_checkItem_state' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsChecklistCheckItemStateByIdCardByIdChecklistByIdCheckItem() + tags: + - card + '/cards/{idCard}/checklists': + get: + operationId: getCardsChecklistsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: none + description: 'One of: all, closed, none, open or visible' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - default: all + description: 'One of: all or none' + in: query + name: checkItems + required: false + type: string + - default: 'name, nameData, pos and state' + description: 'all or a comma-separated list of: name, nameData, pos, state or type' + in: query + name: checkItem_fields + required: false + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idBoard, idCard, name or pos' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsChecklistsByIdCard() + tags: + - card + post: + operationId: addCardsChecklistsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Checklists" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_checklists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsChecklistsByIdCard() + tags: + - card + '/cards/{idCard}/checklists/{idChecklist}': + delete: + operationId: deleteCardsChecklistsByIdCardByIdChecklist + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsChecklistsByIdCardByIdChecklist() + tags: + - card + '/cards/{idCard}/closed': + put: + operationId: updateCardsClosedByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Closed" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_closed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsClosedByIdCard() + tags: + - card + '/cards/{idCard}/desc': + put: + operationId: updateCardsDescByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Desc" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_desc' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsDescByIdCard() + tags: + - card + '/cards/{idCard}/due': + put: + operationId: updateCardsDueByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Due" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_due' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsDueByIdCard() + tags: + - card + '/cards/{idCard}/idAttachmentCover': + put: + operationId: updateCardsIdAttachmentCoverByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Id Attachment Cover" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_idAttachmentCover' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsIdAttachmentCoverByIdCard() + tags: + - card + '/cards/{idCard}/idBoard': + put: + operationId: updateCardsIdBoardByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Id Board" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_idBoard' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsIdBoardByIdCard() + tags: + - card + '/cards/{idCard}/idLabels': + post: + operationId: addCardsIdLabelsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Id Labels" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_idLabels' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsIdLabelsByIdCard() + tags: + - card + '/cards/{idCard}/idLabels/{idLabel}': + delete: + operationId: deleteCardsIdLabelsByIdCardByIdLabel + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idLabel + in: path + name: idLabel + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsIdLabelsByIdCardByIdLabel() + tags: + - card + '/cards/{idCard}/idList': + put: + operationId: updateCardsIdListByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Id List" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_idList' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsIdListByIdCard() + tags: + - card + '/cards/{idCard}/idMembers': + post: + operationId: addCardsIdMembersByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Id Members" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_idMembers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsIdMembersByIdCard() + tags: + - card + put: + operationId: updateCardsIdMembersByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Id Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_idMembers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsIdMembersByIdCard() + tags: + - card + '/cards/{idCard}/idMembers/{idMember}': + delete: + operationId: deleteCardsIdMembersByIdCardByIdMember + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsIdMembersByIdCardByIdMember() + tags: + - card + '/cards/{idCard}/labels': + post: + operationId: addCardsLabelsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Labels" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_labels' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsLabelsByIdCard() + tags: + - card + put: + operationId: updateCardsLabelsByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Labels" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_labels' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsLabelsByIdCard() + tags: + - card + '/cards/{idCard}/labels/{color}': + delete: + operationId: deleteCardsLabelsByIdCardByColor + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: color + in: path + name: color + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsLabelsByIdCardByColor() + tags: + - card + '/cards/{idCard}/list': + get: + operationId: getCardsListByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsListByIdCard() + tags: + - card + '/cards/{idCard}/list/{field}': + get: + operationId: getCardsListByIdCardByField + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsListByIdCardByField() + tags: + - card + '/cards/{idCard}/markAssociatedNotificationsRead': + post: + operationId: addCardsMarkAssociatedNotificationsReadByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsMarkAssociatedNotificationsReadByIdCard() + tags: + - card + '/cards/{idCard}/members': + get: + operationId: getCardsMembersByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsMembersByIdCard() + tags: + - card + '/cards/{idCard}/membersVoted': + get: + operationId: getCardsMembersVotedByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsMembersVotedByIdCard() + tags: + - card + post: + operationId: addCardsMembersVotedByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Members Voted" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_membersVoted' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsMembersVotedByIdCard() + tags: + - card + '/cards/{idCard}/membersVoted/{idMember}': + delete: + operationId: deleteCardsMembersVotedByIdCardByIdMember + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsMembersVotedByIdCardByIdMember() + tags: + - card + '/cards/{idCard}/name': + put: + operationId: updateCardsNameByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsNameByIdCard() + tags: + - card + '/cards/{idCard}/pos': + put: + operationId: updateCardsPosByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Pos" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_pos' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsPosByIdCard() + tags: + - card + '/cards/{idCard}/stickers': + get: + operationId: getCardsStickersByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - default: all + description: 'all or a comma-separated list of: image, imageScaled, imageUrl, left, rotate, top or zIndex' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsStickersByIdCard() + tags: + - card + post: + operationId: addCardsStickersByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Stickers" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_stickers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addCardsStickersByIdCard() + tags: + - card + '/cards/{idCard}/stickers/{idSticker}': + delete: + operationId: deleteCardsStickersByIdCardByIdSticker + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idSticker + in: path + name: idSticker + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteCardsStickersByIdCardByIdSticker() + tags: + - card + get: + operationId: getCardsStickersByIdCardByIdSticker + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idSticker + in: path + name: idSticker + required: true + type: string + - default: all + description: 'all or a comma-separated list of: image, imageScaled, imageUrl, left, rotate, top or zIndex' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getCardsStickersByIdCardByIdSticker() + tags: + - card + put: + operationId: updateCardsStickersByIdCardByIdSticker + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: idSticker + in: path + name: idSticker + required: true + type: string + - description: Attributes of "Cards Stickers" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_stickers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsStickersByIdCardByIdSticker() + tags: + - card + '/cards/{idCard}/subscribed': + put: + operationId: updateCardsSubscribedByIdCard + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: Attributes of "Cards Subscribed" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/cards_subscribed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateCardsSubscribedByIdCard() + tags: + - card + '/cards/{idCard}/{field}': + get: + operationId: getCardsByIdCardByField + parameters: + - description: card id or shortlink + in: path + name: idCard + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getCardsByIdCardByField() + tags: + - card + /checklists: + post: + operationId: addChecklists + parameters: + - description: Attributes of "Checklists" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/checklists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addChecklists() + tags: + - checklist + '/checklists/{idChecklist}': + delete: + operationId: deleteChecklistsByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteChecklistsByIdChecklist() + tags: + - checklist + get: + operationId: getChecklistsByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - default: none + description: 'One of: all, closed, none, open or visible' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - default: all + description: 'One of: all or none' + in: query + name: checkItems + required: false + type: string + - default: 'name, nameData, pos and state' + description: 'all or a comma-separated list of: name, nameData, pos, state or type' + in: query + name: checkItem_fields + required: false + type: string + - default: all + description: 'all or a comma-separated list of: idBoard, idCard, name or pos' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getChecklistsByIdChecklist() + tags: + - checklist + put: + operationId: updateChecklistsByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: Attributes of "Checklists" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/checklists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateChecklistsByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/board': + get: + operationId: getChecklistsBoardByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getChecklistsBoardByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/board/{field}': + get: + operationId: getChecklistsBoardByIdChecklistByField + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getChecklistsBoardByIdChecklistByField() + tags: + - checklist + '/checklists/{idChecklist}/cards': + get: + operationId: getChecklistsCardsByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: stickers + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - description: a number from 1 to 1000 + in: query + name: limit + required: false + type: string + - description: 'A date, or null' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: open + description: 'One of: all, closed, none or open' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getChecklistsCardsByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/cards/{filter}': + get: + operationId: getChecklistsCardsByIdChecklistByFilter + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getChecklistsCardsByIdChecklistByFilter() + tags: + - checklist + '/checklists/{idChecklist}/checkItems': + get: + operationId: getChecklistsCheckItemsByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - default: 'name, nameData, pos and state' + description: 'all or a comma-separated list of: name, nameData, pos, state or type' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getChecklistsCheckItemsByIdChecklist() + tags: + - checklist + post: + operationId: addChecklistsCheckItemsByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: Attributes of "Checklists Check Items" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/checklists_checkItems' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addChecklistsCheckItemsByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/checkItems/{idCheckItem}': + delete: + operationId: deleteChecklistsCheckItemsByIdChecklistByIdCheckItem + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteChecklistsCheckItemsByIdChecklistByIdCheckItem() + tags: + - checklist + get: + operationId: getChecklistsCheckItemsByIdChecklistByIdCheckItem + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: idCheckItem + in: path + name: idCheckItem + required: true + type: string + - default: 'name, nameData, pos and state' + description: 'all or a comma-separated list of: name, nameData, pos, state or type' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getChecklistsCheckItemsByIdChecklistByIdCheckItem() + tags: + - checklist + '/checklists/{idChecklist}/idCard': + put: + operationId: updateChecklistsIdCardByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: Attributes of "Checklists Id Card" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/checklists_idCard' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateChecklistsIdCardByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/name': + put: + operationId: updateChecklistsNameByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: Attributes of "Checklists Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/checklists_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateChecklistsNameByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/pos': + put: + operationId: updateChecklistsPosByIdChecklist + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: Attributes of "Checklists Pos" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/checklists_pos' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateChecklistsPosByIdChecklist() + tags: + - checklist + '/checklists/{idChecklist}/{field}': + get: + operationId: getChecklistsByIdChecklistByField + parameters: + - description: idChecklist + in: path + name: idChecklist + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getChecklistsByIdChecklistByField() + tags: + - checklist + /labels: + post: + operationId: addLabels + parameters: + - description: Attributes of "Labels" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labels' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addLabels() + tags: + - label + '/labels/{idLabel}': + delete: + operationId: deleteLabelsByIdLabel + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteLabelsByIdLabel() + tags: + - label + get: + operationId: getLabelsByIdLabel + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - default: all + description: 'all or a comma-separated list of: color, idBoard, name or uses' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getLabelsByIdLabel() + tags: + - label + put: + operationId: updateLabelsByIdLabel + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - description: Attributes of "Labels" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labels' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateLabelsByIdLabel() + tags: + - label + '/labels/{idLabel}/board': + get: + operationId: getLabelsBoardByIdLabel + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getLabelsBoardByIdLabel() + tags: + - label + '/labels/{idLabel}/board/{field}': + get: + operationId: getLabelsBoardByIdLabelByField + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getLabelsBoardByIdLabelByField() + tags: + - label + '/labels/{idLabel}/color': + put: + operationId: updateLabelsColorByIdLabel + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - description: Attributes of "Labels Color" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labels_color' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateLabelsColorByIdLabel() + tags: + - label + '/labels/{idLabel}/name': + put: + operationId: updateLabelsNameByIdLabel + parameters: + - description: idLabel + in: path + name: idLabel + required: true + type: string + - description: Attributes of "Labels Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/labels_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateLabelsNameByIdLabel() + tags: + - label + /lists: + post: + operationId: addLists + parameters: + - description: Attributes of "Lists" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addLists() + tags: + - list + '/lists/{idList}': + get: + operationId: getListsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - default: none + description: 'One of: all, closed, none or open' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - description: ' true or false' + in: query + name: board + required: false + type: string + - default: 'name, desc, descData, closed, idOrganization, pinned, url and prefs' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - default: 'name, closed, idBoard and pos' + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getListsByIdList() + tags: + - list + put: + operationId: updateListsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateListsByIdList() + tags: + - list + '/lists/{idList}/actions': + get: + operationId: getListsActionsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - default: all + description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: '0' + description: Page * limit must be less than 1000 + in: query + name: page + required: false + type: string + - description: Only return actions related to these model ids + in: query + name: idModels + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getListsActionsByIdList() + tags: + - list + '/lists/{idList}/archiveAllCards': + post: + operationId: addListsArchiveAllCardsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addListsArchiveAllCardsByIdList() + tags: + - list + '/lists/{idList}/board': + get: + operationId: getListsBoardByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getListsBoardByIdList() + tags: + - list + '/lists/{idList}/board/{field}': + get: + operationId: getListsBoardByIdListByField + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getListsBoardByIdListByField() + tags: + - list + '/lists/{idList}/cards': + get: + operationId: getListsCardsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: stickers + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - description: a number from 1 to 1000 + in: query + name: limit + required: false + type: string + - description: 'A date, or null' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: open + description: 'One of: all, closed, none or open' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getListsCardsByIdList() + tags: + - list + post: + operationId: addListsCardsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Cards" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_cards' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addListsCardsByIdList() + tags: + - list + '/lists/{idList}/cards/{filter}': + get: + operationId: getListsCardsByIdListByFilter + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getListsCardsByIdListByFilter() + tags: + - list + '/lists/{idList}/closed': + put: + operationId: updateListsClosedByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Closed" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_closed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateListsClosedByIdList() + tags: + - list + '/lists/{idList}/idBoard': + put: + operationId: updateListsIdBoardByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Id Board" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_idBoard' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateListsIdBoardByIdList() + tags: + - list + '/lists/{idList}/moveAllCards': + post: + operationId: addListsMoveAllCardsByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Move All Cards" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_moveAllCards' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addListsMoveAllCardsByIdList() + tags: + - list + '/lists/{idList}/name': + put: + operationId: updateListsNameByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateListsNameByIdList() + tags: + - list + '/lists/{idList}/pos': + put: + operationId: updateListsPosByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Pos" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_pos' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateListsPosByIdList() + tags: + - list + '/lists/{idList}/subscribed': + put: + operationId: updateListsSubscribedByIdList + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: Attributes of "Lists Subscribed" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/lists_subscribed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateListsSubscribedByIdList() + tags: + - list + '/lists/{idList}/{field}': + get: + operationId: getListsByIdListByField + parameters: + - description: idList + in: path + name: idList + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getListsByIdListByField() + tags: + - list + '/members/{idMember}': + get: + description: 'If you specify ''me'' as the username, this call will respond as if you had supplied the username associated with the supplied token' + operationId: getMembersByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: actions_display + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: action_since + required: false + type: string + - description: 'A date, or null' + in: query + name: action_before + required: false + type: string + - default: none + description: 'One of: all, closed, none, open or visible' + in: query + name: cards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - description: ' true or false' + in: query + name: card_members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: card_member_fields + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: card_attachments + required: false + type: string + - default: url and previews + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: card_attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: card_stickers + required: false + type: string + - description: 'all or a comma-separated list of: closed, members, open, organization, pinned, public, starred or unpinned' + in: query + name: boards + required: false + type: string + - default: 'name, closed, idOrganization and pinned' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: board_actions + required: false + type: string + - description: ' true or false' + in: query + name: board_actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: board_actions_display + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: board_actions_format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: board_actions_since + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: board_actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: board_action_fields + required: false + type: string + - default: none + description: 'One of: all, closed, none or open' + in: query + name: board_lists + required: false + type: string + - default: none + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: board_memberships + required: false + type: string + - description: ' true or false' + in: query + name: board_organization + required: false + type: string + - default: name and displayName + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: board_organization_fields + required: false + type: string + - description: 'all or a comma-separated list of: closed, members, open, organization, pinned, public, starred or unpinned' + in: query + name: boardsInvited + required: false + type: string + - default: 'name, closed, idOrganization and pinned' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: boardsInvited_fields + required: false + type: string + - description: ' true or false' + in: query + name: boardStars + required: false + type: string + - description: ' true or false' + in: query + name: savedSearches + required: false + type: string + - default: none + description: 'One of: all, members, none or public' + in: query + name: organizations + required: false + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organization_fields + required: false + type: string + - description: ' true or false' + in: query + name: organization_paid_account + required: false + type: string + - default: none + description: 'One of: all, members, none or public' + in: query + name: organizationsInvited + required: false + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organizationsInvited_fields + required: false + type: string + - description: 'all or a comma-separated list of: addAdminToBoard, addAdminToOrganization, addedAttachmentToCard, addedMemberToCard, addedToBoard, addedToCard, addedToOrganization, cardDueSoon, changeCard, closeBoard, commentCard, createdCard, declinedInvitationToBoard, declinedInvitationToOrganization, invitedToBoard, invitedToOrganization, makeAdminOfBoard, makeAdminOfOrganization, memberJoinedTrello, mentionedOnCard, removedFromBoard, removedFromCard, removedFromOrganization, removedMemberFromCard, unconfirmedInvitedToBoard, unconfirmedInvitedToOrganization or updateCheckItemStateOnCard' + in: query + name: notifications + required: false + type: string + - description: ' true or false' + in: query + name: notifications_entities + required: false + type: string + - description: ' true or false' + in: query + name: notifications_display + required: false + type: string + - default: '50' + description: a number from 1 to 1000 + in: query + name: notifications_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator, type or unread' + in: query + name: notification_fields + required: false + type: string + - description: ' true or false' + in: query + name: notification_memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: notification_memberCreator_fields + required: false + type: string + - description: 'An id, or null' + in: query + name: notification_before + required: false + type: string + - description: 'An id, or null' + in: query + name: notification_since + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: tokens + required: false + type: string + - description: ' true or false' + in: query + name: paid_account + required: false + type: string + - default: none + description: 'One of: all, custom, default, none or premium' + in: query + name: boardBackgrounds + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: customBoardBackgrounds + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: customStickers + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: customEmoji + required: false + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersByIdMember() + tags: + - member + put: + operationId: updateMembersByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersByIdMember() + tags: + - member + '/members/{idMember}/actions': + get: + operationId: getMembersActionsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - default: all + description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: '0' + description: Page * limit must be less than 1000 + in: query + name: page + required: false + type: string + - description: Only return actions related to these model ids + in: query + name: idModels + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersActionsByIdMember() + tags: + - member + '/members/{idMember}/avatar': + post: + operationId: addMembersAvatarByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Avatar" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_avatar' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersAvatarByIdMember() + tags: + - member + '/members/{idMember}/avatarSource': + put: + operationId: updateMembersAvatarSourceByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Avatar Source" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_avatarSource' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersAvatarSourceByIdMember() + tags: + - member + '/members/{idMember}/bio': + put: + operationId: updateMembersBioByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Bio" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_bio' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersBioByIdMember() + tags: + - member + '/members/{idMember}/boardBackgrounds': + get: + operationId: getMembersBoardBackgroundsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'One of: all, custom, default, none or premium' + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardBackgroundsByIdMember() + tags: + - member + post: + operationId: addMembersBoardBackgroundsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Board Backgrounds" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_boardBackgrounds' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersBoardBackgroundsByIdMember() + tags: + - member + '/members/{idMember}/boardBackgrounds/{idBoardBackground}': + delete: + operationId: deleteMembersBoardBackgroundsByIdMemberByIdBoardBackground + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardBackground + in: path + name: idBoardBackground + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteMembersBoardBackgroundsByIdMemberByIdBoardBackground() + tags: + - member + get: + operationId: getMembersBoardBackgroundsByIdMemberByIdBoardBackground + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardBackground + in: path + name: idBoardBackground + required: true + type: string + - default: all + description: 'all or a comma-separated list of: brightness, fullSizeUrl, scaled or tile' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardBackgroundsByIdMemberByIdBoardBackground() + tags: + - member + put: + operationId: updateMembersBoardBackgroundsByIdMemberByIdBoardBackground + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardBackground + in: path + name: idBoardBackground + required: true + type: string + - description: Attributes of "Members Board Backgrounds" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_boardBackgrounds' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersBoardBackgroundsByIdMemberByIdBoardBackground() + tags: + - member + '/members/{idMember}/boardStars': + get: + operationId: getMembersBoardStarsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardStarsByIdMember() + tags: + - member + post: + operationId: addMembersBoardStarsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Board Stars" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_boardStars' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersBoardStarsByIdMember() + tags: + - member + '/members/{idMember}/boardStars/{idBoardStar}': + delete: + operationId: deleteMembersBoardStarsByIdMemberByIdBoardStar + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardStar + in: path + name: idBoardStar + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteMembersBoardStarsByIdMemberByIdBoardStar() + tags: + - member + get: + operationId: getMembersBoardStarsByIdMemberByIdBoardStar + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardStar + in: path + name: idBoardStar + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardStarsByIdMemberByIdBoardStar() + tags: + - member + put: + operationId: updateMembersBoardStarsByIdMemberByIdBoardStar + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardStar + in: path + name: idBoardStar + required: true + type: string + - description: Attributes of "Members Board Stars" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_boardStars' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersBoardStarsByIdMemberByIdBoardStar() + tags: + - member + '/members/{idMember}/boardStars/{idBoardStar}/idBoard': + put: + operationId: updateMembersBoardStarsIdBoardByIdMemberByIdBoardStar + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardStar + in: path + name: idBoardStar + required: true + type: string + - description: Attributes of "Members Board Stars Id Board" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_boardStars_idBoard' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersBoardStarsIdBoardByIdMemberByIdBoardStar() + tags: + - member + '/members/{idMember}/boardStars/{idBoardStar}/pos': + put: + operationId: updateMembersBoardStarsPosByIdMemberByIdBoardStar + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardStar + in: path + name: idBoardStar + required: true + type: string + - description: Attributes of "Members Board Stars Pos" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_boardStars_pos' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersBoardStarsPosByIdMemberByIdBoardStar() + tags: + - member + '/members/{idMember}/boards': + get: + operationId: getMembersBoardsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, members, open, organization, pinned, public, starred or unpinned' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: actions_format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: actions_since + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - default: none + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: memberships + required: false + type: string + - description: ' true or false' + in: query + name: organization + required: false + type: string + - default: name and displayName + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organization_fields + required: false + type: string + - default: none + description: 'One of: all, closed, none or open' + in: query + name: lists + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardsByIdMember() + tags: + - member + '/members/{idMember}/boards/{filter}': + get: + operationId: getMembersBoardsByIdMemberByFilter + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getMembersBoardsByIdMemberByFilter() + tags: + - member + '/members/{idMember}/boardsInvited': + get: + operationId: getMembersBoardsInvitedByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardsInvitedByIdMember() + tags: + - member + '/members/{idMember}/boardsInvited/{field}': + get: + operationId: getMembersBoardsInvitedByIdMemberByField + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersBoardsInvitedByIdMemberByField() + tags: + - member + '/members/{idMember}/cards': + get: + operationId: getMembersCardsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: stickers + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - description: a number from 1 to 1000 + in: query + name: limit + required: false + type: string + - description: 'A date, or null' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: visible + description: 'One of: all, closed, none, open or visible' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCardsByIdMember() + tags: + - member + '/members/{idMember}/cards/{filter}': + get: + operationId: getMembersCardsByIdMemberByFilter + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getMembersCardsByIdMemberByFilter() + tags: + - member + '/members/{idMember}/customBoardBackgrounds': + get: + operationId: getMembersCustomBoardBackgroundsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCustomBoardBackgroundsByIdMember() + tags: + - member + post: + operationId: addMembersCustomBoardBackgroundsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Custom Board Backgrounds" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_customBoardBackgrounds' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersCustomBoardBackgroundsByIdMember() + tags: + - member + '/members/{idMember}/customBoardBackgrounds/{idBoardBackground}': + delete: + operationId: deleteMembersCustomBoardBackgroundsByIdMemberByIdBoardBackground + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardBackground + in: path + name: idBoardBackground + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteMembersCustomBoardBackgroundsByIdMemberByIdBoardBackground() + tags: + - member + get: + operationId: getMembersCustomBoardBackgroundsByIdMemberByIdBoardBackground + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardBackground + in: path + name: idBoardBackground + required: true + type: string + - default: all + description: 'all or a comma-separated list of: brightness, fullSizeUrl, scaled or tile' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCustomBoardBackgroundsByIdMemberByIdBoardBackground() + tags: + - member + put: + operationId: updateMembersCustomBoardBackgroundsByIdMemberByIdBoardBackground + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idBoardBackground + in: path + name: idBoardBackground + required: true + type: string + - description: Attributes of "Members Custom Board Backgrounds" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_customBoardBackgrounds' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersCustomBoardBackgroundsByIdMemberByIdBoardBackground() + tags: + - member + '/members/{idMember}/customEmoji': + get: + description: This gets the list of all of the user’s uploaded emoji + operationId: getMembersCustomEmojiByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCustomEmojiByIdMember() + tags: + - member + post: + operationId: addMembersCustomEmojiByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Custom Emoji" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_customEmoji' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersCustomEmojiByIdMember() + tags: + - member + '/members/{idMember}/customEmoji/{idCustomEmoji}': + get: + operationId: getMembersCustomEmojiByIdMemberByIdCustomEmoji + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idCustomEmoji + in: path + name: idCustomEmoji + required: true + type: string + - default: all + description: 'all or a comma-separated list of: name or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCustomEmojiByIdMemberByIdCustomEmoji() + tags: + - member + '/members/{idMember}/customStickers': + get: + description: This gets a list of all of the user’s uploaded stickers + operationId: getMembersCustomStickersByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCustomStickersByIdMember() + tags: + - member + post: + operationId: addMembersCustomStickersByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Custom Stickers" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_customStickers' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersCustomStickersByIdMember() + tags: + - member + '/members/{idMember}/customStickers/{idCustomSticker}': + delete: + operationId: deleteMembersCustomStickersByIdMemberByIdCustomSticker + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idCustomSticker + in: path + name: idCustomSticker + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteMembersCustomStickersByIdMemberByIdCustomSticker() + tags: + - member + get: + operationId: getMembersCustomStickersByIdMemberByIdCustomSticker + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idCustomSticker + in: path + name: idCustomSticker + required: true + type: string + - default: all + description: 'all or a comma-separated list of: scaled or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersCustomStickersByIdMemberByIdCustomSticker() + tags: + - member + '/members/{idMember}/deltas': + get: + operationId: getMembersDeltasByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: A valid tag for subscribing + in: query + name: tags + required: true + type: string + - description: a number from -1 to Infinity + in: query + name: ixLastUpdate + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersDeltasByIdMember() + tags: + - member + '/members/{idMember}/fullName': + put: + operationId: updateMembersFullNameByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Full Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_fullName' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersFullNameByIdMember() + tags: + - member + '/members/{idMember}/initials': + put: + operationId: updateMembersInitialsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Initials" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_initials' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersInitialsByIdMember() + tags: + - member + '/members/{idMember}/notifications': + get: + description: You can only read the notifications for the member associated with the supplied token + operationId: getMembersNotificationsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - default: all + description: 'all or a comma-separated list of: addAdminToBoard, addAdminToOrganization, addedAttachmentToCard, addedMemberToCard, addedToBoard, addedToCard, addedToOrganization, cardDueSoon, changeCard, closeBoard, commentCard, createdCard, declinedInvitationToBoard, declinedInvitationToOrganization, invitedToBoard, invitedToOrganization, makeAdminOfBoard, makeAdminOfOrganization, memberJoinedTrello, mentionedOnCard, removedFromBoard, removedFromCard, removedFromOrganization, removedMemberFromCard, unconfirmedInvitedToBoard, unconfirmedInvitedToOrganization or updateCheckItemStateOnCard' + in: query + name: filter + required: false + type: string + - default: all + description: 'One of: all, read or unread' + in: query + name: read_filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator, type or unread' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 1 to 1000 + in: query + name: limit + required: false + type: string + - default: '0' + description: a number from 0 to 100 + in: query + name: page + required: false + type: string + - description: 'An id, or null' + in: query + name: before + required: false + type: string + - description: 'An id, or null' + in: query + name: since + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersNotificationsByIdMember() + tags: + - member + '/members/{idMember}/notifications/{filter}': + get: + operationId: getMembersNotificationsByIdMemberByFilter + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getMembersNotificationsByIdMemberByFilter() + tags: + - member + '/members/{idMember}/oneTimeMessagesDismissed': + post: + operationId: addMembersOneTimeMessagesDismissedByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members One Time Messages Dismissed" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_oneTimeMessagesDismissed' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersOneTimeMessagesDismissedByIdMember() + tags: + - member + '/members/{idMember}/organizations': + get: + operationId: getMembersOrganizationsByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'One of: all, members, none or public' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: fields + required: false + type: string + - description: ' true or false' + in: query + name: paid_account + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersOrganizationsByIdMember() + tags: + - member + '/members/{idMember}/organizations/{filter}': + get: + operationId: getMembersOrganizationsByIdMemberByFilter + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getMembersOrganizationsByIdMemberByFilter() + tags: + - member + '/members/{idMember}/organizationsInvited': + get: + operationId: getMembersOrganizationsInvitedByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersOrganizationsInvitedByIdMember() + tags: + - member + '/members/{idMember}/organizationsInvited/{field}': + get: + operationId: getMembersOrganizationsInvitedByIdMemberByField + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersOrganizationsInvitedByIdMemberByField() + tags: + - member + '/members/{idMember}/prefs/colorBlind': + put: + operationId: updateMembersPrefsColorBlindByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Prefs Color Blind" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_colorBlind' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersPrefsColorBlindByIdMember() + tags: + - member + '/members/{idMember}/prefs/locale': + put: + operationId: updateMembersPrefsLocaleByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Prefs Locale" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_locale' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersPrefsLocaleByIdMember() + tags: + - member + '/members/{idMember}/prefs/minutesBetweenSummaries': + put: + operationId: updateMembersPrefsMinutesBetweenSummariesByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Prefs Minutes Between Summaries" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_minutesBetweenSummaries' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersPrefsMinutesBetweenSummariesByIdMember() + tags: + - member + '/members/{idMember}/savedSearches': + get: + operationId: getMembersSavedSearchesByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersSavedSearchesByIdMember() + tags: + - member + post: + operationId: addMembersSavedSearchesByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Saved Searches" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_savedSearches' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addMembersSavedSearchesByIdMember() + tags: + - member + '/members/{idMember}/savedSearches/{idSavedSearch}': + delete: + operationId: deleteMembersSavedSearchesByIdMemberByIdSavedSearch + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idSavedSearch + in: path + name: idSavedSearch + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteMembersSavedSearchesByIdMemberByIdSavedSearch() + tags: + - member + get: + operationId: getMembersSavedSearchesByIdMemberByIdSavedSearch + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idSavedSearch + in: path + name: idSavedSearch + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersSavedSearchesByIdMemberByIdSavedSearch() + tags: + - member + put: + operationId: updateMembersSavedSearchesByIdMemberByIdSavedSearch + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idSavedSearch + in: path + name: idSavedSearch + required: true + type: string + - description: Attributes of "Members Saved Searches" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_savedSearches' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersSavedSearchesByIdMemberByIdSavedSearch() + tags: + - member + '/members/{idMember}/savedSearches/{idSavedSearch}/name': + put: + operationId: updateMembersSavedSearchesNameByIdMemberByIdSavedSearch + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idSavedSearch + in: path + name: idSavedSearch + required: true + type: string + - description: Attributes of "Members Saved Searches Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_savedSearches_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersSavedSearchesNameByIdMemberByIdSavedSearch() + tags: + - member + '/members/{idMember}/savedSearches/{idSavedSearch}/pos': + put: + operationId: updateMembersSavedSearchesPosByIdMemberByIdSavedSearch + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idSavedSearch + in: path + name: idSavedSearch + required: true + type: string + - description: Attributes of "Members Saved Searches Pos" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_savedSearches_pos' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersSavedSearchesPosByIdMemberByIdSavedSearch() + tags: + - member + '/members/{idMember}/savedSearches/{idSavedSearch}/query': + put: + operationId: updateMembersSavedSearchesQueryByIdMemberByIdSavedSearch + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: idSavedSearch + in: path + name: idSavedSearch + required: true + type: string + - description: Attributes of "Members Saved Searches Query" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_savedSearches_query' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersSavedSearchesQueryByIdMemberByIdSavedSearch() + tags: + - member + '/members/{idMember}/tokens': + get: + operationId: getMembersTokensByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - default: all + description: 'One of: all or none' + in: query + name: filter + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getMembersTokensByIdMember() + tags: + - member + '/members/{idMember}/username': + put: + operationId: updateMembersUsernameByIdMember + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: Attributes of "Members Username" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/members_username' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateMembersUsernameByIdMember() + tags: + - member + '/members/{idMember}/{field}': + get: + operationId: getMembersByIdMemberByField + parameters: + - description: idMember or username + in: path + name: idMember + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getMembersByIdMemberByField() + tags: + - member + /notifications/all/read: + post: + operationId: addNotificationsAllRead + parameters: + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addNotificationsAllRead() + tags: + - notification + '/notifications/{idNotification}': + get: + operationId: getNotificationsByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator, type or unread' + in: query + name: fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: ' true or false' + in: query + name: board + required: false + type: string + - default: name + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - description: ' true or false' + in: query + name: list + required: false + type: string + - description: ' true or false' + in: query + name: card + required: false + type: string + - default: name + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - description: ' true or false' + in: query + name: organization + required: false + type: string + - default: displayName + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organization_fields + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsByIdNotification() + tags: + - notification + put: + operationId: updateNotificationsByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: Attributes of "Notifications" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/notifications' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateNotificationsByIdNotification() + tags: + - notification + '/notifications/{idNotification}/board': + get: + operationId: getNotificationsBoardByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsBoardByIdNotification() + tags: + - notification + '/notifications/{idNotification}/board/{field}': + get: + operationId: getNotificationsBoardByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsBoardByIdNotificationByField() + tags: + - notification + '/notifications/{idNotification}/card': + get: + operationId: getNotificationsCardByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsCardByIdNotification() + tags: + - notification + '/notifications/{idNotification}/card/{field}': + get: + operationId: getNotificationsCardByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsCardByIdNotificationByField() + tags: + - notification + '/notifications/{idNotification}/display': + get: + operationId: getNotificationsDisplayByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsDisplayByIdNotification() + tags: + - notification + '/notifications/{idNotification}/entities': + get: + operationId: getNotificationsEntitiesByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsEntitiesByIdNotification() + tags: + - notification + '/notifications/{idNotification}/list': + get: + operationId: getNotificationsListByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsListByIdNotification() + tags: + - notification + '/notifications/{idNotification}/list/{field}': + get: + operationId: getNotificationsListByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsListByIdNotificationByField() + tags: + - notification + '/notifications/{idNotification}/member': + get: + operationId: getNotificationsMemberByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsMemberByIdNotification() + tags: + - notification + '/notifications/{idNotification}/member/{field}': + get: + operationId: getNotificationsMemberByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsMemberByIdNotificationByField() + tags: + - notification + '/notifications/{idNotification}/memberCreator': + get: + operationId: getNotificationsMemberCreatorByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsMemberCreatorByIdNotification() + tags: + - notification + '/notifications/{idNotification}/memberCreator/{field}': + get: + operationId: getNotificationsMemberCreatorByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsMemberCreatorByIdNotificationByField() + tags: + - notification + '/notifications/{idNotification}/organization': + get: + operationId: getNotificationsOrganizationByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - default: all + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsOrganizationByIdNotification() + tags: + - notification + '/notifications/{idNotification}/organization/{field}': + get: + operationId: getNotificationsOrganizationByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getNotificationsOrganizationByIdNotificationByField() + tags: + - notification + '/notifications/{idNotification}/unread': + put: + operationId: updateNotificationsUnreadByIdNotification + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: Attributes of "Notifications Unread" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/notifications_unread' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateNotificationsUnreadByIdNotification() + tags: + - notification + '/notifications/{idNotification}/{field}': + get: + operationId: getNotificationsByIdNotificationByField + parameters: + - description: idNotification + in: path + name: idNotification + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getNotificationsByIdNotificationByField() + tags: + - notification + /organizations: + post: + operationId: addOrganizations + parameters: + - description: Attributes of "Organizations" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addOrganizations() + tags: + - organization + '/organizations/{idOrg}': + delete: + operationId: deleteOrganizationsByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteOrganizationsByIdOrg() + tags: + - organization + get: + operationId: getOrganizationsByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: actions_display + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - default: none + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: memberships + required: false + type: string + - description: ' true or false' + in: query + name: memberships_member + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberships_member_fields + required: false + type: string + - default: none + description: 'One of: admins, all, none, normal or owners' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials, username and confirmed' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: true or false ; works for premium organizations only. + in: query + name: member_activity + required: false + type: string + - default: none + description: 'One of: admins, all, none, normal or owners' + in: query + name: membersInvited + required: false + type: string + - default: 'avatarHash, initials, fullName and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: membersInvited_fields + required: false + type: string + - default: none + description: 'all or a comma-separated list of: closed, members, open, organization, pinned, public, starred or unpinned' + in: query + name: boards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: board_actions + required: false + type: string + - description: ' true or false' + in: query + name: board_actions_entities + required: false + type: string + - description: ' true or false' + in: query + name: board_actions_display + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: board_actions_format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: board_actions_since + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: board_actions_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: board_action_fields + required: false + type: string + - default: none + description: 'One of: all, closed, none or open' + in: query + name: board_lists + required: false + type: string + - description: ' true or false' + in: query + name: paid_account + required: false + type: string + - default: 'name, displayName, desc, descData, url, website, logoHash, products and powerUps' + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsByIdOrg() + tags: + - organization + put: + operationId: updateOrganizationsByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsByIdOrg() + tags: + - organization + '/organizations/{idOrg}/actions': + get: + operationId: getOrganizationsActionsByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: ' true or false' + in: query + name: entities + required: false + type: string + - description: ' true or false' + in: query + name: display + required: false + type: string + - default: all + description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: fields + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: since + required: false + type: string + - description: 'A date, or null' + in: query + name: before + required: false + type: string + - default: '0' + description: Page * limit must be less than 1000 + in: query + name: page + required: false + type: string + - description: Only return actions related to these model ids + in: query + name: idModels + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: memberCreator + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: memberCreator_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsActionsByIdOrg() + tags: + - organization + '/organizations/{idOrg}/boards': + get: + operationId: getOrganizationsBoardsByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - default: all + description: 'all or a comma-separated list of: closed, members, open, organization, pinned, public, starred or unpinned' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: ' true or false' + in: query + name: actions_entities + required: false + type: string + - default: '50' + description: a number from 0 to 1000 + in: query + name: actions_limit + required: false + type: string + - default: list + description: 'One of: count, list or minimal' + in: query + name: actions_format + required: false + type: string + - description: 'A date, null or lastView' + in: query + name: actions_since + required: false + type: string + - default: all + description: 'all or a comma-separated list of: data, date, idMemberCreator or type' + in: query + name: action_fields + required: false + type: string + - default: none + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: memberships + required: false + type: string + - description: ' true or false' + in: query + name: organization + required: false + type: string + - default: name and displayName + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organization_fields + required: false + type: string + - default: none + description: 'One of: all, closed, none or open' + in: query + name: lists + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsBoardsByIdOrg() + tags: + - organization + '/organizations/{idOrg}/boards/{filter}': + get: + operationId: getOrganizationsBoardsByIdOrgByFilter + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getOrganizationsBoardsByIdOrgByFilter() + tags: + - organization + '/organizations/{idOrg}/deltas': + get: + operationId: getOrganizationsDeltasByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: A valid tag for subscribing + in: query + name: tags + required: true + type: string + - description: a number from -1 to Infinity + in: query + name: ixLastUpdate + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsDeltasByIdOrg() + tags: + - organization + '/organizations/{idOrg}/desc': + put: + operationId: updateOrganizationsDescByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations Desc" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_desc' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsDescByIdOrg() + tags: + - organization + '/organizations/{idOrg}/displayName': + put: + operationId: updateOrganizationsDisplayNameByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations Display Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_displayName' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsDisplayNameByIdOrg() + tags: + - organization + '/organizations/{idOrg}/logo': + delete: + operationId: deleteOrganizationsLogoByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteOrganizationsLogoByIdOrg() + tags: + - organization + post: + operationId: addOrganizationsLogoByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations Logo" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_logo' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addOrganizationsLogoByIdOrg() + tags: + - organization + '/organizations/{idOrg}/members': + get: + operationId: getOrganizationsMembersByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - default: all + description: 'One of: admins, all, none, normal or owners' + in: query + name: filter + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: fields + required: false + type: string + - description: true or false ; works for premium organizations only. + in: query + name: activity + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsMembersByIdOrg() + tags: + - organization + put: + operationId: updateOrganizationsMembersByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_members' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsMembersByIdOrg() + tags: + - organization + '/organizations/{idOrg}/members/{filter}': + get: + operationId: getOrganizationsMembersByIdOrgByFilter + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: filter + in: path + name: filter + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getOrganizationsMembersByIdOrgByFilter() + tags: + - organization + '/organizations/{idOrg}/members/{idMember}': + delete: + operationId: deleteOrganizationsMembersByIdOrgByIdMember + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteOrganizationsMembersByIdOrgByIdMember() + tags: + - organization + put: + operationId: updateOrganizationsMembersByIdOrgByIdMember + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: Attributes of "Organizations Members" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_members' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsMembersByIdOrgByIdMember() + tags: + - organization + '/organizations/{idOrg}/members/{idMember}/all': + delete: + operationId: deleteOrganizationsMembersAllByIdOrgByIdMember + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteOrganizationsMembersAllByIdOrgByIdMember() + tags: + - organization + '/organizations/{idOrg}/members/{idMember}/cards': + get: + operationId: getOrganizationsMembersCardsByIdOrgByIdMember + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: 'all or a comma-separated list of: addAttachmentToCard, addChecklistToCard, addMemberToBoard, addMemberToCard, addMemberToOrganization, addToOrganizationBoard, commentCard, convertToCardFromCheckItem, copyBoard, copyCard, copyCommentCard, createBoard, createCard, createList, createOrganization, deleteAttachmentFromCard, deleteBoardInvitation, deleteCard, deleteOrganizationInvitation, disablePowerUp, emailCard, enablePowerUp, makeAdminOfBoard, makeNormalMemberOfBoard, makeNormalMemberOfOrganization, makeObserverOfBoard, memberJoinedTrello, moveCardFromBoard, moveCardToBoard, moveListFromBoard, moveListToBoard, removeChecklistFromCard, removeFromOrganizationBoard, removeMemberFromCard, unconfirmedBoardInvitation, unconfirmedOrganizationInvitation, updateBoard, updateCard, updateCard:closed, updateCard:desc, updateCard:idList, updateCard:name, updateCheckItemStateOnCard, updateChecklist, updateList, updateList:closed, updateList:name, updateMember or updateOrganization' + in: query + name: actions + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: attachments + required: false + type: string + - default: all + description: 'all or a comma-separated list of: bytes, date, edgeColor, idMember, isUpload, mimeType, name, previews or url' + in: query + name: attachment_fields + required: false + type: string + - description: ' true or false' + in: query + name: members + required: false + type: string + - default: 'avatarHash, fullName, initials and username' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: ' true or false' + in: query + name: checkItemStates + required: false + type: string + - default: none + description: 'One of: all or none' + in: query + name: checklists + required: false + type: string + - description: ' true or false' + in: query + name: board + required: false + type: string + - default: 'name, desc, closed, idOrganization, pinned, url and prefs' + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - description: ' true or false' + in: query + name: list + required: false + type: string + - default: all + description: 'all or a comma-separated list of: closed, idBoard, name, pos or subscribed' + in: query + name: list_fields + required: false + type: string + - default: visible + description: 'One of: all, closed, none, open or visible' + in: query + name: filter + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsMembersCardsByIdOrgByIdMember() + tags: + - organization + '/organizations/{idOrg}/members/{idMember}/deactivated': + put: + operationId: updateOrganizationsMembersDeactivatedByIdOrgByIdMember + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMember + in: path + name: idMember + required: true + type: string + - description: Attributes of "Organizations Members Deactivated" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_members_deactivated' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsMembersDeactivatedByIdOrgByIdMember() + tags: + - organization + '/organizations/{idOrg}/membersInvited': + get: + operationId: getOrganizationsMembersInvitedByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsMembersInvitedByIdOrg() + tags: + - organization + '/organizations/{idOrg}/membersInvited/{field}': + get: + operationId: getOrganizationsMembersInvitedByIdOrgByField + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsMembersInvitedByIdOrgByField() + tags: + - organization + '/organizations/{idOrg}/memberships': + get: + operationId: getOrganizationsMembershipsByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - default: all + description: 'all or a comma-separated list of: active, admin, deactivated, me or normal' + in: query + name: filter + required: false + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsMembershipsByIdOrg() + tags: + - organization + '/organizations/{idOrg}/memberships/{idMembership}': + get: + operationId: getOrganizationsMembershipsByIdOrgByIdMembership + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMembership + in: path + name: idMembership + required: true + type: string + - description: ' true or false' + in: query + name: member + required: false + type: string + - default: fullName and username + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getOrganizationsMembershipsByIdOrgByIdMembership() + tags: + - organization + put: + operationId: updateOrganizationsMembershipsByIdOrgByIdMembership + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: idMembership + in: path + name: idMembership + required: true + type: string + - description: Attributes of "Organizations Memberships" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_memberships' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsMembershipsByIdOrgByIdMembership() + tags: + - organization + '/organizations/{idOrg}/name': + put: + operationId: updateOrganizationsNameByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations Name" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_name' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsNameByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/associatedDomain': + delete: + operationId: deleteOrganizationsPrefsAssociatedDomainByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteOrganizationsPrefsAssociatedDomainByIdOrg() + tags: + - organization + put: + operationId: updateOrganizationsPrefsAssociatedDomainByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Associated Domain" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_associatedDomain' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsAssociatedDomainByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/boardVisibilityRestrict/org': + put: + operationId: updateOrganizationsPrefsBoardVisibilityRestrictOrgByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Board Visibility Restrict" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_boardVisibilityRestrict' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsBoardVisibilityRestrictOrgByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/boardVisibilityRestrict/private': + put: + operationId: updateOrganizationsPrefsBoardVisibilityRestrictPrivateByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Board Visibility Restrict" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_boardVisibilityRestrict' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsBoardVisibilityRestrictPrivateByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/boardVisibilityRestrict/public': + put: + operationId: updateOrganizationsPrefsBoardVisibilityRestrictPublicByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Board Visibility Restrict" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_boardVisibilityRestrict' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsBoardVisibilityRestrictPublicByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/externalMembersDisabled': + put: + operationId: updateOrganizationsPrefsExternalMembersDisabledByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs External Members Disabled" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_externalMembersDisabled' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsExternalMembersDisabledByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/googleAppsVersion': + put: + operationId: updateOrganizationsPrefsGoogleAppsVersionByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Google Apps Version" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_googleAppsVersion' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsGoogleAppsVersionByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/orgInviteRestrict': + delete: + operationId: deleteOrganizationsPrefsOrgInviteRestrictByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: An email address with optional expansion tokens + in: query + name: value + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteOrganizationsPrefsOrgInviteRestrictByIdOrg() + tags: + - organization + put: + operationId: updateOrganizationsPrefsOrgInviteRestrictByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Org Invite Restrict" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_orgInviteRestrict' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsOrgInviteRestrictByIdOrg() + tags: + - organization + '/organizations/{idOrg}/prefs/permissionLevel': + put: + operationId: updateOrganizationsPrefsPermissionLevelByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Prefs Permission Level" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/prefs_permissionLevel' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsPrefsPermissionLevelByIdOrg() + tags: + - organization + '/organizations/{idOrg}/website': + put: + operationId: updateOrganizationsWebsiteByIdOrg + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: Attributes of "Organizations Website" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/organizations_website' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateOrganizationsWebsiteByIdOrg() + tags: + - organization + '/organizations/{idOrg}/{field}': + get: + operationId: getOrganizationsByIdOrgByField + parameters: + - description: idOrg or name + in: path + name: idOrg + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getOrganizationsByIdOrgByField() + tags: + - organization + /search: + get: + operationId: getSearch + parameters: + - description: a string with a length from 1 to 16384 + in: query + name: query + required: true + type: string + - default: mine + description: 'A comma-separated list of objectIds, 24-character hex strings' + in: query + name: idBoards + required: false + type: string + - description: 'A comma-separated list of objectIds, 24-character hex strings' + in: query + name: idOrganizations + required: true + type: string + - description: 'A comma-separated list of objectIds, 24-character hex strings' + in: query + name: idCards + required: false + type: string + - default: all + description: 'all or a comma-separated list of: actions, boards, cards, members or organizations' + in: query + name: modelTypes + required: false + type: string + - default: name and idOrganization + description: 'all or a comma-separated list of: closed, dateLastActivity, dateLastView, desc, descData, idOrganization, invitations, invited, labelNames, memberships, name, pinned, powerUps, prefs, shortLink, shortUrl, starred, subscribed or url' + in: query + name: board_fields + required: false + type: string + - default: '10' + description: a number from 1 to 1000 + in: query + name: boards_limit + required: false + type: string + - default: all + description: 'all or a comma-separated list of: badges, checkItemStates, closed, dateLastActivity, desc, descData, due, email, idAttachmentCover, idBoard, idChecklists, idLabels, idList, idMembers, idMembersVoted, idShort, labels, manualCoverAttachment, name, pos, shortLink, shortUrl, subscribed or url' + in: query + name: card_fields + required: false + type: string + - default: '10' + description: a number from 1 to 1000 + in: query + name: cards_limit + required: false + type: string + - default: '0' + description: a number from 0 to 100 + in: query + name: cards_page + required: false + type: string + - description: ' true or false' + in: query + name: card_board + required: false + type: string + - description: ' true or false' + in: query + name: card_list + required: false + type: string + - description: ' true or false' + in: query + name: card_members + required: false + type: string + - description: ' true or false' + in: query + name: card_stickers + required: false + type: string + - description: A boolean value or "cover" for only card cover attachments + in: query + name: card_attachments + required: false + type: string + - default: name and displayName + description: 'all or a comma-separated list of: billableMemberCount, desc, descData, displayName, idBoards, invitations, invited, logoHash, memberships, name, powerUps, prefs, premiumFeatures, products, url or website' + in: query + name: organization_fields + required: false + type: string + - default: '10' + description: a number from 1 to 1000 + in: query + name: organizations_limit + required: false + type: string + - default: 'avatarHash, fullName, initials, username and confirmed' + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + in: query + name: member_fields + required: false + type: string + - default: '10' + description: a number from 1 to 1000 + in: query + name: members_limit + required: false + type: string + - description: ' true or false' + in: query + name: partial + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getSearch() + tags: + - search + /search/members: + get: + operationId: getSearchMembers + parameters: + - description: a string with a length from 1 to 16384 + in: query + name: query + required: true + type: string + - default: '8' + description: a number from 1 to 20 + in: query + name: limit + required: false + type: string + - description: 'An id, or null' + in: query + name: idBoard + required: false + type: string + - description: 'An id, or null' + in: query + name: idOrganization + required: false + type: string + - description: A boolean + in: query + name: onlyOrgMembers + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getSearchMembers() + tags: + - search + /sessions: + post: + operationId: addSessions + parameters: + - description: Attributes of "Sessions" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/sessions' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addSessions() + tags: + - session + /sessions/socket: + get: + description: This is the route for WebSocket requests. See the socket API reference for a description of WebSocket usage. + operationId: getSessionsSocket + parameters: + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getSessionsSocket() + tags: + - session + '/sessions/{idSession}': + put: + operationId: updateSessionsByIdSession + parameters: + - description: idSession + in: path + name: idSession + required: true + type: string + - description: Attributes of "Sessions" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/sessions' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateSessionsByIdSession() + tags: + - session + '/sessions/{idSession}/status': + put: + operationId: updateSessionsStatusByIdSession + parameters: + - description: idSession + in: path + name: idSession + required: true + type: string + - description: Attributes of "Sessions Status" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/sessions_status' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateSessionsStatusByIdSession() + tags: + - session + '/tokens/{token}': + delete: + operationId: deleteTokensByToken + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteTokensByToken() + tags: + - token + get: + operationId: getTokensByToken + parameters: + - description: token + in: path + name: token + required: true + type: string + - default: all + description: 'all or a comma-separated list of: dateCreated, dateExpires, idMember, identifier or permissions' + in: query + name: fields + required: false + type: string + - description: ' true or false' + in: query + name: webhooks + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getTokensByToken() + tags: + - token + '/tokens/{token}/member': + get: + operationId: getTokensMemberByToken + parameters: + - description: token + in: path + name: token + required: true + type: string + - default: all + description: 'all or a comma-separated list of: avatarHash, avatarSource, bio, bioData, confirmed, email, fullName, gravatarHash, idBoards, idBoardsPinned, idOrganizations, idPremOrgsAdmin, initials, loginTypes, memberType, oneTimeMessagesDismissed, prefs, premiumFeatures, products, status, status, trophies, uploadedAvatarHash, url or username' + in: query + name: fields + required: false + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getTokensMemberByToken() + tags: + - token + '/tokens/{token}/member/{field}': + get: + operationId: getTokensMemberByTokenByField + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getTokensMemberByTokenByField() + tags: + - token + '/tokens/{token}/webhooks': + get: + operationId: getTokensWebhooksByToken + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getTokensWebhooksByToken() + tags: + - token + post: + operationId: addTokensWebhooksByToken + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: Attributes of "Tokens Webhooks" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/tokens_webhooks' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addTokensWebhooksByToken() + tags: + - token + put: + operationId: updateTokensWebhooksByToken + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: Attributes of "Tokens Webhooks" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/tokens_webhooks' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateTokensWebhooksByToken() + tags: + - token + '/tokens/{token}/webhooks/{idWebhook}': + delete: + operationId: deleteTokensWebhooksByTokenByIdWebhook + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteTokensWebhooksByTokenByIdWebhook() + tags: + - token + get: + operationId: getTokensWebhooksByTokenByIdWebhook + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getTokensWebhooksByTokenByIdWebhook() + tags: + - token + '/tokens/{token}/{field}': + get: + operationId: getTokensByTokenByField + parameters: + - description: token + in: path + name: token + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getTokensByTokenByField() + tags: + - token + '/types/{id}': + get: + operationId: getTypesById + parameters: + - description: id + in: path + name: id + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getTypesById() + tags: + - type + /webhooks: + post: + operationId: addWebhooks + parameters: + - description: Attributes of "Webhooks" to be added. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: addWebhooks() + tags: + - webhook + /webhooks/: + put: + operationId: updateWebhooks + parameters: + - description: Attributes of "Webhooks" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateWebhooks() + tags: + - webhook + '/webhooks/{idWebhook}': + delete: + operationId: deleteWebhooksByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: deleteWebhooksByIdWebhook() + tags: + - webhook + get: + operationId: getWebhooksByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: getWebhooksByIdWebhook() + tags: + - webhook + put: + operationId: updateWebhooksByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: Attributes of "Webhooks" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateWebhooksByIdWebhook() + tags: + - webhook + '/webhooks/{idWebhook}/active': + put: + operationId: updateWebhooksActiveByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: Attributes of "Webhooks Active" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks_active' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateWebhooksActiveByIdWebhook() + tags: + - webhook + '/webhooks/{idWebhook}/callbackURL': + put: + operationId: updateWebhooksCallbackURLByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: Attributes of "Webhooks Callback Url" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks_callbackURL' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateWebhooksCallbackURLByIdWebhook() + tags: + - webhook + '/webhooks/{idWebhook}/description': + put: + operationId: updateWebhooksDescriptionByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: Attributes of "Webhooks Description" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks_description' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateWebhooksDescriptionByIdWebhook() + tags: + - webhook + '/webhooks/{idWebhook}/idModel': + put: + operationId: updateWebhooksIdModelByIdWebhook + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: Attributes of "Webhooks Id Model" to be updated. + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks_idModel' + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + security: + - api_key: [] + - api_token: [] + summary: updateWebhooksIdModelByIdWebhook() + tags: + - webhook + '/webhooks/{idWebhook}/{field}': + get: + operationId: getWebhooksByIdWebhookByField + parameters: + - description: idWebhook + in: path + name: idWebhook + required: true + type: string + - description: field + in: path + name: field + required: true + type: string + - description: 'Generate your application key' + in: query + name: key + required: true + type: string + - description: 'Getting a token from a user' + in: query + name: token + required: true + type: string + responses: + '200': + description: Success + '400': + description: Server rejection + summary: getWebhooksByIdWebhookByField() + tags: + - webhook +definitions: + actions: + properties: + text: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: action + actions_comments: + properties: + text: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: card + actions_text: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: action + boards: + properties: + closed: + description: ' true or false' + type: string + desc: + description: a string with a length from 0 to 16384 + type: string + idBoardSource: + description: The id of the board to copy into the new board + type: string + idOrganization: + description: The id or name of the organization to add the board to. + type: string + keepFromSource: + description: Components of the source board to copy. + type: string + labelNames/blue: + description: a string with a length from 0 to 16384 + type: string + labelNames/green: + description: a string with a length from 0 to 16384 + type: string + labelNames/orange: + description: a string with a length from 0 to 16384 + type: string + labelNames/purple: + description: a string with a length from 0 to 16384 + type: string + labelNames/red: + description: a string with a length from 0 to 16384 + type: string + labelNames/yellow: + description: a string with a length from 0 to 16384 + type: string + name: + description: a string with a length from 1 to 16384 + type: string + powerUps: + description: 'all or a comma-separated list of: calendar, cardAging, recap or voting' + type: string + prefs/background: + description: 'A standard background name, or the id of a custom background' + type: string + prefs/calendarFeedEnabled: + description: ' true or false' + type: string + prefs/cardAging: + description: 'One of: pirate or regular' + type: string + prefs/cardCovers: + description: ' true or false' + type: string + prefs/comments: + description: 'One of: disabled, members, observers, org or public' + type: string + prefs/invitations: + description: 'One of: admins or members' + type: string + prefs/permissionLevel: + description: 'One of: org, private or public' + type: string + prefs/selfJoin: + description: ' true or false' + type: string + prefs/voting: + description: 'One of: disabled, members, observers, org or public' + type: string + prefs_background: + description: a string with a length from 0 to 16384 + type: string + prefs_cardAging: + description: 'One of: pirate or regular' + type: string + prefs_cardCovers: + description: ' true or false' + type: string + prefs_comments: + description: 'One of: disabled, members, observers, org or public' + type: string + prefs_invitations: + description: 'One of: admins or members' + type: string + prefs_permissionLevel: + description: 'One of: org, private or public' + type: string + prefs_selfJoin: + description: ' true or false' + type: string + prefs_voting: + description: 'One of: disabled, members, observers, org or public' + type: string + subscribed: + description: ' true or false' + type: string + type: object + xml: + name: board + boards_checklists: + properties: + name: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: board + boards_closed: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + boards_desc: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + boards_idOrganization: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + boards_labels: + properties: + color: + description: A valid label color or null + type: string + name: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + boards_lists: + properties: + name: + description: a string with a length from 1 to 16384 + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: board + boards_members: + properties: + email: + description: An email address + type: string + fullName: + description: A string with a length of at least 1. Cannot begin or end with a space. + type: string + type: + description: 'One of: admin, normal or observer' + type: string + type: object + xml: + name: board + boards_memberships: + properties: + member_fields: + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + type: string + type: + description: 'One of: admin, normal or observer' + type: string + type: object + xml: + name: board + boards_name: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: board + boards_powerUps: + properties: + value: + description: 'One of: calendar, cardAging, recap or voting' + type: string + type: object + xml: + name: board + boards_subscribed: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + cards: + properties: + closed: + description: ' true or false' + type: string + desc: + description: a string with a length from 0 to 16384 + type: string + due: + description: 'A date, or null' + type: string + fileSource: + description: A file + type: string + idAttachmentCover: + description: 'Id of the image attachment of this card to use as its cover, or null for no cover' + type: string + idBoard: + description: id of the board the card should be moved to + type: string + idCardSource: + description: The id of the card to copy into a new card. + type: string + idLabels: + description: 'A comma-separated list of objectIds, 24-character hex strings' + type: string + idList: + description: id of the list that the card should be added to + type: string + idMembers: + description: 'A comma-separated list of objectIds, 24-character hex strings' + type: string + keepFromSource: + description: Properties of the card to copy over from the source. + type: string + labels: + description: 'all or a comma-separated list of: blue, green, orange, purple, red or yellow' + type: string + name: + description: 'The name of the new card. It isn't required if the name is being copied from provided by a URL, file or card that is being copied.' + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + subscribed: + description: ' true or false' + type: string + urlSource: + description: 'A URL starting with http:// or https:// or null' + type: string + type: object + xml: + name: card + cards_actions_comments: + properties: + text: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: card + cards_attachments: + properties: + file: + description: A file + type: string + mimeType: + description: a string with a length from 0 to 256 + type: string + name: + description: a string with a length from 0 to 256 + type: string + url: + description: 'A URL starting with http:// or https:// or null' + type: string + type: object + xml: + name: card + cards_checklist_checkItem: + properties: + name: + description: a string with a length from 1 to 16384 + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: card + cards_checklist_checkItem_name: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: card + cards_checklist_checkItem_pos: + properties: + value: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: card + cards_checklist_checkItem_state: + properties: + value: + description: 'One of: complete, false, incomplete or true' + type: string + type: object + xml: + name: card + cards_checklist_idChecklistCurrent_checkItem: + properties: + idChecklist: + description: 'An id, or null' + type: string + name: + description: a string with a length from 1 to 16384 + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + state: + description: 'One of: complete, false, incomplete or true' + type: string + type: object + xml: + name: card + cards_checklists: + properties: + idChecklistSource: + description: The id of the source checklist to copy into a new checklist. + type: string + name: + description: a string with a length from 0 to 16384 + type: string + value: + description: 'The id of the checklist to add to the card, or null to create a new one.' + type: string + type: object + xml: + name: card + cards_closed: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: card + cards_desc: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: card + cards_due: + properties: + value: + description: 'A date, or null' + type: string + type: object + xml: + name: card + cards_idAttachmentCover: + properties: + value: + description: 'Id of the image attachment of this card to use as its cover, or null for no cover' + type: string + type: object + xml: + name: card + cards_idBoard: + properties: + idList: + description: id of the list that the card should be moved to on the new board + type: string + value: + description: id of the board the card should be moved to + type: string + type: object + xml: + name: card + cards_idLabels: + properties: + value: + description: The id of the label to add + type: string + type: object + xml: + name: card + cards_idList: + properties: + value: + description: id of the list the card should be moved to + type: string + type: object + xml: + name: card + cards_idMembers: + properties: + value: + description: The id of the member to add to the card + type: string + type: object + xml: + name: card + cards_labels: + properties: + color: + description: A valid label color or null + type: string + name: + description: a string with a length from 0 to 16384 + type: string + value: + description: 'all or a comma-separated list of: blue, green, orange, purple, red or yellow' + type: string + type: object + xml: + name: card + cards_membersVoted: + properties: + value: + description: 'The id of the member to vote 'yes' on the card' + type: string + type: object + xml: + name: card + cards_name: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: card + cards_pos: + properties: + value: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: card + cards_stickers: + properties: + image: + description: a string with a length from 0 to 16384 + type: string + left: + description: undefined + type: string + rotate: + description: undefined + type: string + top: + description: undefined + type: string + zIndex: + description: 'Valid Z values for stickers, must be an integer' + type: string + type: object + xml: + name: card + cards_subscribed: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: card + checklists: + properties: + idBoard: + description: id of the board that the checklist should be added to + type: string + idCard: + description: id of the card that the checklist should be added to + type: string + idChecklistSource: + description: The id of the source checklist to copy into a new checklist. + type: string + name: + description: a string with a length from 0 to 16384 + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: checklist + checklists_checkItems: + properties: + checked: + description: ' true or false' + type: string + name: + description: a string with a length from 1 to 16384 + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: checklist + checklists_idCard: + properties: + value: + description: The id of the card that the checklist is on + type: string + type: object + xml: + name: checklist + checklists_name: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: checklist + checklists_pos: + properties: + value: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: checklist + labelNames_blue: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + labelNames_green: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + labelNames_orange: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + labelNames_purple: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + labelNames_red: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + labelNames_yellow: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: board + labels: + properties: + color: + description: A valid label color or null + type: string + idBoard: + description: An id + type: string + name: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: label + labels_color: + properties: + value: + description: A valid label color or null + type: string + type: object + xml: + name: label + labels_name: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: label + lists: + properties: + closed: + description: ' true or false' + type: string + idBoard: + description: id of the board that the list should be added to + type: string + idListSource: + description: The id of the list to copy into a new list. + type: string + name: + description: a string with a length from 1 to 16384 + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + subscribed: + description: ' true or false' + type: string + type: object + xml: + name: list + lists_cards: + properties: + desc: + description: a string with a length from 0 to 16384 + type: string + due: + description: 'A date, or null' + type: string + idMembers: + description: 'A comma-separated list of objectIds, 24-character hex strings' + type: string + labels: + description: 'all or a comma-separated list of: blue, green, orange, purple, red or yellow' + type: string + name: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: list + lists_closed: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: list + lists_idBoard: + properties: + pos: + description: position of the list on the new board + type: string + value: + description: id of the board the list should be moved to + type: string + type: object + xml: + name: list + lists_moveAllCards: + properties: + idBoard: + description: id of the board that the cards should be moved to + type: string + type: object + xml: + name: list + lists_name: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: list + lists_pos: + properties: + value: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: list + lists_subscribed: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: list + members: + properties: + avatarSource: + description: 'One of: gravatar, none or upload' + type: string + bio: + description: a string with a length from 0 to 16384 + type: string + fullName: + description: A string with a length of at least 1. Cannot begin or end with a space. + type: string + initials: + description: A string with a length from 1 to 4. Cannot begin or end with a space + type: string + prefs/colorBlind: + description: ' true or false' + type: string + prefs/locale: + description: a string with a length from 0 to 255 + type: string + prefs/minutesBetweenSummaries: + description: '-1 (disabled), 1 or 60' + type: string + username: + description: 'A string with a length of at least 3. Only lowercase letters, underscores, and numbers are allowed. Must be unique.' + type: string + type: object + xml: + name: member + members_avatar: + properties: + file: + description: A file + type: string + type: object + xml: + name: member + members_avatarSource: + properties: + value: + description: 'One of: gravatar, none or upload' + type: string + type: object + xml: + name: member + members_bio: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: member + members_boardBackgrounds: + properties: + brightness: + description: 'One of: dark, light or unknown' + type: string + file: + description: A file + type: string + tile: + description: ' true or false' + type: string + type: object + xml: + name: member + members_boardStars: + properties: + idBoard: + description: The id of the board to star + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: member + members_boardStars_idBoard: + properties: + value: + description: An id + type: string + type: object + xml: + name: member + members_boardStars_pos: + properties: + value: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: member + members_customBoardBackgrounds: + properties: + brightness: + description: 'One of: dark, light or unknown' + type: string + file: + description: A file + type: string + tile: + description: ' true or false' + type: string + type: object + xml: + name: member + members_customEmoji: + properties: + file: + description: A file + type: string + name: + description: a string with a length from 2 to 64 + type: string + type: object + xml: + name: member + members_customStickers: + properties: + file: + description: A file + type: string + type: object + xml: + name: member + members_fullName: + properties: + value: + description: A string with a length of at least 1. Cannot begin or end with a space. + type: string + type: object + xml: + name: member + members_initials: + properties: + value: + description: A string with a length from 1 to 4. Cannot begin or end with a space + type: string + type: object + xml: + name: member + members_oneTimeMessagesDismissed: + properties: + value: + description: Type of message dismissed + type: string + type: object + xml: + name: member + members_savedSearches: + properties: + name: + description: A non-empty string with at least one non-space character + type: string + pos: + description: 'A position. top , bottom , or a positive number.' + type: string + query: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: member + members_savedSearches_name: + properties: + value: + description: A non-empty string with at least one non-space character + type: string + type: object + xml: + name: member + members_savedSearches_pos: + properties: + value: + description: 'A position. top , bottom , or a positive number.' + type: string + type: object + xml: + name: member + members_savedSearches_query: + properties: + value: + description: a string with a length from 1 to 16384 + type: string + type: object + xml: + name: member + members_username: + properties: + value: + description: 'A string with a length of at least 3. Only lowercase letters, underscores, and numbers are allowed. Must be unique.' + type: string + type: object + xml: + name: member + myPrefs_emailPosition: + properties: + value: + description: 'One of: bottom or top' + type: string + type: object + xml: + name: board + myPrefs_idEmailList: + properties: + value: + description: An id + type: string + type: object + xml: + name: board + myPrefs_showListGuide: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + myPrefs_showSidebar: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + myPrefs_showSidebarActivity: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + myPrefs_showSidebarBoardActions: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + myPrefs_showSidebarMembers: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + notifications: + properties: + unread: + description: ' true or false' + type: string + type: object + xml: + name: notification + notifications_unread: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: notification + organizations: + properties: + desc: + description: a string with a length from 0 to 16384 + type: string + displayName: + description: A string with a length of at least 1. Cannot begin or end with a space. + type: string + name: + description: a string with a length from 0 to 16384 + type: string + prefs/associatedDomain: + description: The google apps domain to link this org to. + type: string + prefs/boardVisibilityRestrict/org: + description: 'One of: admin, none or org' + type: string + prefs/boardVisibilityRestrict/private: + description: 'One of: admin, none or org' + type: string + prefs/boardVisibilityRestrict/public: + description: 'One of: admin, none or org' + type: string + prefs/externalMembersDisabled: + description: ' true or false' + type: string + prefs/googleAppsVersion: + description: a number from 1 to 2 + type: string + prefs/orgInviteRestrict: + description: An email address with optional expansion tokens + type: string + prefs/permissionLevel: + description: 'One of: private or public' + type: string + website: + description: 'A URL starting with http:// or https:// or null' + type: string + type: object + xml: + name: organization + organizations_desc: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: organization + organizations_displayName: + properties: + value: + description: A string with a length of at least 1. Cannot begin or end with a space. + type: string + type: object + xml: + name: organization + organizations_logo: + properties: + file: + description: A file + type: string + type: object + xml: + name: organization + organizations_members: + properties: + email: + description: An email address + type: string + fullName: + description: A string with a length of at least 1. Cannot begin or end with a space. + type: string + type: + description: 'One of: admin, normal or observer' + type: string + type: object + xml: + name: organization + organizations_members_deactivated: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: organization + organizations_memberships: + properties: + member_fields: + description: 'all or a comma-separated list of: avatarHash, bio, bioData, confirmed, fullName, idPremOrgsAdmin, initials, memberType, products, status, url or username' + type: string + type: + description: 'One of: admin, normal or observer' + type: string + type: object + xml: + name: organization + organizations_name: + properties: + value: + description: 'A string with a length of at least 3. Only lowercase letters, underscores, and numbers are allowed. Must be unique.' + type: string + type: object + xml: + name: organization + organizations_website: + properties: + value: + description: 'A URL starting with http:// or https:// or null' + type: string + type: object + xml: + name: organization + prefs_associatedDomain: + properties: + value: + description: The google apps domain to link this org to. + type: string + type: object + xml: + name: organization + prefs_background: + properties: + value: + description: 'A standard background name, or the id of a custom background' + type: string + type: object + xml: + name: board + prefs_boardVisibilityRestrict: + properties: + value: + description: 'One of: admin, none or org' + type: string + type: object + xml: + name: organization + prefs_calendarFeedEnabled: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + prefs_cardAging: + properties: + value: + description: 'One of: pirate or regular' + type: string + type: object + xml: + name: board + prefs_cardCovers: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + prefs_colorBlind: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: member + prefs_comments: + properties: + value: + description: 'One of: disabled, members, observers, org or public' + type: string + type: object + xml: + name: board + prefs_externalMembersDisabled: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: organization + prefs_googleAppsVersion: + properties: + value: + description: a number from 1 to 2 + type: string + type: object + xml: + name: organization + prefs_invitations: + properties: + value: + description: 'One of: admins or members' + type: string + type: object + xml: + name: board + prefs_locale: + properties: + value: + description: a string with a length from 0 to 255 + type: string + type: object + xml: + name: member + prefs_minutesBetweenSummaries: + properties: + value: + description: '-1 (disabled), 1 or 60' + type: string + type: object + xml: + name: member + prefs_orgInviteRestrict: + properties: + value: + description: An email address with optional expansion tokens + type: string + type: object + xml: + name: organization + prefs_permissionLevel: + properties: + value: + description: 'One of: private or public' + type: string + type: object + xml: + name: board + prefs_selfJoin: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: board + prefs_voting: + properties: + value: + description: 'One of: disabled, members, observers, org or public' + type: string + type: object + xml: + name: board + sessions: + properties: + idBoard: + description: 'The id of the board you're viewing. Boards with no viewers will not get updates about members' statuses.' + type: string + status: + description: 'One of: active, disconnected or idle' + type: string + type: object + xml: + name: session + sessions_status: + properties: + value: + description: 'One of: active, disconnected or idle' + type: string + type: object + xml: + name: session + tokens_webhooks: + properties: + callbackURL: + description: A valid URL that is reachable with a HEAD request + type: string + description: + description: a string with a length from 0 to 16384 + type: string + idModel: + description: id of the model to be monitored + type: string + type: object + xml: + name: token + webhooks: + properties: + active: + description: ' true or false' + type: string + callbackURL: + description: A valid URL that is reachable with a HEAD request + type: string + description: + description: a string with a length from 0 to 16384 + type: string + idModel: + description: id of the model that should be hooked + type: string + type: object + xml: + name: webhook + webhooks_active: + properties: + value: + description: ' true or false' + type: string + type: object + xml: + name: webhook + webhooks_callbackURL: + properties: + value: + description: A valid URL that is reachable with a HEAD request + type: string + type: object + xml: + name: webhook + webhooks_description: + properties: + value: + description: a string with a length from 0 to 16384 + type: string + type: object + xml: + name: webhook + webhooks_idModel: + properties: + value: + description: id of the model to be monitored + type: string + type: object + xml: + name: webhook diff --git a/packages/v1-ready/trello/package.json b/packages/trello/package.json similarity index 100% rename from packages/v1-ready/trello/package.json rename to packages/trello/package.json diff --git a/packages/twilio/README.md b/packages/twilio/README.md new file mode 100644 index 0000000..ef94357 --- /dev/null +++ b/packages/twilio/README.md @@ -0,0 +1,133 @@ +# Twilio API Module + +A comprehensive Twilio API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +TWILIO_ACCOUNT_SID=your_account_sid +TWILIO_API_KEY=your_api_key +TWILIO_API_SECRET=your_api_secret +``` + +### Getting Twilio API Credentials + +1. Sign up for a Twilio account at [https://www.twilio.com/](https://www.twilio.com/) +2. Go to the [Twilio Console](https://console.twilio.com/) +3. Your Account SID is displayed on the console dashboard +4. Navigate to Settings > API Keys & Tokens +5. Create a new API Key and copy the Key and Secret + +### Authentication + +This module uses HTTP Basic Authentication with your API Key as the username and API Secret as the password. + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-twilio'); + +// Initialize with credentials +const twilioApi = new Api({ + account_sid: process.env.TWILIO_ACCOUNT_SID, + api_key: process.env.TWILIO_API_KEY, + api_secret: process.env.TWILIO_API_SECRET +}); + +// Send an SMS +const message = await twilioApi.sendMessage( + '+1234567890', // to + '+0987654321', // from (your Twilio number) + 'Hello from Twilio!' +); + +// Make a call +const call = await twilioApi.makeCall( + '+1234567890', // to + '+0987654321', // from (your Twilio number) + 'https://your-app.com/twiml' // TwiML URL +); +``` + +## Available Methods + +### Account Methods +- `getAccount()` - Get account information +- `updateAccount(params)` - Update account settings + +### Messages Methods +- `getMessages(params)` - List messages with optional filters +- `getMessage(messageSid)` - Get specific message +- `sendMessage(to, from, body, params)` - Send SMS/MMS message +- `deleteMessage(messageSid)` - Delete a message + +### Calls Methods +- `getCalls(params)` - List calls with optional filters +- `getCall(callSid)` - Get specific call +- `makeCall(to, from, url, params)` - Make a phone call +- `updateCall(callSid, params)` - Update call in progress +- `deleteCall(callSid)` - Delete call record + +### Phone Numbers Methods +- `getIncomingPhoneNumbers(params)` - List your phone numbers +- `getIncomingPhoneNumber(phoneNumberSid)` - Get specific phone number +- `purchasePhoneNumber(phoneNumber, params)` - Purchase a phone number +- `updateIncomingPhoneNumber(phoneNumberSid, params)` - Update phone number settings +- `deleteIncomingPhoneNumber(phoneNumberSid)` - Release a phone number +- `getAvailablePhoneNumbers(countryCode, params)` - Search available numbers + +### Applications Methods +- `getApplications(params)` - List applications +- `getApplication(applicationSid)` - Get specific application +- `createApplication(friendlyName, params)` - Create new application +- `updateApplication(applicationSid, params)` - Update application +- `deleteApplication(applicationSid)` - Delete application + +### Conferences Methods +- `getConferences(params)` - List conferences +- `getConference(conferenceSid)` - Get specific conference +- `getConferenceParticipants(conferenceSid)` - List conference participants +- `getConferenceParticipant(conferenceSid, participantSid)` - Get specific participant +- `updateConferenceParticipant(conferenceSid, participantSid, params)` - Update participant +- `deleteConferenceParticipant(conferenceSid, participantSid)` - Remove participant + +### Recordings Methods +- `getRecordings(params)` - List recordings +- `getRecording(recordingSid)` - Get specific recording +- `deleteRecording(recordingSid)` - Delete recording + +### Usage Methods +- `getUsageRecords(params)` - Get usage records with filters +- `getUsageToday()` - Get today's usage +- `getUsageYesterday()` - Get yesterday's usage +- `getUsageThisMonth()` - Get current month's usage +- `getUsageLastMonth()` - Get last month's usage + +## Error Handling + +Twilio returns detailed error information in responses. Always wrap API calls in try-catch blocks: + +```javascript +try { + const message = await twilioApi.sendMessage(to, from, body); + console.log('Message sent:', message.sid); +} catch (error) { + console.error('Error sending message:', error.message); +} +``` + +## Rate Limiting + +Twilio enforces rate limits on API requests. The module does not implement automatic retry logic - you should handle rate limiting in your application. + +## Webhooks + +Twilio can send webhooks to your application for various events. You'll need to configure webhook URLs in your Twilio Console or via the API. + +## Documentation + +For detailed Twilio API documentation, visit: https://www.twilio.com/docs/api \ No newline at end of file diff --git a/packages/twilio/api.js b/packages/twilio/api.js new file mode 100644 index 0000000..865c288 --- /dev/null +++ b/packages/twilio/api.js @@ -0,0 +1,406 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + this.account_sid = get(params, 'account_sid', null); + this.api_key = get(params, 'api_key', null); + this.api_secret = get(params, 'api_secret', null); + + this.baseUrl = `https://api.twilio.com/2010-04-01/Accounts/${this.account_sid}`; + + this.URLs = { + // Account + account: '', + + // Messages + messages: '/Messages.json', + messageById: (sid) => `/Messages/${sid}.json`, + + // Calls + calls: '/Calls.json', + callById: (sid) => `/Calls/${sid}.json`, + + // Phone Numbers + incomingPhoneNumbers: '/IncomingPhoneNumbers.json', + incomingPhoneNumberById: (sid) => `/IncomingPhoneNumbers/${sid}.json`, + availablePhoneNumbers: (countryCode) => `/AvailablePhoneNumbers/${countryCode}/Local.json`, + + // Applications + applications: '/Applications.json', + applicationById: (sid) => `/Applications/${sid}.json`, + + // Conferences + conferences: '/Conferences.json', + conferenceById: (sid) => `/Conferences/${sid}.json`, + conferenceParticipants: (conferenceSid) => `/Conferences/${conferenceSid}/Participants.json`, + conferenceParticipantById: (conferenceSid, participantSid) => `/Conferences/${conferenceSid}/Participants/${participantSid}.json`, + + // Queues + queues: '/Queues.json', + queueById: (sid) => `/Queues/${sid}.json`, + queueMembers: (queueSid) => `/Queues/${queueSid}/Members.json`, + + // Recordings + recordings: '/Recordings.json', + recordingById: (sid) => `/Recordings/${sid}.json`, + + // Usage + usage: '/Usage/Records.json', + usageToday: '/Usage/Records/Today.json', + usageYesterday: '/Usage/Records/Yesterday.json', + usageThisMonth: '/Usage/Records/ThisMonth.json', + usageLastMonth: '/Usage/Records/LastMonth.json', + }; + } + + addAuthHeaders(headers = {}) { + const credentials = Buffer.from(`${this.api_key}:${this.api_secret}`).toString('base64'); + headers.Authorization = `Basic ${credentials}`; + return headers; + } + + async _request(url, options = {}) { + options.headers = this.addAuthHeaders(options.headers); + return super._request(url, options); + } + + // ************************** Account Methods ********************************** + + async getAccount() { + const options = { + url: this.baseUrl + this.URLs.account + '.json', + }; + return this._get(options); + } + + async updateAccount(params) { + const options = { + url: this.baseUrl + this.URLs.account + '.json', + body: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + // ************************** Messages Methods ********************************** + + async getMessages(params = {}) { + const options = { + url: this.baseUrl + this.URLs.messages, + query: params, + }; + return this._get(options); + } + + async getMessage(messageSid) { + const options = { + url: this.baseUrl + this.URLs.messageById(messageSid), + }; + return this._get(options); + } + + async sendMessage(to, from, body, params = {}) { + const messageData = { + To: to, + From: from, + Body: body, + ...params, + }; + + const options = { + url: this.baseUrl + this.URLs.messages, + body: new URLSearchParams(messageData).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async deleteMessage(messageSid) { + const options = { + url: this.baseUrl + this.URLs.messageById(messageSid), + }; + return this._delete(options); + } + + // ************************** Calls Methods ********************************** + + async getCalls(params = {}) { + const options = { + url: this.baseUrl + this.URLs.calls, + query: params, + }; + return this._get(options); + } + + async getCall(callSid) { + const options = { + url: this.baseUrl + this.URLs.callById(callSid), + }; + return this._get(options); + } + + async makeCall(to, from, url, params = {}) { + const callData = { + To: to, + From: from, + Url: url, + ...params, + }; + + const options = { + url: this.baseUrl + this.URLs.calls, + body: new URLSearchParams(callData).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async updateCall(callSid, params) { + const options = { + url: this.baseUrl + this.URLs.callById(callSid), + body: new URLSearchParams(params).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async deleteCall(callSid) { + const options = { + url: this.baseUrl + this.URLs.callById(callSid), + }; + return this._delete(options); + } + + // ************************** Phone Numbers Methods ********************************** + + async getIncomingPhoneNumbers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.incomingPhoneNumbers, + query: params, + }; + return this._get(options); + } + + async getIncomingPhoneNumber(phoneNumberSid) { + const options = { + url: this.baseUrl + this.URLs.incomingPhoneNumberById(phoneNumberSid), + }; + return this._get(options); + } + + async purchasePhoneNumber(phoneNumber, params = {}) { + const phoneData = { + PhoneNumber: phoneNumber, + ...params, + }; + + const options = { + url: this.baseUrl + this.URLs.incomingPhoneNumbers, + body: new URLSearchParams(phoneData).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async updateIncomingPhoneNumber(phoneNumberSid, params) { + const options = { + url: this.baseUrl + this.URLs.incomingPhoneNumberById(phoneNumberSid), + body: new URLSearchParams(params).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async deleteIncomingPhoneNumber(phoneNumberSid) { + const options = { + url: this.baseUrl + this.URLs.incomingPhoneNumberById(phoneNumberSid), + }; + return this._delete(options); + } + + async getAvailablePhoneNumbers(countryCode, params = {}) { + const options = { + url: this.baseUrl + this.URLs.availablePhoneNumbers(countryCode), + query: params, + }; + return this._get(options); + } + + // ************************** Applications Methods ********************************** + + async getApplications(params = {}) { + const options = { + url: this.baseUrl + this.URLs.applications, + query: params, + }; + return this._get(options); + } + + async getApplication(applicationSid) { + const options = { + url: this.baseUrl + this.URLs.applicationById(applicationSid), + }; + return this._get(options); + } + + async createApplication(friendlyName, params = {}) { + const appData = { + FriendlyName: friendlyName, + ...params, + }; + + const options = { + url: this.baseUrl + this.URLs.applications, + body: new URLSearchParams(appData).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async updateApplication(applicationSid, params) { + const options = { + url: this.baseUrl + this.URLs.applicationById(applicationSid), + body: new URLSearchParams(params).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async deleteApplication(applicationSid) { + const options = { + url: this.baseUrl + this.URLs.applicationById(applicationSid), + }; + return this._delete(options); + } + + // ************************** Conferences Methods ********************************** + + async getConferences(params = {}) { + const options = { + url: this.baseUrl + this.URLs.conferences, + query: params, + }; + return this._get(options); + } + + async getConference(conferenceSid) { + const options = { + url: this.baseUrl + this.URLs.conferenceById(conferenceSid), + }; + return this._get(options); + } + + async getConferenceParticipants(conferenceSid) { + const options = { + url: this.baseUrl + this.URLs.conferenceParticipants(conferenceSid), + }; + return this._get(options); + } + + async getConferenceParticipant(conferenceSid, participantSid) { + const options = { + url: this.baseUrl + this.URLs.conferenceParticipantById(conferenceSid, participantSid), + }; + return this._get(options); + } + + async updateConferenceParticipant(conferenceSid, participantSid, params) { + const options = { + url: this.baseUrl + this.URLs.conferenceParticipantById(conferenceSid, participantSid), + body: new URLSearchParams(params).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + return this._post(options, false); + } + + async deleteConferenceParticipant(conferenceSid, participantSid) { + const options = { + url: this.baseUrl + this.URLs.conferenceParticipantById(conferenceSid, participantSid), + }; + return this._delete(options); + } + + // ************************** Recordings Methods ********************************** + + async getRecordings(params = {}) { + const options = { + url: this.baseUrl + this.URLs.recordings, + query: params, + }; + return this._get(options); + } + + async getRecording(recordingSid) { + const options = { + url: this.baseUrl + this.URLs.recordingById(recordingSid), + }; + return this._get(options); + } + + async deleteRecording(recordingSid) { + const options = { + url: this.baseUrl + this.URLs.recordingById(recordingSid), + }; + return this._delete(options); + } + + // ************************** Usage Methods ********************************** + + async getUsageRecords(params = {}) { + const options = { + url: this.baseUrl + this.URLs.usage, + query: params, + }; + return this._get(options); + } + + async getUsageToday() { + const options = { + url: this.baseUrl + this.URLs.usageToday, + }; + return this._get(options); + } + + async getUsageYesterday() { + const options = { + url: this.baseUrl + this.URLs.usageYesterday, + }; + return this._get(options); + } + + async getUsageThisMonth() { + const options = { + url: this.baseUrl + this.URLs.usageThisMonth, + }; + return this._get(options); + } + + async getUsageLastMonth() { + const options = { + url: this.baseUrl + this.URLs.usageLastMonth, + }; + return this._get(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/twilio/defaultConfig.json b/packages/twilio/defaultConfig.json new file mode 100644 index 0000000..ff1123a --- /dev/null +++ b/packages/twilio/defaultConfig.json @@ -0,0 +1,15 @@ +{ + "name": "twilio", + "label": "Twilio", + "productUrl": "https://twilio.com", + "apiDocs": "https://www.twilio.com/docs/api", + "logoUrl": "https://www.twilio.com/content/dam/twilio-com/global/en/logos/red/twilio-logo-red.svg", + "categories": [ + "Communications", + "SMS", + "Voice", + "Video", + "Messaging" + ], + "description": "Twilio is a cloud communications platform that allows software developers to programmatically make and receive phone calls, send and receive text messages, and perform other communication functions" +} \ No newline at end of file diff --git a/packages/twilio/definition.js b/packages/twilio/definition.js new file mode 100644 index 0000000..c45e3a1 --- /dev/null +++ b/packages/twilio/definition.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Twilio', + requiredAuthMethods: { + getAuthorizationRequirements: async function (params) { + return { + type: 'basic_auth', + url: 'https://console.twilio.com/account/keys-credentials/api-keys', + description: 'Generate an API Key and Secret from your Twilio Console. You will also need your Account SID.' + }; + }, + getCredentialDetails: async function (api, userId) { + const accountDetails = await api.getAccount(); + return { + identifiers: { externalId: accountDetails.sid, user: userId }, + details: {} + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const accountDetails = await api.getAccount(); + return { + identifiers: { externalId: accountDetails.sid, user: userId }, + details: { + name: accountDetails.friendly_name || accountDetails.sid, + type: accountDetails.type + } + }; + }, + testAuthRequest: async function (api) { + return api.getAccount(); + }, + apiPropertiesToPersist: { + credential: ['account_sid', 'api_key', 'api_secret'], + entity: [] + } + }, + env: { + account_sid: process.env.TWILIO_ACCOUNT_SID, + api_key: process.env.TWILIO_API_KEY, + api_secret: process.env.TWILIO_API_SECRET + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/twilio/index.js b/packages/twilio/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/twilio/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/twilio/openapi.json b/packages/twilio/openapi.json new file mode 100644 index 0000000..1d1b7b9 --- /dev/null +++ b/packages/twilio/openapi.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82fa0c213f5466f444c9754b4743211ea3659e0d42c901384bba4e0482cd9286 +size 2134903 diff --git a/packages/twilio/package.json b/packages/twilio/package.json new file mode 100644 index 0000000..c44aef6 --- /dev/null +++ b/packages/twilio/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/twilio", + "version": "0.0.1", + "description": "Twilio API Module for Frigg", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "frigg", + "twilio", + "api", + "integration" + ], + "author": "Frigg", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0" + }, + "devDependencies": { + "@friggframework/test": "^1.0.0", + "jest": "^27.0.0" + } +} \ No newline at end of file diff --git a/packages/typeform/README.md b/packages/typeform/README.md new file mode 100644 index 0000000..2b4904a --- /dev/null +++ b/packages/typeform/README.md @@ -0,0 +1,389 @@ +# Typeform API Module + +A comprehensive Typeform API integration module for the Frigg Framework. + +## Setup + +### Environment Variables + +Add the following environment variables to your `.env` file: + +```bash +TYPEFORM_CLIENT_ID=your_typeform_client_id +TYPEFORM_CLIENT_SECRET=your_typeform_client_secret +TYPEFORM_SCOPE=forms:read forms:write responses:read accounts:read workspaces:read +REDIRECT_URI=your_redirect_uri_base +``` + +### Getting Typeform API Credentials + +1. Go to the [Typeform Developer Portal](https://developer.typeform.com/) +2. Sign in with your Typeform account +3. Create a new app in your developer account +4. Get your Client ID and Client Secret +5. Set up your redirect URI (e.g., `https://yourdomain.com/typeform`) + +### OAuth2 Scopes + +Available scopes for Typeform API: +- `accounts:read` - Read account information +- `forms:read` - Read forms and their structure +- `forms:write` - Create and modify forms +- `images:read` - Read images +- `images:write` - Upload and manage images +- `themes:read` - Read themes +- `themes:write` - Create and modify themes +- `responses:read` - Read form responses +- `webhooks:read` - Read webhook configurations +- `webhooks:write` - Create and manage webhooks +- `workspaces:read` - Read workspace information +- `workspaces:write` - Create and manage workspaces + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-typeform'); + +// Initialize with credentials +const typeformApi = new Api({ + client_id: process.env.TYPEFORM_CLIENT_ID, + client_secret: process.env.TYPEFORM_CLIENT_SECRET, + redirect_uri: process.env.REDIRECT_URI + '/typeform', + scope: 'forms:read forms:write responses:read accounts:read workspaces:read' +}); + +// Get authorization URL +const authUrl = typeformApi.getAuthUri(); + +// Exchange code for tokens +const tokens = await typeformApi.getTokenFromCode(authorizationCode); + +// Get user information +const me = await typeformApi.getMe(); + +// Get all forms +const forms = await typeformApi.getForms(); + +// Get responses for a form +const responses = await typeformApi.getFormResponses('form-id'); +``` + +## Available Methods + +### User/Account Methods +- `getMe()` - Get current user account information + +### Forms Methods +- `getForms(params)` - List forms with pagination and filters +- `createForm(formData)` - Create new form +- `getForm(formId)` - Get specific form +- `updateForm(formId, formData)` - Update form +- `deleteForm(formId)` - Delete form +- `duplicateForm(formId, targetWorkspaceHref)` - Duplicate form +- `createSimpleForm(title, fields, workspaceHref)` - Helper for simple forms +- `searchForms(searchTerm, workspaceId)` - Search forms by title + +### Form Messages Methods +- `getFormMessages(formId)` - Get form custom messages +- `updateFormMessages(formId, messagesData)` - Update form messages + +### Responses Methods +- `getFormResponses(formId, params)` - Get form responses +- `deleteFormResponses(formId, responseIds)` - Delete specific responses +- `getFormResponsesSince(formId, since)` - Get responses since timestamp +- `getFormResponsesWithAnswers(formId, params)` - Get completed responses +- `extractAnswers(response)` - Helper to extract answer values + +### Images Methods +- `getImages()` - List uploaded images +- `uploadImage(imageData)` - Upload new image +- `getImage(imageId)` - Get specific image +- `deleteImage(imageId)` - Delete image + +### Themes Methods +- `getThemes(params)` - List themes +- `createTheme(themeData)` - Create custom theme +- `getTheme(themeId)` - Get specific theme +- `updateTheme(themeId, themeData)` - Update theme +- `deleteTheme(themeId)` - Delete theme + +### Workspaces Methods +- `getWorkspaces(params)` - List workspaces +- `createWorkspace(workspaceData)` - Create new workspace +- `getWorkspace(workspaceId)` - Get specific workspace +- `updateWorkspace(workspaceId, workspaceData)` - Update workspace +- `deleteWorkspace(workspaceId)` - Delete workspace +- `getWorkspaceForms(workspaceId, params)` - Get forms in workspace + +### Webhooks Methods +- `getFormWebhooks(formId)` - List webhooks for form +- `createFormWebhook(formId, webhookData)` - Create webhook +- `getFormWebhook(formId, webhookTag)` - Get specific webhook +- `updateFormWebhook(formId, webhookTag, webhookData)` - Update webhook +- `deleteFormWebhook(formId, webhookTag)` - Delete webhook + +## Usage Examples + +### Creating a Simple Form +```javascript +const fields = [ + { + title: 'What is your name?', + type: 'short_text', + properties: { + description: 'Please enter your full name' + }, + validations: { + required: true + } + }, + { + title: 'What is your email?', + type: 'email', + validations: { + required: true + } + }, + { + title: 'How satisfied are you?', + type: 'rating', + properties: { + steps: 5, + labels: { + left: 'Not satisfied', + right: 'Very satisfied' + } + } + } +]; + +const form = await typeformApi.createSimpleForm( + 'Customer Feedback Survey', + fields +); + +console.log('Form created:', form.id); +console.log('Form URL:', form._links.display); +``` + +### Creating a Complex Form +```javascript +const formData = { + title: 'Product Feedback Form', + type: 'quiz', + workspace: { + href: 'https://api.typeform.com/workspaces/workspace-id' + }, + theme: { + href: 'https://api.typeform.com/themes/theme-id' + }, + settings: { + is_public: true, + is_trial: false, + language: 'en', + progress_bar: 'proportion', + show_progress_bar: true, + show_typeform_branding: true, + meta: { + allow_indexing: false + } + }, + welcome_screens: [ + { + title: 'Welcome to our survey!', + properties: { + description: 'Thank you for taking the time to provide feedback.', + button_text: 'Start' + } + } + ], + thankyou_screens: [ + { + title: 'Thank you!', + properties: { + description: 'We appreciate your feedback.' + } + } + ], + fields: [ + { + title: 'What product are you reviewing?', + type: 'dropdown', + properties: { + choices: [ + { label: 'Product A' }, + { label: 'Product B' }, + { label: 'Product C' } + ] + }, + validations: { + required: true + } + }, + { + title: 'Rate this product', + type: 'opinion_scale', + properties: { + start_at_one: true, + steps: 10, + labels: { + left: 'Poor', + right: 'Excellent' + } + } + } + ] +}; + +const form = await typeformApi.createForm(formData); +``` + +### Getting and Processing Responses +```javascript +// Get all completed responses +const responses = await typeformApi.getFormResponsesWithAnswers('form-id'); + +responses.items.forEach(response => { + console.log('Response ID:', response.response_id); + console.log('Submitted:', response.submitted_at); + + // Extract answers using helper method + const answers = typeformApi.extractAnswers(response); + console.log('Answers:', answers); + + // Or process answers manually + response.answers.forEach(answer => { + console.log(`Question: ${answer.field.id}`); + console.log(`Answer: ${answer.text || answer.email || answer.number}`); + }); +}); +``` + +### Setting up Webhooks +```javascript +const webhookData = { + url: 'https://yoursite.com/webhooks/typeform', + enabled: true, + secret: 'your-webhook-secret', + verify_ssl: true +}; + +const webhook = await typeformApi.createFormWebhook('form-id', webhookData); +console.log('Webhook created with tag:', webhook.tag); +``` + +### Working with Workspaces +```javascript +// Get all workspaces +const workspaces = await typeformApi.getWorkspaces(); + +// Create a new workspace +const newWorkspace = await typeformApi.createWorkspace({ + name: 'Marketing Team Workspace' +}); + +// Get forms in a workspace +const workspaceForms = await typeformApi.getWorkspaceForms(newWorkspace.id); +``` + +### Uploading and Managing Images +```javascript +// Note: Image upload requires multipart/form-data +const imageFormData = new FormData(); +imageFormData.append('image', fileBuffer, 'image.jpg'); +imageFormData.append('media_type', 'image'); + +const uploadedImage = await typeformApi.uploadImage(imageFormData); + +// Use the image in a form +const formField = { + title: 'Rate this image', + type: 'rating', + attachment: { + type: 'image', + href: uploadedImage.src + } +}; +``` + +### Filtering Responses +```javascript +// Get responses from the last 7 days +const weekAgo = new Date(); +weekAgo.setDate(weekAgo.getDate() - 7); + +const recentResponses = await typeformApi.getFormResponses('form-id', { + since: weekAgo.toISOString(), + completed: true, + page_size: 100 +}); + +// Get responses with specific completion status +const incompleteResponses = await typeformApi.getFormResponses('form-id', { + completed: false +}); +``` + +## Authentication Flow + +Typeform uses OAuth2: + +1. Redirect users to Typeform's authorization URL +2. Handle the callback with the authorization code +3. Exchange the code for access and refresh tokens +4. Use tokens for API requests + +## Error Handling + +Typeform returns detailed error information. Always wrap API calls in try-catch blocks: + +```javascript +try { + const form = await typeformApi.createForm(formData); + console.log('Form created successfully'); +} catch (error) { + console.error('Typeform error:', error.message); + if (error.details) { + error.details.forEach(detail => { + console.error('Error detail:', detail.description); + }); + } +} +``` + +## Rate Limiting + +Typeform enforces rate limits on API requests: +- 4 requests per second for most endpoints +- Different limits for different operations + +The module does not implement automatic retry logic - you should handle rate limiting in your application. + +## Webhooks + +Typeform sends webhooks when forms receive new responses. Configure webhooks to receive real-time notifications about form submissions. + +Webhook payload includes: +- Event type (`form_response`) +- Form ID and response data +- Timestamp and response ID + +## Field Types + +Typeform supports various field types: +- `short_text` - Short text input +- `long_text` - Long text area +- `email` - Email validation +- `number` - Numeric input +- `multiple_choice` - Single selection +- `yes_no` - Yes/No question +- `dropdown` - Dropdown selection +- `rating` - Star rating +- `opinion_scale` - Numeric scale +- `date` - Date picker +- `file_upload` - File upload +- `payment` - Payment processing + +## Documentation + +For detailed Typeform API documentation, visit: https://developer.typeform.com/ \ No newline at end of file diff --git a/packages/typeform/api.js b/packages/typeform/api.js new file mode 100644 index 0000000..9fa1095 --- /dev/null +++ b/packages/typeform/api.js @@ -0,0 +1,426 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.typeform.com'; + + this.URLs = { + authorization: '/oauth/authorize', + access_token: '/oauth/token', + + // User/Account + me: '/me', + + // Forms + forms: '/forms', + formById: (formId) => `/forms/${formId}`, + formMessages: (formId) => `/forms/${formId}/messages`, + + // Responses + formResponses: (formId) => `/forms/${formId}/responses`, + + // Images + images: '/images', + imageById: (imageId) => `/images/${imageId}`, + + // Themes + themes: '/themes', + themeById: (themeId) => `/themes/${themeId}`, + + // Workspaces + workspaces: '/workspaces', + workspaceById: (workspaceId) => `/workspaces/${workspaceId}`, + workspaceForms: (workspaceId) => `/workspaces/${workspaceId}/forms`, + + // Webhooks + formWebhooks: (formId) => `/forms/${formId}/webhooks`, + formWebhookById: (formId, webhookTag) => `/forms/${formId}/webhooks/${webhookTag}`, + }; + + this.authorizationUri = encodeURI( + `https://admin.typeform.com/oauth/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${this.scope}&response_type=code&state=${this.state}` + ); + this.tokenUri = 'https://api.typeform.com/oauth/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + addJsonHeaders(options) { + const jsonHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + options.headers = { + ...jsonHeaders, + ...options.headers, + }; + } + + async _post(options, stringify = true) { + this.addJsonHeaders(options); + return super._post(options, stringify); + } + + async _patch(options, stringify = true) { + this.addJsonHeaders(options); + return super._patch(options, stringify); + } + + async _put(options, stringify = true) { + this.addJsonHeaders(options); + return super._put(options, stringify); + } + + // ************************** User/Account Methods ********************************** + + async getMe() { + const options = { + url: this.baseUrl + this.URLs.me, + }; + return this._get(options); + } + + // ************************** Forms Methods ********************************** + + async getForms(params = {}) { + const options = { + url: this.baseUrl + this.URLs.forms, + query: params, + }; + return this._get(options); + } + + async createForm(formData) { + const options = { + url: this.baseUrl + this.URLs.forms, + body: formData, + }; + return this._post(options); + } + + async getForm(formId) { + const options = { + url: this.baseUrl + this.URLs.formById(formId), + }; + return this._get(options); + } + + async updateForm(formId, formData) { + const options = { + url: this.baseUrl + this.URLs.formById(formId), + body: formData, + }; + return this._put(options); + } + + async deleteForm(formId) { + const options = { + url: this.baseUrl + this.URLs.formById(formId), + }; + return this._delete(options); + } + + async duplicateForm(formId, targetWorkspaceHref = null) { + const body = {}; + if (targetWorkspaceHref) { + body.target_workspace_href = targetWorkspaceHref; + } + + const options = { + url: this.baseUrl + this.URLs.formById(formId) + '/copy', + body, + }; + return this._post(options); + } + + // ************************** Form Messages Methods ********************************** + + async getFormMessages(formId) { + const options = { + url: this.baseUrl + this.URLs.formMessages(formId), + }; + return this._get(options); + } + + async updateFormMessages(formId, messagesData) { + const options = { + url: this.baseUrl + this.URLs.formMessages(formId), + body: messagesData, + }; + return this._put(options); + } + + // ************************** Responses Methods ********************************** + + async getFormResponses(formId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.formResponses(formId), + query: params, + }; + return this._get(options); + } + + async deleteFormResponses(formId, responseIds) { + const options = { + url: this.baseUrl + this.URLs.formResponses(formId), + body: { included_response_ids: responseIds }, + }; + return this._delete(options); + } + + // ************************** Images Methods ********************************** + + async getImages() { + const options = { + url: this.baseUrl + this.URLs.images, + }; + return this._get(options); + } + + async uploadImage(imageData) { + // Note: This requires multipart/form-data, not JSON + const options = { + url: this.baseUrl + this.URLs.images, + body: imageData, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }; + return this._post(options, false); + } + + async getImage(imageId) { + const options = { + url: this.baseUrl + this.URLs.imageById(imageId), + }; + return this._get(options); + } + + async deleteImage(imageId) { + const options = { + url: this.baseUrl + this.URLs.imageById(imageId), + }; + return this._delete(options); + } + + // ************************** Themes Methods ********************************** + + async getThemes(params = {}) { + const options = { + url: this.baseUrl + this.URLs.themes, + query: params, + }; + return this._get(options); + } + + async createTheme(themeData) { + const options = { + url: this.baseUrl + this.URLs.themes, + body: themeData, + }; + return this._post(options); + } + + async getTheme(themeId) { + const options = { + url: this.baseUrl + this.URLs.themeById(themeId), + }; + return this._get(options); + } + + async updateTheme(themeId, themeData) { + const options = { + url: this.baseUrl + this.URLs.themeById(themeId), + body: themeData, + }; + return this._put(options); + } + + async deleteTheme(themeId) { + const options = { + url: this.baseUrl + this.URLs.themeById(themeId), + }; + return this._delete(options); + } + + // ************************** Workspaces Methods ********************************** + + async getWorkspaces(params = {}) { + const options = { + url: this.baseUrl + this.URLs.workspaces, + query: params, + }; + return this._get(options); + } + + async createWorkspace(workspaceData) { + const options = { + url: this.baseUrl + this.URLs.workspaces, + body: workspaceData, + }; + return this._post(options); + } + + async getWorkspace(workspaceId) { + const options = { + url: this.baseUrl + this.URLs.workspaceById(workspaceId), + }; + return this._get(options); + } + + async updateWorkspace(workspaceId, workspaceData) { + const options = { + url: this.baseUrl + this.URLs.workspaceById(workspaceId), + body: workspaceData, + }; + return this._patch(options); + } + + async deleteWorkspace(workspaceId) { + const options = { + url: this.baseUrl + this.URLs.workspaceById(workspaceId), + }; + return this._delete(options); + } + + async getWorkspaceForms(workspaceId, params = {}) { + const options = { + url: this.baseUrl + this.URLs.workspaceForms(workspaceId), + query: params, + }; + return this._get(options); + } + + // ************************** Webhooks Methods ********************************** + + async getFormWebhooks(formId) { + const options = { + url: this.baseUrl + this.URLs.formWebhooks(formId), + }; + return this._get(options); + } + + async createFormWebhook(formId, webhookData) { + const options = { + url: this.baseUrl + this.URLs.formWebhooks(formId), + body: webhookData, + }; + return this._post(options); + } + + async getFormWebhook(formId, webhookTag) { + const options = { + url: this.baseUrl + this.URLs.formWebhookById(formId, webhookTag), + }; + return this._get(options); + } + + async updateFormWebhook(formId, webhookTag, webhookData) { + const options = { + url: this.baseUrl + this.URLs.formWebhookById(formId, webhookTag), + body: webhookData, + }; + return this._put(options); + } + + async deleteFormWebhook(formId, webhookTag) { + const options = { + url: this.baseUrl + this.URLs.formWebhookById(formId, webhookTag), + }; + return this._delete(options); + } + + // ************************** Helper Methods ********************************** + + async createSimpleForm(title, fields, workspaceHref = null) { + const formData = { + title: title, + fields: fields.map((field, index) => ({ + id: `field_${index + 1}`, + title: field.title, + type: field.type, + properties: field.properties || {}, + validations: field.validations || {} + })), + settings: { + is_public: true, + is_trial: false + } + }; + + if (workspaceHref) { + formData.workspace = { href: workspaceHref }; + } + + return this.createForm(formData); + } + + async getFormResponsesSince(formId, since) { + const params = { + since: since, + completed: true + }; + return this.getFormResponses(formId, params); + } + + async getFormResponsesWithAnswers(formId, params = {}) { + const defaultParams = { + completed: true, + ...params + }; + return this.getFormResponses(formId, defaultParams); + } + + async searchForms(searchTerm, workspaceId = null) { + const params = { + search: searchTerm + }; + + if (workspaceId) { + params.workspace_id = workspaceId; + } + + return this.getForms(params); + } + + // Extract answer values from response + extractAnswers(response) { + const answers = {}; + + if (response.answers) { + response.answers.forEach(answer => { + const fieldId = answer.field.id; + + // Extract value based on field type + if (answer.text) { + answers[fieldId] = answer.text; + } else if (answer.email) { + answers[fieldId] = answer.email; + } else if (answer.number) { + answers[fieldId] = answer.number; + } else if (answer.boolean !== undefined) { + answers[fieldId] = answer.boolean; + } else if (answer.choice) { + answers[fieldId] = answer.choice.label; + } else if (answer.choices) { + answers[fieldId] = answer.choices.labels; + } else if (answer.date) { + answers[fieldId] = answer.date; + } else if (answer.file_url) { + answers[fieldId] = answer.file_url; + } + }); + } + + return answers; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/typeform/defaultConfig.json b/packages/typeform/defaultConfig.json new file mode 100644 index 0000000..a1104e3 --- /dev/null +++ b/packages/typeform/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "typeform", + "label": "Typeform", + "productUrl": "https://typeform.com", + "apiDocs": "https://developer.typeform.com/", + "logoUrl": "https://images.typeform.com/images/2dpnUBBkzaVz", + "categories": [ + "Forms", + "Surveys", + "Data Collection", + "Customer Feedback" + ], + "description": "Typeform is a platform for creating interactive forms, surveys, quizzes, and landing pages" +} \ No newline at end of file diff --git a/packages/typeform/definition.js b/packages/typeform/definition.js new file mode 100644 index 0000000..0e873e9 --- /dev/null +++ b/packages/typeform/definition.js @@ -0,0 +1,54 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Typeform', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const meInfo = await api.getMe(); + return { + identifiers: { externalId: meInfo.user_id, user: userId }, + details: { + name: `${meInfo.first_name} ${meInfo.last_name}`.trim(), + email: meInfo.email, + alias: meInfo.alias + } + }; + }, + apiPropertiesToPersist: { + credential: [ + 'access_token', 'refresh_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const meInfo = await api.getMe(); + return { + identifiers: { externalId: meInfo.user_id, user: userId }, + details: {} + }; + }, + testAuthRequest: async function (api) { + return api.getMe(); + }, + }, + env: { + client_id: process.env.TYPEFORM_CLIENT_ID, + client_secret: process.env.TYPEFORM_CLIENT_SECRET, + scope: process.env.TYPEFORM_SCOPE || 'forms:read forms:write responses:read accounts:read workspaces:read', + redirect_uri: `${process.env.REDIRECT_URI}/typeform`, + } +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/typeform/index.js b/packages/typeform/index.js new file mode 100644 index 0000000..e900729 --- /dev/null +++ b/packages/typeform/index.js @@ -0,0 +1,9 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/v1-ready/pipedrive/.eslintrc.json b/packages/unbabel-projects/.eslintrc.json similarity index 100% rename from packages/v1-ready/pipedrive/.eslintrc.json rename to packages/unbabel-projects/.eslintrc.json diff --git a/packages/v1-ready/unbabel-projects/.gitignore b/packages/unbabel-projects/.gitignore similarity index 100% rename from packages/v1-ready/unbabel-projects/.gitignore rename to packages/unbabel-projects/.gitignore diff --git a/packages/v1-ready/unbabel-projects/CHANGELOG.md b/packages/unbabel-projects/CHANGELOG.md similarity index 100% rename from packages/v1-ready/unbabel-projects/CHANGELOG.md rename to packages/unbabel-projects/CHANGELOG.md diff --git a/packages/v1-ready/unbabel-projects/LICENSE.md b/packages/unbabel-projects/LICENSE.md similarity index 100% rename from packages/v1-ready/unbabel-projects/LICENSE.md rename to packages/unbabel-projects/LICENSE.md diff --git a/packages/v1-ready/unbabel-projects/README.md b/packages/unbabel-projects/README.md similarity index 100% rename from packages/v1-ready/unbabel-projects/README.md rename to packages/unbabel-projects/README.md diff --git a/packages/v1-ready/unbabel-projects/api.js b/packages/unbabel-projects/api.js similarity index 100% rename from packages/v1-ready/unbabel-projects/api.js rename to packages/unbabel-projects/api.js diff --git a/packages/v1-ready/unbabel-projects/authFields.js b/packages/unbabel-projects/authFields.js similarity index 100% rename from packages/v1-ready/unbabel-projects/authFields.js rename to packages/unbabel-projects/authFields.js diff --git a/packages/v1-ready/unbabel-projects/defaultConfig.json b/packages/unbabel-projects/defaultConfig.json similarity index 100% rename from packages/v1-ready/unbabel-projects/defaultConfig.json rename to packages/unbabel-projects/defaultConfig.json diff --git a/packages/v1-ready/unbabel-projects/definition.js b/packages/unbabel-projects/definition.js similarity index 100% rename from packages/v1-ready/unbabel-projects/definition.js rename to packages/unbabel-projects/definition.js diff --git a/packages/unbabel-projects/index.js b/packages/unbabel-projects/index.js new file mode 100644 index 0000000..24444e5 --- /dev/null +++ b/packages/unbabel-projects/index.js @@ -0,0 +1,13 @@ +const {Api} = require('./api'); +const {Credential} = require('./models/credential'); +const {Entity} = require('./models/entity'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Credential, + Entity, + Definition, + Config, +}; diff --git a/packages/v1-ready/linear/jest.config.js b/packages/unbabel-projects/jest.config.js similarity index 100% rename from packages/v1-ready/linear/jest.config.js rename to packages/unbabel-projects/jest.config.js diff --git a/packages/v1-ready/unbabel-projects/package.json b/packages/unbabel-projects/package.json similarity index 100% rename from packages/v1-ready/unbabel-projects/package.json rename to packages/unbabel-projects/package.json diff --git a/packages/v1-ready/unbabel-projects/tests/api.test.js b/packages/unbabel-projects/tests/api.test.js similarity index 100% rename from packages/v1-ready/unbabel-projects/tests/api.test.js rename to packages/unbabel-projects/tests/api.test.js diff --git a/packages/v1-ready/unbabel-projects/tests/auther.test.js b/packages/unbabel-projects/tests/auther.test.js similarity index 100% rename from packages/v1-ready/unbabel-projects/tests/auther.test.js rename to packages/unbabel-projects/tests/auther.test.js diff --git a/packages/v1-ready/unbabel-projects/tests/manager.test.js b/packages/unbabel-projects/tests/manager.test.js similarity index 100% rename from packages/v1-ready/unbabel-projects/tests/manager.test.js rename to packages/unbabel-projects/tests/manager.test.js diff --git a/packages/v1-ready/unbabel-projects/tests/test.txt b/packages/unbabel-projects/tests/test.txt similarity index 100% rename from packages/v1-ready/unbabel-projects/tests/test.txt rename to packages/unbabel-projects/tests/test.txt diff --git a/packages/v1-ready/salesforce/.eslintrc.json b/packages/unbabel/.eslintrc.json similarity index 100% rename from packages/v1-ready/salesforce/.eslintrc.json rename to packages/unbabel/.eslintrc.json diff --git a/packages/v1-ready/unbabel/.gitignore b/packages/unbabel/.gitignore similarity index 100% rename from packages/v1-ready/unbabel/.gitignore rename to packages/unbabel/.gitignore diff --git a/packages/v1-ready/unbabel/CHANGELOG.md b/packages/unbabel/CHANGELOG.md similarity index 100% rename from packages/v1-ready/unbabel/CHANGELOG.md rename to packages/unbabel/CHANGELOG.md diff --git a/packages/v1-ready/unbabel/LICENSE.md b/packages/unbabel/LICENSE.md similarity index 100% rename from packages/v1-ready/unbabel/LICENSE.md rename to packages/unbabel/LICENSE.md diff --git a/packages/v1-ready/unbabel/README.md b/packages/unbabel/README.md similarity index 100% rename from packages/v1-ready/unbabel/README.md rename to packages/unbabel/README.md diff --git a/packages/v1-ready/unbabel/api.js b/packages/unbabel/api.js similarity index 100% rename from packages/v1-ready/unbabel/api.js rename to packages/unbabel/api.js diff --git a/packages/v1-ready/unbabel/authFields.js b/packages/unbabel/authFields.js similarity index 100% rename from packages/v1-ready/unbabel/authFields.js rename to packages/unbabel/authFields.js diff --git a/packages/v1-ready/unbabel/defaultConfig.json b/packages/unbabel/defaultConfig.json similarity index 100% rename from packages/v1-ready/unbabel/defaultConfig.json rename to packages/unbabel/defaultConfig.json diff --git a/packages/v1-ready/unbabel/definition.js b/packages/unbabel/definition.js similarity index 100% rename from packages/v1-ready/unbabel/definition.js rename to packages/unbabel/definition.js diff --git a/packages/v1-ready/unbabel/index.js b/packages/unbabel/index.js similarity index 100% rename from packages/v1-ready/unbabel/index.js rename to packages/unbabel/index.js diff --git a/packages/v1-ready/unbabel-projects/jest.config.js b/packages/unbabel/jest.config.js similarity index 100% rename from packages/v1-ready/unbabel-projects/jest.config.js rename to packages/unbabel/jest.config.js diff --git a/packages/v1-ready/unbabel/package.json b/packages/unbabel/package.json similarity index 100% rename from packages/v1-ready/unbabel/package.json rename to packages/unbabel/package.json diff --git a/packages/v1-ready/unbabel/tests/api.test.js b/packages/unbabel/tests/api.test.js similarity index 100% rename from packages/v1-ready/unbabel/tests/api.test.js rename to packages/unbabel/tests/api.test.js diff --git a/packages/v1-ready/unbabel/tests/api.unit.test.js b/packages/unbabel/tests/api.unit.test.js similarity index 100% rename from packages/v1-ready/unbabel/tests/api.unit.test.js rename to packages/unbabel/tests/api.unit.test.js diff --git a/packages/v1-ready/unbabel/tests/auther.test.js b/packages/unbabel/tests/auther.test.js similarity index 100% rename from packages/v1-ready/unbabel/tests/auther.test.js rename to packages/unbabel/tests/auther.test.js diff --git a/packages/v1-ready/unbabel/tests/sample-data/html_submission.json b/packages/unbabel/tests/sample-data/html_submission.json similarity index 100% rename from packages/v1-ready/unbabel/tests/sample-data/html_submission.json rename to packages/unbabel/tests/sample-data/html_submission.json diff --git a/packages/v1-ready/unbabel/tests/sample-data/json_submission.json b/packages/unbabel/tests/sample-data/json_submission.json similarity index 100% rename from packages/v1-ready/unbabel/tests/sample-data/json_submission.json rename to packages/unbabel/tests/sample-data/json_submission.json diff --git a/packages/v1-ready/unbabel/tests/sample-data/long_submission.json b/packages/unbabel/tests/sample-data/long_submission.json similarity index 100% rename from packages/v1-ready/unbabel/tests/sample-data/long_submission.json rename to packages/unbabel/tests/sample-data/long_submission.json diff --git a/packages/v1-ready/unbabel/tests/sample-data/pipelines.json b/packages/unbabel/tests/sample-data/pipelines.json similarity index 100% rename from packages/v1-ready/unbabel/tests/sample-data/pipelines.json rename to packages/unbabel/tests/sample-data/pipelines.json diff --git a/packages/v1-ready/unbabel/tests/sample-data/sample_submission.json b/packages/unbabel/tests/sample-data/sample_submission.json similarity index 100% rename from packages/v1-ready/unbabel/tests/sample-data/sample_submission.json rename to packages/unbabel/tests/sample-data/sample_submission.json diff --git a/packages/v1-ready/42matters/jest-setup.js b/packages/v1-ready/42matters/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/42matters/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/42matters/jest-teardown.js b/packages/v1-ready/42matters/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/42matters/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/asana/jest-setup.js b/packages/v1-ready/asana/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/v1-ready/asana/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/v1-ready/asana/jest-teardown.js b/packages/v1-ready/asana/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/asana/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/connectwise/jest-setup.js b/packages/v1-ready/connectwise/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/connectwise/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/connectwise/jest-teardown.js b/packages/v1-ready/connectwise/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/connectwise/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/contentful/jest-setup.js b/packages/v1-ready/contentful/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/contentful/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/contentful/jest-teardown.js b/packages/v1-ready/contentful/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/contentful/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/contentstack/jest-setup.js b/packages/v1-ready/contentstack/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/contentstack/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/contentstack/jest-teardown.js b/packages/v1-ready/contentstack/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/contentstack/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/crossbeam/jest-setup.js b/packages/v1-ready/crossbeam/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/crossbeam/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/crossbeam/jest-teardown.js b/packages/v1-ready/crossbeam/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/crossbeam/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/deel/jest-setup.js b/packages/v1-ready/deel/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/deel/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/deel/jest-teardown.js b/packages/v1-ready/deel/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/deel/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/fathom/jest-setup.js b/packages/v1-ready/fathom/jest-setup.js deleted file mode 100644 index e3ed2dd..0000000 --- a/packages/v1-ready/fathom/jest-setup.js +++ /dev/null @@ -1 +0,0 @@ -require('dotenv').config({ path: '../../../.env' }); \ No newline at end of file diff --git a/packages/v1-ready/fathom/jest-teardown.js b/packages/v1-ready/fathom/jest-teardown.js deleted file mode 100644 index b2ac5fb..0000000 --- a/packages/v1-ready/fathom/jest-teardown.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = async () => { - // Add any global teardown logic here if needed -}; \ No newline at end of file diff --git a/packages/v1-ready/frontify/jest-setup.js b/packages/v1-ready/frontify/jest-setup.js deleted file mode 100644 index 6c1672c..0000000 --- a/packages/v1-ready/frontify/jest-setup.js +++ /dev/null @@ -1,13 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -const dotenv = require('dotenv'); - -const parsed = { - FRONTIFY_SCOPE: 'frontify_scope_test', - FRONTIFY_CLIENT_ID: 'frontify_client_id_test', - FRONTIFY_CLIENT_SECRET: 'frontify_client_secret_test', - REDIRECT_URI: 'http://redirect_uri_test' -}; - -dotenv.populate(process.env, parsed); - -module.exports = globalSetup; diff --git a/packages/v1-ready/frontify/jest-teardown.js b/packages/v1-ready/frontify/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/frontify/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/google-calendar/jest-setup.js b/packages/v1-ready/google-calendar/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/google-calendar/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/google-calendar/jest-teardown.js b/packages/v1-ready/google-calendar/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/google-calendar/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/google-drive/jest-setup.js b/packages/v1-ready/google-drive/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/google-drive/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/google-drive/jest-teardown.js b/packages/v1-ready/google-drive/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/google-drive/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/helpscout/api.js b/packages/v1-ready/helpscout/api.js deleted file mode 100644 index aadf886..0000000 --- a/packages/v1-ready/helpscout/api.js +++ /dev/null @@ -1,97 +0,0 @@ -const {OAuth2Requester} = require('@friggframework/module-plugin'); -const {get} = require('@friggframework/assertions'); - -class Api extends OAuth2Requester { - constructor(params) { - super(params); - // The majority of the properties for OAuth are default loaded by OAuth2Requester. - // This includes the `client_id`, `client_secret`, `scopes`, and `redirect_uri`. - this.baseUrl = 'https://api.helpscout.net'; - - this.URLs = { - me: '/v2/users/me', - conversations: '/v2/conversations', - mailboxes: '/v2/mailboxes', - customers: '/v2/customers', - deleteCustomerById: (customerId) => `/v2/customers/${customerId}`, - }; - - this.authorizationUri = encodeURI( - `https://secure.helpscout.net/authentication/authorizeClientApplication?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${this.scope}&state=${this.state}` - ); - this.tokenUri = 'https://api.helpscout.net/v2/oauth2/token'; - - this.access_token = get(params, 'access_token', null); - this.refresh_token = get(params, 'refresh_token', null); - } - - getAuthUri() { - return this.authorizationUri; - } - - // ************************** User (me) ********************************** - async getUserDetails() { - const options = { - url: this.baseUrl + this.URLs.me, - }; - - return this._get(options); - } - - async getTokenIdentity() { - const user = await this.getUserDetails(); - return {identifier: user.id, name: user.firstName + ' ' + user.lastName}; - } - - // ************************** Customers ********************************** - async listCustomers() { - const options = { - url: this.baseUrl + this.URLs.customers, - }; - - return this._get(options); - } - - async createCustomer(body) { - const options = { - url: this.baseUrl + this.URLs.customers, - body, - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - returnFullRes: true - }; - - return this._post(options); - } - - async deleteCustomer(id) { - const options = { - url: `${this.baseUrl}${this.URLs.deleteCustomerById(id)}`, - }; - return this._delete(options); - } - - // ************************** Conversations ********************************** - - async listConversations() { - const options = { - url: this.baseUrl + this.URLs.conversations, - }; - - return this._get(options); - } - - // ************************** Mailboxes ********************************** - - async listMailboxes() { - const options = { - url: this.baseUrl + this.URLs.mailboxes, - }; - - return this._get(options); - } -} - -module.exports = {Api}; diff --git a/packages/v1-ready/helpscout/jest-setup.js b/packages/v1-ready/helpscout/jest-setup.js deleted file mode 100644 index 32fe35c..0000000 --- a/packages/v1-ready/helpscout/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test-environment'); -module.exports = globalSetup; diff --git a/packages/v1-ready/helpscout/jest-teardown.js b/packages/v1-ready/helpscout/jest-teardown.js deleted file mode 100644 index 3d5ec75..0000000 --- a/packages/v1-ready/helpscout/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test-environment'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/helpscout/models/credential.js b/packages/v1-ready/helpscout/models/credential.js deleted file mode 100644 index 17035b1..0000000 --- a/packages/v1-ready/helpscout/models/credential.js +++ /dev/null @@ -1,18 +0,0 @@ -const {mongoose} = require('@friggframework/database/mongoose'); -const {Credential: Parent} = require('@friggframework/module-plugin'); -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - refresh_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - expires_in: {type: Number}, -}); -const name = 'HelpscoutCredential'; -const Credential = Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/v1-ready/helpscout/models/entity.js b/packages/v1-ready/helpscout/models/entity.js deleted file mode 100644 index 5ee00a1..0000000 --- a/packages/v1-ready/helpscout/models/entity.js +++ /dev/null @@ -1,8 +0,0 @@ -const {mongoose} = require('@friggframework/database/mongoose'); -const {Entity: Parent} = require('@friggframework/module-plugin'); - -const schema = new mongoose.Schema({}); -const name = 'HelpscoutEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/v1-ready/helpscout/tests/auther.test.js b/packages/v1-ready/helpscout/tests/auther.test.js deleted file mode 100644 index 6e99923..0000000 --- a/packages/v1-ready/helpscout/tests/auther.test.js +++ /dev/null @@ -1,86 +0,0 @@ -const {Definition} = require('../definition'); -const {Auther} = require('@friggframework/module-plugin'); -const {connectToDatabase, disconnectFromDatabase, createObjectId} = require('@friggframework/database/mongo'); -const {Authenticator, testDefinition} = require("@friggframework/test-environment"); - -describe('HelpScout Auther Tests', () => { - let auther; - beforeAll(async () => { - await connectToDatabase(); - auther = await Auther.getInstance({ - definition: Definition, - userId: createObjectId(), - }); - }); - - afterAll(async () => { - await auther.CredentialModel.deleteMany(); - await auther.EntityModel.deleteMany(); - await disconnectFromDatabase(); - }); - - describe('getAuthorizationRequirements() test', () => { - it('should return auth requirements', async () => { - const requirements = auther.getAuthorizationRequirements(); - expect(requirements).toBeDefined(); - expect(requirements.type).toEqual('oauth2'); - expect(requirements.url).toBeDefined(); - }); - }); - - describe('Authorization requests', () => { - let authUrl, firstRes; - beforeAll(async () => { - const requirements = auther.getAuthorizationRequirements(); - authUrl = requirements.url; - }); - - it('processAuthorizationCallback()', async () => { - const response = await Authenticator.oauth2(authUrl, 3000, 'google chrome'); - firstRes = await auther.processAuthorizationCallback({ - data: { - code: response.data.code, - }, - }); - expect(firstRes).toBeDefined(); - expect(firstRes.entity_id).toBeDefined(); - expect(firstRes.credential_id).toBeDefined(); - }, 10000); - it.skip('retrieves existing entity on subsequent calls', async () => { - const response = await Authenticator.oauth2(authUrl, 3000, 'google chrome'); - const res = await auther.processAuthorizationCallback({ - data: { - code: response.data.code, - }, - }); - expect(res).toEqual(firstRes); - }, 10000); - it('Should test the Definition methods', async () => { - await testDefinition(auther.api, Definition, undefined, undefined, auther.userId); - }, 10000) - }); - describe('Test credential retrieval and auther instantiation', () => { - it('retrieve by entity id', async () => { - const newAuther = await Auther.getInstance({ - userId: auther.userId, - entityId: auther.entity.id, - definition: Definition, - }); - expect(newAuther).toBeDefined(); - expect(newAuther.entity).toBeDefined(); - expect(newAuther.credential).toBeDefined(); - expect(await newAuther.testAuth()).toBeTruthy(); - }); - - it('retrieve by credential id', async () => { - const newAuther = await Auther.getInstance({ - userId: auther.userId, - credentialId: auther.credential.id, - definition: Definition, - }); - expect(newAuther).toBeDefined(); - expect(newAuther.credential).toBeDefined(); - expect(await newAuther.testAuth()).toBeTruthy(); - }); - }); -}); diff --git a/packages/v1-ready/hubspot/jest-setup.js b/packages/v1-ready/hubspot/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/v1-ready/hubspot/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/v1-ready/hubspot/jest-teardown.js b/packages/v1-ready/hubspot/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/hubspot/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/ironclad/jest-setup.js b/packages/v1-ready/ironclad/jest-setup.js deleted file mode 100644 index c2ff95b..0000000 --- a/packages/v1-ready/ironclad/jest-setup.js +++ /dev/null @@ -1,14 +0,0 @@ -const { globalSetup } = require('@friggframework/test'); - -module.exports = async () => { - globalSetup(); - - process.env = { - ...process.env, - IRONCLAD_CLIENT_ID: 'some-client-id', - IRONCLAD_CLIENT_SECRET: 'some-client-secret', - IRONCLAD_SCOPE: 'scope1 scope2', - IRONCLAD_SUBDOMAIN: 'subdomain', - REDIRECT_URI: 'https://example.com', - }; -}; diff --git a/packages/v1-ready/ironclad/jest-teardown.js b/packages/v1-ready/ironclad/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/ironclad/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/linear/index.js b/packages/v1-ready/linear/index.js deleted file mode 100644 index d6bda72..0000000 --- a/packages/v1-ready/linear/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const Config = require('./defaultConfig'); -const {Definition} = require('./definition'); - -module.exports = { - Api, - Credential, - Entity, - Config, - Definition, -}; diff --git a/packages/v1-ready/linear/jest-setup.js b/packages/v1-ready/linear/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/linear/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/linear/jest-teardown.js b/packages/v1-ready/linear/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/linear/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/linear/manager.js b/packages/v1-ready/linear/manager.js deleted file mode 100644 index 3bfed2b..0000000 --- a/packages/v1-ready/linear/manager.js +++ /dev/null @@ -1,174 +0,0 @@ -const {Auther, get, debug, flushDebugLog} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const config = require('./defaultConfig.json') - -// class Manager extends ModuleManager { -// static Entity = Entity; -// static Credential = Credential; -// -// constructor(params) { -// super(params); -// } -// -// static getName() { -// return config.name; -// } -// -// static async getInstance(params) { -// let instance = new this(params); -// const apiParams = { -// client_id: process.env.LINEAR_CLIENT_ID, -// client_secret: process.env.LINEAR_CLIENT_SECRET, -// redirect_uri: `${process.env.REDIRECT_URI}/linear`, -// scope: process.env.LINEAR_SCOPE, -// delegate: instance -// }; -// if (params.entityId) { -// instance.entity = await Entity.findById(params.entityId); -// instance.credential = await Credential.findById(instance.entity.credential); -// } else if (params.credentialId) { -// instance.credential = await Credential.findById(params.credentialId); -// } -// if (instance.credential) { -// apiParams.access_token = instance.credential.access_token; -// apiParams.refresh_token = instance.credential.refresh_token; -// } -// instance.api = await new Api(apiParams); -// return instance; -// } -// -// // Change to whatever your api uses to return identifying information -// async testAuth() { -// let validAuth = false; -// try { -// if (await this.api.getUserDetails()) validAuth = true; -// } catch (e) { -// flushDebugLog(e); -// } -// return validAuth; -// } -// -// getAuthorizationRequirements(params) { -// return { -// url: this.api.getAuthorizationUri(), -// type: ModuleConstants.authType.oauth2, -// }; -// } -// -// -// async processAuthorizationCallback(params) { -// const code = get(params.data, 'code'); -// // For OAuth2, generate the token and store in this.credential and the DB -// await this.api.getTokenFromCode(code); -// // TODO: get entity identifying information from the api. You'll need to format this. -// const entityDetails = await this.api.getTokenIdentity(); -// await this.findOrCreateEntity(entityDetails); -// -// return { -// credential_id: this.credential.id, -// entity_id: this.entity.id, -// type: Manager.getName(), -// }; -// } -// -// async findOrCreateEntity(params) { -// const identifier = get(params, 'identifier'); -// const name = get(params, 'name'); -// -// const search = await Entity.find({ -// user: this.userId, -// externalId: identifier, -// }); -// if (search.length === 0) { -// // validate choices!!! -// // create entity -// const createObj = { -// credential: this.credential.id, -// user: this.userId, -// name, -// externalId: identifier, -// }; -// this.entity = await Entity.create(createObj); -// } else if (search.length === 1) { -// this.entity = search[0]; -// } else { -// debug( -// 'Multiple entities found with the same external ID:', -// identifier -// ); -// throw new Error('Multiple entities found with the same external ID: ' + identifier); -// } -// } -// -// async receiveNotification(notifier, delegateString, object = null) { -// if (!(notifier instanceof Api)) { -// // no-op -// } -// else if (delegateString === this.api.DLGT_TOKEN_UPDATE) { -// await this.updateOrCreateCredential(); -// } -// else if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { -// await this.deauthorize(); -// } -// else if (delegateString === this.api.DLGT_INVALID_AUTH) { -// await this.markCredentialsInvalid(); -// } -// } -// -// async updateOrCreateCredential() { -// const userDetails = await this.api.getTokenIdentity(); -// const updatedToken = { -// user: this.userId.toString(), -// auth_is_valid: true, -// }; -// if (this.api.access_token) { updatedToken.access_token = this.api.access_token}; -// if (this.api.refresh_token) { updatedToken.refresh_token = this.api.refresh_token}; -// -// // search for a credential for this user and identifier -// // skip if we already have a credential -// if (!this.credential){ -// const credentialSearch = await Credential.find({ -// identifier: userDetails.identifier -// }) -// if (credentialSearch.length > 1) { -// debug(`Multiple credentials found with same identifier: ${userDetails.identifier}`); -// throw new Error(`Multiple credentials found with same identifier: ${userDetails.identifier}`); -// } -// else if (credentialSearch === 1 && credentialSearch[0].user !== this.userId){ -// debug(`A credential already exists with this identifier: ${userDetails.identifier}`); -// throw new Error(`A credential already exists with this identifier: ${userDetails.identifier}`); -// } -// else if (credentialSearch === 1) { -// // found exactly one credential with this identifier -// this.credential = credentialSearch[0]; -// } -// else { -// // found no credential with this identifier (match none for insert) -// this.credential = {$exists: false}; -// } -// } -// // update credential or create if none was found -// this.credential = await Credential.findOneAndUpdate( -// {_id: this.credential}, -// {$set: updatedToken}, -// {useFindAndModify: true, new: true, upsert: true} -// ); -// } -// -// async deauthorize() { -// // wipe api connection -// this.api = new Api(); -// -// // delete credentials from the database -// const entity = await Entity.getByUserId(this.userId); -// if (entity.credential) { -// await Credential.delete(entity.credential); -// entity.credential = undefined; -// await entity.save(); -// } -// } -// } - -module.exports = Manager; diff --git a/packages/v1-ready/linear/models/credential.js b/packages/v1-ready/linear/models/credential.js deleted file mode 100644 index b21486a..0000000 --- a/packages/v1-ready/linear/models/credential.js +++ /dev/null @@ -1,12 +0,0 @@ -const {Credential: Parent, mongoose} = require('@friggframework/core'); -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - expires_at: {type: Number}, -}); -const name = 'LinearCredential'; -const Credential = Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/v1-ready/linear/models/entity.js b/packages/v1-ready/linear/models/entity.js deleted file mode 100644 index 4c960a6..0000000 --- a/packages/v1-ready/linear/models/entity.js +++ /dev/null @@ -1,7 +0,0 @@ -const {Entity: Parent, mongoose} = require('@friggframework/core'); - -const schema = new mongoose.Schema({}); -const name = 'LinearEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/v1-ready/microsoft-teams/index.js b/packages/v1-ready/microsoft-teams/index.js deleted file mode 100644 index 981f04a..0000000 --- a/packages/v1-ready/microsoft-teams/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Api} = require('./api/api'); -const Config = require('./defaultConfig'); -const {Definition} = require('./definition'); - -module.exports = { - Api, - Config, - Definition -}; diff --git a/packages/v1-ready/microsoft-teams/jest-setup.js b/packages/v1-ready/microsoft-teams/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/v1-ready/microsoft-teams/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/v1-ready/microsoft-teams/jest-teardown.js b/packages/v1-ready/microsoft-teams/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/microsoft-teams/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/microsoft-teams/manager.js b/packages/v1-ready/microsoft-teams/manager.js deleted file mode 100644 index 3ce5450..0000000 --- a/packages/v1-ready/microsoft-teams/manager.js +++ /dev/null @@ -1,200 +0,0 @@ -const {ModuleManager, ModuleConstants, get, debug, flushDebugLog} = require('@friggframework/core'); -const {Api} = require('./api/api'); -const {graphApi} = require('./api/graph'); -const {botFrameworkApi} = require('./api/botFramework'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - this.tenant_id = get(params, 'tenant_id', null); - this.redirect_uri = get(params, 'redirect_uri', `${process.env.REDIRECT_URI}/microsoft-teams`) - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return config.name; - } - - static async getInstance(params) { - const instance = new this(params); - // All async code here - - // initializes the Api - let teamsParams = { - client_id: process.env.TEAMS_CLIENT_ID, - client_secret: process.env.TEAMS_CLIENT_SECRET, - redirect_uri: instance.redirect_uri, - scope: process.env.TEAMS_SCOPE, - delegate: instance, - }; - - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - instance.credential = await Credential.findById( - instance.entity.credential - ); - teamsParams = { - ...teamsParams, - ...instance.credential.toObject(), - tenant_id: instance.entity.externalId - }; - } - instance.api = new Api(teamsParams); - - return instance; - } - - async testAuth() { - let validAuth = false; - try { - const response = await this.api.graphApi.getOrganization(); - validAuth = true; - } catch (e) { - debug(e); - } - return validAuth; - } - - async getAuthorizationRequirements(params) { - return { - url: await this.api.graphApi.getAuthUri(), - type: ModuleConstants.authType.oauth2, - data: {}, - }; - } - - async processAuthorizationCallback(params) { - if (params) { - const code = get(params.data, 'code', null); - try { - await this.api.graphApi.getTokenFromCode(code); - } catch (e) { - flushDebugLog(e); - throw new Error('Auth Error'); - } - const authRes = await this.testAuth(); - if (!authRes) throw new Error('Auth Error'); - } else { - await this.api.getTokenFromClientCredentials(); - const authCheck = await this.testAuth(); - if (!authCheck) throw new Error('Auth Error'); - } - - const orgDetails = await this.api.graphApi.getOrganization(); - this.tenant_id = orgDetails.id; - await this.findAndUpsertCredential({ - tenant_id: this.tenant_id, - graph_access_token: this.api.graphApi.access_token, - graph_refresh_token: this.api.graphApi.refresh_token, - bot_api_access_token: this.api.botFrameworkApi.access_token, - }); - - await this.findOrCreateEntity({ - externalId: orgDetails.id, - name: orgDetails.displayName, - }); - return { - entity_id: this.entity.id, - credential_id: this.credential.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const externalId = get(params, 'externalId'); - const name = get(params, 'name'); - - // TODO-new... this doesn't allow for multiple entities for a specific User. - const search = await Entity.find({ - user: this.userId, - externalId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name, - externalId, - }; - this.entity = await Entity.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug('Multiple entities found with the same portal ID:', portalId); - this.throwException(''); - } - } - - async findAndUpsertCredential(params) { - if (this.credential) { - this.credential = await Credential.findOneAndUpdate( - {_id: this.credential}, - {$set: params}, - {useFindAndModify: true, new: true, upsert: true} - ); - } else { - this.credential = await Credential.findOneAndUpdate( - {tenant_id: this.tenant_id}, - {$set: params}, - {useFindAndModify: true, new: true, upsert: true} - ); - } - } - - //------------------------------------------------------------ - - async receiveNotification(notifier, delegateString, object = null) { - if ( - notifier instanceof Api || - notifier instanceof botFrameworkApi || - notifier instanceof graphApi - ) { - if ( - delegateString === this.api.graphApi.DLGT_TOKEN_UPDATE && - this.tenant_id - ) { - const updatedToken = { - user: this.userId?.toString(), - graph_access_token: this.api.graphApi.access_token, - graph_refresh_token: this.api.graphApi.refresh_token, - bot_api_access_token: this.api.botFrameworkApi.access_token, - tenant_id: this.tenant_id, - }; - - await this.findAndUpsertCredential(updatedToken); - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - return this.markCredentialsInvalid(); - } - } - } - - // TODO-new (globally) normalize "deauthorization" - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await Entity.findByUserId(this.userId); - if (entity.credential) { - await Credential.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - this.credential = undefined; - } -} - -module.exports = Manager; diff --git a/packages/v1-ready/microsoft-teams/models/credential.js b/packages/v1-ready/microsoft-teams/models/credential.js deleted file mode 100644 index 4e67213..0000000 --- a/packages/v1-ready/microsoft-teams/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - graph_access_token: {type: String, trim: true, lhEncrypt: true}, - graph_refresh_token: {type: String, trim: true, lhEncrypt: true}, - bot_access_token: {type: String, trim: true, lhEncrypt: true}, - tenant_id: {type: String, trim: true}, - user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: false, - }, -}); - -const name = 'MicrosoftTeamsCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/v1-ready/microsoft-teams/models/entity.js b/packages/v1-ready/microsoft-teams/models/entity.js deleted file mode 100644 index d1c9370..0000000 --- a/packages/v1-ready/microsoft-teams/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); - -const name = 'MicrosoftTeamsEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/v1-ready/openphone/jest-setup.js b/packages/v1-ready/openphone/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/v1-ready/openphone/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/v1-ready/openphone/jest-teardown.js b/packages/v1-ready/openphone/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/openphone/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/payjunction/specs/openAPI.yaml b/packages/v1-ready/payjunction/specs/openAPI.yaml deleted file mode 100644 index de3f545..0000000 --- a/packages/v1-ready/payjunction/specs/openAPI.yaml +++ /dev/null @@ -1,568 +0,0 @@ -openapi: 3.0.3 -info: - title: PayJunction API - version: '1.0.0' - description: |- - OpenAPI specification for the PayJunction API, including endpoints for transactions, customers, recurring payments, batches, refunds, and surcharges. - contact: - name: PayJunction Support - url: https://developer.payjunction.com/hc/en-us -servers: - - url: https://api.payjunction.com -security: - - ApiKeyAuth: [] -components: - securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: Authorization - schemas: - CreditCard: - type: object - properties: - number: - type: string - expiration_month: - type: string - expiration_year: - type: string - masked_number: - type: string - Address: - type: object - properties: - name: - type: string - street_address: - type: string - street_address2: - type: string - city: - type: string - state: - type: string - zip: - type: string - country: - type: string - Customer: - type: object - properties: - customer_id: - type: string - credit_card: - $ref: '#/components/schemas/CreditCard' - billing_address: - $ref: '#/components/schemas/Address' - shipping_address: - $ref: '#/components/schemas/Address' - email: - type: string - phone: - type: string - fax: - type: string - Transaction: - type: object - properties: - transaction_id: - type: string - amount: - type: number - transaction_type: - type: string - description: - type: string - invoice_id: - type: string - billing_address: - $ref: '#/components/schemas/Address' - shipping_address: - $ref: '#/components/schemas/Address' - customer_id: - type: string - status_code: - type: string - status_message: - type: string - created: - type: string - settled: - type: string - RecurringPayment: - type: object - properties: - id: - type: string - amount: - type: number - customer_id: - type: string - frequency: - type: string - start_date: - type: string - total_count: - type: string - transaction_type: - type: string - description: - type: string - Batch: - type: object - properties: - number: - type: string - created: - type: string - transaction_count: - type: integer - net_amount: - type: number - sales_count: - type: integer - sales_amount: - type: number - refund_count: - type: integer - refund_amount: - type: number - Refund: - type: object - properties: - refund_id: - type: string - transaction_id: - type: string - amount: - type: number - status: - type: string - created: - type: string - Surcharge: - type: object - properties: - surcharge_id: - type: string - transaction_id: - type: string - amount: - type: number - description: - type: string - -paths: - /transactions: - get: - tags: [Transactions] - summary: List transactions - operationId: listTransactions - security: - - ApiKeyAuth: [] - responses: - '200': - description: A list of transactions - content: - application/json: - schema: - type: object - properties: - transactions: - type: array - items: - $ref: '#/components/schemas/Transaction' - post: - tags: [Transactions] - summary: Create a transaction - operationId: createTransaction - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Transaction' - responses: - '201': - description: Transaction created - content: - application/json: - schema: - $ref: '#/components/schemas/Transaction' - /transactions/{id}: - get: - tags: [Transactions] - summary: Get transaction by ID - operationId: getTransactionById - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '200': - description: Transaction details - content: - application/json: - schema: - $ref: '#/components/schemas/Transaction' - put: - tags: [Transactions] - summary: Update a transaction - operationId: updateTransaction - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Transaction' - responses: - '200': - description: Transaction updated - content: - application/json: - schema: - $ref: '#/components/schemas/Transaction' - delete: - tags: [Transactions] - summary: Delete a transaction - operationId: deleteTransaction - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '204': - description: Transaction deleted - /customers: - get: - tags: [Customers] - summary: List customers - operationId: listCustomers - security: - - ApiKeyAuth: [] - responses: - '200': - description: A list of customers - content: - application/json: - schema: - type: object - properties: - customers: - type: array - items: - $ref: '#/components/schemas/Customer' - post: - tags: [Customers] - summary: Create a customer - operationId: createCustomer - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Customer' - responses: - '201': - description: Customer created - content: - application/json: - schema: - $ref: '#/components/schemas/Customer' - /customers/{id}: - get: - tags: [Customers] - summary: Get customer by ID - operationId: getCustomerById - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '200': - description: Customer details - content: - application/json: - schema: - $ref: '#/components/schemas/Customer' - put: - tags: [Customers] - summary: Update a customer - operationId: updateCustomer - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Customer' - responses: - '200': - description: Customer updated - content: - application/json: - schema: - $ref: '#/components/schemas/Customer' - delete: - tags: [Customers] - summary: Delete a customer - operationId: deleteCustomer - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '204': - description: Customer deleted - /recurring-payments: - get: - tags: [RecurringPayments] - summary: List recurring payments - operationId: listRecurringPayments - security: - - ApiKeyAuth: [] - responses: - '200': - description: A list of recurring payments - content: - application/json: - schema: - type: object - properties: - recurring_payments: - type: array - items: - $ref: '#/components/schemas/RecurringPayment' - post: - tags: [RecurringPayments] - summary: Create a recurring payment - operationId: createRecurringPayment - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RecurringPayment' - responses: - '201': - description: Recurring payment created - content: - application/json: - schema: - $ref: '#/components/schemas/RecurringPayment' - /recurring-payments/{id}: - get: - tags: [RecurringPayments] - summary: Get recurring payment by ID - operationId: getRecurringPaymentById - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '200': - description: Recurring payment details - content: - application/json: - schema: - $ref: '#/components/schemas/RecurringPayment' - put: - tags: [RecurringPayments] - summary: Update a recurring payment - operationId: updateRecurringPayment - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RecurringPayment' - responses: - '200': - description: Recurring payment updated - content: - application/json: - schema: - $ref: '#/components/schemas/RecurringPayment' - delete: - tags: [RecurringPayments] - summary: Delete a recurring payment - operationId: deleteRecurringPayment - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '204': - description: Recurring payment deleted - /batches: - get: - tags: [Batches] - summary: List batches - operationId: listBatches - security: - - ApiKeyAuth: [] - responses: - '200': - description: A list of batches - content: - application/json: - schema: - type: object - properties: - batches: - type: array - items: - $ref: '#/components/schemas/Batch' - /batches/{id}: - get: - tags: [Batches] - summary: Get batch by ID - operationId: getBatchById - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '200': - description: Batch details - content: - application/json: - schema: - $ref: '#/components/schemas/Batch' - /refunds: - post: - tags: [Refunds] - summary: Initiate a refund - operationId: createRefund - security: - - ApiKeyAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Refund' - responses: - '201': - description: Refund initiated - content: - application/json: - schema: - $ref: '#/components/schemas/Refund' - /refunds/{id}: - get: - tags: [Refunds] - summary: Get refund by ID - operationId: getRefundById - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '200': - description: Refund details - content: - application/json: - schema: - $ref: '#/components/schemas/Refund' - /surcharges: - get: - tags: [Surcharges] - summary: List surcharges - operationId: listSurcharges - security: - - ApiKeyAuth: [] - responses: - '200': - description: A list of surcharges - content: - application/json: - schema: - type: object - properties: - surcharges: - type: array - items: - $ref: '#/components/schemas/Surcharge' - /surcharges/{id}: - get: - tags: [Surcharges] - summary: Get surcharge by ID - operationId: getSurchargeById - parameters: - - in: path - name: id - required: true - schema: - type: string - security: - - ApiKeyAuth: [] - responses: - '200': - description: Surcharge details - content: - application/json: - schema: - $ref: '#/components/schemas/Surcharge' \ No newline at end of file diff --git a/packages/v1-ready/pipedrive/CHANGELOG.md b/packages/v1-ready/pipedrive/CHANGELOG.md deleted file mode 100644 index 8bacb81..0000000 --- a/packages/v1-ready/pipedrive/CHANGELOG.md +++ /dev/null @@ -1,209 +0,0 @@ -# v0.10.0 (Wed Mar 20 2024) - -:tada: This release contains work from new contributors! :tada: - -Thanks for all your work! - -:heart: Nicolas Leal ([@nicolasmelo1](https://github.com/nicolasmelo1)) - -:heart: nmilcoff ([@nmilcoff](https://github.com/nmilcoff)) - -#### 🚀 Enhancement - -#### 🐛 Bug Fix - -- correct some bad automated edits, though they are not in relevant - files ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 4 - -- [@MichaelRyanWebber](https://github.com/MichaelRyanWebber) -- Nicolas Leal ([@nicolasmelo1](https://github.com/nicolasmelo1)) -- nmilcoff ([@nmilcoff](https://github.com/nmilcoff)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.9.0 (Wed Sep 06 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.26 (Thu Jun 08 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.25 (Thu May 25 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.24 (Tue Apr 04 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.23 (Tue Feb 21 2023) - -#### 🐛 Bug Fix - -- Merge branch 'main' into hubspot-updates ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.21 (Tue Jan 31 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.19 (Wed Jan 11 2023) - -#### 🐛 Bug Fix - -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.18 (Tue Jan 10 2023) - -:tada: This release contains work from a new contributor! :tada: - -Thank you, Jonathan O'Donnell ([@joncodo](https://github.com/joncodo)), for all your work! - -#### 🐛 Bug Fix - -- Merge branch 'main' of github.com:friggframework/frigg into doc-updates ([@joncodo](https://github.com/joncodo)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Jonathan O'Donnell ([@joncodo](https://github.com/joncodo)) -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.17 (Mon Jan 09 2023) - -#### 🐛 Bug Fix - -- Merge remote-tracking branch 'origin/main' into - gitbook-updates [#48](https://github.com/friggframework/frigg/pull/48) ([@seanspeaks](https://github.com/seanspeaks)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) -- A lot of changes all rolled into - one [#21](https://github.com/friggframework/frigg/pull/21) ([@seanspeaks](https://github.com/seanspeaks)) -- Updated API modules with support for sls offline, and made sure optional chaining with discriminators was in - place ([@seanspeaks](https://github.com/seanspeaks)) -- Fixing dependencies across all API Modules ([@seanspeaks](https://github.com/seanspeaks)) -- More import issues (Exports are named objects, imports needed to object - destructure) ([@seanspeaks](https://github.com/seanspeaks)) -- Updates to API Modules for proper export/imports ([@seanspeaks](https://github.com/seanspeaks)) -- Merge remote-tracking branch 'origin/main' into - simplify-mongoose-models ([@seanspeaks](https://github.com/seanspeaks)) -- Update all api modules to use module-plugin models ([@seanspeaks](https://github.com/seanspeaks)) -- Add READMEs for all packages and - api-modules [#20](https://github.com/friggframework/frigg/pull/20) ([@seanspeaks](https://github.com/seanspeaks)) -- Add READMEs for all packages and api-modules ([@seanspeaks](https://github.com/seanspeaks)) - -#### ⚠️ Pushed to `main` - -- Merge branch 'main' into gitbook-updates ([@seanspeaks](https://github.com/seanspeaks)) -- Finish initial formatting and publishing of all modules ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.14 (Tue Dec 06 2022) - -#### 🐛 Bug Fix - -- fix modules to - @friggframework [#74](https://github.com/friggframework/frigg/pull/74) ([@sheehantoufiq](https://github.com/sheehantoufiq)) -- Bump independent versions \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 2 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) -- Sheehan Toufiq Khan ([@sheehantoufiq](https://github.com/sheehantoufiq)) - ---- - -# v0.8.11 (Mon Sep 19 2022) - -#### 🐛 Bug Fix - -- Test environment setup for all - modules [#49](https://github.com/friggframework/frigg/pull/49) ([@seanspeaks](https://github.com/seanspeaks)) -- Test environment setup for all modules ([@seanspeaks](https://github.com/seanspeaks)) -- Merge remote-tracking branch 'origin/main' into - gitbook-updates [#48](https://github.com/friggframework/frigg/pull/48) ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) - ---- - -# v0.8.10 (Thu Sep 01 2022) - -#### 🐛 Bug Fix - -- version bumped to address tag - issue [#43](https://github.com/friggframework/frigg/pull/43) ([@seanspeaks](https://github.com/seanspeaks)) -- version bumped ([@seanspeaks](https://github.com/seanspeaks)) -- Publish ([@seanspeaks](https://github.com/seanspeaks)) -- Add nx and - licenses [#37](https://github.com/friggframework/frigg/pull/37) ([@seanspeaks](https://github.com/seanspeaks)) -- MIT to all packages ([@seanspeaks](https://github.com/seanspeaks)) - -#### Authors: 1 - -- Sean Matthews ([@seanspeaks](https://github.com/seanspeaks)) diff --git a/packages/v1-ready/pipedrive/LICENSE.md b/packages/v1-ready/pipedrive/LICENSE.md deleted file mode 100644 index 77f5cc2..0000000 --- a/packages/v1-ready/pipedrive/LICENSE.md +++ /dev/null @@ -1,16 +0,0 @@ -MIT License - -Copyright (c) 2022 Left Hook Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/v1-ready/pipedrive/defaultConfig.json b/packages/v1-ready/pipedrive/defaultConfig.json deleted file mode 100644 index 3485c8a..0000000 --- a/packages/v1-ready/pipedrive/defaultConfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "pipedrive", - "label": "PipeDrive CRM", - "productUrl": "https://pipedrive.com", - "apiDocs": "https://developer.pipedrive.com", - "logoUrl": "https://friggframework.org/assets/img/pipedrive-icon.png", - "categories": [ - "Sales" - ], - "description": "Pipedrive" -} diff --git a/packages/v1-ready/pipedrive/index.js b/packages/v1-ready/pipedrive/index.js deleted file mode 100644 index 13b0c76..0000000 --- a/packages/v1-ready/pipedrive/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/v1-ready/pipedrive/jest-setup.js b/packages/v1-ready/pipedrive/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/pipedrive/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/pipedrive/jest-teardown.js b/packages/v1-ready/pipedrive/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/pipedrive/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/pipedrive/manager.js b/packages/v1-ready/pipedrive/manager.js deleted file mode 100644 index eef514c..0000000 --- a/packages/v1-ready/pipedrive/manager.js +++ /dev/null @@ -1,193 +0,0 @@ -const { - ModuleManager, - ModuleConstants, - flushDebugLog, - debug -} = require('@friggframework/core'); -const {Api} = require('./api.js'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static - Entity = Entity; - - static - Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return Config.name; - } - - static - async getInstance(params) { - const instance = new this(params); - - const apiParams = {delegate: instance}; - if (params.entityId) { - instance.entity = await instance.entityMO.get(params.entityId); - instance.credential = await instance.credentialMO.get( - instance.entity.credential - ); - } else if (params.credentialId) { - instance.credential = await instance.credentialMO.get( - params.credentialId - ); - } - if (instance.entity?.credential) { - apiParams.access_token = instance.credential.accessToken; - apiParams.refresh_token = instance.credential.refreshToken; - apiParams.companyDomain = instance.credential.companyDomain; - } - instance.api = await new Api(apiParams); - - return instance; - } - - async getAuthorizationRequirements(params) { - return { - url: this.api.authorizationUri, - type: ModuleConstants.authType.oauth2, - }; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.getUser()) validAuth = true; - } catch (e) { - await this.markCredentialsInvalid(); - flushDebugLog(e); - } - return validAuth; - } - - async processAuthorizationCallback(params) { - const code = get(params.data, 'code'); - await this.api.getTokenFromCode(code); - await this.testAuth(); - - const userProfile = await this.api.getUser(); - await this.findOrCreateEntity({ - companyId: userProfile.data.company_id, - companyName: userProfile.data.company_name, - }); - - return { - credential_id: this.credential.id, - entity_id: this.entity.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const companyId = get(params, 'companyId'); - const companyName = get(params, 'companyName'); - - const search = await this.entityMO.list({ - user: this.userId, - externalId: companyId, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name: companyName, - externalId: companyId, - }; - this.entity = await this.entityMO.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug( - 'Multiple entities found with the same Company ID:', - companyId - ); - } - - return { - entity_id: this.entity.id, - }; - } - - async deauthorize() { - this.api = new Api(); - - const entity = await this.entityMO.getByUserId(this.userId); - if (entity.credential) { - await this.credentialMO.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - const userProfile = await this.api.getUser(); - const pipedriveUserId = userProfile.data.id; - const updatedToken = { - user: this.userId, - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - accessTokenExpire: this.api.accessTokenExpire, - externalId: pipedriveUserId, - companyDomain: object.api_domain, - auth_is_valid: true, - }; - - if (!this.credential) { - let credentialSearch = await this.credentialMO.list({ - externalId: pipedriveUserId, - }); - if (credentialSearch.length === 0) { - this.credential = await this.credentialMO.create( - updatedToken - ); - } else if (credentialSearch.length === 1) { - if ( - credentialSearch[0].user.toString() === - this.userId.toString() - ) { - this.credential = await this.credentialMO.update( - credentialSearch[0], - updatedToken - ); - } else { - debug( - 'Somebody else already created a credential with the same User ID:', - pipedriveUserId - ); - } - } else { - // Handling multiple credentials found with an error for the time being - debug( - 'Multiple credentials found with the same User ID:', - pipedriveUserId - ); - } - } else { - this.credential = await this.credentialMO.update( - this.credential, - updatedToken - ); - } - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } - if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - } -} - -module.exports = Manager; diff --git a/packages/v1-ready/pipedrive/manager.test.js b/packages/v1-ready/pipedrive/manager.test.js deleted file mode 100644 index b4c8e08..0000000 --- a/packages/v1-ready/pipedrive/manager.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const Manager = require('./manager'); -const mongoose = require('mongoose'); -const config = require('./defaultConfig.json'); - -describe(`Should fully test the ${config.label} Manager`, () => { - let manager, userManager; - - beforeAll(async () => { - await mongoose.connect(process.env.MONGO_URI); - manager = await Manager.getInstance({ - userId: new mongoose.Types.ObjectId(), - }); - }); - - afterAll(async () => { - await Manager.Credential.deleteMany(); - await Manager.Entity.deleteMany(); - await mongoose.disconnect(); - }); - - it('should return auth requirements', async () => { - const requirements = await manager.getAuthorizationRequirements(); - expect(requirements).exists; - expect(requirements.type).toEqual('oauth2'); - }); -}); diff --git a/packages/v1-ready/pipedrive/mocks/activities/createActivity.js b/packages/v1-ready/pipedrive/mocks/activities/createActivity.js deleted file mode 100644 index 01b3508..0000000 --- a/packages/v1-ready/pipedrive/mocks/activities/createActivity.js +++ /dev/null @@ -1,172 +0,0 @@ -module.exports = { - data: { - type: 'task', - id: 31, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-11-06T02:52:48.000Z', - dueAt: '2021-11-06T02:52:48.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-11-06T02:52:48.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=31', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=31', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/31', - }, - }, -}; diff --git a/packages/v1-ready/pipedrive/mocks/activities/deleteActivity.js b/packages/v1-ready/pipedrive/mocks/activities/deleteActivity.js deleted file mode 100644 index 26c393f..0000000 --- a/packages/v1-ready/pipedrive/mocks/activities/deleteActivity.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - status: 204, -}; diff --git a/packages/v1-ready/pipedrive/mocks/activities/listActivities.js b/packages/v1-ready/pipedrive/mocks/activities/listActivities.js deleted file mode 100644 index 57d5c9c..0000000 --- a/packages/v1-ready/pipedrive/mocks/activities/listActivities.js +++ /dev/null @@ -1,1538 +0,0 @@ -module.exports = { - data: [ - { - type: 'task', - id: 1, - attributes: { - action: 'action_item', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-21T18:49:12.000Z', - dueAt: '2021-10-21T18:49:03.000Z', - note: 'Do it you will', - opportunityAssociation: 'recent_created', - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-21T18:49:12.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 12, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=1', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=1', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=1', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=1', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=1', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=1', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 12, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 4, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/1', - }, - }, - { - type: 'task', - id: 2, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T15:00:56.000Z', - dueAt: '2021-10-29T15:00:56.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T15:00:56.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=2', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=2', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=2', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=2', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=2', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=2', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/2', - }, - }, - { - type: 'task', - id: 3, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T15:10:21.000Z', - dueAt: '2021-10-29T15:10:21.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T15:10:21.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=3', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=3', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=3', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=3', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=3', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=3', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/3', - }, - }, - { - type: 'task', - id: 4, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T15:10:32.000Z', - dueAt: '2021-10-29T15:10:32.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T15:10:32.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=4', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=4', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=4', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=4', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=4', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=4', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/4', - }, - }, - { - type: 'task', - id: 5, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T15:10:53.000Z', - dueAt: '2021-10-29T15:10:53.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T15:10:53.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=5', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=5', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=5', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=5', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=5', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=5', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/5', - }, - }, - { - type: 'task', - id: 6, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T15:11:07.000Z', - dueAt: '2021-10-29T15:11:07.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T15:14:35.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 3, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=6', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=6', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=6', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=6', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=6', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=6', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 3, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/6', - }, - }, - { - type: 'task', - id: 7, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T16:05:44.000Z', - dueAt: '2021-10-29T16:05:44.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T16:05:44.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=7', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=7', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=7', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=7', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=7', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=7', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/7', - }, - }, - { - type: 'task', - id: 8, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-10-29T17:27:05.000Z', - dueAt: '2021-10-29T17:27:05.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-10-29T17:27:05.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=8', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=8', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=8', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=8', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=8', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=8', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/8', - }, - }, - { - type: 'task', - id: 31, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-11-06T02:52:48.000Z', - dueAt: '2021-11-06T02:52:48.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-11-06T02:52:48.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 1, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=31', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=31', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 1, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/31', - }, - }, - ], - meta: { - count: 9, - count_truncated: false, - }, -}; diff --git a/packages/v1-ready/pipedrive/mocks/activities/updateActivity.js b/packages/v1-ready/pipedrive/mocks/activities/updateActivity.js deleted file mode 100644 index d8657fb..0000000 --- a/packages/v1-ready/pipedrive/mocks/activities/updateActivity.js +++ /dev/null @@ -1,172 +0,0 @@ -module.exports = { - data: { - type: 'task', - id: 31, - attributes: { - action: 'email', - autoskipAt: null, - compiledSequenceTemplateHtml: null, - completed: false, - completedAt: null, - createdAt: '2021-11-06T02:52:48.000Z', - dueAt: '2021-11-06T02:52:48.000Z', - note: null, - opportunityAssociation: null, - scheduledAt: null, - state: 'incomplete', - stateChangedAt: null, - taskType: 'manual', - updatedAt: '2021-11-06T03:04:55.000Z', - }, - relationships: { - account: { - data: { - type: 'account', - id: 3, - }, - }, - call: { - data: null, - }, - calls: { - links: { - related: - 'https://api.pipedrive.io/api/v2/calls?filter%5Btask%5D%5Bid%5D=31', - }, - }, - completer: { - data: null, - }, - creator: { - data: { - type: 'user', - id: 1, - }, - }, - defaultPluginMapping: { - data: null, - }, - mailing: { - data: null, - }, - mailings: { - links: { - related: - 'https://api.pipedrive.io/api/v2/mailings?filter%5Btask%5D%5Bid%5D=31', - }, - }, - opportunity: { - data: null, - }, - owner: { - data: { - type: 'user', - id: 1, - }, - }, - prospect: { - data: null, - }, - prospectAccount: { - data: null, - }, - prospectContacts: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/emailAddresses?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - prospectOwner: { - data: null, - }, - prospectPhoneNumbers: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/phoneNumbers?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - prospectStage: { - data: null, - }, - sequence: { - data: null, - }, - sequenceSequenceSteps: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/sequenceSteps?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - sequenceState: { - data: null, - }, - sequenceStateSequenceStep: { - data: null, - }, - sequenceStateSequenceStepOverrides: { - data: [], - meta: { - count: 0, - }, - }, - sequenceStateStartingTemplate: { - data: null, - }, - sequenceStep: { - data: null, - }, - sequenceStepOverrideTemplates: { - data: [], - links: { - related: - 'https://api.pipedrive.io/api/v2/templates?filter%5Btask%5D%5Bid%5D=31', - }, - meta: { - count: 0, - }, - }, - sequenceTemplate: { - data: null, - }, - sequenceTemplateTemplate: { - data: null, - }, - subject: { - data: { - type: 'account', - id: 3, - }, - }, - taskPriority: { - data: { - type: 'taskPriority', - id: 3, - }, - }, - taskTheme: { - data: { - type: 'taskTheme', - id: 1, - }, - }, - template: { - data: null, - }, - }, - links: { - self: 'https://api.pipedrive.io/api/v2/tasks/31', - }, - }, -}; diff --git a/packages/v1-ready/pipedrive/mocks/apiMock.js b/packages/v1-ready/pipedrive/mocks/apiMock.js deleted file mode 100644 index e92c85f..0000000 --- a/packages/v1-ready/pipedrive/mocks/apiMock.js +++ /dev/null @@ -1,30 +0,0 @@ -class MockApi { - constructor() { - } - - /** * Deals ** */ - - async listDeals() { - return require('./deals/listDeals'); - } - - /** * Activities ** */ - - async createActivity() { - return require('./activities/createActivity'); - } - - async listActivities() { - return require('./activities/listActivities'); - } - - async deleteActivity() { - return require('./activities/deleteActivity'); - } - - async updateActivity() { - return require('./activities/updateActivity'); - } -} - -module.exports = MockApi; diff --git a/packages/v1-ready/pipedrive/mocks/deals/listDeals.js b/packages/v1-ready/pipedrive/mocks/deals/listDeals.js deleted file mode 100644 index 2555ad6..0000000 --- a/packages/v1-ready/pipedrive/mocks/deals/listDeals.js +++ /dev/null @@ -1,236 +0,0 @@ -module.exports = { - success: true, - data: [ - { - id: 1, - creator_user_id: { - id: 1811658, - name: 'Tom Elliott', - email: 'projectteam@lefthook.co', - has_pic: 1, - pic_hash: 'de38c66276cb325d7c0e84d4fae1f0ce', - active_flag: true, - value: 1811658, - }, - user_id: { - id: 1811658, - name: 'Tom Elliott', - email: 'projectteam@lefthook.co', - has_pic: 1, - pic_hash: 'de38c66276cb325d7c0e84d4fae1f0ce', - active_flag: true, - value: 1811658, - }, - person_id: { - active_flag: true, - name: 'Example Person', - email: [ - { - value: '', - primary: true, - }, - ], - phone: [ - { - value: '', - primary: true, - }, - ], - owner_id: 1811658, - value: 1, - }, - org_id: null, - stage_id: 1, - title: 'Example Person deal', - value: 0, - currency: 'USD', - add_time: '2020-07-06 19:08:03', - update_time: '2020-07-06 19:08:03', - stage_change_time: null, - active: true, - deleted: false, - status: 'open', - probability: null, - next_activity_date: null, - next_activity_time: null, - next_activity_id: null, - last_activity_id: null, - last_activity_date: null, - lost_reason: null, - visible_to: '3', - close_time: null, - pipeline_id: 1, - won_time: null, - first_won_time: null, - lost_time: null, - products_count: 0, - files_count: 0, - notes_count: 0, - followers_count: 1, - email_messages_count: 0, - activities_count: 0, - done_activities_count: 0, - undone_activities_count: 0, - participants_count: 1, - expected_close_date: null, - last_incoming_mail_time: null, - last_outgoing_mail_time: null, - label: null, - renewal_type: 'one_time', - stage_order_nr: 1, - person_name: 'Example Person', - org_name: null, - next_activity_subject: null, - next_activity_type: null, - next_activity_duration: null, - next_activity_note: null, - group_id: null, - group_name: null, - formatted_value: '$0', - weighted_value: 0, - formatted_weighted_value: '$0', - weighted_value_currency: 'USD', - rotten_time: null, - owner_name: 'Tom Elliott', - cc_email: 'lefthook-sandbox-41e8b7+deal1@pipedrivemail.com', - org_hidden: false, - person_hidden: false, - }, - { - id: 2, - creator_user_id: { - id: 1811658, - name: 'Tom Elliott', - email: 'projectteam@lefthook.co', - has_pic: 1, - pic_hash: 'de38c66276cb325d7c0e84d4fae1f0ce', - active_flag: true, - value: 1811658, - }, - user_id: { - id: 1811658, - name: 'Tom Elliott', - email: 'projectteam@lefthook.co', - has_pic: 1, - pic_hash: 'de38c66276cb325d7c0e84d4fae1f0ce', - active_flag: true, - value: 1811658, - }, - person_id: null, - org_id: { - name: 'Left Hook', - people_count: 0, - owner_id: 1811658, - address: null, - active_flag: true, - cc_email: 'lefthook-sandbox-41e8b7@pipedrivemail.com', - value: 1, - }, - stage_id: 1, - title: 'New Deal gotta find person', - value: 0, - currency: 'USD', - add_time: '2021-11-19 19:14:43', - update_time: '2021-11-19 19:14:43', - stage_change_time: null, - active: true, - deleted: false, - status: 'open', - probability: null, - next_activity_date: null, - next_activity_time: null, - next_activity_id: null, - last_activity_id: null, - last_activity_date: null, - lost_reason: null, - visible_to: '3', - close_time: null, - pipeline_id: 1, - won_time: null, - first_won_time: null, - lost_time: null, - products_count: 0, - files_count: 0, - notes_count: 0, - followers_count: 1, - email_messages_count: 0, - activities_count: 0, - done_activities_count: 0, - undone_activities_count: 0, - participants_count: 0, - expected_close_date: null, - last_incoming_mail_time: null, - last_outgoing_mail_time: null, - label: null, - renewal_type: 'one_time', - stage_order_nr: 1, - person_name: null, - org_name: 'Left Hook', - next_activity_subject: null, - next_activity_type: null, - next_activity_duration: null, - next_activity_note: null, - group_id: null, - group_name: null, - formatted_value: '$0', - weighted_value: 0, - formatted_weighted_value: '$0', - weighted_value_currency: 'USD', - rotten_time: null, - owner_name: 'Tom Elliott', - cc_email: 'lefthook-sandbox-41e8b7+deal2@pipedrivemail.com', - org_hidden: false, - person_hidden: false, - }, - ], - additional_data: { - pagination: { - start: 0, - limit: 100, - more_items_in_collection: false, - }, - }, - related_objects: { - user: { - 1811658: { - id: 1811658, - name: 'Tom Elliott', - email: 'projectteam@lefthook.co', - has_pic: 1, - pic_hash: 'de38c66276cb325d7c0e84d4fae1f0ce', - active_flag: true, - }, - }, - person: { - 1: { - active_flag: true, - id: 1, - name: 'Example Person', - email: [ - { - value: '', - primary: true, - }, - ], - phone: [ - { - value: '', - primary: true, - }, - ], - owner_id: 1811658, - }, - }, - organization: { - 1: { - id: 1, - name: 'Left Hook', - people_count: 0, - owner_id: 1811658, - address: null, - active_flag: true, - cc_email: 'lefthook-sandbox-41e8b7@pipedrivemail.com', - }, - }, - }, -}; diff --git a/packages/v1-ready/pipedrive/models/credential.js b/packages/v1-ready/pipedrive/models/credential.js deleted file mode 100644 index 29afde1..0000000 --- a/packages/v1-ready/pipedrive/models/credential.js +++ /dev/null @@ -1,21 +0,0 @@ -const {Credential: Parent} = require('@friggframework/core'); -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - trim: true, - lhEncrypt: true, - }, - companyDomain: {type: String}, -}); - -const name = 'PipedriveCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/v1-ready/pipedrive/models/entity.js b/packages/v1-ready/pipedrive/models/entity.js deleted file mode 100644 index 6e02de8..0000000 --- a/packages/v1-ready/pipedrive/models/entity.js +++ /dev/null @@ -1,9 +0,0 @@ -const {Entity: Parent} = require('@friggframework/core'); -'use strict'; -const mongoose = require('mongoose'); - -const schema = new mongoose.Schema({}); -const name = 'PipedriveEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/v1-ready/pipedrive/specs/openAPI.yaml b/packages/v1-ready/pipedrive/specs/openAPI.yaml deleted file mode 100644 index e0370d5..0000000 --- a/packages/v1-ready/pipedrive/specs/openAPI.yaml +++ /dev/null @@ -1,11129 +0,0 @@ -openapi: 3.0.1 -info: - title: Pipedrive API v2 - version: 2.0.0 -servers: - - url: 'https://api.pipedrive.com/api/v2' -tags: - - name: Activities - description: | - Activities are appointments/tasks/events on a calendar that can be associated with a deal, a lead, a person and an organization. Activities can be of different type (such as call, meeting, lunch or a custom type - see ActivityTypes object) and can be assigned to a particular user. Note that activities can also be created without a specific date/time. - - name: Deals - description: | - Deals represent ongoing, lost or won sales to an organization or to a person. Each deal has a monetary value and must be placed in a stage. Deals can be owned by a user, and followed by one or many users. Each deal consists of standard data fields but can also contain a number of custom fields. The custom fields can be recognized by long hashes as keys. These hashes can be mapped against `DealField.key`. The corresponding label for each such custom field can be obtained from `DealField.name`. - - name: Products - description: | - Products are the goods or services you are dealing with. Each product can have N different price points - firstly, each product can have a price in N different currencies, and secondly, each product can have N variations of itself, each having N prices in different currencies. Note that only one price per variation per currency is supported. Products can be instantiated to deals. In the context of instatiation, a custom price, quantity and discount can be applied. - - name: Leads - description: | - Leads are potential deals stored in Leads Inbox before they are archived or converted to a deal. Each lead needs to be named (using the `title` field) and be linked to a person or an organization. In addition to that, a lead can contain most of the fields a deal can (such as `value` or `expected_close_date`). - - name: Organizations - description: | - Organizations are companies and other kinds of organizations you are making deals with. Persons can be associated with organizations so that each organization can contain one or more persons. - - name: Persons - description: | - Persons are your contacts, the customers you are doing deals with. Each person can belong to an organization. Persons should not be confused with users. - - name: ItemSearch - description: | - Ordered reference objects, pointing to either deals, persons, organizations, leads, products, files or mail attachments. - - name: Stages - description: | - Stage is a logical component of a pipeline, and essentially a bucket that can hold a number of deals. In the context of the pipeline a stage belongs to, it has an order number which defines the order of stages in that pipeline. - - name: Pipelines - description: | - Pipelines are essentially ordered collections of stages. -paths: - /activities: - get: - summary: Get all activities - description: Returns data about all activities. - x-token-cost: 10 - operationId: getActivities - tags: - - Activities - security: - - api_key: [] - - oauth2: - - 'activities:read' - - 'activities:full' - parameters: - - in: query - name: filter_id - schema: - type: integer - description: 'If supplied, only activities matching the specified filter are returned' - - in: query - name: ids - description: 'Optional comma separated string array of up to 100 entity ids to fetch. If filter_id is provided, this is ignored. If any of the requested entities do not exist or are not visible, they are not included in the response.' - schema: - type: string - - in: query - name: owner_id - schema: - type: integer - description: 'If supplied, only activities owned by the specified user are returned. If filter_id is provided, this is ignored.' - - in: query - name: deal_id - schema: - type: integer - description: 'If supplied, only activities linked to the specified deal are returned. If filter_id is provided, this is ignored.' - - in: query - name: lead_id - schema: - type: string - description: 'If supplied, only activities linked to the specified lead are returned. If filter_id is provided, this is ignored.' - - in: query - name: person_id - schema: - type: integer - description: 'If supplied, only activities whose primary participant is the given person are returned. If filter_id is provided, this is ignored.' - - in: query - name: org_id - schema: - type: integer - description: 'If supplied, only activities linked to the specified organization are returned. If filter_id is provided, this is ignored.' - - in: query - name: done - schema: - type: boolean - description: 'If supplied, only activities with specified ''done'' flag value are returned' - - in: query - name: updated_since - schema: - type: string - description: 'If set, only activities with an `update_time` later than or equal to this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: updated_until - schema: - type: string - description: 'If set, only activities with an `update_time` earlier than this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`, `due_date`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - due_date - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - attendees - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all activities - content: - application/json: - schema: - type: object - title: GetActivitiesResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Activities array - items: - type: object - title: ActivityItem - properties: - id: - type: integer - description: The ID of the activity - subject: - type: string - description: The subject of the activity - type: - type: string - description: The type of the activity - owner_id: - type: integer - description: The ID of the user who owns the activity - creator_user_id: - type: integer - description: The ID of the user who created the activity - is_deleted: - type: boolean - description: Whether the activity is deleted or not - add_time: - type: string - description: The creation date and time of the activity - update_time: - type: string - description: The last updated date and time of the activity - deal_id: - type: integer - description: The ID of the deal linked to the activity - lead_id: - type: string - description: The ID of the lead linked to the activity - person_id: - type: integer - description: The ID of the person linked to the activity - org_id: - type: integer - description: The ID of the organization linked to the activity - project_id: - type: integer - description: The ID of the project linked to the activity - due_date: - type: string - description: The due date of the activity - due_time: - type: string - description: The due time of the activity - duration: - type: string - description: The duration of the activity - busy: - type: boolean - description: Whether the activity marks the assignee as busy or not in their calendar - done: - type: boolean - description: Whether the activity is marked as done or not - marked_as_done_time: - type: string - description: The date and time when the activity was marked as done - location: - type: object - description: Location of the activity - properties: - value: - type: string - description: The full address of the activity - country: - type: string - description: Country of the activity - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the activity - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the activity - locality: - type: string - description: Locality (e.g. city) of the activity - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the activity - route: - type: string - description: Route (e.g. street) of the activity - street_number: - type: string - description: Street number of the activity - postal_code: - type: string - description: Postal code of the activity - participants: - type: array - description: The participants of the activity - items: - type: object - properties: - person_id: - type: integer - description: The ID of the person - primary: - type: boolean - description: Whether the person is the primary participant or not - attendees: - type: array - description: The attendees of the activity - items: - type: object - properties: - email: - type: string - description: The email address of the attendee - name: - type: string - description: The name of the attendee - status: - type: string - description: The status of the attendee - is_organizer: - type: boolean - description: Whether the attendee is the organizer or not - person_id: - type: integer - description: The ID of the person if the attendee has a person record - user_id: - type: integer - description: The ID of the user if the attendee is a user - conference_meeting_client: - type: string - description: The client used for the conference meeting - conference_meeting_url: - type: string - description: The URL of the conference meeting - conference_meeting_id: - type: string - description: The ID of the conference meeting - public_description: - type: string - description: The public description of the activity - priority: - type: integer - description: The priority of the activity. Mappable to a specific string using activityFields API. - note: - type: string - description: The note of the activity - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - subject: Activity Subject - type: activity_type - owner_id: 1 - creator_user_id: 1 - is_deleted: false - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - deal_id: 5 - lead_id: abc-def - person_id: 6 - org_id: 7 - project_id: 8 - due_date: '2021-01-01' - due_time: '15:00:00' - duration: '01:00:00' - busy: true - done: true - marked_as_done_time: '2021-01-01T00:00:00Z' - location: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - participants: - - person_id: 1 - primary: true - attendees: - - email: some@email.com - name: Some Name - status: accepted - is_organizer: true - person_id: 1 - user_id: 1 - conference_meeting_client: google_meet - conference_meeting_url: 'https://meet.google.com/abc-xyz' - conference_meeting_id: abc-xyz - public_description: Public Description - priority: 263 - note: Note - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a new activity - description: Adds a new activity. - x-token-cost: 5 - operationId: addActivity - tags: - - Activities - security: - - api_key: [] - - oauth2: - - 'activities:full' - requestBody: - content: - application/json: - schema: - type: object - properties: - subject: - type: string - description: The subject of the activity - type: - type: string - description: The type of the activity - owner_id: - type: integer - description: The ID of the user who owns the activity - deal_id: - type: integer - description: The ID of the deal linked to the activity - lead_id: - type: string - description: The ID of the lead linked to the activity - person_id: - type: integer - description: The ID of the person linked to the activity - org_id: - type: integer - description: The ID of the organization linked to the activity - project_id: - type: integer - description: The ID of the project linked to the activity - due_date: - type: string - description: The due date of the activity - due_time: - type: string - description: The due time of the activity - duration: - type: string - description: The duration of the activity - busy: - type: boolean - description: Whether the activity marks the assignee as busy or not in their calendar - done: - type: boolean - description: Whether the activity is marked as done or not - location: - type: object - description: Location of the activity - properties: - value: - type: string - description: The full address of the activity - country: - type: string - description: Country of the activity - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the activity - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the activity - locality: - type: string - description: Locality (e.g. city) of the activity - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the activity - route: - type: string - description: Route (e.g. street) of the activity - street_number: - type: string - description: Street number of the activity - postal_code: - type: string - description: Postal code of the activity - participants: - type: array - description: The participants of the activity - items: - type: object - properties: - person_id: - type: integer - description: The ID of the person - primary: - type: boolean - description: Whether the person is the primary participant or not - attendees: - type: array - description: The attendees of the activity - items: - type: object - properties: - email: - type: string - description: The email address of the attendee - name: - type: string - description: The name of the attendee - status: - type: string - description: The status of the attendee - is_organizer: - type: boolean - description: Whether the attendee is the organizer or not - person_id: - type: integer - description: The ID of the person if the attendee has a person record - user_id: - type: integer - description: The ID of the user if the attendee is a user - public_description: - type: string - description: The public description of the activity - priority: - type: integer - description: The priority of the activity. Mappable to a specific string using activityFields API. - note: - type: string - description: The note of the activity - responses: - '200': - description: Add activity - content: - application/json: - schema: - type: object - title: UpsertActivityResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertActivityResponseData - properties: - data: - type: object - title: ActivityItem - properties: - id: - type: integer - description: The ID of the activity - subject: - type: string - description: The subject of the activity - type: - type: string - description: The type of the activity - owner_id: - type: integer - description: The ID of the user who owns the activity - creator_user_id: - type: integer - description: The ID of the user who created the activity - is_deleted: - type: boolean - description: Whether the activity is deleted or not - add_time: - type: string - description: The creation date and time of the activity - update_time: - type: string - description: The last updated date and time of the activity - deal_id: - type: integer - description: The ID of the deal linked to the activity - lead_id: - type: string - description: The ID of the lead linked to the activity - person_id: - type: integer - description: The ID of the person linked to the activity - org_id: - type: integer - description: The ID of the organization linked to the activity - project_id: - type: integer - description: The ID of the project linked to the activity - due_date: - type: string - description: The due date of the activity - due_time: - type: string - description: The due time of the activity - duration: - type: string - description: The duration of the activity - busy: - type: boolean - description: Whether the activity marks the assignee as busy or not in their calendar - done: - type: boolean - description: Whether the activity is marked as done or not - marked_as_done_time: - type: string - description: The date and time when the activity was marked as done - location: - type: object - description: Location of the activity - properties: - value: - type: string - description: The full address of the activity - country: - type: string - description: Country of the activity - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the activity - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the activity - locality: - type: string - description: Locality (e.g. city) of the activity - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the activity - route: - type: string - description: Route (e.g. street) of the activity - street_number: - type: string - description: Street number of the activity - postal_code: - type: string - description: Postal code of the activity - participants: - type: array - description: The participants of the activity - items: - type: object - properties: - person_id: - type: integer - description: The ID of the person - primary: - type: boolean - description: Whether the person is the primary participant or not - attendees: - type: array - description: The attendees of the activity - items: - type: object - properties: - email: - type: string - description: The email address of the attendee - name: - type: string - description: The name of the attendee - status: - type: string - description: The status of the attendee - is_organizer: - type: boolean - description: Whether the attendee is the organizer or not - person_id: - type: integer - description: The ID of the person if the attendee has a person record - user_id: - type: integer - description: The ID of the user if the attendee is a user - conference_meeting_client: - type: string - description: The client used for the conference meeting - conference_meeting_url: - type: string - description: The URL of the conference meeting - conference_meeting_id: - type: string - description: The ID of the conference meeting - public_description: - type: string - description: The public description of the activity - priority: - type: integer - description: The priority of the activity. Mappable to a specific string using activityFields API. - note: - type: string - description: The note of the activity - description: The activity object - example: - success: true - data: - id: 1 - subject: Activity Subject - type: activity_type - owner_id: 1 - creator_user_id: 1 - is_deleted: false - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - deal_id: 5 - lead_id: abc-def - person_id: 6 - org_id: 7 - project_id: 8 - due_date: '2021-01-01' - due_time: '15:00:00' - duration: '01:00:00' - busy: true - done: true - marked_as_done_time: '2021-01-01T00:00:00Z' - location: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - participants: - - person_id: 1 - primary: true - attendees: - - email: some@email.com - name: Some Name - status: accepted - is_organizer: true - person_id: 1 - user_id: 1 - conference_meeting_client: google_meet - conference_meeting_url: 'https://meet.google.com/abc-xyz' - conference_meeting_id: abc-xyz - public_description: Public Description - priority: 263 - note: Note - '/activities/{id}': - delete: - summary: Delete an activity - description: 'Marks an activity as deleted. After 30 days, the activity will be permanently deleted.' - x-token-cost: 3 - operationId: deleteActivity - tags: - - Activities - security: - - api_key: [] - - oauth2: - - 'activities:full' - parameters: - - in: path - name: id - description: The ID of the activity - required: true - schema: - type: integer - responses: - '200': - description: Delete activity - content: - application/json: - schema: - title: DeleteActivityResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: Deleted activity ID - example: - success: true - data: - id: 1 - get: - summary: Get details of an activity - description: Returns the details of a specific activity. - x-token-cost: 1 - operationId: getActivity - tags: - - Activities - security: - - api_key: [] - - oauth2: - - 'activities:read' - - 'activities:full' - parameters: - - in: path - name: id - description: The ID of the activity - required: true - schema: - type: integer - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - attendees - responses: - '200': - description: Get activity - content: - application/json: - schema: - type: object - title: UpsertActivityResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertActivityResponseData - properties: - data: - type: object - title: ActivityItem - properties: - id: - type: integer - description: The ID of the activity - subject: - type: string - description: The subject of the activity - type: - type: string - description: The type of the activity - owner_id: - type: integer - description: The ID of the user who owns the activity - creator_user_id: - type: integer - description: The ID of the user who created the activity - is_deleted: - type: boolean - description: Whether the activity is deleted or not - add_time: - type: string - description: The creation date and time of the activity - update_time: - type: string - description: The last updated date and time of the activity - deal_id: - type: integer - description: The ID of the deal linked to the activity - lead_id: - type: string - description: The ID of the lead linked to the activity - person_id: - type: integer - description: The ID of the person linked to the activity - org_id: - type: integer - description: The ID of the organization linked to the activity - project_id: - type: integer - description: The ID of the project linked to the activity - due_date: - type: string - description: The due date of the activity - due_time: - type: string - description: The due time of the activity - duration: - type: string - description: The duration of the activity - busy: - type: boolean - description: Whether the activity marks the assignee as busy or not in their calendar - done: - type: boolean - description: Whether the activity is marked as done or not - marked_as_done_time: - type: string - description: The date and time when the activity was marked as done - location: - type: object - description: Location of the activity - properties: - value: - type: string - description: The full address of the activity - country: - type: string - description: Country of the activity - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the activity - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the activity - locality: - type: string - description: Locality (e.g. city) of the activity - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the activity - route: - type: string - description: Route (e.g. street) of the activity - street_number: - type: string - description: Street number of the activity - postal_code: - type: string - description: Postal code of the activity - participants: - type: array - description: The participants of the activity - items: - type: object - properties: - person_id: - type: integer - description: The ID of the person - primary: - type: boolean - description: Whether the person is the primary participant or not - attendees: - type: array - description: The attendees of the activity - items: - type: object - properties: - email: - type: string - description: The email address of the attendee - name: - type: string - description: The name of the attendee - status: - type: string - description: The status of the attendee - is_organizer: - type: boolean - description: Whether the attendee is the organizer or not - person_id: - type: integer - description: The ID of the person if the attendee has a person record - user_id: - type: integer - description: The ID of the user if the attendee is a user - conference_meeting_client: - type: string - description: The client used for the conference meeting - conference_meeting_url: - type: string - description: The URL of the conference meeting - conference_meeting_id: - type: string - description: The ID of the conference meeting - public_description: - type: string - description: The public description of the activity - priority: - type: integer - description: The priority of the activity. Mappable to a specific string using activityFields API. - note: - type: string - description: The note of the activity - description: The activity object - example: - success: true - data: - id: 1 - subject: Activity Subject - type: activity_type - owner_id: 1 - creator_user_id: 1 - is_deleted: false - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - deal_id: 5 - lead_id: abc-def - person_id: 6 - org_id: 7 - project_id: 8 - due_date: '2021-01-01' - due_time: '15:00:00' - duration: '01:00:00' - busy: true - done: true - marked_as_done_time: '2021-01-01T00:00:00Z' - location: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - participants: - - person_id: 1 - primary: true - attendees: - - email: some@email.com - name: Some Name - status: accepted - is_organizer: true - person_id: 1 - user_id: 1 - conference_meeting_client: google_meet - conference_meeting_url: 'https://meet.google.com/abc-xyz' - conference_meeting_id: abc-xyz - public_description: Public Description - priority: 263 - note: Note - patch: - summary: Update an activity - description: Updates the properties of an activity. - x-token-cost: 5 - operationId: updateActivity - tags: - - Activities - security: - - api_key: [] - - oauth2: - - 'activities:full' - parameters: - - in: path - name: id - description: The ID of the activity - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - type: object - properties: - subject: - type: string - description: The subject of the activity - type: - type: string - description: The type of the activity - owner_id: - type: integer - description: The ID of the user who owns the activity - deal_id: - type: integer - description: The ID of the deal linked to the activity - lead_id: - type: string - description: The ID of the lead linked to the activity - person_id: - type: integer - description: The ID of the person linked to the activity - org_id: - type: integer - description: The ID of the organization linked to the activity - project_id: - type: integer - description: The ID of the project linked to the activity - due_date: - type: string - description: The due date of the activity - due_time: - type: string - description: The due time of the activity - duration: - type: string - description: The duration of the activity - busy: - type: boolean - description: Whether the activity marks the assignee as busy or not in their calendar - done: - type: boolean - description: Whether the activity is marked as done or not - location: - type: object - description: Location of the activity - properties: - value: - type: string - description: The full address of the activity - country: - type: string - description: Country of the activity - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the activity - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the activity - locality: - type: string - description: Locality (e.g. city) of the activity - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the activity - route: - type: string - description: Route (e.g. street) of the activity - street_number: - type: string - description: Street number of the activity - postal_code: - type: string - description: Postal code of the activity - participants: - type: array - description: The participants of the activity - items: - type: object - properties: - person_id: - type: integer - description: The ID of the person - primary: - type: boolean - description: Whether the person is the primary participant or not - attendees: - type: array - description: The attendees of the activity - items: - type: object - properties: - email: - type: string - description: The email address of the attendee - name: - type: string - description: The name of the attendee - status: - type: string - description: The status of the attendee - is_organizer: - type: boolean - description: Whether the attendee is the organizer or not - person_id: - type: integer - description: The ID of the person if the attendee has a person record - user_id: - type: integer - description: The ID of the user if the attendee is a user - public_description: - type: string - description: The public description of the activity - priority: - type: integer - description: The priority of the activity. Mappable to a specific string using activityFields API. - note: - type: string - description: The note of the activity - responses: - '200': - description: Edit activity - content: - application/json: - schema: - type: object - title: UpsertActivityResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertActivityResponseData - properties: - data: - type: object - title: ActivityItem - properties: - id: - type: integer - description: The ID of the activity - subject: - type: string - description: The subject of the activity - type: - type: string - description: The type of the activity - owner_id: - type: integer - description: The ID of the user who owns the activity - creator_user_id: - type: integer - description: The ID of the user who created the activity - is_deleted: - type: boolean - description: Whether the activity is deleted or not - add_time: - type: string - description: The creation date and time of the activity - update_time: - type: string - description: The last updated date and time of the activity - deal_id: - type: integer - description: The ID of the deal linked to the activity - lead_id: - type: string - description: The ID of the lead linked to the activity - person_id: - type: integer - description: The ID of the person linked to the activity - org_id: - type: integer - description: The ID of the organization linked to the activity - project_id: - type: integer - description: The ID of the project linked to the activity - due_date: - type: string - description: The due date of the activity - due_time: - type: string - description: The due time of the activity - duration: - type: string - description: The duration of the activity - busy: - type: boolean - description: Whether the activity marks the assignee as busy or not in their calendar - done: - type: boolean - description: Whether the activity is marked as done or not - marked_as_done_time: - type: string - description: The date and time when the activity was marked as done - location: - type: object - description: Location of the activity - properties: - value: - type: string - description: The full address of the activity - country: - type: string - description: Country of the activity - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the activity - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the activity - locality: - type: string - description: Locality (e.g. city) of the activity - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the activity - route: - type: string - description: Route (e.g. street) of the activity - street_number: - type: string - description: Street number of the activity - postal_code: - type: string - description: Postal code of the activity - participants: - type: array - description: The participants of the activity - items: - type: object - properties: - person_id: - type: integer - description: The ID of the person - primary: - type: boolean - description: Whether the person is the primary participant or not - attendees: - type: array - description: The attendees of the activity - items: - type: object - properties: - email: - type: string - description: The email address of the attendee - name: - type: string - description: The name of the attendee - status: - type: string - description: The status of the attendee - is_organizer: - type: boolean - description: Whether the attendee is the organizer or not - person_id: - type: integer - description: The ID of the person if the attendee has a person record - user_id: - type: integer - description: The ID of the user if the attendee is a user - conference_meeting_client: - type: string - description: The client used for the conference meeting - conference_meeting_url: - type: string - description: The URL of the conference meeting - conference_meeting_id: - type: string - description: The ID of the conference meeting - public_description: - type: string - description: The public description of the activity - priority: - type: integer - description: The priority of the activity. Mappable to a specific string using activityFields API. - note: - type: string - description: The note of the activity - description: The activity object - example: - success: true - data: - id: 1 - subject: Activity Subject - type: activity_type - owner_id: 1 - creator_user_id: 1 - is_deleted: false - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - deal_id: 5 - lead_id: abc-def - person_id: 6 - org_id: 7 - project_id: 8 - due_date: '2021-01-01' - due_time: '15:00:00' - duration: '01:00:00' - busy: true - done: true - marked_as_done_time: '2021-01-01T00:00:00Z' - location: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - participants: - - person_id: 1 - primary: true - attendees: - - email: some@email.com - name: Some Name - status: accepted - is_organizer: true - person_id: 1 - user_id: 1 - conference_meeting_client: google_meet - conference_meeting_url: 'https://meet.google.com/abc-xyz' - conference_meeting_id: abc-xyz - public_description: Public Description - priority: 263 - note: Note - /deals: - get: - summary: Get all deals - description: Returns data about all not archived deals. - x-token-cost: 10 - operationId: getDeals - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: query - name: filter_id - schema: - type: integer - description: 'If supplied, only deals matching the specified filter are returned' - - in: query - name: ids - description: 'Optional comma separated string array of up to 100 entity ids to fetch. If filter_id is provided, this is ignored. If any of the requested entities do not exist or are not visible, they are not included in the response.' - schema: - type: string - - in: query - name: owner_id - schema: - type: integer - description: 'If supplied, only deals owned by the specified user are returned. If filter_id is provided, this is ignored.' - - in: query - name: person_id - schema: - type: integer - description: 'If supplied, only deals linked to the specified person are returned. If filter_id is provided, this is ignored.' - - in: query - name: org_id - schema: - type: integer - description: 'If supplied, only deals linked to the specified organization are returned. If filter_id is provided, this is ignored.' - - in: query - name: pipeline_id - schema: - type: integer - description: 'If supplied, only deals in the specified pipeline are returned. If filter_id is provided, this is ignored.' - - in: query - name: stage_id - schema: - type: integer - description: 'If supplied, only deals in the specified stage are returned. If filter_id is provided, this is ignored.' - - in: query - name: status - schema: - type: string - enum: - - open - - won - - lost - - deleted - description: 'Only fetch deals with a specific status. If omitted, all not deleted deals are returned. If set to deleted, deals that have been deleted up to 30 days ago will be included. Multiple statuses can be included as a comma separated array. If filter_id is provided, this is ignored.' - - in: query - name: updated_since - schema: - type: string - description: 'If set, only deals with an `update_time` later than or equal to this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: updated_until - schema: - type: string - description: 'If set, only deals with an `update_time` earlier than this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - first_won_time - - products_count - - files_count - - notes_count - - followers_count - - email_messages_count - - activities_count - - done_activities_count - - undone_activities_count - - participants_count - - last_incoming_mail_time - - last_outgoing_mail_time - - smart_bcc_email - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all not archived deals - content: - application/json: - schema: - type: object - title: GetDealsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Deals array - items: - type: object - title: DealItem - properties: - id: - type: integer - description: The ID of the deal - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal - won_time: - type: string - description: The date and time of changing the deal status as won - lost_time: - type: string - description: The date and time of changing the deal status as lost - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - origin: - type: string - description: The way this Deal was created. `origin` field is set by Pipedrive when Deal is created and cannot be changed. - origin_id: - type: string - nullable: true - description: The optional ID to further distinguish the origin of the deal - e.g. Which API integration created this Deal. - channel: - type: integer - nullable: true - description: 'The ID of your Marketing channel this Deal was created from. Recognized Marketing channels can be configured in your Company settings.' - channel_id: - type: string - nullable: true - description: The optional ID to further distinguish the Marketing channel. - arr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Recurring Revenue of the deal - - Null if there are no products attached to the deal - mrr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Monthly Recurring Revenue of the deal - - Null if there are no products attached to the deal - acv: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Contract Value of the deal - - Null if there are no products attached to the deal - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - title: Deal Title - creator_user_id: 1 - owner_id: 1 - value: 200 - person_id: 1 - org_id: 1 - stage_id: 1 - pipeline_id: 1 - currency: USD - archive_time: '2021-01-01T00:00:00Z' - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - stage_change_time: '2021-01-01T00:00:00Z' - status: open - is_archived: false - is_deleted: false - probability: 90 - lost_reason: Lost Reason - visible_to: 7 - close_time: '2021-01-01T00:00:00Z' - won_time: '2021-01-01T00:00:00Z' - lost_time: '2021-01-01T00:00:00Z' - local_won_date: '2021-01-01' - local_lost_date: '2021-01-01' - local_close_date: '2021-01-01' - expected_close_date: '2021-01-01' - label_ids: - - 1 - - 2 - - 3 - origin: ManuallyCreated - origin_id: null - channel: 52 - channel_id: Jun23 Billboards - acv: 120 - arr: 120 - mrr: 10 - custom_fields: {} - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a new deal - description: Adds a new deal. - x-token-cost: 5 - operationId: addDeal - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:full' - requestBody: - content: - application/json: - schema: - required: - - title - type: object - properties: - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - archive_time: - type: string - description: 'The optional date and time of archiving the deal in UTC. Format: YYYY-MM-DD HH:MM:SS. If omitted and `is_archived` is true, it will be set to the current date and time.' - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal. Can only be set if deal status is lost. - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal. Can only be set if deal status is won or lost. - won_time: - type: string - description: The date and time of changing the deal status as won. Can only be set if deal status is won. - lost_time: - type: string - description: The date and time of changing the deal status as lost. Can only be set if deal status is lost. - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - responses: - '200': - description: Add deal - content: - application/json: - schema: - type: object - title: UpsertDealResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertDealResponseData - properties: - data: - type: object - title: DealItem - properties: - id: - type: integer - description: The ID of the deal - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal - won_time: - type: string - description: The date and time of changing the deal status as won - lost_time: - type: string - description: The date and time of changing the deal status as lost - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - origin: - type: string - description: The way this Deal was created. `origin` field is set by Pipedrive when Deal is created and cannot be changed. - origin_id: - type: string - nullable: true - description: The optional ID to further distinguish the origin of the deal - e.g. Which API integration created this Deal. - channel: - type: integer - nullable: true - description: 'The ID of your Marketing channel this Deal was created from. Recognized Marketing channels can be configured in your Company settings.' - channel_id: - type: string - nullable: true - description: The optional ID to further distinguish the Marketing channel. - arr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Recurring Revenue of the deal - - Null if there are no products attached to the deal - mrr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Monthly Recurring Revenue of the deal - - Null if there are no products attached to the deal - acv: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Contract Value of the deal - - Null if there are no products attached to the deal - description: The deal object - example: - success: true - data: - id: 1 - title: Deal Title - creator_user_id: 1 - owner_id: 1 - value: 200 - person_id: 1 - org_id: 1 - stage_id: 1 - pipeline_id: 1 - currency: USD - archive_time: '2021-01-01T00:00:00Z' - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - stage_change_time: '2021-01-01T00:00:00Z' - status: open - is_archived: false - is_deleted: false - probability: 90 - lost_reason: Lost Reason - visible_to: 7 - close_time: '2021-01-01T00:00:00Z' - won_time: '2021-01-01T00:00:00Z' - lost_time: '2021-01-01T00:00:00Z' - local_won_date: '2021-01-01' - local_lost_date: '2021-01-01' - local_close_date: '2021-01-01' - expected_close_date: '2021-01-01' - label_ids: - - 1 - - 2 - - 3 - origin: ManuallyCreated - origin_id: null - channel: 52 - channel_id: Jun23 Billboards - acv: 120 - arr: 120 - mrr: 10 - custom_fields: {} - /deals/archived: - get: - summary: Get all archived deals - description: Returns data about all archived deals. - x-token-cost: 20 - operationId: getArchivedDeals - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: query - name: filter_id - schema: - type: integer - description: 'If supplied, only deals matching the specified filter are returned' - - in: query - name: ids - description: 'Optional comma separated string array of up to 100 entity ids to fetch. If filter_id is provided, this is ignored. If any of the requested entities do not exist or are not visible, they are not included in the response.' - schema: - type: string - - in: query - name: owner_id - schema: - type: integer - description: 'If supplied, only deals owned by the specified user are returned. If filter_id is provided, this is ignored.' - - in: query - name: person_id - schema: - type: integer - description: 'If supplied, only deals linked to the specified person are returned. If filter_id is provided, this is ignored.' - - in: query - name: org_id - schema: - type: integer - description: 'If supplied, only deals linked to the specified organization are returned. If filter_id is provided, this is ignored.' - - in: query - name: pipeline_id - schema: - type: integer - description: 'If supplied, only deals in the specified pipeline are returned. If filter_id is provided, this is ignored.' - - in: query - name: stage_id - schema: - type: integer - description: 'If supplied, only deals in the specified stage are returned. If filter_id is provided, this is ignored.' - - in: query - name: status - schema: - type: string - enum: - - open - - won - - lost - - deleted - description: 'Only fetch deals with a specific status. If omitted, all not deleted deals are returned. If set to deleted, deals that have been deleted up to 30 days ago will be included. Multiple statuses can be included as a comma separated array. If filter_id is provided, this is ignored.' - - in: query - name: updated_since - schema: - type: string - description: 'If set, only deals with an `update_time` later than or equal to this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: updated_until - schema: - type: string - description: 'If set, only deals with an `update_time` earlier than this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - first_won_time - - products_count - - files_count - - notes_count - - followers_count - - email_messages_count - - activities_count - - done_activities_count - - undone_activities_count - - participants_count - - last_incoming_mail_time - - last_outgoing_mail_time - - smart_bcc_email - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all archived deals - content: - application/json: - schema: - type: object - title: GetDealsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Deals array - items: - type: object - title: DealItem - properties: - id: - type: integer - description: The ID of the deal - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal - won_time: - type: string - description: The date and time of changing the deal status as won - lost_time: - type: string - description: The date and time of changing the deal status as lost - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - origin: - type: string - description: The way this Deal was created. `origin` field is set by Pipedrive when Deal is created and cannot be changed. - origin_id: - type: string - nullable: true - description: The optional ID to further distinguish the origin of the deal - e.g. Which API integration created this Deal. - channel: - type: integer - nullable: true - description: 'The ID of your Marketing channel this Deal was created from. Recognized Marketing channels can be configured in your Company settings.' - channel_id: - type: string - nullable: true - description: The optional ID to further distinguish the Marketing channel. - arr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Recurring Revenue of the deal - - Null if there are no products attached to the deal - mrr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Monthly Recurring Revenue of the deal - - Null if there are no products attached to the deal - acv: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Contract Value of the deal - - Null if there are no products attached to the deal - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - title: Deal Title - creator_user_id: 1 - owner_id: 1 - value: 200 - person_id: 1 - org_id: 1 - stage_id: 1 - pipeline_id: 1 - currency: USD - archive_time: '2021-01-01T00:00:00Z' - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - stage_change_time: '2021-01-01T00:00:00Z' - status: open - is_archived: true - is_deleted: false - probability: 90 - lost_reason: Lost Reason - visible_to: 7 - close_time: '2021-01-01T00:00:00Z' - won_time: '2021-01-01T00:00:00Z' - lost_time: '2021-01-01T00:00:00Z' - local_won_date: '2021-01-01' - local_lost_date: '2021-01-01' - local_close_date: '2021-01-01' - expected_close_date: '2021-01-01' - label_ids: - - 1 - - 2 - - 3 - origin: ManuallyCreated - origin_id: null - channel: 52 - channel_id: Jun23 Billboards - acv: 120 - arr: 120 - mrr: 10 - custom_fields: {} - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/deals/{id}': - delete: - summary: Delete a deal - description: 'Marks a deal as deleted. After 30 days, the deal will be permanently deleted.' - x-token-cost: 3 - operationId: deleteDeal - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - responses: - '200': - description: Delete deal - content: - application/json: - schema: - title: DeleteDealResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: Deleted deal ID - example: - success: true - data: - id: 1 - get: - summary: Get details of a deal - description: Returns the details of a specific deal. - x-token-cost: 1 - operationId: getDeal - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - first_won_time - - products_count - - files_count - - notes_count - - followers_count - - email_messages_count - - activities_count - - done_activities_count - - undone_activities_count - - participants_count - - last_incoming_mail_time - - last_outgoing_mail_time - - smart_bcc_email - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - responses: - '200': - description: Get deal - content: - application/json: - schema: - type: object - title: UpsertDealResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertDealResponseData - properties: - data: - type: object - title: DealItem - properties: - id: - type: integer - description: The ID of the deal - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal - won_time: - type: string - description: The date and time of changing the deal status as won - lost_time: - type: string - description: The date and time of changing the deal status as lost - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - origin: - type: string - description: The way this Deal was created. `origin` field is set by Pipedrive when Deal is created and cannot be changed. - origin_id: - type: string - nullable: true - description: The optional ID to further distinguish the origin of the deal - e.g. Which API integration created this Deal. - channel: - type: integer - nullable: true - description: 'The ID of your Marketing channel this Deal was created from. Recognized Marketing channels can be configured in your Company settings.' - channel_id: - type: string - nullable: true - description: The optional ID to further distinguish the Marketing channel. - arr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Recurring Revenue of the deal - - Null if there are no products attached to the deal - mrr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Monthly Recurring Revenue of the deal - - Null if there are no products attached to the deal - acv: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Contract Value of the deal - - Null if there are no products attached to the deal - description: The deal object - example: - success: true - data: - id: 1 - title: Deal Title - creator_user_id: 1 - owner_id: 1 - value: 200 - person_id: 1 - org_id: 1 - stage_id: 1 - pipeline_id: 1 - currency: USD - archive_time: '2021-01-01T00:00:00Z' - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - stage_change_time: '2021-01-01T00:00:00Z' - status: open - is_archived: false - is_deleted: false - probability: 90 - lost_reason: Lost Reason - visible_to: 7 - close_time: '2021-01-01T00:00:00Z' - won_time: '2021-01-01T00:00:00Z' - lost_time: '2021-01-01T00:00:00Z' - local_won_date: '2021-01-01' - local_lost_date: '2021-01-01' - local_close_date: '2021-01-01' - expected_close_date: '2021-01-01' - label_ids: - - 1 - - 2 - - 3 - origin: ManuallyCreated - origin_id: null - channel: 52 - channel_id: Jun23 Billboards - acv: 120 - arr: 120 - mrr: 10 - custom_fields: {} - patch: - summary: Update a deal - description: Updates the properties of a deal. - x-token-cost: 5 - operationId: updateDeal - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - type: object - properties: - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - archive_time: - type: string - description: 'The optional date and time of archiving the deal in UTC. Format: YYYY-MM-DD HH:MM:SS. If omitted and `is_archived` is true, it will be set to the current date and time.' - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal. Can only be set if deal status is lost. - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal. Can only be set if deal status is won or lost. - won_time: - type: string - description: The date and time of changing the deal status as won. Can only be set if deal status is won. - lost_time: - type: string - description: The date and time of changing the deal status as lost. Can only be set if deal status is lost. - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - responses: - '200': - description: Edit deal - content: - application/json: - schema: - type: object - title: UpsertDealResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertDealResponseData - properties: - data: - type: object - title: DealItem - properties: - id: - type: integer - description: The ID of the deal - title: - type: string - description: The title of the deal - owner_id: - type: integer - description: The ID of the user who owns the deal - person_id: - type: integer - description: The ID of the person linked to the deal - org_id: - type: integer - description: The ID of the organization linked to the deal - pipeline_id: - type: integer - description: The ID of the pipeline associated with the deal - stage_id: - type: integer - description: The ID of the deal stage - value: - type: number - description: The value of the deal - currency: - type: string - description: The currency associated with the deal - add_time: - type: string - description: The creation date and time of the deal - update_time: - type: string - description: The last updated date and time of the deal - stage_change_time: - type: string - description: The last updated date and time of the deal stage - is_deleted: - type: boolean - description: Whether the deal is deleted or not - is_archived: - type: boolean - description: Whether the deal is archived or not - status: - type: string - description: The status of the deal - probability: - type: number - nullable: true - description: The success probability percentage of the deal - lost_reason: - type: string - nullable: true - description: The reason for losing the deal - visible_to: - type: integer - description: The visibility of the deal - close_time: - type: string - nullable: true - description: The date and time of closing the deal - won_time: - type: string - description: The date and time of changing the deal status as won - lost_time: - type: string - description: The date and time of changing the deal status as lost - expected_close_date: - type: string - format: date - description: The expected close date of the deal - label_ids: - type: array - description: The IDs of labels assigned to the deal - items: - type: integer - origin: - type: string - description: The way this Deal was created. `origin` field is set by Pipedrive when Deal is created and cannot be changed. - origin_id: - type: string - nullable: true - description: The optional ID to further distinguish the origin of the deal - e.g. Which API integration created this Deal. - channel: - type: integer - nullable: true - description: 'The ID of your Marketing channel this Deal was created from. Recognized Marketing channels can be configured in your Company settings.' - channel_id: - type: string - nullable: true - description: The optional ID to further distinguish the Marketing channel. - arr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Recurring Revenue of the deal - - Null if there are no products attached to the deal - mrr: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Monthly Recurring Revenue of the deal - - Null if there are no products attached to the deal - acv: - type: number - nullable: true - description: | - Only available in Advanced and above plans - - The Annual Contract Value of the deal - - Null if there are no products attached to the deal - description: The deal object - example: - success: true - data: - id: 1 - title: Deal Title - creator_user_id: 1 - owner_id: 1 - value: 200 - person_id: 1 - org_id: 1 - stage_id: 1 - pipeline_id: 1 - currency: USD - archive_time: '2021-01-01T00:00:00Z' - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - stage_change_time: '2021-01-01T00:00:00Z' - status: open - is_archived: false - is_deleted: false - probability: 90 - lost_reason: Lost Reason - visible_to: 7 - close_time: '2021-01-01T00:00:00Z' - won_time: '2021-01-01T00:00:00Z' - lost_time: '2021-01-01T00:00:00Z' - local_won_date: '2021-01-01' - local_lost_date: '2021-01-01' - local_close_date: '2021-01-01' - expected_close_date: '2021-01-01' - label_ids: - - 1 - - 2 - - 3 - origin: ManuallyCreated - origin_id: null - channel: 52 - channel_id: Jun23 Billboards - acv: 120 - arr: 120 - mrr: 10 - custom_fields: {} - '/deals/{id}/followers': - get: - summary: List followers of a deal - description: Lists users who are following the deal. - x-token-cost: 10 - operationId: getDealFollowers - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowersResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Followers array - items: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a follower to a deal - description: Adds a user as a follower to the deal. - x-token-cost: 5 - operationId: addDealFollower - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - required: - - user_id - type: object - properties: - user_id: - type: integer - description: The ID of the user to add as a follower - responses: - '201': - description: Add a follower - content: - application/json: - schema: - type: object - title: AddFollowerResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - description: The follower object - example: - success: true - data: - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - '/deals/{id}/followers/changelog': - get: - summary: List followers changelog of a deal - description: Lists changelogs about users have followed the deal. - x-token-cost: 10 - operationId: getDealFollowersChangelog - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowerChangelogsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Follower changelogs array - items: - type: object - title: FollowerChangelogItem - properties: - action: - type: string - description: The type of change - actor_user_id: - type: integer - description: The ID of the user who did the change - follower_user_id: - type: integer - description: The ID of the user who was following the entity - time: - type: string - description: The time at which the change happened - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - action: added - actor_user_id: 1 - follower_user_id: 1 - time: '2024-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/deals/{id}/followers/{follower_id}': - delete: - summary: Delete a follower from a deal - description: Deletes a user follower from the deal. - x-token-cost: 3 - operationId: deleteDealFollower - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: follower_id - required: true - schema: - type: integer - description: The ID of the following user - responses: - '200': - description: Remove a follower - content: - application/json: - schema: - title: DeleteFollowerResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - user_id: - type: integer - description: Deleted follower user ID - example: - success: true - data: - user_id: 1 - /deals/products: - get: - summary: Get deal products of several deals - description: Returns data about products attached to deals - x-token-cost: 10 - operationId: getDealsProducts - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - - 'deals:read' - - 'deals:full' - parameters: - - in: query - name: deal_ids - required: true - schema: - type: array - items: - type: integer - description: An array of integers with the IDs of the deals for which the attached products will be returned. A maximum of 100 deal IDs can be provided. - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `deal_id`, `add_time`, `update_time`.' - schema: - type: string - default: id - enum: - - id - - deal_id - - add_time - - update_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - responses: - '200': - description: List of products attached to deals - content: - application/json: - schema: - title: GetDealsProductsResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: Array containing data for all products attached to deals - items: - allOf: - - type: object - properties: - id: - type: integer - description: The ID of the deal-product (the ID of the product attached to the deal) - sum: - type: number - description: The sum of all the products attached to the deal - tax: - type: number - description: The product tax - deal_id: - type: integer - description: The ID of the deal - name: - type: string - description: The product name - product_id: - type: integer - description: The ID of the product - product_variation_id: - type: integer - nullable: true - description: The ID of the product variation - add_time: - type: string - description: The date and time when the product was added to the deal - update_time: - type: string - description: The date and time when the deal product was last updated - comments: - type: string - description: The comments of the product - currency: - type: string - description: The currency associated with the deal product - discount: - type: number - default: 0 - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - discount_type: - type: string - enum: - - percentage - - amount - default: percentage - description: The type of the discount's value - quantity: - type: integer - description: The quantity of the product - item_price: - type: number - description: The price value of the product - tax_method: - type: string - enum: - - exclusive - - inclusive - - none - description: 'The tax option to be applied to the products. When using `inclusive`, the tax percentage will already be included in the price. When using `exclusive`, the tax will not be included in the price. When using `none`, no tax will be added. Use the `tax` field for defining the tax percentage amount. By default, the user setting value for tax options will be used. Changing this in one product affects the rest of the products attached to the deal' - is_enabled: - type: boolean - description: Whether this product is enabled for the deal - default: true - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - To set `billing_frequency` different than `one-time`, the deal must not have installments associated - - A deal can have up to 20 products attached with `billing_frequency` different than `one-time` - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - properties: - billing_start_date: - default: null - type: string - format: YYYY-MM-DD - nullable: true - description: | - Only available in Advanced and above plans - - The billing start date. Must be between 10 years in the past and 10 years in the future - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 3 - sum: 90 - tax: 0 - deal_id: 1 - name: Mechanical Pencil - product_id: 1 - product_variation_id: null - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - comments: '' - currency: USD - discount: 0 - quantity: 1 - item_price: 90 - tax_method: inclusive - discount_type: percentage - is_enabled: true - billing_frequency: one-time - billing_frequency_cycles: null - billing_start_date: '2019-12-19' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - /deals/search: - get: - summary: Search deals - description: 'Searches all deals by title, notes and/or custom fields. This endpoint is a wrapper of /v1/itemSearch with a narrower OAuth scope. Found deals can be filtered by the person ID and the organization ID.' - x-token-cost: 20 - operationId: searchDeals - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if using `exact_match`). Please note that the search term has to be URL encoded. - - in: query - name: fields - schema: - type: string - enum: - - custom_fields - - notes - - title - description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them. Only the following custom field types are searchable: `address`, `varchar`, `text`, `varchar_auto`, `double`, `monetary` and `phone`. Read more about searching by custom fields here.' - - in: query - name: exact_match - schema: - type: boolean - description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.' - - in: query - name: person_id - schema: - type: integer - description: Will filter deals by the provided person ID. The upper limit of found deals associated with the person is 2000. - - in: query - name: organization_id - schema: - type: integer - description: Will filter deals by the provided organization ID. The upper limit of found deals associated with the organization is 2000. - - in: query - name: status - schema: - type: string - enum: - - open - - won - - lost - description: 'Will filter deals by the provided specific status. open = Open, won = Won, lost = Lost. The upper limit of found deals associated with the status is 2000.' - - in: query - name: include_fields - schema: - type: string - enum: - - deal.cc_email - description: Supports including optional fields in the results which are not provided by default - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetDealSearchResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - properties: - items: - type: array - description: The array of deals - items: - type: object - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - properties: - id: - type: integer - description: The ID of the deal - type: - type: string - description: The type of the item - title: - type: string - description: The title of the deal - value: - type: integer - description: The value of the deal - currency: - type: string - description: The currency of the deal - status: - type: string - description: The status of the deal - visible_to: - type: integer - description: The visibility of the deal - owner: - type: object - properties: - id: - type: integer - description: The ID of the owner of the deal - stage: - type: object - properties: - id: - type: integer - description: The ID of the stage of the deal - name: - type: string - description: The name of the stage of the deal - person: - type: object - nullable: true - properties: - id: - type: integer - description: The ID of the person the deal is associated with - name: - type: string - description: The name of the person the deal is associated with - organization: - type: object - nullable: true - properties: - id: - type: integer - description: The ID of the organization the deal is associated with - name: - type: string - description: The name of the organization the deal is associated with - custom_fields: - type: array - items: - type: string - description: Custom fields - notes: - type: array - description: An array of notes - items: - type: string - is_archived: - type: boolean - description: A flag indicating whether the deal is archived or not - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - items: - - result_score: 1.22 - item: - id: 1 - type: deal - title: Jane Doe deal - value: 100 - currency: USD - status: open - visible_to: 3 - owner: - id: 1 - stage: - id: 1 - name: Lead In - person: - id: 1 - name: Jane Doe - organization: null - custom_fields: [] - notes: [] - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/deals/{id}/products': - get: - summary: List products attached to a deal - description: Lists products attached to a deal. - x-token-cost: 10 - operationId: getDealProducts - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `add_time`, `update_time`.' - schema: - default: id - type: string - enum: - - id - - add_time - - update_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - default: asc - type: string - enum: - - asc - - desc - responses: - '200': - description: List of products attached to deals - content: - application/json: - schema: - title: GetDealsProductsResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: Array containing data for all products attached to deals - items: - allOf: - - type: object - properties: - id: - type: integer - description: The ID of the deal-product (the ID of the product attached to the deal) - sum: - type: number - description: The sum of all the products attached to the deal - tax: - type: number - description: The product tax - deal_id: - type: integer - description: The ID of the deal - name: - type: string - description: The product name - product_id: - type: integer - description: The ID of the product - product_variation_id: - type: integer - nullable: true - description: The ID of the product variation - add_time: - type: string - description: The date and time when the product was added to the deal - update_time: - type: string - description: The date and time when the deal product was last updated - comments: - type: string - description: The comments of the product - currency: - type: string - description: The currency associated with the deal product - discount: - type: number - default: 0 - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - discount_type: - type: string - enum: - - percentage - - amount - default: percentage - description: The type of the discount's value - quantity: - type: integer - description: The quantity of the product - item_price: - type: number - description: The price value of the product - tax_method: - type: string - enum: - - exclusive - - inclusive - - none - description: 'The tax option to be applied to the products. When using `inclusive`, the tax percentage will already be included in the price. When using `exclusive`, the tax will not be included in the price. When using `none`, no tax will be added. Use the `tax` field for defining the tax percentage amount. By default, the user setting value for tax options will be used. Changing this in one product affects the rest of the products attached to the deal' - is_enabled: - type: boolean - description: Whether this product is enabled for the deal - default: true - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - To set `billing_frequency` different than `one-time`, the deal must not have installments associated - - A deal can have up to 20 products attached with `billing_frequency` different than `one-time` - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - properties: - billing_start_date: - default: null - type: string - format: YYYY-MM-DD - nullable: true - description: | - Only available in Advanced and above plans - - The billing start date. Must be between 10 years in the past and 10 years in the future - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 3 - sum: 90 - tax: 0 - deal_id: 1 - name: Mechanical Pencil - product_id: 1 - product_variation_id: null - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - comments: '' - currency: USD - discount: 0 - quantity: 1 - item_price: 90 - tax_method: inclusive - discount_type: percentage - is_enabled: true - billing_frequency: one-time - billing_frequency_cycles: null - billing_start_date: '2019-12-19' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a product to a deal - description: 'Adds a product to a deal, creating a new item called a deal-product.' - x-token-cost: 5 - operationId: addDealProduct - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'products:full' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: addDealProductRequest - required: - - item_price - - quantity - - product_id - allOf: - - required: - - product_id - - item_price - - quantity - title: dealProductRequestBody - type: object - properties: - product_id: - type: integer - description: The ID of the product - item_price: - type: number - description: The price value of the product - quantity: - type: number - description: The quantity of the product - tax: - type: number - default: 0 - description: The product tax - comments: - type: string - description: The comments of the product - discount: - type: number - default: 0 - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - is_enabled: - type: boolean - description: | - Whether this product is enabled for the deal - - Not possible to disable the product if the deal has installments associated and the product is the last one enabled - - Not possible to enable the product if the deal has installments associated and the product is recurring - default: true - tax_method: - type: string - enum: - - exclusive - - inclusive - - none - description: 'The tax option to be applied to the products. When using `inclusive`, the tax percentage will already be included in the price. When using `exclusive`, the tax will not be included in the price. When using `none`, no tax will be added. Use the `tax` field for defining the tax percentage amount. By default, the user setting value for tax options will be used. Changing this in one product affects the rest of the products attached to the deal' - discount_type: - type: string - enum: - - percentage - - amount - default: percentage - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - product_variation_id: - type: integer - nullable: true - description: The ID of the product variation - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - To set `billing_frequency` different than `one-time`, the deal must not have installments associated - - A deal can have up to 20 products attached with `billing_frequency` different than `one-time` - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - properties: - billing_start_date: - default: null - type: string - format: YYYY-MM-DD - nullable: true - description: | - Only available in Advanced and above plans - - The billing start date. Must be between 10 years in the past and 10 years in the future - responses: - '201': - description: Add a product to the deal - content: - application/json: - schema: - title: AddDealProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - allOf: - - type: object - properties: - id: - type: integer - description: The ID of the deal-product (the ID of the product attached to the deal) - sum: - type: number - description: The sum of all the products attached to the deal - tax: - type: number - description: The product tax - deal_id: - type: integer - description: The ID of the deal - name: - type: string - description: The product name - product_id: - type: integer - description: The ID of the product - product_variation_id: - type: integer - nullable: true - description: The ID of the product variation - add_time: - type: string - description: The date and time when the product was added to the deal - update_time: - type: string - description: The date and time when the deal product was last updated - comments: - type: string - description: The comments of the product - currency: - type: string - description: The currency associated with the deal product - discount: - type: number - default: 0 - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - discount_type: - type: string - enum: - - percentage - - amount - default: percentage - description: The type of the discount's value - quantity: - type: integer - description: The quantity of the product - item_price: - type: number - description: The price value of the product - tax_method: - type: string - enum: - - exclusive - - inclusive - - none - description: 'The tax option to be applied to the products. When using `inclusive`, the tax percentage will already be included in the price. When using `exclusive`, the tax will not be included in the price. When using `none`, no tax will be added. Use the `tax` field for defining the tax percentage amount. By default, the user setting value for tax options will be used. Changing this in one product affects the rest of the products attached to the deal' - is_enabled: - type: boolean - description: Whether this product is enabled for the deal - default: true - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - To set `billing_frequency` different than `one-time`, the deal must not have installments associated - - A deal can have up to 20 products attached with `billing_frequency` different than `one-time` - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - properties: - billing_start_date: - default: null - type: string - format: YYYY-MM-DD - nullable: true - description: | - Only available in Advanced and above plans - - The billing start date. Must be between 10 years in the past and 10 years in the future - example: - success: true - data: - id: 3 - sum: 90 - tax: 0 - deal_id: 1 - name: Mechanical Pencil - product_id: 1 - product_variation_id: null - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - comments: '' - currency: USD - discount: 0 - quantity: 1 - item_price: 90 - tax_method: inclusive - discount_type: percentage - is_enabled: true - billing_frequency: one-time - billing_frequency_cycles: null - billing_start_date: '2019-12-19' - '/deals/{id}/products/{product_attachment_id}': - patch: - summary: Update the product attached to a deal - description: Updates the details of the product that has been attached to a deal. - x-token-cost: 5 - operationId: updateDealProduct - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'products:full' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: product_attachment_id - required: true - schema: - type: integer - description: The ID of the deal-product (the ID of the product attached to the deal) - requestBody: - content: - application/json: - schema: - title: updateDealProductRequest - allOf: - - title: dealProductRequestBody - type: object - properties: - product_id: - type: integer - description: The ID of the product - item_price: - type: number - description: The price value of the product - quantity: - type: number - description: The quantity of the product - tax: - type: number - default: 0 - description: The product tax - comments: - type: string - description: The comments of the product - discount: - type: number - default: 0 - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - is_enabled: - type: boolean - description: | - Whether this product is enabled for the deal - - Not possible to disable the product if the deal has installments associated and the product is the last one enabled - - Not possible to enable the product if the deal has installments associated and the product is recurring - default: true - tax_method: - type: string - enum: - - exclusive - - inclusive - - none - description: 'The tax option to be applied to the products. When using `inclusive`, the tax percentage will already be included in the price. When using `exclusive`, the tax will not be included in the price. When using `none`, no tax will be added. Use the `tax` field for defining the tax percentage amount. By default, the user setting value for tax options will be used. Changing this in one product affects the rest of the products attached to the deal' - discount_type: - type: string - enum: - - percentage - - amount - default: percentage - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - product_variation_id: - type: integer - nullable: true - description: The ID of the product variation - - type: object - properties: - billing_frequency: - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - To set `billing_frequency` different than `one-time`, the deal must not have installments associated - - A deal can have up to 20 products attached with `billing_frequency` different than `one-time` - - type: object - properties: - billing_frequency_cycles: - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - properties: - billing_start_date: - type: string - format: YYYY-MM-DD - nullable: true - description: | - Only available in Advanced and above plans - - The billing start date. Must be between 10 years in the past and 10 years in the future - responses: - '200': - description: Add a product to the deal - content: - application/json: - schema: - title: AddDealProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - allOf: - - type: object - properties: - id: - type: integer - description: The ID of the deal-product (the ID of the product attached to the deal) - sum: - type: number - description: The sum of all the products attached to the deal - tax: - type: number - description: The product tax - deal_id: - type: integer - description: The ID of the deal - name: - type: string - description: The product name - product_id: - type: integer - description: The ID of the product - product_variation_id: - type: integer - nullable: true - description: The ID of the product variation - add_time: - type: string - description: The date and time when the product was added to the deal - update_time: - type: string - description: The date and time when the deal product was last updated - comments: - type: string - description: The comments of the product - currency: - type: string - description: The currency associated with the deal product - discount: - type: number - default: 0 - description: The value of the discount. The `discount_type` field can be used to specify whether the value is an amount or a percentage - discount_type: - type: string - enum: - - percentage - - amount - default: percentage - description: The type of the discount's value - quantity: - type: integer - description: The quantity of the product - item_price: - type: number - description: The price value of the product - tax_method: - type: string - enum: - - exclusive - - inclusive - - none - description: 'The tax option to be applied to the products. When using `inclusive`, the tax percentage will already be included in the price. When using `exclusive`, the tax will not be included in the price. When using `none`, no tax will be added. Use the `tax` field for defining the tax percentage amount. By default, the user setting value for tax options will be used. Changing this in one product affects the rest of the products attached to the deal' - is_enabled: - type: boolean - description: Whether this product is enabled for the deal - default: true - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - To set `billing_frequency` different than `one-time`, the deal must not have installments associated - - A deal can have up to 20 products attached with `billing_frequency` different than `one-time` - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - properties: - billing_start_date: - default: null - type: string - format: YYYY-MM-DD - nullable: true - description: | - Only available in Advanced and above plans - - The billing start date. Must be between 10 years in the past and 10 years in the future - example: - success: true - data: - id: 3 - sum: 90 - tax: 0 - deal_id: 1 - name: Mechanical Pencil - product_id: 1 - product_variation_id: null - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - comments: '' - currency: USD - discount: 0 - quantity: 1 - item_price: 90 - tax_method: inclusive - discount_type: percentage - is_enabled: true - billing_frequency: one-time - billing_frequency_cycles: null - billing_start_date: '2019-12-19' - delete: - summary: Delete an attached product from a deal - description: 'Deletes a product attachment from a deal, using the `product_attachment_id`.' - x-token-cost: 3 - operationId: deleteDealProduct - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:full' - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: product_attachment_id - required: true - schema: - type: integer - description: The product attachment ID - responses: - '200': - description: Delete an attached product from a deal - content: - application/json: - schema: - title: DeleteDealProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: The ID of an attached product that was deleted from the deal - example: - success: true - data: - id: 123 - '/deals/{id}/discounts': - get: - summary: List discounts added to a deal - description: Lists discounts attached to a deal. - x-token-cost: 10 - operationId: getAdditionalDiscounts - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - responses: - '200': - description: List of discounts added to deal - content: - application/json: - schema: - title: GetAdditionalDiscountsResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: Array containing data for all discounts added to a deal - items: - type: object - properties: - id: - type: string - description: The ID of the additional discount - type: - type: string - enum: - - percentage - - amount - description: Determines whether the discount is applied as a percentage or a fixed amount. - amount: - type: number - description: The discount amount. - description: - type: string - description: The name of the discount. - deal_id: - type: integer - description: The ID of the deal the discount was added to. - created_at: - type: string - description: The date and time of when the discount was created in the ISO 8601 format. - created_by: - type: integer - description: The ID of the user that created the discount. - updated_at: - type: string - description: The date and time of when the discount was created in the ISO 8601 format. - updated_by: - type: integer - description: The ID of the user that last updated the discount. - example: - success: true - data: - - id: 30195b0e-7577-4f52-a5cf-f3ee39b9d1e0 - description: 10% - amount: 10 - type: percentage - deal_id: 1 - created_at: '2024-03-12T10:30:05Z' - created_by: 1 - updated_at: '2024-03-12T10:30:05Z' - updated_by: 1 - post: - summary: Add a discount to a deal - description: 'Adds a discount to a deal changing, the deal value if the deal has one-time products attached.' - x-token-cost: 5 - operationId: postAdditionalDiscount - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: AddAdditionalDiscountRequestBody - required: - - description - - amount - - type - properties: - description: - type: string - description: The name of the discount. - amount: - type: number - description: The discount amount. Must be a positive number (excluding 0). - type: - type: string - enum: - - percentage - - amount - description: Determines whether the discount is applied as a percentage or a fixed amount. - responses: - '201': - description: Discount added to deal - content: - application/json: - schema: - title: AddAdditionalDiscountResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: string - description: The ID of the additional discount - type: - type: string - enum: - - percentage - - amount - description: Determines whether the discount is applied as a percentage or a fixed amount. - amount: - type: number - description: The discount amount. - description: - type: string - description: The name of the discount. - deal_id: - type: integer - description: The ID of the deal the discount was added to. - created_at: - type: string - description: The date and time of when the discount was created in the ISO 8601 format. - created_by: - type: integer - description: The ID of the user that created the discount. - updated_at: - type: string - description: The date and time of when the discount was created in the ISO 8601 format. - updated_by: - type: integer - description: The ID of the user that last updated the discount. - example: - success: true - data: - id: 30195b0e-7577-4f52-a5cf-f3ee39b9d1e0 - description: 10% - amount: 10 - type: percentage - deal_id: 1 - created_at: '2024-03-12T10:30:05Z' - created_by: 1 - updated_at: '2024-03-12T10:30:05Z' - updated_by: 1 - '/deals/{id}/discounts/{discount_id}': - patch: - summary: Update a discount added to a deal - description: 'Edits a discount added to a deal, changing the deal value if the deal has one-time products attached.' - x-token-cost: 5 - operationId: updateAdditionalDiscount - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: discount_id - required: true - schema: - type: integer - description: The ID of the discount - requestBody: - content: - application/json: - schema: - title: updateAdditionalDiscountRequestBody - properties: - description: - type: string - description: The name of the discount. - amount: - type: number - description: The discount amount. Must be a positive number (excluding 0). - type: - type: string - enum: - - percentage - - amount - description: Determines whether the discount is applied as a percentage or a fixed amount. - responses: - '200': - description: Edited discount. - content: - application/json: - schema: - title: UpdateAdditionalDiscountResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: string - description: The ID of the additional discount - type: - type: string - enum: - - percentage - - amount - description: Determines whether the discount is applied as a percentage or a fixed amount. - amount: - type: number - description: The discount amount. - description: - type: string - description: The name of the discount. - deal_id: - type: integer - description: The ID of the deal the discount was added to. - created_at: - type: string - description: The date and time of when the discount was created in the ISO 8601 format. - created_by: - type: integer - description: The ID of the user that created the discount. - updated_at: - type: string - description: The date and time of when the discount was created in the ISO 8601 format. - updated_by: - type: integer - description: The ID of the user that last updated the discount. - example: - success: true - data: - id: 30195b0e-7577-4f52-a5cf-f3ee39b9d1e0 - description: 10% - amount: 10 - type: percentage - deal_id: 1 - created_at: '2024-03-12T10:30:05Z' - created_by: 1 - updated_at: '2024-03-12T10:30:05Z' - updated_by: 1 - delete: - summary: Delete a discount from a deal - description: 'Removes a discount from a deal, changing the deal value if the deal has one-time products attached.' - x-token-cost: 3 - operationId: deleteAdditionalDiscount - tags: - - Deals - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: discount_id - required: true - schema: - type: integer - description: The ID of the discount - responses: - '200': - description: The ID of the deleted discount. - content: - application/json: - schema: - title: DeleteAdditionalDiscountResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: The ID of the discount that was deleted from the deal - example: - success: true - data: - id: 123 - /deals/installments: - get: - summary: List installments added to a list of deals - description: | - Lists installments attached to a list of deals. - - Only available in Advanced and above plans. - x-token-cost: 10 - operationId: getInstallments - tags: - - Deals - - Beta - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: query - name: deal_ids - required: true - schema: - type: array - items: - type: integer - description: An array of integers with the IDs of the deals for which the attached installments will be returned. A maximum of 100 deal IDs can be provided. - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `billing_date`, `deal_id`.' - schema: - default: id - type: string - enum: - - id - - billing_date - - deal_id - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - default: asc - type: string - enum: - - asc - - desc - responses: - '200': - description: List installments added to a deal - content: - application/json: - schema: - title: GetInstallmentsResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: Array containing data for all installments added to a deal - items: - type: object - properties: - id: - type: integer - description: The ID of the installment - amount: - type: number - description: The installment amount. - billing_date: - type: string - description: The date which the installment will be charged. - description: - type: string - description: The name of installment. - deal_id: - type: integer - description: The ID of the deal the installment was added to. - example: - success: true - data: - - id: 1 - amount: 10 - billing_date: '2025-03-10' - deal_id: 1 - description: Delivery Fee - '/deals/{id}/installments': - post: - summary: Add an installment to a deal - description: | - Adds an installment to a deal. - - An installment can only be added if the deal includes at least one one-time product. - If the deal contains at least one recurring product, adding installments is not allowed. - - Only available in Advanced and above plans. - x-token-cost: 5 - operationId: postInstallment - tags: - - Deals - - Beta - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: AddInstallmentRequestBody - required: - - description - - amount - - billing_date - properties: - description: - type: string - description: The name of the installment. - amount: - type: number - description: The installment amount. Must be a positive number (excluding 0). - billing_date: - type: string - description: The date which the installment will be charged. Must be in the format YYYY-MM-DD. - responses: - '200': - description: Installment added to deal - content: - application/json: - schema: - title: AddAInstallmentResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: The ID of the installment - amount: - type: number - description: The installment amount. - billing_date: - type: string - description: The date which the installment will be charged. - description: - type: string - description: The name of installment. - deal_id: - type: integer - description: The ID of the deal the installment was added to. - example: - success: true - data: - id: 1 - amount: 10 - billing_date: '2025-03-10' - deal_id: 1 - description: Delivery Fee - '/deals/{id}/installments/{installment_id}': - patch: - summary: Update an installment added to a deal - description: | - Edits an installment added to a deal. - - Only available in Advanced and above plans. - x-token-cost: 5 - operationId: updateInstallment - tags: - - Deals - - Beta - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: installment_id - required: true - schema: - type: integer - description: The ID of the installment - requestBody: - content: - application/json: - schema: - title: UpdateInstallmentRequestBody - properties: - description: - type: string - description: The name of the installment. - amount: - type: number - description: The installment amount. Must be a positive number (excluding 0). - billing_date: - type: string - description: The date which the installment will be charged. Must be in the format YYYY-MM-DD. - responses: - '200': - description: Edited installment. - content: - application/json: - schema: - title: UpdateInstallmentResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: The ID of the installment - amount: - type: number - description: The installment amount. - billing_date: - type: string - description: The date which the installment will be charged. - description: - type: string - description: The name of installment. - deal_id: - type: integer - description: The ID of the deal the installment was added to. - example: - success: true - data: - id: 1 - amount: 10 - billing_date: '2025-03-10' - deal_id: 1 - description: Delivery Fee - delete: - summary: Delete an installment from a deal - description: | - Removes an installment from a deal. - - Only available in Advanced and above plans. - x-token-cost: 3 - operationId: deleteInstallment - tags: - - Deals - - Beta - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - parameters: - - in: path - name: id - description: The ID of the deal - required: true - schema: - type: integer - - in: path - name: installment_id - required: true - schema: - type: integer - description: The ID of the installment - responses: - '200': - description: The ID of the deleted installment. - content: - application/json: - schema: - title: DeleteInstallmentResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: The ID of the installment that was deleted from the deal - example: - success: true - data: - id: 1 - '/deals/{id}/convert/lead': - post: - security: - - api_key: [] - - oauth2: - - 'deals:full' - tags: - - Deals - - Beta - summary: Convert a deal to a lead (BETA) - description: 'Initiates a conversion of a deal to a lead. The return value is an ID of a job that was assigned to perform the conversion. Related entities (notes, files, emails, activities, ...) are transferred during the process to the target entity. There are exceptions for entities like invoices or history that are not transferred and remain linked to the original deal. If the conversion is successful, the deal is marked as deleted. To retrieve the created entity ID and the result of the conversion, call the /api/v2/deals/{deal_id}/convert/status/{conversion_id} endpoint.' - operationId: convertDealToLead - x-token-cost: 40 - parameters: - - in: path - name: id - required: true - schema: - type: integer - description: The ID of the deal to convert - responses: - '200': - description: Successful response containing payload in the `data` field - content: - application/json: - schema: - title: AddConvertDealToLeadResponse - type: object - properties: - success: - type: boolean - data: - type: object - description: An object containing conversion job id that performs the conversion - required: - - conversion_id - properties: - conversion_id: - description: The ID of the conversion job that can be used to retrieve conversion status and details. The ID has UUID format. - type: string - format: uuid - additional_data: - type: object - nullable: true - example: null - example: - success: true - data: - conversion_id: 4b40248b-945a-4802-b996-60fdff8c5c69 - additional_data: null - '404': - description: A resource describing an error - content: - application/json: - schema: - type: object - title: GetConvertResponse - properties: - success: - type: boolean - example: false - error: - type: string - description: The description of the error - error_info: - type: string - description: A message describing how to solve the problem - data: - type: object - nullable: true - example: null - additional_data: - type: object - nullable: true - example: null - example: - success: false - error: Entity was not found - error_info: Object was not found. - data: null - additional_data: null - '/deals/{id}/convert/status/{conversion_id}': - get: - security: - - api_key: [] - - oauth2: - - 'deals:full' - - 'deals:read' - tags: - - Deals - - Beta - summary: Get Deal conversion status (BETA) - description: 'Returns information about the conversion. Status is always present and its value (not_started, running, completed, failed, rejected) represents the current state of the conversion. Lead ID is only present if the conversion was successfully finished. This data is only temporary and removed after a few days.' - operationId: getDealConversionStatus - x-token-cost: 1 - parameters: - - in: path - name: id - required: true - schema: - type: integer - description: The ID of a deal - - in: path - name: conversion_id - required: true - schema: - type: string - format: uuid - description: The ID of the conversion - responses: - '200': - description: Successful response containing payload in the `data` field - content: - application/json: - example: - success: true - data: - lead_id: 9f3e6e50-9d99-11ee-9538-29c81a92c0d1 - conversion_id: 4b40248b-945a-4802-b996-60fdff8c5c69 - status: completed - additional_data: null - schema: - title: GetConvertResponse - type: object - required: - - success - - data - properties: - success: - type: boolean - data: - type: object - description: An object containing conversion status. After successful conversion the converted entity ID is also present. - required: - - conversion_id - - status - properties: - lead_id: - description: The ID of the new lead. - type: string - format: uuid - deal_id: - description: The ID of the new deal. - type: integer - conversion_id: - description: The ID of the conversion job. The ID can be used to retrieve conversion status and details. The ID has UUID format. - type: string - format: uuid - status: - description: Status of the conversion job. - type: string - enum: - - not_started - - running - - completed - - failed - - rejected - additional_data: - type: object - nullable: true - example: null - '404': - description: A resource describing an error - content: - application/json: - schema: - type: object - title: GetConvertResponse - properties: - success: - type: boolean - example: false - error: - type: string - description: The description of the error - error_info: - type: string - description: A message describing how to solve the problem - data: - type: object - nullable: true - example: null - additional_data: - type: object - nullable: true - example: null - example: - success: false - error: Entity was not found - error_info: Object was not found. - data: null - additional_data: null - /persons: - get: - summary: Get all persons - description: 'Returns data about all persons. Fields `ims`, `postal_address`, `notes`, `birthday`, and `job_title` are only included if contact sync is enabled for the company.' - x-token-cost: 10 - operationId: getPersons - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: query - name: filter_id - schema: - type: integer - description: 'If supplied, only persons matching the specified filter are returned' - - in: query - name: ids - description: 'Optional comma separated string array of up to 100 entity ids to fetch. If filter_id is provided, this is ignored. If any of the requested entities do not exist or are not visible, they are not included in the response.' - schema: - type: string - - in: query - name: owner_id - schema: - type: integer - description: 'If supplied, only persons owned by the specified user are returned. If filter_id is provided, this is ignored.' - - in: query - name: org_id - schema: - type: integer - description: 'If supplied, only persons linked to the specified organization are returned. If filter_id is provided, this is ignored.' - - in: query - name: updated_since - schema: - type: string - description: 'If set, only persons with an `update_time` later than or equal to this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: updated_until - schema: - type: string - description: 'If set, only persons with an `update_time` earlier than this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include. `marketing_status` and `doi_status` can only be included if the company has marketing app enabled. - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - open_deals_count - - related_open_deals_count - - closed_deals_count - - related_closed_deals_count - - participant_open_deals_count - - participant_closed_deals_count - - email_messages_count - - activities_count - - done_activities_count - - undone_activities_count - - files_count - - notes_count - - followers_count - - won_deals_count - - related_won_deals_count - - lost_deals_count - - related_lost_deals_count - - last_incoming_mail_time - - last_outgoing_mail_time - - marketing_status - - doi_status - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all persons - content: - application/json: - schema: - type: object - title: GetPersonsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Persons array - items: - type: object - properties: - id: - type: integer - description: The ID of the person - name: - type: string - description: The name of the person - first_name: - type: string - description: The first name of the person - last_name: - type: string - description: The last name of the person - owner_id: - type: integer - description: The ID of the user who owns the person - org_id: - type: integer - description: The ID of the organization linked to the person - add_time: - type: string - description: The creation date and time of the person - update_time: - type: string - description: The last updated date and time of the person - emails: - type: array - description: The emails of the person - items: - type: object - properties: - value: - type: string - description: The email address of the person - primary: - type: boolean - description: Whether the email is primary or not - label: - type: string - description: The email address classification label - phones: - type: array - description: The phones of the person - items: - type: object - properties: - value: - type: string - description: The phone number of the person - primary: - type: boolean - description: Whether the phone number is primary or not - label: - type: string - description: The phone number classification label - is_deleted: - type: boolean - description: Whether the person is deleted or not - visible_to: - type: integer - description: The visibility of the person - label_ids: - type: array - description: The IDs of labels assigned to the person - items: - type: integer - picture_id: - type: integer - description: The ID of the picture associated with the person - postal_address: - type: object - description: 'Postal address of the person, included if contact sync is enabled for the company' - properties: - value: - type: string - description: The full address of the person - country: - type: string - description: Country of the person - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the person - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the person - locality: - type: string - description: Locality (e.g. city) of the person - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the person - route: - type: string - description: Route (e.g. street) of the person - street_number: - type: string - description: Street number of the person - postal_code: - type: string - description: Postal code of the person - notes: - type: string - description: 'Contact sync notes of the person, maximum 10 000 characters, included if contact sync is enabled for the company' - im: - type: array - description: 'The instant messaging accounts of the person, included if contact sync is enabled for the company' - items: - type: object - properties: - value: - type: string - description: The instant messaging account of the person - primary: - type: boolean - description: Whether the instant messaging account is primary or not - label: - type: string - description: The instant messaging account classification label - birthday: - type: string - description: 'The birthday of the person, included if contact sync is enabled for the company' - job_title: - type: string - description: 'The job title of the person, included if contact sync is enabled for the company' - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - name: Person Name - first_name: Person - last_name: Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - emails: - - value: email1@email.com - primary: true - label: work - - value: email2@email.com - primary: false - label: home - phones: - - value: '12345' - primary: true - label: work - - value: '54321' - primary: false - label: home - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - picture_id: 1 - custom_fields: {} - notes: Notes from contact sync - im: - - value: skypeusername - primary: true - label: skype - - value: whatsappusername - primary: false - label: whatsapp - birthday: '2000-12-31' - job_title: Manager - postal_address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a new person - description: 'Adds a new person. If the company uses the [Campaigns product](https://pipedrive.readme.io/docs/campaigns-in-pipedrive-api), then this endpoint will also accept and return the `marketing_status` field.' - x-token-cost: 5 - operationId: addPerson - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:full' - requestBody: - content: - application/json: - schema: - required: - - title - type: object - properties: - name: - type: string - description: The name of the person - owner_id: - type: integer - description: The ID of the user who owns the person - org_id: - type: integer - description: The ID of the organization linked to the person - add_time: - type: string - description: The creation date and time of the person - update_time: - type: string - description: The last updated date and time of the person - emails: - type: array - description: The emails of the person - items: - type: object - properties: - value: - type: string - description: The email address of the person - primary: - type: boolean - description: Whether the email is primary or not - label: - type: boolean - description: The email address classification label - phones: - type: array - description: The phones of the person - items: - type: object - properties: - value: - type: string - description: The phone number of the person - primary: - type: boolean - description: Whether the phone number is primary or not - label: - type: boolean - description: The phone number classification label - visible_to: - type: integer - description: The visibility of the person - label_ids: - type: array - description: The IDs of labels assigned to the person - items: - type: integer - marketing_status: - type: string - description: 'If the person does not have a valid email address, then the marketing status is **not set** and `no_consent` is returned for the `marketing_status` value when the new person is created. If the change is forbidden, the status will remain unchanged for every call that tries to modify the marketing status. Please be aware that it is only allowed **once** to change the marketing status from an old status to a new one.
ValueDescription
`no_consent`The customer has not given consent to receive any marketing communications
`unsubscribed`The customers have unsubscribed from ALL marketing communications
`subscribed`The customers are subscribed and are counted towards marketing caps
`archived`The customers with `subscribed` status can be moved to `archived` to save consent, but they are not paid for
' - enum: - - no_consent - - unsubscribed - - subscribed - - archived - responses: - '200': - description: Add person - content: - application/json: - schema: - type: object - title: UpsertPersonResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertPersonResponseData - properties: - data: - type: object - properties: - id: - type: integer - description: The ID of the person - name: - type: string - description: The name of the person - first_name: - type: string - description: The first name of the person - last_name: - type: string - description: The last name of the person - owner_id: - type: integer - description: The ID of the user who owns the person - org_id: - type: integer - description: The ID of the organization linked to the person - add_time: - type: string - description: The creation date and time of the person - update_time: - type: string - description: The last updated date and time of the person - emails: - type: array - description: The emails of the person - items: - type: object - properties: - value: - type: string - description: The email address of the person - primary: - type: boolean - description: Whether the email is primary or not - label: - type: string - description: The email address classification label - phones: - type: array - description: The phones of the person - items: - type: object - properties: - value: - type: string - description: The phone number of the person - primary: - type: boolean - description: Whether the phone number is primary or not - label: - type: string - description: The phone number classification label - is_deleted: - type: boolean - description: Whether the person is deleted or not - visible_to: - type: integer - description: The visibility of the person - label_ids: - type: array - description: The IDs of labels assigned to the person - items: - type: integer - picture_id: - type: integer - description: The ID of the picture associated with the person - postal_address: - type: object - description: 'Postal address of the person, included if contact sync is enabled for the company' - properties: - value: - type: string - description: The full address of the person - country: - type: string - description: Country of the person - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the person - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the person - locality: - type: string - description: Locality (e.g. city) of the person - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the person - route: - type: string - description: Route (e.g. street) of the person - street_number: - type: string - description: Street number of the person - postal_code: - type: string - description: Postal code of the person - notes: - type: string - description: 'Contact sync notes of the person, maximum 10 000 characters, included if contact sync is enabled for the company' - im: - type: array - description: 'The instant messaging accounts of the person, included if contact sync is enabled for the company' - items: - type: object - properties: - value: - type: string - description: The instant messaging account of the person - primary: - type: boolean - description: Whether the instant messaging account is primary or not - label: - type: string - description: The instant messaging account classification label - birthday: - type: string - description: 'The birthday of the person, included if contact sync is enabled for the company' - job_title: - type: string - description: 'The job title of the person, included if contact sync is enabled for the company' - description: The person object - example: - success: true - data: - id: 1 - name: Person Name - first_name: Person - last_name: Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - emails: - - value: email1@email.com - primary: true - label: work - - value: email2@email.com - primary: false - label: home - phones: - - value: '12345' - primary: true - label: work - - value: '54321' - primary: false - label: home - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - picture_id: 1 - custom_fields: {} - notes: Notes from contact sync - im: - - value: skypeusername - primary: true - label: skype - - value: whatsappusername - primary: false - label: whatsapp - birthday: '2000-12-31' - job_title: Manager - postal_address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - '/persons/{id}': - delete: - summary: Delete a person - description: 'Marks a person as deleted. After 30 days, the person will be permanently deleted.' - x-token-cost: 3 - operationId: deletePerson - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - responses: - '200': - description: Delete person - content: - application/json: - schema: - title: DeletePersonResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: Deleted person ID - example: - success: true - data: - id: 1 - get: - summary: Get details of a person - description: 'Returns the details of a specific person. Fields `ims`, `postal_address`, `notes`, `birthday`, and `job_title` are only included if contact sync is enabled for the company.' - x-token-cost: 1 - operationId: getPerson - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include. `marketing_status` and `doi_status` can only be included if the company has marketing app enabled. - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - open_deals_count - - related_open_deals_count - - closed_deals_count - - related_closed_deals_count - - participant_open_deals_count - - participant_closed_deals_count - - email_messages_count - - activities_count - - done_activities_count - - undone_activities_count - - files_count - - notes_count - - followers_count - - won_deals_count - - related_won_deals_count - - lost_deals_count - - related_lost_deals_count - - last_incoming_mail_time - - last_outgoing_mail_time - - marketing_status - - doi_status - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - responses: - '200': - description: Get person - content: - application/json: - schema: - type: object - title: UpsertPersonResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertPersonResponseData - properties: - data: - type: object - properties: - id: - type: integer - description: The ID of the person - name: - type: string - description: The name of the person - first_name: - type: string - description: The first name of the person - last_name: - type: string - description: The last name of the person - owner_id: - type: integer - description: The ID of the user who owns the person - org_id: - type: integer - description: The ID of the organization linked to the person - add_time: - type: string - description: The creation date and time of the person - update_time: - type: string - description: The last updated date and time of the person - emails: - type: array - description: The emails of the person - items: - type: object - properties: - value: - type: string - description: The email address of the person - primary: - type: boolean - description: Whether the email is primary or not - label: - type: string - description: The email address classification label - phones: - type: array - description: The phones of the person - items: - type: object - properties: - value: - type: string - description: The phone number of the person - primary: - type: boolean - description: Whether the phone number is primary or not - label: - type: string - description: The phone number classification label - is_deleted: - type: boolean - description: Whether the person is deleted or not - visible_to: - type: integer - description: The visibility of the person - label_ids: - type: array - description: The IDs of labels assigned to the person - items: - type: integer - picture_id: - type: integer - description: The ID of the picture associated with the person - postal_address: - type: object - description: 'Postal address of the person, included if contact sync is enabled for the company' - properties: - value: - type: string - description: The full address of the person - country: - type: string - description: Country of the person - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the person - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the person - locality: - type: string - description: Locality (e.g. city) of the person - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the person - route: - type: string - description: Route (e.g. street) of the person - street_number: - type: string - description: Street number of the person - postal_code: - type: string - description: Postal code of the person - notes: - type: string - description: 'Contact sync notes of the person, maximum 10 000 characters, included if contact sync is enabled for the company' - im: - type: array - description: 'The instant messaging accounts of the person, included if contact sync is enabled for the company' - items: - type: object - properties: - value: - type: string - description: The instant messaging account of the person - primary: - type: boolean - description: Whether the instant messaging account is primary or not - label: - type: string - description: The instant messaging account classification label - birthday: - type: string - description: 'The birthday of the person, included if contact sync is enabled for the company' - job_title: - type: string - description: 'The job title of the person, included if contact sync is enabled for the company' - description: The person object - example: - success: true - data: - id: 1 - name: Person Name - first_name: Person - last_name: Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - emails: - - value: email1@email.com - primary: true - label: work - - value: email2@email.com - primary: false - label: home - phones: - - value: '12345' - primary: true - label: work - - value: '54321' - primary: false - label: home - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - picture_id: 1 - custom_fields: {} - notes: Notes from contact sync - im: - - value: skypeusername - primary: true - label: skype - - value: whatsappusername - primary: false - label: whatsapp - birthday: '2000-12-31' - job_title: Manager - postal_address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - patch: - summary: Update a person - description: 'Updates the properties of a person.
If the company uses the [Campaigns product](https://pipedrive.readme.io/docs/campaigns-in-pipedrive-api), then this endpoint will also accept and return the `marketing_status` field.' - x-token-cost: 5 - operationId: updatePerson - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: The name of the person - owner_id: - type: integer - description: The ID of the user who owns the person - org_id: - type: integer - description: The ID of the organization linked to the person - add_time: - type: string - description: The creation date and time of the person - update_time: - type: string - description: The last updated date and time of the person - emails: - type: array - description: The emails of the person - items: - type: object - properties: - value: - type: string - description: The email address of the person - primary: - type: boolean - description: Whether the email is primary or not - label: - type: boolean - description: The email address classification label - phones: - type: array - description: The phones of the person - items: - type: object - properties: - value: - type: string - description: The phone number of the person - primary: - type: boolean - description: Whether the phone number is primary or not - label: - type: boolean - description: The phone number classification label - visible_to: - type: integer - description: The visibility of the person - label_ids: - type: array - description: The IDs of labels assigned to the person - items: - type: integer - marketing_status: - type: string - description: 'If the person does not have a valid email address, then the marketing status is **not set** and `no_consent` is returned for the `marketing_status` value when the new person is created. If the change is forbidden, the status will remain unchanged for every call that tries to modify the marketing status. Please be aware that it is only allowed **once** to change the marketing status from an old status to a new one.
ValueDescription
`no_consent`The customer has not given consent to receive any marketing communications
`unsubscribed`The customers have unsubscribed from ALL marketing communications
`subscribed`The customers are subscribed and are counted towards marketing caps
`archived`The customers with `subscribed` status can be moved to `archived` to save consent, but they are not paid for
' - enum: - - no_consent - - unsubscribed - - subscribed - - archived - responses: - '200': - description: Edit person - content: - application/json: - schema: - type: object - title: UpsertPersonResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertPersonResponseData - properties: - data: - type: object - properties: - id: - type: integer - description: The ID of the person - name: - type: string - description: The name of the person - first_name: - type: string - description: The first name of the person - last_name: - type: string - description: The last name of the person - owner_id: - type: integer - description: The ID of the user who owns the person - org_id: - type: integer - description: The ID of the organization linked to the person - add_time: - type: string - description: The creation date and time of the person - update_time: - type: string - description: The last updated date and time of the person - emails: - type: array - description: The emails of the person - items: - type: object - properties: - value: - type: string - description: The email address of the person - primary: - type: boolean - description: Whether the email is primary or not - label: - type: string - description: The email address classification label - phones: - type: array - description: The phones of the person - items: - type: object - properties: - value: - type: string - description: The phone number of the person - primary: - type: boolean - description: Whether the phone number is primary or not - label: - type: string - description: The phone number classification label - is_deleted: - type: boolean - description: Whether the person is deleted or not - visible_to: - type: integer - description: The visibility of the person - label_ids: - type: array - description: The IDs of labels assigned to the person - items: - type: integer - picture_id: - type: integer - description: The ID of the picture associated with the person - postal_address: - type: object - description: 'Postal address of the person, included if contact sync is enabled for the company' - properties: - value: - type: string - description: The full address of the person - country: - type: string - description: Country of the person - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the person - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the person - locality: - type: string - description: Locality (e.g. city) of the person - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the person - route: - type: string - description: Route (e.g. street) of the person - street_number: - type: string - description: Street number of the person - postal_code: - type: string - description: Postal code of the person - notes: - type: string - description: 'Contact sync notes of the person, maximum 10 000 characters, included if contact sync is enabled for the company' - im: - type: array - description: 'The instant messaging accounts of the person, included if contact sync is enabled for the company' - items: - type: object - properties: - value: - type: string - description: The instant messaging account of the person - primary: - type: boolean - description: Whether the instant messaging account is primary or not - label: - type: string - description: The instant messaging account classification label - birthday: - type: string - description: 'The birthday of the person, included if contact sync is enabled for the company' - job_title: - type: string - description: 'The job title of the person, included if contact sync is enabled for the company' - description: The person object - example: - success: true - data: - id: 1 - name: Person Name - first_name: Person - last_name: Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - emails: - - value: email1@email.com - primary: true - label: work - - value: email2@email.com - primary: false - label: home - phones: - - value: '12345' - primary: true - label: work - - value: '54321' - primary: false - label: home - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - picture_id: 1 - custom_fields: {} - notes: Notes from contact sync - im: - - value: skypeusername - primary: true - label: skype - - value: whatsappusername - primary: false - label: whatsapp - birthday: '2000-12-31' - job_title: Manager - postal_address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - '/persons/{id}/followers': - get: - summary: List followers of a person - description: Lists users who are following the person. - x-token-cost: 10 - operationId: getPersonFollowers - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowersResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Followers array - items: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a follower to a person - description: Adds a user as a follower to the person. - x-token-cost: 5 - operationId: addPersonFollower - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - required: - - user_id - type: object - properties: - user_id: - type: integer - description: The ID of the user to add as a follower - responses: - '201': - description: Add a follower - content: - application/json: - schema: - type: object - title: AddFollowerResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - description: The follower object - example: - success: true - data: - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - '/persons/{id}/followers/changelog': - get: - summary: List followers changelog of a person - description: Lists changelogs about users have followed the person. - x-token-cost: 10 - operationId: getPersonFollowersChangelog - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowerChangelogsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Follower changelogs array - items: - type: object - title: FollowerChangelogItem - properties: - action: - type: string - description: The type of change - actor_user_id: - type: integer - description: The ID of the user who did the change - follower_user_id: - type: integer - description: The ID of the user who was following the entity - time: - type: string - description: The time at which the change happened - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - action: added - actor_user_id: 1 - follower_user_id: 1 - time: '2024-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/persons/{id}/followers/{follower_id}': - delete: - summary: Delete a follower from a person - description: Deletes a user follower from the person. - x-token-cost: 3 - operationId: deletePersonFollower - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the person - required: true - schema: - type: integer - - in: path - name: follower_id - required: true - schema: - type: integer - description: The ID of the following user - responses: - '200': - description: Remove a follower - content: - application/json: - schema: - title: DeleteFollowerResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - user_id: - type: integer - description: Deleted follower user ID - example: - success: true - data: - user_id: 1 - /organizations: - get: - summary: Get all organizations - description: Returns data about all organizations. - x-token-cost: 10 - operationId: getOrganizations - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: query - name: filter_id - schema: - type: integer - description: 'If supplied, only organizations matching the specified filter are returned' - - in: query - name: ids - description: 'Optional comma separated string array of up to 100 entity ids to fetch. If filter_id is provided, this is ignored. If any of the requested entities do not exist or are not visible, they are not included in the response.' - schema: - type: string - - in: query - name: owner_id - schema: - type: integer - description: 'If supplied, only organization owned by the specified user are returned. If filter_id is provided, this is ignored.' - - in: query - name: updated_since - schema: - type: string - description: 'If set, only organizations with an `update_time` later than or equal to this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: updated_until - schema: - type: string - description: 'If set, only organizations with an `update_time` earlier than this time are returned. In RFC3339 format, e.g. 2025-01-01T10:20:00Z.' - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - open_deals_count - - related_open_deals_count - - closed_deals_count - - related_closed_deals_count - - email_messages_count - - people_count - - activities_count - - done_activities_count - - undone_activities_count - - files_count - - notes_count - - followers_count - - won_deals_count - - related_won_deals_count - - lost_deals_count - - related_lost_deals_count - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all organizations - content: - application/json: - schema: - type: object - title: GetOrganizationsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Organizations array - items: - type: object - title: OrganizationItem - properties: - id: - type: integer - description: The ID of the organization - name: - type: string - description: The name of the organization - owner_id: - type: integer - description: The ID of the user who owns the organization - add_time: - type: string - description: The creation date and time of the organization - update_time: - type: string - description: The last updated date and time of the organization - is_deleted: - type: boolean - description: Whether the organization is deleted or not - visible_to: - type: integer - description: The visibility of the organization - address: - type: object - properties: - value: - type: string - description: The full address of the organization - country: - type: string - description: Country of the organization - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the organization - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the organization - locality: - type: string - description: Locality (e.g. city) of the organization - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the organization - route: - type: string - description: Route (e.g. street) of the organization - street_number: - type: string - description: Street number of the organization - postal_code: - type: string - description: Postal code of the organization - label_ids: - type: array - description: The IDs of labels assigned to the organization - items: - type: integer - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - name: Organization Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - custom_fields: {} - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a new organization - description: Adds a new organization. - x-token-cost: 5 - operationId: addOrganization - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:full' - requestBody: - content: - application/json: - schema: - required: - - title - type: object - properties: - name: - type: string - description: The name of the organization - owner_id: - type: integer - description: The ID of the user who owns the organization - add_time: - type: string - description: The creation date and time of the organization - update_time: - type: string - description: The last updated date and time of the organization - visible_to: - type: integer - description: The visibility of the organization - label_ids: - type: array - description: The IDs of labels assigned to the organization - items: - type: integer - responses: - '200': - description: Add organization - content: - application/json: - schema: - type: object - title: UpsertOrganizationResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertOrganizationResponseData - properties: - data: - type: object - title: OrganizationItem - properties: - id: - type: integer - description: The ID of the organization - name: - type: string - description: The name of the organization - owner_id: - type: integer - description: The ID of the user who owns the organization - add_time: - type: string - description: The creation date and time of the organization - update_time: - type: string - description: The last updated date and time of the organization - is_deleted: - type: boolean - description: Whether the organization is deleted or not - visible_to: - type: integer - description: The visibility of the organization - address: - type: object - properties: - value: - type: string - description: The full address of the organization - country: - type: string - description: Country of the organization - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the organization - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the organization - locality: - type: string - description: Locality (e.g. city) of the organization - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the organization - route: - type: string - description: Route (e.g. street) of the organization - street_number: - type: string - description: Street number of the organization - postal_code: - type: string - description: Postal code of the organization - label_ids: - type: array - description: The IDs of labels assigned to the organization - items: - type: integer - description: The organization object - example: - success: true - data: - id: 1 - name: Organization Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - custom_fields: {} - '/organizations/{id}': - delete: - summary: Delete a organization - description: 'Marks a organization as deleted. After 30 days, the organization will be permanently deleted.' - x-token-cost: 3 - operationId: deleteOrganization - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - responses: - '200': - description: Delete organization - content: - application/json: - schema: - title: DeleteOrganizationResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: Deleted organization ID - example: - success: true - data: - id: 1 - get: - summary: Get details of a organization - description: Returns the details of a specific organization. - x-token-cost: 1 - operationId: getOrganization - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - - in: query - name: include_fields - description: Optional comma separated string array of additional fields to include - schema: - type: string - enum: - - next_activity_id - - last_activity_id - - open_deals_count - - related_open_deals_count - - closed_deals_count - - related_closed_deals_count - - email_messages_count - - people_count - - activities_count - - done_activities_count - - undone_activities_count - - files_count - - notes_count - - followers_count - - won_deals_count - - related_won_deals_count - - lost_deals_count - - related_lost_deals_count - - in: query - name: custom_fields - description: 'Optional comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for faster results and smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - responses: - '200': - description: Get organization - content: - application/json: - schema: - type: object - title: UpsertOrganizationResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertOrganizationResponseData - properties: - data: - type: object - title: OrganizationItem - properties: - id: - type: integer - description: The ID of the organization - name: - type: string - description: The name of the organization - owner_id: - type: integer - description: The ID of the user who owns the organization - add_time: - type: string - description: The creation date and time of the organization - update_time: - type: string - description: The last updated date and time of the organization - is_deleted: - type: boolean - description: Whether the organization is deleted or not - visible_to: - type: integer - description: The visibility of the organization - address: - type: object - properties: - value: - type: string - description: The full address of the organization - country: - type: string - description: Country of the organization - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the organization - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the organization - locality: - type: string - description: Locality (e.g. city) of the organization - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the organization - route: - type: string - description: Route (e.g. street) of the organization - street_number: - type: string - description: Street number of the organization - postal_code: - type: string - description: Postal code of the organization - label_ids: - type: array - description: The IDs of labels assigned to the organization - items: - type: integer - description: The organization object - example: - success: true - data: - id: 1 - name: Organization Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - custom_fields: {} - patch: - summary: Update a organization - description: Updates the properties of a organization. - x-token-cost: 5 - operationId: updateOrganization - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: The name of the organization - owner_id: - type: integer - description: The ID of the user who owns the organization - add_time: - type: string - description: The creation date and time of the organization - update_time: - type: string - description: The last updated date and time of the organization - visible_to: - type: integer - description: The visibility of the organization - label_ids: - type: array - description: The IDs of labels assigned to the organization - items: - type: integer - responses: - '200': - description: Edit organization - content: - application/json: - schema: - type: object - title: UpsertOrganizationResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertOrganizationResponseData - properties: - data: - type: object - title: OrganizationItem - properties: - id: - type: integer - description: The ID of the organization - name: - type: string - description: The name of the organization - owner_id: - type: integer - description: The ID of the user who owns the organization - add_time: - type: string - description: The creation date and time of the organization - update_time: - type: string - description: The last updated date and time of the organization - is_deleted: - type: boolean - description: Whether the organization is deleted or not - visible_to: - type: integer - description: The visibility of the organization - address: - type: object - properties: - value: - type: string - description: The full address of the organization - country: - type: string - description: Country of the organization - admin_area_level_1: - type: string - description: Admin area level 1 (e.g. state) of the organization - admin_area_level_2: - type: string - description: Admin area level 2 (e.g. county) of the organization - locality: - type: string - description: Locality (e.g. city) of the organization - sublocality: - type: string - description: Sublocality (e.g. neighborhood) of the organization - route: - type: string - description: Route (e.g. street) of the organization - street_number: - type: string - description: Street number of the organization - postal_code: - type: string - description: Postal code of the organization - label_ids: - type: array - description: The IDs of labels assigned to the organization - items: - type: integer - description: The organization object - example: - success: true - data: - id: 1 - name: Organization Name - owner_id: 1 - org_id: 1 - add_time: '2021-01-01T00:00:00Z' - update_time: '2021-01-01T00:00:00Z' - address: - value: 123 Main St - country: USA - admin_area_level_1: CA - admin_area_level_2: Santa Clara - locality: Sunnyvale - sublocality: Downtown - route: Main St - street_number: '123' - postal_code: '94085' - is_deleted: false - visible_to: 7 - label_ids: - - 1 - - 2 - - 3 - custom_fields: {} - '/organizations/{id}/followers': - get: - summary: List followers of an organization - description: Lists users who are following the organization. - x-token-cost: 10 - operationId: getOrganizationFollowers - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowersResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Followers array - items: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a follower to an organization - description: Adds a user as a follower to the organization. - x-token-cost: 5 - operationId: addOrganizationFollower - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - required: - - user_id - type: object - properties: - user_id: - type: integer - description: The ID of the user to add as a follower - responses: - '201': - description: Add a follower - content: - application/json: - schema: - type: object - title: AddFollowerResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - description: The follower object - example: - success: true - data: - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - '/organizations/{id}/followers/changelog': - get: - summary: List followers changelog of an organization - description: Lists changelogs about users have followed the organization. - x-token-cost: 10 - operationId: getOrganizationFollowersChangelog - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowerChangelogsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Follower changelogs array - items: - type: object - title: FollowerChangelogItem - properties: - action: - type: string - description: The type of change - actor_user_id: - type: integer - description: The ID of the user who did the change - follower_user_id: - type: integer - description: The ID of the user who was following the entity - time: - type: string - description: The time at which the change happened - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - action: added - actor_user_id: 1 - follower_user_id: 1 - time: '2024-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/organizations/{id}/followers/{follower_id}': - delete: - summary: Delete a follower from an organization - description: Deletes a user follower from the organization. - x-token-cost: 3 - operationId: deleteOrganizationFollower - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:full' - parameters: - - in: path - name: id - description: The ID of the organization - required: true - schema: - type: integer - - in: path - name: follower_id - required: true - schema: - type: integer - description: The ID of the following user - responses: - '200': - description: Remove a follower - content: - application/json: - schema: - title: DeleteFollowerResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - user_id: - type: integer - description: Deleted follower user ID - example: - success: true - data: - user_id: 1 - /products: - get: - summary: Get all products - description: Returns data about all products. - x-token-cost: 10 - operationId: getProducts - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - parameters: - - in: query - name: owner_id - schema: - type: integer - description: 'If supplied, only products owned by the given user will be returned' - - in: query - name: ids - description: 'Optional comma separated string array of up to 100 entity ids to fetch. If filter_id is provided, this is ignored. If any of the requested entities do not exist or are not visible, they are not included in the response.' - schema: - type: string - - in: query - name: filter_id - schema: - type: integer - description: The ID of the filter to use - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `name`, `add_time`, `update_time`.' - schema: - type: string - default: id - enum: - - id - - name - - add_time - - update_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: custom_fields - description: 'Comma separated string array of custom fields keys to include. If you are only interested in a particular set of custom fields, please use this parameter for a smaller response.
A maximum of 15 keys is allowed.' - schema: - type: string - responses: - '200': - description: List of products - content: - application/json: - schema: - title: GetProductsResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: Array containing data for all products - items: - title: GetProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - allOf: - - title: BaseProduct - allOf: - - type: object - properties: - id: - type: number - description: The ID of the product - name: - type: string - description: The name of the product - code: - type: string - description: The product code - unit: - type: string - description: The unit in which this product is sold - tax: - type: number - description: The tax percentage - default: 0 - is_deleted: - type: boolean - description: Whether this product will be made marked as deleted or not - default: false - is_linkable: - type: boolean - description: Whether this product can be added to a deal or not - default: true - visible_to: - allOf: - - type: number - enum: - - 1 - - 3 - - 5 - - 7 - description: Visibility of the product - owner_id: - type: integer - description: Information about the Pipedrive user who owns the product - custom_fields: - type: object - additionalProperties: true - description: An object where each key represents a custom field. All custom fields are referenced as randomly generated 40-character hashes - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - title: PricesArray - properties: - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_id (number), currency (string), price (number), cost (number), direct_cost (number | null), notes (string)' - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - name: Mechanical Pencil - code: MPENCIL - description: Product description - unit: '' - tax: 0 - category: Retail - is_linkable: true - is_deleted: false - visible_to: 3 - owner_id: 1234 - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - billing_frequency: monthly - billing_frequency_cycles: 4 - prices: - - product_id: 1 - price: 5 - currency: EUR - cost: 2 - direct_cost: 1 - notes: this is a note - custom_fields: - 6d74315176adcc4c97108440449b93ba57d20704: 16 - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a product - description: 'Adds a new product to the Products inventory. For more information, see the tutorial for adding a product.' - x-token-cost: 5 - operationId: addProduct - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - requestBody: - content: - application/json: - schema: - title: addProductRequest - allOf: - - required: - - name - type: object - properties: - name: - type: string - description: The name of the product. Cannot be an empty string - - title: productRequest - type: object - properties: - code: - type: string - description: The product code - description: - type: string - description: The product description - unit: - type: string - description: The unit in which this product is sold - tax: - type: number - description: The tax percentage - default: 0 - category: - type: number - description: The category of the product - owner_id: - type: integer - description: 'The ID of the user who will be marked as the owner of this product. When omitted, the authorized user ID will be used' - is_linkable: - type: boolean - description: Whether this product can be added to a deal or not - default: true - visible_to: - type: number - allOf: - - type: number - enum: - - 1 - - 3 - - 5 - - 7 - description: 'The visibility of the product. If omitted, the visibility will be set to the default visibility setting of this item type for the authorized user. Read more about visibility groups here.

Essential / Advanced plan

ValueDescription
`1`Owner & followers
`3`Entire company

Professional / Enterprise plan

ValueDescription
`1`Owner only
`3`Owner''s visibility group
`5`Owner''s visibility group and sub-groups
`7`Entire company
' - prices: - type: array - items: - type: object - description: 'An array of objects, each containing: `currency` (string), `price` (number), `cost` (number, optional), `direct_cost` (number, optional). Note that there can only be one price per product per currency. When `prices` is omitted altogether, a default price of 0 and the user''s default currency will be assigned.' - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - responses: - '201': - description: Add product data - content: - application/json: - schema: - title: GetProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - allOf: - - title: BaseProduct - allOf: - - type: object - properties: - id: - type: number - description: The ID of the product - name: - type: string - description: The name of the product - code: - type: string - description: The product code - unit: - type: string - description: The unit in which this product is sold - tax: - type: number - description: The tax percentage - default: 0 - is_deleted: - type: boolean - description: Whether this product will be made marked as deleted or not - default: false - is_linkable: - type: boolean - description: Whether this product can be added to a deal or not - default: true - visible_to: - allOf: - - type: number - enum: - - 1 - - 3 - - 5 - - 7 - description: Visibility of the product - owner_id: - type: integer - description: Information about the Pipedrive user who owns the product - custom_fields: - type: object - additionalProperties: true - description: An object where each key represents a custom field. All custom fields are referenced as randomly generated 40-character hashes - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - title: PricesArray - properties: - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_id (number), currency (string), price (number), cost (number), direct_cost (number | null), notes (string)' - example: - success: true - data: - id: 1 - name: Mechanical Pencil - code: MPENCIL - description: Product description - unit: '' - tax: 0 - category: Retail - is_linkable: true - is_deleted: false - visible_to: 3 - owner_id: 1234 - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - billing_frequency: monthly - billing_frequency_cycles: 4 - prices: - - product_id: 1 - price: 5 - currency: EUR - cost: 2 - direct_cost: 1 - notes: this is a note - custom_fields: - 6d74315176adcc4c97108440449b93ba57d20704: 16 - '/products/{id}/followers': - get: - summary: List followers of a product - description: Lists users who are following the product. - x-token-cost: 10 - operationId: getProductFollowers - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowersResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Followers array - items: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a follower to a product - description: Adds a user as a follower to the product. - x-token-cost: 5 - operationId: addProductFollower - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - required: - - user_id - type: object - properties: - user_id: - type: integer - description: The ID of the user to add as a follower - responses: - '201': - description: Add a follower - content: - application/json: - schema: - type: object - title: AddFollowerResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - description: The follower object - example: - success: true - data: - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - '/products/{id}/followers/changelog': - get: - summary: List followers changelog of a product - description: Lists changelogs about users have followed the product. - x-token-cost: 10 - operationId: getProductFollowersChangelog - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowerChangelogsResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Follower changelogs array - items: - type: object - title: FollowerChangelogItem - properties: - action: - type: string - description: The type of change - actor_user_id: - type: integer - description: The ID of the user who did the change - follower_user_id: - type: integer - description: The ID of the user who was following the entity - time: - type: string - description: The time at which the change happened - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - action: added - actor_user_id: 1 - follower_user_id: 1 - time: '2024-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/products/{id}/followers/{follower_id}': - delete: - summary: Delete a follower from a product - description: Deletes a user follower from the product. - x-token-cost: 3 - operationId: deleteProductFollower - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - - in: path - name: follower_id - required: true - schema: - type: integer - description: The ID of the following user - responses: - '200': - description: Remove a follower - content: - application/json: - schema: - title: DeleteFollowerResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - user_id: - type: integer - description: Deleted follower user ID - example: - success: true - data: - user_id: 1 - /products/search: - get: - summary: Search products - description: 'Searches all products by name, code and/or custom fields. This endpoint is a wrapper of /v1/itemSearch with a narrower OAuth scope.' - x-token-cost: 20 - operationId: searchProducts - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if using `exact_match`). Please note that the search term has to be URL encoded. - - in: query - name: fields - schema: - type: string - enum: - - code - - custom_fields - - name - description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them. Only the following custom field types are searchable: `address`, `varchar`, `text`, `varchar_auto`, `double`, `monetary` and `phone`. Read more about searching by custom fields here.' - - in: query - name: exact_match - schema: - type: boolean - description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.' - - in: query - name: include_fields - schema: - type: string - enum: - - product.price - description: Supports including optional fields in the results which are not provided by default - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetProductSearchResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - properties: - items: - type: array - description: The array of found items - items: - type: object - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - properties: - id: - type: integer - description: The ID of the product - type: - type: string - description: The type of the item - name: - type: string - description: The name of the product - code: - type: integer - description: The code of the product - visible_to: - type: integer - description: The visibility of the product - owner: - type: object - properties: - id: - type: integer - description: The ID of the owner of the product - custom_fields: - type: array - items: - type: string - description: The custom fields - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - items: - - result_score: 0.8766 - item: - id: 1 - type: product - name: Some product - code: 123 - visible_to: 3 - owner: - id: 1 - custom_fields: [] - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/products/{id}': - delete: - summary: Delete a product - description: 'Marks a product as deleted. After 30 days, the product will be permanently deleted.' - x-token-cost: 3 - operationId: deleteProduct - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - responses: - '200': - description: Deletes a product - content: - application/json: - schema: - title: DeleteProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - description: The ID of the removed product - type: integer - example: - success: true - data: - id: 1 - get: - summary: Get one product - description: Returns data about a specific product. - x-token-cost: 1 - operationId: getProduct - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - responses: - '200': - description: Get product information by id - content: - application/json: - schema: - title: GetProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - allOf: - - title: BaseProduct - allOf: - - type: object - properties: - id: - type: number - description: The ID of the product - name: - type: string - description: The name of the product - code: - type: string - description: The product code - unit: - type: string - description: The unit in which this product is sold - tax: - type: number - description: The tax percentage - default: 0 - is_deleted: - type: boolean - description: Whether this product will be made marked as deleted or not - default: false - is_linkable: - type: boolean - description: Whether this product can be added to a deal or not - default: true - visible_to: - allOf: - - type: number - enum: - - 1 - - 3 - - 5 - - 7 - description: Visibility of the product - owner_id: - type: integer - description: Information about the Pipedrive user who owns the product - custom_fields: - type: object - additionalProperties: true - description: An object where each key represents a custom field. All custom fields are referenced as randomly generated 40-character hashes - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - title: PricesArray - properties: - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_id (number), currency (string), price (number), cost (number), direct_cost (number | null), notes (string)' - example: - success: true - data: - id: 1 - name: Mechanical Pencil - code: MPENCIL - description: Product description - unit: '' - tax: 0 - category: Retail - is_linkable: true - is_deleted: false - visible_to: 3 - owner_id: 1234 - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - billing_frequency: monthly - billing_frequency_cycles: 4 - prices: - - product_id: 1 - price: 5 - currency: EUR - cost: 2 - direct_cost: 1 - notes: this is a note - custom_fields: - 6d74315176adcc4c97108440449b93ba57d20704: 16 - patch: - summary: Update a product - description: Updates product data. - x-token-cost: 5 - operationId: updateProduct - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: updateProductRequest - allOf: - - type: object - properties: - name: - type: string - description: The name of the product. Cannot be an empty string - - title: productRequest - type: object - properties: - code: - type: string - description: The product code - description: - type: string - description: The product description - unit: - type: string - description: The unit in which this product is sold - tax: - type: number - description: The tax percentage - default: 0 - category: - type: number - description: The category of the product - owner_id: - type: integer - description: 'The ID of the user who will be marked as the owner of this product. When omitted, the authorized user ID will be used' - is_linkable: - type: boolean - description: Whether this product can be added to a deal or not - default: true - visible_to: - type: number - allOf: - - type: number - enum: - - 1 - - 3 - - 5 - - 7 - description: 'The visibility of the product. If omitted, the visibility will be set to the default visibility setting of this item type for the authorized user. Read more about visibility groups here.

Essential / Advanced plan

ValueDescription
`1`Owner & followers
`3`Entire company

Professional / Enterprise plan

ValueDescription
`1`Owner only
`3`Owner''s visibility group
`5`Owner''s visibility group and sub-groups
`7`Entire company
' - prices: - type: array - items: - type: object - description: 'An array of objects, each containing: `currency` (string), `price` (number), `cost` (number, optional), `direct_cost` (number, optional). Note that there can only be one price per product per currency. When `prices` is omitted altogether, a default price of 0 and the user''s default currency will be assigned.' - - type: object - properties: - billing_frequency: - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - type: object - properties: - billing_frequency_cycles: - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - responses: - '200': - description: Updates product data - content: - application/json: - schema: - title: UpdateProductResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - allOf: - - title: BaseProduct - allOf: - - type: object - properties: - id: - type: number - description: The ID of the product - name: - type: string - description: The name of the product - code: - type: string - description: The product code - unit: - type: string - description: The unit in which this product is sold - tax: - type: number - description: The tax percentage - default: 0 - is_deleted: - type: boolean - description: Whether this product will be made marked as deleted or not - default: false - is_linkable: - type: boolean - description: Whether this product can be added to a deal or not - default: true - visible_to: - allOf: - - type: number - enum: - - 1 - - 3 - - 5 - - 7 - description: Visibility of the product - owner_id: - type: integer - description: Information about the Pipedrive user who owns the product - custom_fields: - type: object - additionalProperties: true - description: An object where each key represents a custom field. All custom fields are referenced as randomly generated 40-character hashes - - type: object - properties: - billing_frequency: - default: one-time - type: string - enum: - - one-time - - annually - - semi-annually - - quarterly - - monthly - - weekly - description: | - Only available in Advanced and above plans - - How often a customer is billed for access to a service or product - - type: object - properties: - billing_frequency_cycles: - default: null - type: integer - nullable: true - description: | - Only available in Advanced and above plans - - The number of times the billing frequency repeats for a product in a deal - - When `billing_frequency` is set to `one-time`, this field must be `null` - - When `billing_frequency` is set to `weekly`, this field cannot be `null` - - For all the other values of `billing_frequency`, `null` represents a product billed indefinitely - - Must be a positive integer less or equal to 208 - - type: object - title: PricesArray - properties: - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_id (number), currency (string), price (number), cost (number), direct_cost (number | null), notes (string)' - example: - success: true - data: - id: 1 - name: Mechanical Pencil - code: MPENCIL - description: Product description - unit: '' - tax: 0 - category: Retail - is_linkable: true - is_deleted: false - visible_to: 3 - owner_id: 1234 - add_time: '2019-12-19T11:36:49Z' - update_time: '2019-12-19T11:36:49Z' - billing_frequency: monthly - billing_frequency_cycles: 4 - prices: - - product_id: 1 - price: 5 - currency: EUR - cost: 2 - direct_cost: 1 - notes: this is a note - custom_fields: - 6d74315176adcc4c97108440449b93ba57d20704: 16 - '/products/{id}/variations': - get: - summary: Get all product variations - description: Returns data about all product variations. - x-token-cost: 10 - operationId: getProductVariations - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:read' - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - responses: - '200': - description: List of product variations - content: - application/json: - schema: - title: GetProductVariationsResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: Array containing data for all products - items: - type: object - properties: - id: - type: number - description: The ID of the product variation - name: - type: string - description: The name of the product variation - product_id: - type: integer - description: The ID of the product - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_variation_id (number), currency (string), price (number), cost (number), direct_cost (number) , notes (string)' - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 2 - name: Upgraded Mechanical Pencil - product_id: 1 - prices: - - product_variation_id: 2 - price: 5 - currency: EUR - cost: 2 - direct_cost: 3 - notes: This is the price for the upgraded mechanical pencil - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a product variation - description: Adds a new product variation. - x-token-cost: 5 - operationId: addProductVariation - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: addProductVariationRequest - required: - - name - type: object - properties: - name: - type: string - description: The name of the product variation. The maximum length is 255 characters. - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: currency (string), price (number), cost (number, optional), direct_cost (number, optional), notes (string, optional). When prices is omitted altogether, a default price of 0, a default cost of 0, a default direct_cost of 0 and the user''s default currency will be assigned.' - responses: - '201': - description: Add a product variation - content: - application/json: - schema: - title: GetProductVariationResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: number - description: The ID of the product variation - name: - type: string - description: The name of the product variation - product_id: - type: integer - description: The ID of the product - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_variation_id (number), currency (string), price (number), cost (number), direct_cost (number) , notes (string)' - example: - success: true - data: - id: 2 - name: Upgraded Mechanical Pencil - product_id: 1 - prices: - - product_variation_id: 2 - price: 5 - currency: EUR - cost: 2 - direct_cost: 3 - notes: This is the price for the upgraded mechanical pencil - '/products/{id}/variations/{product_variation_id}': - patch: - summary: Update a product variation - description: Updates product variation data. - x-token-cost: 5 - operationId: updateProductVariation - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - - in: path - name: product_variation_id - description: The ID of the product variation - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: updateProductVariationRequest - type: object - properties: - name: - type: string - description: The name of the product variation. The maximum length is 255 characters. - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: currency (string), price (number), cost (number, optional), direct_cost (number, optional), notes (string, optional). When prices is omitted altogether, a default price of 0, a default cost of 0, a default direct_cost of 0 and the user''s default currency will be assigned.' - responses: - '200': - description: Update product variation data - content: - application/json: - schema: - title: GetProductVariationResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: number - description: The ID of the product variation - name: - type: string - description: The name of the product variation - product_id: - type: integer - description: The ID of the product - prices: - type: array - items: - type: object - description: 'Array of objects, each containing: product_variation_id (number), currency (string), price (number), cost (number), direct_cost (number) , notes (string)' - example: - success: true - data: - id: 2 - name: Upgraded Mechanical Pencil - product_id: 1 - prices: - - product_variation_id: 2 - price: 5 - currency: EUR - cost: 2 - direct_cost: 3 - notes: This is the price for the upgraded mechanical pencil - delete: - summary: Delete a product variation - description: Deletes a product variation. - x-token-cost: 3 - operationId: deleteProductVariation - tags: - - Products - security: - - api_key: [] - - oauth2: - - 'products:full' - parameters: - - in: path - name: id - description: The ID of the product - required: true - schema: - type: integer - - in: path - name: product_variation_id - description: The ID of the product variation - required: true - schema: - type: integer - responses: - '200': - description: Delete a product variation - content: - application/json: - schema: - title: DeleteProductVariationResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: The ID of a deleted product variant - example: - success: true - data: - id: 123 - /leads/search: - get: - summary: Search leads - description: 'Searches all leads by title, notes and/or custom fields. This endpoint is a wrapper of /v1/itemSearch with a narrower OAuth scope. Found leads can be filtered by the person ID and the organization ID.' - x-token-cost: 20 - operationId: searchLeads - tags: - - Leads - security: - - api_key: [] - - oauth2: - - 'leads:read' - - 'leads:full' - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if using `exact_match`). Please note that the search term has to be URL encoded. - - in: query - name: fields - schema: - type: string - enum: - - custom_fields - - notes - - title - description: A comma-separated string array. The fields to perform the search from. Defaults to all of them. - - in: query - name: exact_match - schema: - type: boolean - description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.' - - in: query - name: person_id - schema: - type: integer - description: Will filter leads by the provided person ID. The upper limit of found leads associated with the person is 2000. - - in: query - name: organization_id - schema: - type: integer - description: Will filter leads by the provided organization ID. The upper limit of found leads associated with the organization is 2000. - - in: query - name: include_fields - schema: - type: string - enum: - - lead.was_seen - description: Supports including optional fields in the results which are not provided by default - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetLeadSearchResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: GetLeadSearchResponseData - properties: - data: - type: object - properties: - items: - type: array - description: The array of leads - items: - type: object - title: LeadSearchItem - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - properties: - id: - type: string - description: The ID of the lead - type: - type: string - description: The type of the item - title: - type: string - description: The title of the lead - owner: - type: object - properties: - id: - type: integer - description: The ID of the owner of the lead - person: - type: object - properties: - id: - type: integer - description: The ID of the person the lead is associated with - name: - type: string - description: The name of the person the lead is associated with - organization: - type: object - properties: - id: - type: integer - description: The ID of the organization the lead is associated with - name: - type: string - description: The name of the organization the lead is associated with - phones: - type: array - items: - type: string - emails: - type: array - items: - type: string - custom_fields: - type: array - items: - type: string - description: Custom fields - notes: - type: array - description: An array of notes - items: - type: string - value: - type: integer - description: The value of the lead - currency: - type: string - description: The currency of the lead - visible_to: - type: integer - description: The visibility of the lead - is_archived: - type: boolean - description: A flag indicating whether the lead is archived or not - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - items: - - result_score: 0.29 - item: - id: 39c433f0-8a4c-11ec-8728-09968f0a1ca0 - type: lead - title: John Doe lead - owner: - id: 1 - person: - id: 1 - name: John Doe - organization: - id: 1 - name: John company - phones: [] - emails: - - john@doe.com - custom_fields: [] - notes: [] - value: 100 - currency: USD - visible_to: 3 - is_archived: false - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - '/leads/{id}/convert/deal': - post: - security: - - api_key: [] - - oauth2: - - 'leads:full' - tags: - - Leads - - Beta - summary: Convert a lead to a deal (BETA) - description: 'Initiates a conversion of a lead to a deal. The return value is an ID of a job that was assigned to perform the conversion. Related entities (notes, files, emails, activities, ...) are transferred during the process to the target entity. If the conversion is successful, the lead is marked as deleted. To retrieve the created entity ID and the result of the conversion, call the /api/v2/leads/{lead_id}/convert/status/{conversion_id} endpoint.' - operationId: convertLeadToDeal - x-token-cost: 40 - parameters: - - in: path - name: id - required: true - schema: - type: string - format: uuid - description: The ID of the lead to convert - requestBody: - content: - application/json: - schema: - additionalProperties: false - type: object - properties: - stage_id: - description: 'The ID of a stage the created deal will be added to. Please note that a pipeline will be assigned automatically based on the `stage_id`. If omitted, the deal will be placed in the first stage of the default pipeline.' - type: integer - pipeline_id: - description: 'The ID of a pipeline the created deal will be added to. By default, the deal will be added to the first stage of the specified pipeline. Please note that `pipeline_id` and `stage_id` should not be used together as `pipeline_id` will be ignored.' - type: integer - responses: - '200': - description: Successful response containing payload in the `data` field - content: - application/json: - schema: - title: AddConvertLeadToDealResponse - type: object - properties: - success: - type: boolean - data: - type: object - description: An object containing conversion job id that performs the conversion - required: - - conversion_id - properties: - conversion_id: - description: The ID of the conversion job that can be used to retrieve conversion status and details. The ID has UUID format. - type: string - format: uuid - additional_data: - type: object - nullable: true - example: null - example: - success: true - data: - conversion_id: 4b40248b-945a-4802-b996-60fdff8c5c69 - additional_data: null - '404': - description: A resource describing an error - content: - application/json: - schema: - type: object - title: GetConvertResponse - properties: - success: - type: boolean - example: false - error: - type: string - description: The description of the error - error_info: - type: string - description: A message describing how to solve the problem - data: - type: object - nullable: true - example: null - additional_data: - type: object - nullable: true - example: null - example: - success: false - error: Entity was not found - error_info: Object was not found. - data: null - additional_data: null - '/leads/{id}/convert/status/{conversion_id}': - get: - security: - - api_key: [] - - oauth2: - - 'leads:full' - - 'leads:read' - tags: - - Leads - - Beta - summary: Get Lead conversion status (BETA) - description: 'Returns data about the conversion. Status is always present and its value (not_started, running, completed, failed, rejected) represents the current state of the conversion. Deal ID is only present if the conversion was successfully finished. This data is only temporary and removed after a few days.' - operationId: getLeadConversionStatus - x-token-cost: 1 - parameters: - - in: path - name: id - required: true - schema: - type: string - format: uuid - description: The ID of a lead - - in: path - name: conversion_id - required: true - schema: - type: string - format: uuid - description: The ID of the conversion - responses: - '200': - description: Successful response containing payload in the `data` field - content: - application/json: - example: - success: true - data: - deal_id: 33 - conversion_id: 4b40248b-945a-4802-b996-60fdff8c5c69 - status: completed - additional_data: null - schema: - title: GetConvertResponse - type: object - required: - - success - - data - properties: - success: - type: boolean - data: - type: object - description: An object containing conversion status. After successful conversion the converted entity ID is also present. - required: - - conversion_id - - status - properties: - lead_id: - description: The ID of the new lead. - type: string - format: uuid - deal_id: - description: The ID of the new deal. - type: integer - conversion_id: - description: The ID of the conversion job. The ID can be used to retrieve conversion status and details. The ID has UUID format. - type: string - format: uuid - status: - description: Status of the conversion job. - type: string - enum: - - not_started - - running - - completed - - failed - - rejected - additional_data: - type: object - nullable: true - example: null - '404': - description: A resource describing an error - content: - application/json: - schema: - type: object - title: GetConvertResponse - properties: - success: - type: boolean - example: false - error: - type: string - description: The description of the error - error_info: - type: string - description: A message describing how to solve the problem - data: - type: object - nullable: true - example: null - additional_data: - type: object - nullable: true - example: null - example: - success: false - error: Entity was not found - error_info: Object was not found. - data: null - additional_data: null - /organizations/search: - get: - summary: Search organizations - description: 'Searches all organizations by name, address, notes and/or custom fields. This endpoint is a wrapper of /v1/itemSearch with a narrower OAuth scope.' - x-token-cost: 20 - operationId: searchOrganization - tags: - - Organizations - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if using `exact_match`). Please note that the search term has to be URL encoded. - - in: query - name: fields - schema: - type: string - enum: - - address - - custom_fields - - notes - - name - description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them. Only the following custom field types are searchable: `address`, `varchar`, `text`, `varchar_auto`, `double`, `monetary` and `phone`. Read more about searching by custom fields here.' - - in: query - name: exact_match - schema: - type: boolean - description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetOrganizationSearchResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - properties: - items: - type: array - description: The array of found items - items: - type: object - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - properties: - id: - type: integer - description: The ID of the organization - type: - type: string - description: The type of the item - name: - type: string - description: The name of the organization - address: - type: string - description: The address of the organization - visible_to: - type: integer - description: The visibility of the organization - owner: - type: object - properties: - id: - type: integer - description: The ID of the owner of the deal - custom_fields: - type: array - items: - type: string - description: Custom fields - notes: - type: array - description: An array of notes - items: - type: string - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - items: - - result_score: 0.316 - item: - id: 1 - type: organization - name: Organization name - address: 'Mustamäe tee 3a, 10615 Tallinn' - visible_to: 3 - owner: - id: 1 - custom_fields: [] - notes: [] - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - /persons/search: - get: - summary: Search persons - description: 'Searches all persons by name, email, phone, notes and/or custom fields. This endpoint is a wrapper of /v1/itemSearch with a narrower OAuth scope. Found persons can be filtered by organization ID.' - x-token-cost: 20 - operationId: searchPersons - tags: - - Persons - security: - - api_key: [] - - oauth2: - - 'contacts:read' - - 'contacts:full' - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if using `exact_match`). Please note that the search term has to be URL encoded. - - in: query - name: fields - schema: - type: string - enum: - - custom_fields - - email - - notes - - phone - - name - description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them. Only the following custom field types are searchable: `address`, `varchar`, `text`, `varchar_auto`, `double`, `monetary` and `phone`. Read more about searching by custom fields here.' - - in: query - name: exact_match - schema: - type: boolean - description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.' - - in: query - name: organization_id - schema: - type: integer - description: Will filter persons by the provided organization ID. The upper limit of found persons associated with the organization is 2000. - - in: query - name: include_fields - schema: - type: string - enum: - - person.picture - description: Supports including optional fields in the results which are not provided by default - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetPersonSearchResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: object - properties: - items: - type: array - description: The array of found items - items: - type: object - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - properties: - id: - type: integer - description: The ID of the person - type: - type: string - description: The type of the item - name: - type: string - description: The name of the person - phones: - type: array - description: An array of phone numbers - items: - type: string - emails: - type: array - description: An array of email addresses - items: - type: string - visible_to: - type: integer - description: The visibility of the person - owner: - type: object - properties: - id: - type: integer - description: The ID of the owner of the person - organization: - type: object - properties: - id: - type: integer - description: The ID of the organization the person is associated with - name: - type: string - description: The name of the organization the person is associated with - custom_fields: - type: array - items: - type: string - description: Custom fields - notes: - type: array - description: An array of notes - items: - type: string - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - items: - - result_score: 0.5092 - item: - id: 1 - type: person - name: Jane Doe - phones: - - +372 555555555 - emails: - - jane@pipedrive.com - visible_to: 3 - owner: - id: 1 - organization: - id: 1 - name: Organization name - address: null - custom_fields: [] - notes: [] - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - /itemSearch: - get: - summary: Perform a search from multiple item types - description: Performs a search from your choice of item types and fields. - x-token-cost: 20 - operationId: searchItem - tags: - - ItemSearch - security: - - api_key: [] - - oauth2: - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if using `exact_match`). Please note that the search term has to be URL encoded. - - in: query - name: item_types - schema: - type: string - enum: - - deal - - person - - organization - - product - - lead - - file - - mail_attachment - - project - description: A comma-separated string array. The type of items to perform the search from. Defaults to all. - - in: query - name: fields - schema: - type: string - enum: - - address - - code - - custom_fields - - email - - name - - notes - - organization_name - - person_name - - phone - - title - - description - description: 'A comma-separated string array. The fields to perform the search from. Defaults to all. Relevant for each item type are:
Item typeField
Deal`custom_fields`, `notes`, `title`
Person`custom_fields`, `email`, `name`, `notes`, `phone`
Organization`address`, `custom_fields`, `name`, `notes`
Product`code`, `custom_fields`, `name`
Lead`custom_fields`, `notes`, `email`, `organization_name`, `person_name`, `phone`, `title`
File`name`
Mail attachment`name`
Project `custom_fields`, `notes`, `title`, `description`

Only the following custom field types are searchable: `address`, `varchar`, `text`, `varchar_auto`, `double`, `monetary` and `phone`. Read more about searching by custom fields here.
When searching for leads, the email, organization_name, person_name, and phone fields will return results only for leads not linked to contacts. For searching leads by person or organization values, please use `search_for_related_items`.' - - in: query - name: search_for_related_items - schema: - type: boolean - description: 'When enabled, the response will include up to 100 newest related leads and 100 newest related deals for each found person and organization and up to 100 newest related persons for each found organization' - - in: query - name: exact_match - schema: - type: boolean - description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.' - - in: query - name: include_fields - schema: - type: string - enum: - - deal.cc_email - - person.picture - - product.price - description: A comma-separated string array. Supports including optional fields in the results which are not provided by default. - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetItemSearchResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: GetItemSearchResponseData - properties: - data: - type: object - properties: - items: - type: array - description: The array of found items - items: - type: object - title: ItemSearchItem - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - description: Item - related_items: - type: array - description: The array of related items if `search_for_related_items` was enabled - items: - type: object - title: ItemSearchItem - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - description: Item - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - items: - - result_score: 1.22724 - item: - id: 42 - type: deal - title: Sample Deal - value: 53883 - currency: USD - status: open - visible_to: 3 - owner: - id: 69 - stage: - id: 3 - name: Demo Scheduled - person: - id: 6 - name: Sample Person - organization: - id: 9 - name: Sample Organization - address: 'Dabas, Hungary' - custom_fields: - - Sample text - notes: - - Sample note - - result_score: 0.31335002 - item: - id: 9 - type: organization - name: Sample Organization - address: 'Dabas, Hungary' - visible_to: 3 - owner: - id: 69 - custom_fields: [] - notes: [] - - result_score: 0.29955 - item: - id: 6 - type: person - name: Sample Person - phones: - - '555123123' - - +372 (55) 123468 - - '0231632772' - emails: - - primary@email.com - - secondary@email.com - visible_to: 1 - owner: - id: 69 - organization: - id: 9 - name: Sample Organization - address: 'Dabas, Hungary' - custom_fields: - - Custom Field Text - notes: - - Person note - - result_score: 0.0093 - item: - id: 4 - type: mail_attachment - name: Sample mail attachment.txt - url: /files/4/download - - result_score: 0.0093 - item: - id: 3 - type: file - name: Sample file attachment.txt - url: /files/3/download - deal: - id: 42 - title: Sample Deal - person: - id: 6 - name: Sample Person - organization: - id: 9 - name: Sample Organization - address: 'Dabas, Hungary' - - result_score: 0.0011999999 - item: - id: 1 - type: product - name: Sample Product - code: product-code - visible_to: 3 - owner: - id: 69 - custom_fields: [] - related_items: - - result_score: 0 - item: - id: 2 - type: deal - title: Other deal - value: 100 - currency: USD - status: open - visible_to: 3 - owner: - id: 1 - stage: - id: 1 - name: Lead In - person: - id: 1 - name: Sample Person - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - /itemSearch/field: - get: - summary: Perform a search using a specific field from an item type - description: 'Performs a search from the values of a specific field. Results can either be the distinct values of the field (useful for searching autocomplete field values), or the IDs of actual items (deals, leads, persons, organizations or products).' - x-token-cost: 20 - operationId: searchItemByField - tags: - - ItemSearch - security: - - api_key: [] - - oauth2: - - 'search:read' - parameters: - - in: query - name: term - required: true - schema: - type: string - description: The search term to look for. Minimum 2 characters (or 1 if `match` is `exact`). Please note that the search term has to be URL encoded. - - in: query - name: entity_type - required: true - schema: - type: string - enum: - - deal - - lead - - person - - organization - - product - - project - description: The type of the field to perform the search from - - in: query - name: match - schema: - type: string - default: exact - enum: - - exact - - beginning - - middle - description: 'The type of match used against the term. The search is case sensitive.

E.g. in case of searching for a value `monkey`,
  • with `exact` match, you will only find it if term is `monkey`
  • with `beginning` match, you will only find it if the term matches the beginning or the whole string, e.g. `monk` and `monkey`
  • with `middle` match, you will find the it if the term matches any substring of the value, e.g. `onk` and `ke`
.' - - in: query - name: field - required: true - schema: - type: string - description: 'The key of the field to search from. The field key can be obtained by fetching the list of the fields using any of the fields'' API GET methods (dealFields, personFields, etc.). Only the following custom field types are searchable: `address`, `varchar`, `text`, `varchar_auto`, `double`, `monetary` and `phone`. Read more about searching by custom fields here.' - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Success - content: - application/json: - schema: - title: GetItemSearchFieldResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: The array of found fields - items: - type: object - title: ItemSearchItem - properties: - result_score: - type: number - description: Search result relevancy - item: - type: object - description: Item - additional_data: - type: object - description: Pagination related data - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - name: Jane Doe - - id: 2 - name: John Doe - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - /stages: - get: - summary: Get all stages - description: Returns data about all stages. - x-token-cost: 5 - operationId: getStages - tags: - - Stages - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - - admin - parameters: - - in: query - name: pipeline_id - schema: - type: integer - description: 'The ID of the pipeline to fetch stages for. If omitted, stages for all pipelines will be fetched.' - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`, `order_nr`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - order_nr - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all stages - content: - application/json: - schema: - title: GetStagesResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: array - description: The array of stages - items: - type: object - title: StageItem - properties: - id: - type: integer - description: The ID of the stage - order_nr: - type: integer - description: Defines the order of the stage - name: - type: string - description: The name of the stage - is_deleted: - type: boolean - description: Whether the stage is marked as deleted or not - deal_probability: - type: integer - description: The success probability percentage of the deal. Used/shown when the deal weighted values are used. - pipeline_id: - type: integer - description: The ID of the pipeline to add the stage to - is_deal_rot_enabled: - type: boolean - description: Whether deals in this stage can become rotten - days_to_rotten: - type: integer - nullable: true - description: The number of days the deals not updated in this stage would become rotten. Applies only if the `is_deal_rot_enabled` is set. - add_time: - type: string - description: The stage creation time - update_time: - type: string - description: The stage update time - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - order_nr: 1 - name: Stage Name - is_deleted: false - deal_probability: 100 - pipeline_id: 1 - is_deal_rot_enabled: true - days_to_rotten: 2 - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a new stage - description: 'Adds a new stage, returns the ID upon success.' - x-token-cost: 5 - operationId: addStage - tags: - - Stages - security: - - api_key: [] - - oauth2: - - admin - requestBody: - content: - application/json: - schema: - title: addStageRequest - required: - - name - - pipeline_id - type: object - properties: - name: - type: string - description: The name of the stage - pipeline_id: - type: integer - description: The ID of the pipeline to add stage to - deal_probability: - type: integer - description: The success probability percentage of the deal. Used/shown when deal weighted values are used. - is_deal_rot_enabled: - type: boolean - description: Whether deals in this stage can become rotten - days_to_rotten: - type: integer - description: The number of days the deals not updated in this stage would become rotten. Applies only if the `is_deal_rot_enabled` is set. - responses: - '200': - description: Add a new stage - content: - application/json: - schema: - title: UpsertStageResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - title: StageItem - properties: - id: - type: integer - description: The ID of the stage - order_nr: - type: integer - description: Defines the order of the stage - name: - type: string - description: The name of the stage - is_deleted: - type: boolean - description: Whether the stage is marked as deleted or not - deal_probability: - type: integer - description: The success probability percentage of the deal. Used/shown when the deal weighted values are used. - pipeline_id: - type: integer - description: The ID of the pipeline to add the stage to - is_deal_rot_enabled: - type: boolean - description: Whether deals in this stage can become rotten - days_to_rotten: - type: integer - nullable: true - description: The number of days the deals not updated in this stage would become rotten. Applies only if the `is_deal_rot_enabled` is set. - add_time: - type: string - description: The stage creation time - update_time: - type: string - description: The stage update time - description: The stage object - example: - success: true - data: - id: 1 - order_nr: 1 - name: Stage Name - is_deleted: false - deal_probability: 100 - pipeline_id: 1 - is_deal_rot_enabled: true - days_to_rotten: 2 - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - '/stages/{id}': - delete: - summary: Delete a stage - description: Marks a stage as deleted. - x-token-cost: 3 - operationId: deleteStage - tags: - - Stages - security: - - api_key: [] - - oauth2: - - admin - parameters: - - in: path - name: id - description: The ID of the stage - required: true - schema: - type: integer - responses: - '200': - description: Delete stage - content: - application/json: - schema: - title: DeleteStageResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: Deleted stage ID - example: - success: true - data: - id: 1 - get: - summary: Get one stage - description: Returns data about a specific stage. - x-token-cost: 1 - operationId: getStage - tags: - - Stages - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - - admin - parameters: - - in: path - name: id - description: The ID of the stage - required: true - schema: - type: integer - responses: - '200': - description: Get one stages - content: - application/json: - schema: - title: UpsertStageResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - title: StageItem - properties: - id: - type: integer - description: The ID of the stage - order_nr: - type: integer - description: Defines the order of the stage - name: - type: string - description: The name of the stage - is_deleted: - type: boolean - description: Whether the stage is marked as deleted or not - deal_probability: - type: integer - description: The success probability percentage of the deal. Used/shown when the deal weighted values are used. - pipeline_id: - type: integer - description: The ID of the pipeline to add the stage to - is_deal_rot_enabled: - type: boolean - description: Whether deals in this stage can become rotten - days_to_rotten: - type: integer - nullable: true - description: The number of days the deals not updated in this stage would become rotten. Applies only if the `is_deal_rot_enabled` is set. - add_time: - type: string - description: The stage creation time - update_time: - type: string - description: The stage update time - description: The stage object - example: - success: true - data: - id: 1 - order_nr: 1 - name: Stage Name - is_deleted: false - deal_probability: 100 - pipeline_id: 1 - is_deal_rot_enabled: true - days_to_rotten: 2 - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - patch: - summary: Update stage details - description: Updates the properties of a stage. - x-token-cost: 5 - operationId: updateStage - tags: - - Stages - security: - - api_key: [] - - oauth2: - - admin - parameters: - - in: path - name: id - description: The ID of the stage - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - title: updateStageRequest - type: object - properties: - name: - type: string - description: The name of the stage - pipeline_id: - type: integer - description: The ID of the pipeline to add stage to - deal_probability: - type: integer - description: The success probability percentage of the deal. Used/shown when deal weighted values are used. - is_deal_rot_enabled: - type: boolean - description: Whether deals in this stage can become rotten - days_to_rotten: - type: integer - description: The number of days the deals not updated in this stage would become rotten. Applies only if the `is_deal_rot_enabled` is set. - responses: - '200': - description: Update an existing stage - content: - application/json: - schema: - title: UpsertStageResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - title: StageItem - properties: - id: - type: integer - description: The ID of the stage - order_nr: - type: integer - description: Defines the order of the stage - name: - type: string - description: The name of the stage - is_deleted: - type: boolean - description: Whether the stage is marked as deleted or not - deal_probability: - type: integer - description: The success probability percentage of the deal. Used/shown when the deal weighted values are used. - pipeline_id: - type: integer - description: The ID of the pipeline to add the stage to - is_deal_rot_enabled: - type: boolean - description: Whether deals in this stage can become rotten - days_to_rotten: - type: integer - nullable: true - description: The number of days the deals not updated in this stage would become rotten. Applies only if the `is_deal_rot_enabled` is set. - add_time: - type: string - description: The stage creation time - update_time: - type: string - description: The stage update time - description: The stage object - example: - success: true - data: - id: 1 - order_nr: 1 - name: Stage Name - is_deleted: false - deal_probability: 100 - pipeline_id: 1 - is_deal_rot_enabled: true - days_to_rotten: 2 - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - /pipelines: - get: - summary: Get all pipelines - description: Returns data about all pipelines. - x-token-cost: 5 - operationId: getPipelines - tags: - - Pipelines - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - - admin - parameters: - - in: query - name: sort_by - description: 'The field to sort by. Supported fields: `id`, `update_time`, `add_time`.' - schema: - type: string - default: id - enum: - - id - - update_time - - add_time - - in: query - name: sort_direction - description: 'The sorting direction. Supported values: `asc`, `desc`.' - schema: - type: string - default: asc - enum: - - asc - - desc - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: Get all pipelines - content: - application/json: - schema: - type: object - title: GetPipelinesResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Pipelines array - items: - type: object - properties: - id: - type: integer - description: The ID of the pipeline - name: - type: string - description: The name of the pipeline - order_nr: - type: integer - description: Defines the order of pipelines. The pipeline with the lowest `order_nr` is considered the default. - is_selected: - type: boolean - description: Whether this pipeline is selected or not - is_deleted: - type: boolean - description: Whether this pipeline is marked as deleted or not - is_deal_probability_enabled: - type: boolean - description: Whether deal probability is disabled or enabled for this pipeline - add_time: - type: string - description: The pipeline creation time - update_time: - type: string - description: The pipeline update time - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - id: 1 - name: Pipeline Name - order_nr: 1 - is_deleted: false - is_deal_probability_enabled: true - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - is_selected: true - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ - post: - summary: Add a new pipeline - description: Adds a new pipeline. - x-token-cost: 5 - operationId: addPipeline - tags: - - Pipelines - security: - - api_key: [] - - oauth2: - - admin - requestBody: - content: - application/json: - schema: - required: - - name - type: object - properties: - name: - type: string - description: The name of the pipeline - is_deal_probability_enabled: - type: boolean - default: false - description: Whether deal probability is disabled or enabled for this pipeline - responses: - '200': - description: Add pipeline - content: - application/json: - schema: - type: object - title: UpsertPipelineResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertPipelineResponseData - properties: - data: - type: object - properties: - id: - type: integer - description: The ID of the pipeline - name: - type: string - description: The name of the pipeline - order_nr: - type: integer - description: Defines the order of pipelines. The pipeline with the lowest `order_nr` is considered the default. - is_selected: - type: boolean - description: Whether this pipeline is selected or not - is_deleted: - type: boolean - description: Whether this pipeline is marked as deleted or not - is_deal_probability_enabled: - type: boolean - description: Whether deal probability is disabled or enabled for this pipeline - add_time: - type: string - description: The pipeline creation time - update_time: - type: string - description: The pipeline update time - description: The pipeline object - example: - success: true - data: - id: 1 - name: Pipeline Name - order_nr: 1 - is_deleted: false - is_deal_probability_enabled: true - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - is_selected: true - '/pipelines/{id}': - delete: - summary: Delete a pipeline - description: Marks a pipeline as deleted. - x-token-cost: 3 - operationId: deletePipeline - tags: - - Pipelines - security: - - api_key: [] - - oauth2: - - admin - parameters: - - in: path - name: id - description: The ID of the pipeline - required: true - schema: - type: integer - responses: - '200': - description: Delete pipeline - content: - application/json: - schema: - title: DeletePipelineResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - data: - type: object - properties: - id: - type: integer - description: Deleted Pipeline ID - example: - success: true - data: - id: 1 - get: - summary: Get one pipeline - description: Returns data about a specific pipeline. - x-token-cost: 1 - operationId: getPipeline - tags: - - Pipelines - security: - - api_key: [] - - oauth2: - - 'deals:read' - - 'deals:full' - - admin - parameters: - - in: path - name: id - description: The ID of the pipeline - required: true - schema: - type: integer - responses: - '200': - description: Get pipeline - content: - application/json: - schema: - type: object - title: UpsertPipelineResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertPipelineResponseData - properties: - data: - type: object - properties: - id: - type: integer - description: The ID of the pipeline - name: - type: string - description: The name of the pipeline - order_nr: - type: integer - description: Defines the order of pipelines. The pipeline with the lowest `order_nr` is considered the default. - is_selected: - type: boolean - description: Whether this pipeline is selected or not - is_deleted: - type: boolean - description: Whether this pipeline is marked as deleted or not - is_deal_probability_enabled: - type: boolean - description: Whether deal probability is disabled or enabled for this pipeline - add_time: - type: string - description: The pipeline creation time - update_time: - type: string - description: The pipeline update time - description: The pipeline object - example: - success: true - data: - id: 1 - name: Pipeline Name - order_nr: 1 - is_deleted: false - is_deal_probability_enabled: true - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - is_selected: true - patch: - summary: Update a pipeline - description: Updates the properties of a pipeline. - x-token-cost: 5 - operationId: updatePipeline - tags: - - Pipelines - security: - - api_key: [] - - oauth2: - - admin - parameters: - - in: path - name: id - description: The ID of the pipeline - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: The name of the pipeline - is_deal_probability_enabled: - type: boolean - default: false - description: Whether deal probability is disabled or enabled for this pipeline - responses: - '200': - description: Edit pipeline - content: - application/json: - schema: - type: object - title: UpsertPipelineResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - title: UpsertPipelineResponseData - properties: - data: - type: object - properties: - id: - type: integer - description: The ID of the pipeline - name: - type: string - description: The name of the pipeline - order_nr: - type: integer - description: Defines the order of pipelines. The pipeline with the lowest `order_nr` is considered the default. - is_selected: - type: boolean - description: Whether this pipeline is selected or not - is_deleted: - type: boolean - description: Whether this pipeline is marked as deleted or not - is_deal_probability_enabled: - type: boolean - description: Whether deal probability is disabled or enabled for this pipeline - add_time: - type: string - description: The pipeline creation time - update_time: - type: string - description: The pipeline update time - description: The pipeline object - example: - success: true - data: - id: 1 - name: Pipeline Name - order_nr: 1 - is_deleted: false - is_deal_probability_enabled: true - add_time: '2024-01-01T00:00:00Z' - update_time: '2024-01-01T00:00:00Z' - is_selected: true - '/users/{id}/followers': - get: - summary: List followers of a user - description: Lists users who are following the user. - x-token-cost: 10 - operationId: getUserFollowers - tags: - - Users - security: - - api_key: [] - - oauth2: - - 'users:read' - parameters: - - in: path - name: id - description: The ID of the user - required: true - schema: - type: integer - - in: query - name: limit - description: 'For pagination, the limit of entries to be returned. If not provided, 100 items will be returned. Please note that a maximum value of 500 is allowed.' - schema: - type: integer - example: 100 - - in: query - name: cursor - required: false - schema: - type: string - description: 'For pagination, the marker (an opaque string value) representing the first item on the next page' - responses: - '200': - description: List entity followers - content: - application/json: - schema: - type: object - title: GetFollowersResponse - allOf: - - title: baseResponse - type: object - properties: - success: - type: boolean - description: If the response is successful or not - - type: object - properties: - data: - type: array - description: Followers array - items: - type: object - title: FollowerItem - properties: - user_id: - type: integer - description: The ID of the user following the entity - add_time: - type: string - description: The add time of the following - additional_data: - type: object - description: The additional data of the list - properties: - next_cursor: - type: string - description: The first item on the next page. The value of the `next_cursor` field will be `null` if you have reached the end of the dataset and there’s no more pages to be returned. - example: - success: true - data: - - user_id: 1 - add_time: '2021-01-01T00:00:00Z' - additional_data: - next_cursor: eyJmaWVsZCI6ImlkIiwiZmllbGRWYWx1ZSI6Nywic29ydERpcmVjdGlvbiI6ImFzYyIsImlkIjo3fQ -components: - securitySchemes: - basic_authentication: - type: http - scheme: basic - description: 'Base 64 encoded string containing the `client_id` and `client_secret` values. The header value should be `Basic `.' - api_key: - type: apiKey - name: x-api-token - in: header - oauth2: - type: oauth2 - description: 'For more information, see https://pipedrive.readme.io/docs/marketplace-oauth-authorization' - flows: - authorizationCode: - authorizationUrl: 'https://oauth.pipedrive.com/oauth/authorize' - tokenUrl: 'https://oauth.pipedrive.com/oauth/token' - refreshUrl: 'https://oauth.pipedrive.com/oauth/token' - scopes: - base: Read settings of the authorized user and currencies in an account - 'deals:read': 'Read most of the data about deals and related entities - deal fields, products, followers, participants; all notes, files, filters, pipelines, stages, and statistics. Does not include access to activities (except the last and next activity related to a deal)' - 'deals:full': 'Create, read, update and delete deals, its participants and followers; all files, notes, and filters. It also includes read access to deal fields, pipelines, stages, and statistics. Does not include access to activities (except the last and next activity related to a deal)' - 'mail:read': Read mail threads and messages - 'mail:full': 'Read, update and delete mail threads. Also grants read access to mail messages' - 'activities:read': 'Read activities, its fields and types; all files and filters' - 'activities:full': 'Create, read, update and delete activities and all files and filters. Also includes read access to activity fields and types' - 'contacts:read': 'Read the data about persons and organizations, their related fields and followers; also all notes, files, filters' - 'contacts:full': 'Create, read, update and delete persons and organizations and their followers; all notes, files, filters. Also grants read access to contacts-related fields' - 'products:read': 'Read products, its fields, files, followers and products connected to a deal' - 'products:full': 'Create, read, update and delete products and its fields; add products to deals' - 'projects:read': 'Read projects and its fields, tasks and project templates' - 'projects:full': 'Create, read, update and delete projects and its fields; add projects templates and project related tasks' - 'users:read': 'Read data about users (people with access to a Pipedrive account), their permissions, roles and followers' - 'recents:read': 'Read all recent changes occurred in an account. Includes data about activities, activity types, deals, files, filters, notes, persons, organizations, pipelines, stages, products and users' - 'search:read': 'Search across the account for deals, persons, organizations, files and products, and see details about the returned results' - admin: 'Allows to do many things that an administrator can do in a Pipedrive company account - create, read, update and delete pipelines and its stages; deal, person and organization fields; activity types; users and permissions, etc. It also allows the app to create webhooks and fetch and delete webhooks that are created by the app' - 'leads:read': Read data about leads and lead labels - 'leads:full': 'Create, read, update and delete leads and lead labels' - phone-integration: 'Enables advanced call integration features like logging call duration and other metadata, and play call recordings inside Pipedrive' - 'goals:read': Read data on all goals - 'goals:full': 'Create, read, update and delete goals' - video-calls: Allows application to register as a video call integration provider and create conference links - messengers-integration: Allows application to register as a messengers integration provider and allows them to deliver incoming messages and their statuses \ No newline at end of file diff --git a/packages/v1-ready/pipedrive/test/Api.test.js b/packages/v1-ready/pipedrive/test/Api.test.js deleted file mode 100644 index 973fab2..0000000 --- a/packages/v1-ready/pipedrive/test/Api.test.js +++ /dev/null @@ -1,157 +0,0 @@ -const chai = require('chai'); -const {expect} = chai; -const should = chai.should(); -const {Api} = require('../api'); -const {mockApi} = require('../../../../test/utils/mockApi'); - -const MockedApi = mockApi(Api, { - authenticationMode: 'browser', - filteringScope: (url) => { - return /^https:[/][/].+[.]pipedrive[.]com/.test(url); - }, -}); - -describe('Pipedrive API class', async () => { - let api; - before(async function () { - await MockedApi.initialize({test: this.test}); - api = await MockedApi.mock(); - }); - - after(async function () { - await MockedApi.clean({test: this.test}); - }); - - describe('User', async () => { - it('should list user profile', async () => { - const response = await api.getUser(); - chai.assert.hasAllKeys(response.data, [ - 'id', - 'name', - 'company_country', - 'company_domain', - 'company_id', - 'company_name', - 'default_currency', - 'locale', - 'lang', - 'last_login', - 'language', - 'email', - 'phone', - 'created', - 'modified', - 'signup_flow_variation', - 'has_created_company', - 'is_admin', - 'active_flag', - 'timezone_name', - 'timezone_offset', - 'role_id', - 'icon_url', - 'is_you', - ]); - }); - }); - - describe('Deals', async () => { - it('should list deals', async () => { - const response = await api.listDeals(); - response.data.length.should.above(0); - response.data[0].should.have.property('id'); - return response; - }); - }); - - describe('Activities', async () => { - const mockActivity = {}; - it('should list all Activity Fields', async () => { - const response = await api.listActivityFields(); - const isRequired = response.data.filter( - (field) => field.mandatory_flag - ); - - for (const field of isRequired) { - mockActivity[field.key] = 'blah'; - } - }); - it('should create an email activity', async () => { - const activity = { - subject: 'Example Activtiy from the local grave', - type: 'email', - due_date: new Date('2021-12-03T15:06:38.700Z'), - user_id: '1811658', - }; - const response = await api.createActivity(activity); - response.success.should.equal(true); - }); - it('should get activities', async () => { - const response = await api.listActivities({ - query: { - user_id: 0, // Gets activities for all users, instead of just the auth'ed user - }, - }); - response.data[0].should.have.property('id'); - response.data.length.should.above(0); - return response; - }); - }); - - describe('Users', async () => { - it('should get users', async () => { - const response = await api.listUsers(); - response.data.should.be.an('array').of.length.greaterThan(0); - response.data[0].should.have.keys( - 'active_flag', - 'created', - 'default_currency', - 'email', - 'has_created_company', - 'icon_url', - 'id', - 'is_admin', - 'is_you', - 'lang', - 'last_login', - 'locale', - 'modified', - 'name', - 'phone', - 'role_id', - 'signup_flow_variation', - 'timezone_name', - 'timezone_offset' - ); - return response; - }); - }); - - describe('Bad Auth', async () => { - it('should refresh bad auth token', async () => { - // Needed to paste a valid JWT, otherwise it's testing the wrong error. - // TODO expand on other error types. - const badAccessToken = - 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZWFuLm1hdHRoZXdzQGxlZnRob29rLmNvbSIsImlhdCI6MTYzNTUzMDk3OCwiZXhwIjoxNjM1NTM4MTc4LCJiZW50byI6ImFwcDFlIiwiYWN0Ijp7InN1YiI6IlZob0NzMFNRZ25Fa2RDanRkaFZLemV5bXBjNW9valZoRXB2am03Rjh1UVEiLCJuYW1lIjoiTGVmdCBIb29rIiwiaXNzIjoiZmxhZ3NoaXAiLCJ0eXBlIjoiYXBwIn0sIm9yZ191c2VyX2lkIjoxLCJhdWQiOiJMZWZ0IEhvb2siLCJzY29wZXMiOiJBSkFBOEFIUUFCQUJRQT09Iiwib3JnX2d1aWQiOiJmNzY3MDEzZC1mNTBiLTRlY2QtYjM1My0zNWU0MWQ5Y2RjNGIiLCJvcmdfc2hvcnRuYW1lIjoibGVmdGhvb2tzYW5kYm94In0.XFmIai0GpAePsYeA4MjRntZS3iW6effmKmIhT7SBzTQ'; - api.access_token = badAccessToken; - - await api.listDeals(); - api.access_token.should.not.equal(badAccessToken); - }); - - it('should throw error with invalid refresh token', async () => { - try { - api.access_token = - 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZWFuLm1hdHRoZXdzQGxlZnRob29rLmNvbSIsImlhdCI6MTYzNTUzMDk3OCwiZXhwIjoxNjM1NTM4MTc4LCJiZW50byI6ImFwcDFlIiwiYWN0Ijp7InN1YiI6IlZob0NzMFNRZ25Fa2RDanRkaFZLemV5bXBjNW9valZoRXB2am03Rjh1UVEiLCJuYW1lIjoiTGVmdCBIb29rIiwiaXNzIjoiZmxhZ3NoaXAiLCJ0eXBlIjoiYXBwIn0sIm9yZ191c2VyX2lkIjoxLCJhdWQiOiJMZWZ0IEhvb2siLCJzY29wZXMiOiJBSkFBOEFIUUFCQUJRQT09Iiwib3JnX2d1aWQiOiJmNzY3MDEzZC1mNTBiLTRlY2QtYjM1My0zNWU0MWQ5Y2RjNGIiLCJvcmdfc2hvcnRuYW1lIjoibGVmdGhvb2tzYW5kYm94In0.XFmIai0GpAePsYeA4MjRntZS3iW6effmKmIhT7SBzTQ'; - api.refresh_token = 'nolongervalid'; - await api.listDeals(); - throw new Error('Expected error not thrown'); - } catch (e) { - e.message.should.contain( - '-----------------------------------------------------\n' + - 'An error ocurred while fetching an external resource.\n' + - '-----------------------------------------------------' - ); - } - }); - }); -}); diff --git a/packages/v1-ready/pipedrive/test/Manager.test.js b/packages/v1-ready/pipedrive/test/Manager.test.js deleted file mode 100644 index 32b386a..0000000 --- a/packages/v1-ready/pipedrive/test/Manager.test.js +++ /dev/null @@ -1,138 +0,0 @@ -const chai = require('chai'); -const {expect} = chai; -const PipedriveManager = require('../manager'); -const Authenticator = require('../../../../test/utils/Authenticator'); -const TestUtils = require('../../../../test/utils/TestUtils'); - -// eslint-disable-next-line no-only-tests/no-only-tests -describe('Pipedrive Manager', async () => { - let manager; - before(async () => { - this.userManager = await TestUtils.getLoggedInTestUserManagerInstance(); - - manager = await PipedriveManager.getInstance({ - userId: this.userManager.getUserId(), - }); - const res = await manager.getAuthorizationRequirements(); - - chai.assert.hasAnyKeys(res, ['url', 'type']); - const {url} = res; - const response = await Authenticator.oauth2(url); - const baseArr = response.base.split('/'); - response.entityType = baseArr[baseArr.length - 1]; - delete response.base; - - const ids = await manager.processAuthorizationCallback({ - userId: this.userManager.getUserId(), - data: response.data, - }); - chai.assert.hasAllKeys(ids, ['credential_id', 'entity_id', 'type']); - }); - - describe('getInstance tests', async () => { - it('should return a manager instance without credential or entity data', async () => { - const userId = this.userManager.getUserId(); - const freshManager = await PipedriveManager.getInstance({ - userId, - }); - expect(freshManager).to.haveOwnProperty('api'); - expect(freshManager).to.haveOwnProperty('userId'); - expect(freshManager.userId).to.equal(userId); - expect(freshManager.entity).to.be.undefined; - expect(freshManager.credential).to.be.undefined; - }); - - it('should return a manager instance with a credential ID', async () => { - const userId = this.userManager.getUserId(); - const freshManager = await PipedriveManager.getInstance({ - userId, - credentialId: manager.credential.id, - }); - expect(freshManager).to.haveOwnProperty('api'); - expect(freshManager).to.haveOwnProperty('userId'); - expect(freshManager.userId).to.equal(userId); - expect(freshManager.entity).to.be.undefined; - expect(freshManager.credential).to.exist; - }); - - it('should return a fresh manager instance with an entity ID', async () => { - const userId = this.userManager.getUserId(); - const freshManager = await PipedriveManager.getInstance({ - userId, - entityId: manager.entity.id, - }); - expect(freshManager).to.haveOwnProperty('api'); - expect(freshManager).to.haveOwnProperty('userId'); - expect(freshManager.userId).to.equal(userId); - expect(freshManager.entity).to.exist; - expect(freshManager.credential).to.exist; - }); - }); - - describe('getAuthorizationRequirements tests', async () => { - it('should return authorization requirements of username and password', async () => { - // Check authorization requirements - const res = await manager.getAuthorizationRequirements(); - expect(res.type).to.equal('oauth2'); - chai.assert.hasAllKeys(res, ['url', 'type']); - }); - }); - - describe('processAuthorizationCallback tests', async () => { - it('asserts that the original manager has a working credential', async () => { - const res = await manager.testAuth(); - expect(res).to.be.true; - }); - }); - - describe('getEntityOptions tests', async () => { - // NA - }); - - describe('findOrCreateEntity tests', async () => { - it('should create a new entity for the selected profile and attach to manager', async () => { - const userDetails = await manager.api.getUser(); - const entityRes = await manager.findOrCreateEntity({ - companyId: userDetails.data.company_id, - companyName: userDetails.data.company_name, - }); - - expect(entityRes.entity_id).to.exist; - }); - }); - describe('testAuth tests', async () => { - it('Should refresh token and update the credential with new token', async () => { - const badAccessToken = 'smith'; - manager.api.access_token = badAccessToken; - await manager.testAuth(); - - const posttoken = manager.api.access_token; - expect('smith').to.not.equal(posttoken); - const credential = await manager.credentialMO.get( - manager.entity.credential - ); - expect(credential.accessToken).to.equal(posttoken); - }); - }); - - describe('receiveNotification tests', async () => { - it('should fail to refresh token and mark auth as invalid', async () => { - // Need to use a valid but old refresh token, - // so we need to refresh first - const oldRefresh = manager.api.refresh_token; - const badAccessToken = - 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZWFuLm1hdHRoZXdzQGxlZnRob29rLmNvbSIsImlhdCI6MTYzNTUzMDk3OCwiZXhwIjoxNjM1NTM4MTc4LCJiZW50byI6ImFwcDFlIiwiYWN0Ijp7InN1YiI6IlZob0NzMFNRZ25Fa2RDanRkaFZLemV5bXBjNW9valZoRXB2am03Rjh1UVEiLCJuYW1lIjoiTGVmdCBIb29rIiwiaXNzIjoiZmxhZ3NoaXAiLCJ0eXBlIjoiYXBwIn0sIm9yZ191c2VyX2lkIjoxLCJhdWQiOiJMZWZ0IEhvb2siLCJzY29wZXMiOiJBSkFBOEFIUUFCQUJRQT09Iiwib3JnX2d1aWQiOiJmNzY3MDEzZC1mNTBiLTRlY2QtYjM1My0zNWU0MWQ5Y2RjNGIiLCJvcmdfc2hvcnRuYW1lIjoibGVmdGhvb2tzYW5kYm94In0.XFmIai0GpAePsYeA4MjRntZS3iW6effmKmIhT7SBzTQ'; - manager.api.access_token = badAccessToken; - await manager.testAuth(); - expect(manager.api.access_token).to.not.equal(badAccessToken); - manager.api.access_token = badAccessToken; - manager.api.refresh_token = undefined; - - const authTest = await manager.testAuth(); - const credential = await manager.credentialMO.get( - manager.entity.credential - ); - credential.auth_is_valid.should.equal(false); - }); - }); -}); diff --git a/packages/v1-ready/recharge/dist/jest-setup.js b/packages/v1-ready/recharge/dist/jest-setup.js deleted file mode 100644 index 1d49579..0000000 --- a/packages/v1-ready/recharge/dist/jest-setup.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; -require('dotenv').config(); -jest.setTimeout(30000); -global.console = { - ...console, - log: jest.fn(), - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), -}; -//# sourceMappingURL=jest-setup.js.map \ No newline at end of file diff --git a/packages/v1-ready/recharge/dist/jest-teardown.js b/packages/v1-ready/recharge/dist/jest-teardown.js deleted file mode 100644 index a538dac..0000000 --- a/packages/v1-ready/recharge/dist/jest-teardown.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -module.exports = async () => { -}; -//# sourceMappingURL=jest-teardown.js.map \ No newline at end of file diff --git a/packages/v1-ready/recharge/jest-setup.js b/packages/v1-ready/recharge/jest-setup.js deleted file mode 100644 index f624431..0000000 --- a/packages/v1-ready/recharge/jest-setup.js +++ /dev/null @@ -1,14 +0,0 @@ -require('dotenv').config(); - -// Increase timeout for API tests -jest.setTimeout(30000); - -// Mock console methods to reduce noise during tests -global.console = { - ...console, - log: jest.fn(), - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), -}; \ No newline at end of file diff --git a/packages/v1-ready/recharge/jest-teardown.js b/packages/v1-ready/recharge/jest-teardown.js deleted file mode 100644 index 45f34d5..0000000 --- a/packages/v1-ready/recharge/jest-teardown.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = async () => { - // Perform any global teardown here if needed - // For example: closing database connections, cleaning up test data, etc. -}; \ No newline at end of file diff --git a/packages/v1-ready/salesforce/LICENSE.md b/packages/v1-ready/salesforce/LICENSE.md deleted file mode 100644 index 77f5cc2..0000000 --- a/packages/v1-ready/salesforce/LICENSE.md +++ /dev/null @@ -1,16 +0,0 @@ -MIT License - -Copyright (c) 2022 Left Hook Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/v1-ready/salesforce/index.js b/packages/v1-ready/salesforce/index.js deleted file mode 100644 index 5b55a4f..0000000 --- a/packages/v1-ready/salesforce/index.js +++ /dev/null @@ -1,12 +0,0 @@ -const {Api} = require('./api'); -//const {Credential} = require('./models/credential'); -//const {Entity} = require('./models/entity'); -//const ModuleManager = require('./manager'); -const {Definition} = require('./definition'); -const Config = require('./defaultConfig'); - -module.exports = { - Api, - Definition, - Config, -}; diff --git a/packages/v1-ready/salesforce/jest-setup.js b/packages/v1-ready/salesforce/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/salesforce/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/salesforce/jest-teardown.js b/packages/v1-ready/salesforce/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/salesforce/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/salesforce/jest.config.js b/packages/v1-ready/salesforce/jest.config.js deleted file mode 100644 index 48f9fcc..0000000 --- a/packages/v1-ready/salesforce/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ -module.exports = { - // preset: '@friggframework/test-environment', - coverageThreshold: { - global: { - statements: 13, - branches: 0, - functions: 1, - lines: 13, - }, - }, - // A path to a module which exports an async function that is triggered once before all test suites - globalSetup: './jest-setup.js', - - // A path to a module which exports an async function that is triggered once after all test suites - globalTeardown: './jest-teardown.js', -}; diff --git a/packages/v1-ready/salesforce/manager.js b/packages/v1-ready/salesforce/manager.js deleted file mode 100644 index 4c2cf57..0000000 --- a/packages/v1-ready/salesforce/manager.js +++ /dev/null @@ -1,195 +0,0 @@ -const {debug, get, ModuleManager} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const Config = require('./defaultConfig.json'); - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - //------------------------------------------------------------ - // Required methods - static getName() { - return Config.name; - } - - static async getInstance(params) { - const instance = new this(params); - - // initializes the credentials and the Api - const salesforceParams = {delegate: instance}; - salesforceParams.client_id = process.env.SALESFORCE_CONSUMER_KEY; - salesforceParams.client_secret = process.env.SALESFORCE_CONSUMER_SECRET; - salesforceParams.redirect_uri = `${process.env.REDIRECT_URI}/salesforce`; - - if (params.entityId) { - try { - instance.entity = await Entity.findById(params.entityId); - const salesforceToken = await Credential.findById( - instance.entity.credential - ); - salesforceParams.access_token = salesforceToken.accessToken; - salesforceParams.refresh_token = salesforceToken.refreshToken; - salesforceParams.instanceUrl = salesforceToken.instanceUrl; - salesforceParams.isSandbox = instance.entity.isSandbox; - } catch (e) { - debug( - `Error retrieving Salesforce credential for Entity ${instance.entity.id}` - ); - } - } - - instance.api = await new Api(salesforceParams); - - return instance; - } - - async getAuthorizationRequirements() { - return { - url: await this.api.getAuthorizationUri(), - type: 'oauth2', - }; - } - - async testAuth() { - let validAuth = false; - try { - if (await this.api.find('Organization')) validAuth = true; - } catch (e) { - console.log(e); - } - return validAuth; - } - - async processAuthorizationCallback(params) { - const data = get(params, 'data'); - const code = get(data, 'code'); - let isSandbox = false; - - // try to get access token. - try { - await this.api.getAccessToken(code); - } catch (e) { - // If that fails, re-set API class as sandbox - // Then try again - console.log(e); - - this.api.resetToSandbox(); - await this.api.getAccessToken(code); - isSandbox = true; - } - - // Get Account details and save on Entity record to `name` and `externalId` field - // Get Username details too - const orgResponse = await this.api.find('Organization'); - const orgDetails = orgResponse[0]; - const sfUserResponse = await this.api.get( - 'User', - this.api.conn.userInfo.id - ); - - await this.findOrCreateEntity({ - name: orgDetails.Name, - externalId: orgDetails.Id, - isSandbox, - orgDetails, - sfUserResponse, - }); - return { - entity_id: this.entity.id, - credential_id: this.credential.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - const {name, externalId, isSandbox, orgDetails, sfUserResponse} = - params; - - const createObj = { - credential: this.credential.id, - user: this.userId, - name, - externalId, - isSandbox, - connectedUsername: sfUserResponse.Username, - }; - this.entity = await Entity.findOneAndUpdate( - { - user: this.userId, - externalId, - isSandbox, - }, - createObj, - { - new: true, - upsert: true, - setDefaultsOnInsert: true, - } - ); - } - - //------------------------------------------------------------ - - checkUserAuthorized() { - return this.api.isAuthenticated(); - } - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await Entity.findByUserId(this.userId); - if (entity.credential) { - await Credential.delete(entity.credential); - entity.credential = undefined; - entity.isSandbox = false; - await entity.save(); - } - } - - async sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - async receiveNotification(notifier, delegateString, object = null) { - try { - if (notifier instanceof Api) { - if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - console.log(`should update the token: ${object}`); - const updatedToken = { - accessToken: this.api.access_token, - refreshToken: this.api.refresh_token, - instanceUrl: this.api.instanceUrl, - }; - this.credential = await Credential.findOneAndUpdate( - { - user: this.userId, - instanceUrl: this.api.instanceUrl, - }, - updatedToken, - { - new: true, - upsert: true, - setDefaultsOnInsert: true, - } - ); - } - if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - console.log(this.checkUserAuthorized()); - } - } - } catch (e) { - console.log('error yo'); - } - } -} - -module.exports = Manager; diff --git a/packages/v1-ready/salesforce/models/credential.js b/packages/v1-ready/salesforce/models/credential.js deleted file mode 100644 index aacf056..0000000 --- a/packages/v1-ready/salesforce/models/credential.js +++ /dev/null @@ -1,20 +0,0 @@ -const {Credential: Parent, mongoose} = require('@friggframework/core'); - -const schema = new mongoose.Schema({ - accessToken: { - type: String, - required: true, - lhEncrypt: true, - }, - refreshToken: { - type: String, - required: true, - lhEncrypt: true, - }, - instanceUrl: {type: String, required: true}, -}); - -const name = 'SalesforceCredential'; -const Credential = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/v1-ready/salesforce/models/entity.js b/packages/v1-ready/salesforce/models/entity.js deleted file mode 100644 index add79af..0000000 --- a/packages/v1-ready/salesforce/models/entity.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Entity: Parent, mongoose} = require('@friggframework/core'); - -const schema = new mongoose.Schema({ - isSandbox: Boolean, - connectedUsername: String, -}); - -const name = 'SalesforceEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; - -module.exports = {Entity}; diff --git a/packages/v1-ready/salesforce/streamHandler.js b/packages/v1-ready/salesforce/streamHandler.js deleted file mode 100644 index 031728f..0000000 --- a/packages/v1-ready/salesforce/streamHandler.js +++ /dev/null @@ -1,58 +0,0 @@ -const nforce = require('nforce'); -const {opportunityPushTopicName} = require('../../constants/StringConstants'); -// All the authenication is part of the configuration for a Connected App in Salesforce - -// consumerKey 3MVG9JEx.BE6yifNujwiP1J0_D6wmZhOtfCns9rCjTMvnlzHfpmbyd5wDTzerxNIOOB8ojv0jxdDwZYTsteJy -// consumer secret 737FBEAE5D1F202FE32552A03949F5AF6BA21D4DE44E83C95E5FE59CD9808CBC -const org = nforce.createConnection({ - clientId: - '3MVG9JEx.BE6yifNujwiP1J0_D6wmZhOtfCns9rCjTMvnlzHfpmbyd5wDTzerxNIOOB8ojv0jxdDwZYTsteJy', - clientSecret: - '737FBEAE5D1F202FE32552A03949F5AF6BA21D4DE44E83C95E5FE59CD9808CBC', - redirectUri: 'http://localhost:3000/oauth/callback', - // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - // Licensed under the Amazon Software License - // http://aws.amazon.com/asl/ - // environment:'sandbox', - apiVersion: 'v44.0', - mode: 'multi', // was single -}); -// const TOPIC = '/event/Raz_Test_Event__e';// 'OppCRUD__e'; -// const REPLAY_ID = -1; -// const USERNAME = 'ryan@coderden.com.salesrightappdev'; -// const PASSWORD = '5688razy'; -// SNS TOPIC -// const TOPIC_ARN = 'Opportunity'; -// exports.handler = function(event, context, callback) {/**/ -// authenticate via oauth process to SFDC -const oauth = { - access_token: - '00D3t00000108T1!AQYAQKCQS8FXTdeFcujm714SovEvshEn7V7nyDibNus5JP.47HsUhgR5uaWinmYSRiF37Wc1n1glcPk3AEUWXEByHO1XdULd', - instance_url: 'https://na123.salesforce.com', -}; -const client = org.createStreamClient({oauth}); -const accs = client.subscribe({ - topic: opportunityPushTopicName, - replayId: -1, - retry: -1, - oauth, -}); -console.log( - `Subscription to ${opportunityPushTopicName} supposedly successful for thing` -); -accs.on('error', (err) => { - console.log(`Error occurred, ${err}`); - client.disconnect(); -}); - -accs.on('data', (data) => { - console.log( - `PushTopic, ${opportunityPushTopicName} detected\nEvent:${JSON.stringify( - data - )}` - ); -}); -const exiting = () => { - console.log('Exiting'); -}; -setTimeout(exiting, 90000); diff --git a/packages/v1-ready/shopify/README.md b/packages/v1-ready/shopify/README.md deleted file mode 100644 index f8c6866..0000000 --- a/packages/v1-ready/shopify/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Shopify API Module - -This module provides API integration and Fenestra UI extension specifications for Shopify. - -## Fenestra UI Extensions - -This module includes comprehensive Fenestra specifications for Shopify UI extensibility. - -### Available Extension Types -See `fenestra/platform.fenestra.yaml` for complete specification. - -### Examples -Check `fenestra/examples/` directory for implementation examples. - -## Installation - -```bash -npm install @api-modules/shopify -``` - -## Usage - -```javascript -const shopifyAPI = require('@api-modules/shopify'); -``` - -## Fenestra Specifications - -- **Platform Spec**: `fenestra/platform.fenestra.yaml` -- **Examples**: `fenestra/examples/` -- **Schemas**: `fenestra/schemas/` diff --git a/packages/v1-ready/unbabel-projects/index.js b/packages/v1-ready/unbabel-projects/index.js deleted file mode 100644 index 9a423c0..0000000 --- a/packages/v1-ready/unbabel-projects/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Api} = require('./api'); -const {Credential} = require('./models/credential'); -const {Entity} = require('./models/entity'); -const ModuleManager = require('./manager'); -const Config = require('./defaultConfig.json'); - -module.exports = { - Api, - Credential, - Entity, - ModuleManager, - Config, -}; diff --git a/packages/v1-ready/unbabel-projects/jest-setup.js b/packages/v1-ready/unbabel-projects/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/unbabel-projects/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/unbabel-projects/jest-teardown.js b/packages/v1-ready/unbabel-projects/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/unbabel-projects/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/unbabel-projects/manager.js b/packages/v1-ready/unbabel-projects/manager.js deleted file mode 100644 index e97cd2a..0000000 --- a/packages/v1-ready/unbabel-projects/manager.js +++ /dev/null @@ -1,169 +0,0 @@ -const {ModuleManager, ModuleConstants, get} = require('@friggframework/core'); -const {Api} = require('./api'); -const {Entity} = require('./models/entity'); -const {Credential} = require('./models/credential'); -const config = require('./defaultConfig.json') - -class Manager extends ModuleManager { - static Entity = Entity; - static Credential = Credential; - - constructor(params) { - super(params); - } - - static getName() { - return config.name; - } - - static async getInstance(params) { - let instance = new this(params); - const managerParams = { - client_id: process.env.UNBABEL_PROJECTS_CLIENT_ID, - delegate: instance - }; - if (params.entityId) { - instance.entity = await Entity.findById(params.entityId); - instance.credential = await Credential.findById(instance.entity.credential); - } else if (params.credentialId) { - instance.credential = await Credential.findById(params.credentialId); - } - if (instance.credential) { - managerParams.access_token = instance.credential.access_token; - managerParams.refresh_token = instance.credential.refresh_token; - } - instance.api = await new Api(managerParams); - - return instance; - } - - // Change to whatever your api uses to return identifying information - async testAuth() { - let validAuth = false; - try { - if (await this.api.getSupportedExtensions()) validAuth = true; - } catch (e) { - flushDebugLog(e); - } - return validAuth; - } - - getAuthorizationRequirements(params) { - return { - url: null, - type: ModuleConstants.authType.oauth2, - }; - } - - - async processAuthorizationCallback(params) { - this.api.client_id = get(params, 'client_id', this.api.client_id); - this.api.customer_id = get(params, 'customer_id', this.api.customer_id); - this.api.username = get(params, 'username', this.api.username); - this.api.password = get(params, 'password', this.api.password); - await this.api.getTokenFromUsernamePassword(); - // get entity identifying information from the api. You'll need to format this. - const entityDetails = await this.api.getTokenIdentity(); - await this.findOrCreateEntity(entityDetails); - return { - credential_id: this.credential.id, - entity_id: this.entity.id, - type: Manager.getName(), - }; - } - - async findOrCreateEntity(params) { - // TODO this should be a changed to your entity needs - const identifier = get(params, 'identifier'); - const name = get(params, 'name'); - - const search = await Entity.find({ - user: this.userId, - externalId: identifier, - }); - if (search.length === 0) { - // validate choices!!! - // create entity - const createObj = { - credential: this.credential.id, - user: this.userId, - name, - externalId: identifier, - }; - this.entity = await Entity.create(createObj); - } else if (search.length === 1) { - this.entity = search[0]; - } else { - debug(`Multiple entities found with the same external ID: ${identifier}`); - this.throwException(`Multiple entities found with the same external ID: ${identifier}`); - } - } - - async receiveNotification(notifier, delegateString, object = null) { - if (!(notifier instanceof Api)) { - // no-op - } else if (delegateString === this.api.DLGT_TOKEN_UPDATE) { - await this.updateOrCreateCredential(); - } else if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) { - await this.deauthorize(); - } else if (delegateString === this.api.DLGT_INVALID_AUTH) { - await this.markCredentialsInvalid(); - } - } - - async updateOrCreateCredential() { - const userDetails = await this.api.getTokenIdentity(); - const updatedToken = { - user: this.userId.toString(), - auth_is_valid: true, - }; - if (this.access_token) { - updatedToken.access_token = this.access_token - } - if (this.refresh_token) { - updatedToken.refresh_token = this.refresh_token - } - - // search for a credential for this user and identifier - // skip if we already have a credential - if (!this.credential) { - const credentialSearch = await Credential.find({ - identifier: userDetails.identifier - }) - if (credentialSearch.length > 1) { - debug(`Multiple credentials found with same identifier: ${userDetails.identifier}`); - this.throwException(`Multiple credentials found with same identifier: ${userDetails.identifier}`); - } else if (credentialSearch === 1 && credentialSearch[0].user !== this.userId) { - debug(`A credential already exists with this identifier: ${userDetails.identifier}`); - this.throwException(`A credential already exists with this identifier: ${userDetails.identifier}`); - } else if (credentialSearch === 1) { - // found exactly one credential with this identifier - this.credential = credentialSearch[0]; - } else { - // found no credential with this identifier (match none for insert) - this.credential = {$exists: false}; - } - } - // update credential or create if none was found - this.credential = await Credential.findOneAndUpdate( - {_id: this.credential}, - {$set: updatedToken}, - {useFindAndModify: true, new: true, upsert: true} - ); - } - - async deauthorize() { - // wipe api connection - this.api = new Api(); - - // delete credentials from the database - const entity = await Entity.getByUserId(this.userId); - if (entity.credential) { - await Credential.delete(entity.credential); - entity.credential = undefined; - await entity.save(); - } - } -} - -module.exports = Manager; diff --git a/packages/v1-ready/unbabel-projects/models/credential.js b/packages/v1-ready/unbabel-projects/models/credential.js deleted file mode 100644 index ad1254f..0000000 --- a/packages/v1-ready/unbabel-projects/models/credential.js +++ /dev/null @@ -1,17 +0,0 @@ -const {Credential: Parent, mongoose} = require('@friggframework/core'); -const schema = new mongoose.Schema({ - access_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - refresh_token: { - type: String, - trim: true, - lhEncrypt: true, - }, - expires_at: {type: Number}, -}); -const name = 'UnbabelProjectsCredential'; -const Credential = Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Credential}; diff --git a/packages/v1-ready/unbabel-projects/models/entity.js b/packages/v1-ready/unbabel-projects/models/entity.js deleted file mode 100644 index 36bbc86..0000000 --- a/packages/v1-ready/unbabel-projects/models/entity.js +++ /dev/null @@ -1,7 +0,0 @@ -const {Entity: Parent, mongoose} = require('@friggframework/core'); - -const schema = new mongoose.Schema({}); -const name = 'UnbabelProjectsEntity'; -const Entity = - Parent.discriminators?.[name] || Parent.discriminator(name, schema); -module.exports = {Entity}; diff --git a/packages/v1-ready/unbabel/.eslintrc.json b/packages/v1-ready/unbabel/.eslintrc.json deleted file mode 100644 index 49541d6..0000000 --- a/packages/v1-ready/unbabel/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@friggframework/eslint-config" -} diff --git a/packages/v1-ready/unbabel/jest-setup.js b/packages/v1-ready/unbabel/jest-setup.js deleted file mode 100644 index 9dd3e0d..0000000 --- a/packages/v1-ready/unbabel/jest-setup.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -module.exports = globalSetup; diff --git a/packages/v1-ready/unbabel/jest-teardown.js b/packages/v1-ready/unbabel/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/unbabel/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/unbabel/jest.config.js b/packages/v1-ready/unbabel/jest.config.js deleted file mode 100644 index cc9441f..0000000 --- a/packages/v1-ready/unbabel/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ - -module.exports = { - // preset: '@friggframework/test-environment', - coverageThreshold: { - global: { - statements: 13, - branches: 0, - functions: 1, - lines: 13, - }, - }, - // A path to a module which exports an async function that is triggered once before all test suites - globalSetup: './jest-setup.js', - - // A path to a module which exports an async function that is triggered once after all test suites - globalTeardown: './jest-teardown.js', -}; diff --git a/packages/v1-ready/zoho-crm/jest-setup.js b/packages/v1-ready/zoho-crm/jest-setup.js deleted file mode 100644 index 9d3161d..0000000 --- a/packages/v1-ready/zoho-crm/jest-setup.js +++ /dev/null @@ -1,3 +0,0 @@ -const {globalSetup} = require('@friggframework/test'); -require('dotenv').config(); -module.exports = globalSetup; diff --git a/packages/v1-ready/zoho-crm/jest-teardown.js b/packages/v1-ready/zoho-crm/jest-teardown.js deleted file mode 100644 index 5bc7251..0000000 --- a/packages/v1-ready/zoho-crm/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const {globalTeardown} = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/zoom/.eslintrc.json b/packages/v1-ready/zoom/.eslintrc.json deleted file mode 100644 index 49541d6..0000000 --- a/packages/v1-ready/zoom/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@friggframework/eslint-config" -} diff --git a/packages/v1-ready/zoom/LICENSE.md b/packages/v1-ready/zoom/LICENSE.md deleted file mode 100644 index 77f5cc2..0000000 --- a/packages/v1-ready/zoom/LICENSE.md +++ /dev/null @@ -1,16 +0,0 @@ -MIT License - -Copyright (c) 2022 Left Hook Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/v1-ready/zoom/jest-setup.js b/packages/v1-ready/zoom/jest-setup.js deleted file mode 100644 index ec2bfca..0000000 --- a/packages/v1-ready/zoom/jest-setup.js +++ /dev/null @@ -1,9 +0,0 @@ -const { globalSetup } = require('@friggframework/test'); - -module.exports = () => { - globalSetup(); - - process.env.ZOOM_CLIENT_ID = 'ZOOM_CLIENT_ID'; - process.env.ZOOM_CLIENT_SECRET = 'ZOOM_CLIENT_SECRET'; - process.env.REDIRECT_URI = 'http://localhost:3000'; -}; diff --git a/packages/v1-ready/zoom/jest-teardown.js b/packages/v1-ready/zoom/jest-teardown.js deleted file mode 100644 index d0c6426..0000000 --- a/packages/v1-ready/zoom/jest-teardown.js +++ /dev/null @@ -1,2 +0,0 @@ -const { globalTeardown } = require('@friggframework/test'); -module.exports = globalTeardown; diff --git a/packages/v1-ready/zoom/jest.config.js b/packages/v1-ready/zoom/jest.config.js deleted file mode 100644 index 48f9fcc..0000000 --- a/packages/v1-ready/zoom/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ -module.exports = { - // preset: '@friggframework/test-environment', - coverageThreshold: { - global: { - statements: 13, - branches: 0, - functions: 1, - lines: 13, - }, - }, - // A path to a module which exports an async function that is triggered once before all test suites - globalSetup: './jest-setup.js', - - // A path to a module which exports an async function that is triggered once after all test suites - globalTeardown: './jest-teardown.js', -}; diff --git a/packages/vonage/README.md b/packages/vonage/README.md new file mode 100644 index 0000000..408076d --- /dev/null +++ b/packages/vonage/README.md @@ -0,0 +1,227 @@ +# Vonage API Module + +A comprehensive Vonage (formerly Nexmo) API module for the Frigg framework, providing voice calls, SMS messaging, number management, and verification services. + +## Features + +- **SMS Messaging**: Send SMS, Unicode, and binary messages +- **Voice Calls**: Make and manage voice calls with NCCO support +- **Number Management**: Search, buy, and manage phone numbers +- **Verify API**: Two-factor authentication and phone verification +- **Number Insight**: Get detailed information about phone numbers +- **Account Management**: Balance, pricing, and account settings +- **Messages v2**: Advanced messaging with WhatsApp, Viber, Facebook Messenger +- **Webhooks**: Handle inbound messages and delivery receipts +- **Call Control**: Real-time call manipulation (mute, transfer, etc.) + +## Installation + +```bash +npm install @friggframework/api-module-vonage +``` + +## Environment Variables + +```env +VONAGE_API_KEY=your_api_key +VONAGE_API_SECRET=your_api_secret +VONAGE_APPLICATION_ID=your_application_id +VONAGE_PRIVATE_KEY=your_private_key_path_or_content +VONAGE_SIGNATURE_SECRET=your_signature_secret +``` + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-vonage'); + +const vonageApi = new Api({ + api_key: process.env.VONAGE_API_KEY, + api_secret: process.env.VONAGE_API_SECRET, + application_id: process.env.VONAGE_APPLICATION_ID, + private_key: process.env.VONAGE_PRIVATE_KEY +}); + +// Send SMS +await vonageApi.sendSMS('ACME Corp', '1234567890', 'Hello from Vonage!'); + +// Make a voice call +await vonageApi.makeCall( + '1234567890', + '0987654321', + 'https://your-domain.com/webhooks/answer' +); + +// Send verification code +const verification = await vonageApi.sendVerification('1234567890', 'YourApp'); +console.log('Request ID:', verification.request_id); + +// Check verification code +const result = await vonageApi.checkVerification(verification.request_id, '123456'); + +// Get number insights +const insights = await vonageApi.getAdvancedNumberInsight('1234567890'); +``` + +## Key Methods + +### SMS Messaging +- `sendSMS(from, to, text, options)` - Send SMS message +- `sendUnicodeSMS(from, to, text, options)` - Send Unicode SMS +- `sendBinarySMS(from, to, body, udh, options)` - Send binary SMS + +### Voice Calls +- `makeCall(from, to, answerUrl, options)` - Initiate voice call +- `getCalls(options)` - Get call history +- `getCall(callId)` - Get specific call details +- `updateCall(callId, action)` - Update call in progress +- `hangupCall(callId)` - End call +- `muteCall(callId)` - Mute call +- `unmuteCall(callId)` - Unmute call +- `transferCall(callId, destination)` - Transfer call + +### Verify API +- `sendVerification(number, brand, options)` - Start verification +- `checkVerification(requestId, code)` - Verify code +- `cancelVerification(requestId)` - Cancel verification +- `searchVerification(requestId)` - Check verification status + +### Number Insight +- `getBasicNumberInsight(number, options)` - Basic number info +- `getStandardNumberInsight(number, options)` - Standard number info +- `getAdvancedNumberInsight(number, callback, options)` - Advanced number info +- `getNumberInsight(number, features, options)` - Custom features + +### Number Management +- `searchNumbers(country, options)` - Search available numbers +- `getOwnNumbers(options)` - Get your numbers +- `buyNumber(country, msisdn, options)` - Purchase number +- `cancelNumber(country, msisdn, options)` - Release number +- `updateNumber(country, msisdn, options)` - Update number settings + +### Account Management +- `getBalance()` - Get account balance +- `getPricing(country, options)` - Get pricing info +- `getSMSPricing(country, options)` - Get SMS pricing +- `getVoicePricing(country, options)` - Get voice pricing +- `getAccountSettings()` - Get account settings +- `updateAccountSettings(params)` - Update account settings + +### Messages v2 (Advanced Messaging) +- `sendTextMessage(from, to, text, options)` - Send text via Messages API +- `sendImageMessage(from, to, imageUrl, caption, options)` - Send image +- `sendFileMessage(from, to, fileUrl, caption, options)` - Send file +- `sendTemplateMessage(from, to, templateName, parameters, options)` - Send template + +### Webhook Handling +- `handleWebhook(body, signature)` - Process webhook data +- `verifyWebhookSignature(body, signature)` - Verify webhook signature + +## Authentication Methods + +Vonage uses different authentication methods for different APIs: + +### API Key & Secret +Used for SMS, Verify, Number Insight, and Number Management: +```javascript +// Automatically added to requests +{ + api_key: 'your_api_key', + api_secret: 'your_api_secret' +} +``` + +### JWT (JSON Web Token) +Used for Voice API, Messages v2, and Conversations: +```javascript +// Automatically generated and added as Bearer token +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +## Voice Call Control + +### Call Actions +Control calls in real-time: +```javascript +// During a call, you can: +await vonageApi.muteCall(callId); // Mute the call +await vonageApi.unmuteCall(callId); // Unmute the call +await vonageApi.earmuffCall(callId); // Prevent caller from hearing +await vonageApi.unearmuffCall(callId); // Restore caller hearing +await vonageApi.transferCall(callId, { // Transfer to another number + type: 'ncco', + url: 'https://example.com/new-ncco' +}); +``` + +### NCCO (Nexmo Call Control Object) +Define call behavior with NCCO: +```javascript +const ncco = [ + { + action: 'talk', + text: 'Please wait while we connect your call' + }, + { + action: 'connect', + endpoint: [{ + type: 'phone', + number: '1234567890' + }] + } +]; +``` + +## Number Verification + +Two-factor authentication flow: +```javascript +// 1. Start verification +const verification = await vonageApi.sendVerification('1234567890', 'YourApp', { + length: 6, + locale: 'en-us', + pin_expiry: 300 +}); + +// 2. User receives SMS with code +// 3. Verify the code +const result = await vonageApi.checkVerification(verification.request_id, userEnteredCode); + +if (result.status === '0') { + console.log('Verification successful!'); +} else { + console.log('Verification failed:', result.error_text); +} +``` + +## Webhook Events + +Handle various webhook events: +- **Inbound SMS**: Receive SMS messages +- **Delivery Receipts**: SMS delivery status +- **Voice Events**: Call status updates +- **Verification**: Verification status updates + +```javascript +app.post('/webhooks/vonage', async (req, res) => { + const event = await vonageApi.handleWebhook(req.body, req.headers['authorization']); + + switch (event.type) { + case 'inbound_message': + console.log('Received SMS:', event.data.text); + break; + case 'voice': + console.log('Call event:', event.data.status); + break; + case 'verify': + console.log('Verification event:', event.data.status); + break; + } + + res.status(200).send('OK'); +}); +``` + +## Error Handling + +Comprehensive error handling with Vonage API error codes and descriptions for debugging delivery and authentication issues. \ No newline at end of file diff --git a/packages/vonage/api.js b/packages/vonage/api.js new file mode 100644 index 0000000..6ed3f04 --- /dev/null +++ b/packages/vonage/api.js @@ -0,0 +1,621 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); + +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + this.api_key = get(params, 'api_key', null); + this.api_secret = get(params, 'api_secret', null); + this.application_id = get(params, 'application_id', null); + this.private_key = get(params, 'private_key', null); + this.signature_secret = get(params, 'signature_secret', null); + + this.baseUrl = 'https://api.nexmo.com'; + this.baseUrlV2 = 'https://api.nexmo.com/v2'; + this.restBaseUrl = 'https://rest.nexmo.com'; + + this.URLs = { + // SMS + sms: '/sms/json', + + // Voice + voice: '/v1/calls', + voiceById: (id) => `/v1/calls/${id}`, + voiceActions: (id) => `/v1/calls/${id}/actions`, + + // Verify + verify: '/verify/json', + verifyCheck: '/verify/check/json', + verifyControl: '/verify/control/json', + verifySearch: '/verify/search/json', + + // Number Insight + numberInsight: '/number/insight/json', + numberInsightBasic: '/ni/basic/json', + numberInsightStandard: '/ni/standard/json', + numberInsightAdvanced: '/ni/advanced/json', + + // Numbers + numbers: '/number/search/json', + numbersOwn: '/account/numbers/json', + numbersBuy: '/number/buy/json', + numbersCancel: '/number/cancel/json', + numbersUpdate: '/number/update/json', + + // Account + account: '/account/get-balance/json', + accountPricing: '/account/get-pricing/outbound/json', + accountSmsOutbound: '/account/get-pricing/outbound/sms/json', + accountVoiceOutbound: '/account/get-pricing/outbound/voice/json', + accountSettings: '/account/settings/json', + accountTopUp: '/account/top-up/json', + + // Messages v2 + messages: '/messages', + + // Conversations + conversations: '/conversations', + conversationById: (id) => `/conversations/${id}`, + conversationEvents: (id) => `/conversations/${id}/events`, + conversationMembers: (id) => `/conversations/${id}/members`, + conversationMemberById: (id, memberId) => `/conversations/${id}/members/${memberId}`, + + // Users + users: '/users', + userById: (id) => `/users/${id}`, + + // Applications + applications: '/applications', + applicationById: (id) => `/applications/${id}`, + + // Webhooks + webhooks: '/webhooks', + }; + } + + // Generate JWT for application authentication + generateJWT() { + if (!this.application_id || !this.private_key) { + throw new Error('Application ID and private key are required for JWT generation'); + } + + const payload = { + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour + application_id: this.application_id + }; + + return jwt.sign(payload, this.private_key, { algorithm: 'RS256' }); + } + + addAuthHeaders(headers = {}) { + // For JWT authentication (Voice, Messages, Conversations) + if (this.application_id && this.private_key) { + headers.Authorization = `Bearer ${this.generateJWT()}`; + } + return headers; + } + + addKeySecretAuth(params = {}) { + // For API key/secret authentication (SMS, Verify, Numbers) + if (this.api_key && this.api_secret) { + params.api_key = this.api_key; + params.api_secret = this.api_secret; + } + return params; + } + + async _request(url, options = {}) { + if (url.includes('/v1/calls') || url.includes('/messages') || url.includes('/conversations') || url.includes('/users')) { + // Use JWT auth for Voice, Messages, Conversations APIs + options.headers = this.addAuthHeaders(options.headers); + } else { + // Use API key/secret for SMS, Verify, Numbers APIs + if (options.body && typeof options.body === 'object') { + options.body = this.addKeySecretAuth(options.body); + } + if (options.query && typeof options.query === 'object') { + options.query = this.addKeySecretAuth(options.query); + } + } + + return super._request(url, options); + } + + // ************************** SMS Methods ********************************** + + async sendSMS(from, to, text, params = {}) { + const smsData = { + from, + to, + text, + ...params + }; + + const options = { + url: this.restBaseUrl + this.URLs.sms, + body: smsData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + // Convert body to URL-encoded format + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(smsData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + async sendUnicodeSMS(from, to, text, params = {}) { + return this.sendSMS(from, to, text, { ...params, type: 'unicode' }); + } + + async sendBinarySMS(from, to, body, udh, params = {}) { + return this.sendSMS(from, to, '', { ...params, type: 'binary', body, udh }); + } + + // ************************** Voice Methods ********************************** + + async makeCall(from, to, answer_url, params = {}) { + const callData = { + from: { type: 'phone', number: from }, + to: [{ type: 'phone', number: to }], + answer_url: [answer_url], + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.voice, + body: callData + }; + return this._post(options); + } + + async getCalls(params = {}) { + const options = { + url: this.baseUrl + this.URLs.voice, + query: params + }; + return this._get(options); + } + + async getCall(callId) { + const options = { + url: this.baseUrl + this.URLs.voiceById(callId), + }; + return this._get(options); + } + + async updateCall(callId, action) { + const options = { + url: this.baseUrl + this.URLs.voiceActions(callId), + body: { action } + }; + return this._put(options); + } + + async hangupCall(callId) { + return this.updateCall(callId, 'hangup'); + } + + async muteCall(callId) { + return this.updateCall(callId, 'mute'); + } + + async unmuteCall(callId) { + return this.updateCall(callId, 'unmute'); + } + + async earmuffCall(callId) { + return this.updateCall(callId, 'earmuff'); + } + + async unearmuffCall(callId) { + return this.updateCall(callId, 'unearmuff'); + } + + async transferCall(callId, destination) { + return this.updateCall(callId, { + action: 'transfer', + destination + }); + } + + // ************************** Verify Methods ********************************** + + async sendVerification(number, brand, params = {}) { + const verifyData = { + number, + brand, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.verify, + body: verifyData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(verifyData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + async checkVerification(request_id, code) { + const checkData = { + request_id, + code + }; + + const options = { + url: this.baseUrl + this.URLs.verifyCheck, + body: checkData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(checkData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + async cancelVerification(request_id) { + const controlData = { + request_id, + cmd: 'cancel' + }; + + const options = { + url: this.baseUrl + this.URLs.verifyControl, + body: controlData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(controlData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + async searchVerification(request_id) { + const options = { + url: this.baseUrl + this.URLs.verifySearch, + query: this.addKeySecretAuth({ request_id }) + }; + return this._get(options); + } + + // ************************** Number Insight Methods ********************************** + + async getNumberInsight(number, features = ['basic'], params = {}) { + const insightData = { + number, + features: features.join(','), + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.numberInsight, + query: this.addKeySecretAuth(insightData) + }; + return this._get(options); + } + + async getBasicNumberInsight(number, params = {}) { + const options = { + url: this.baseUrl + this.URLs.numberInsightBasic, + query: this.addKeySecretAuth({ number, ...params }) + }; + return this._get(options); + } + + async getStandardNumberInsight(number, params = {}) { + const options = { + url: this.baseUrl + this.URLs.numberInsightStandard, + query: this.addKeySecretAuth({ number, ...params }) + }; + return this._get(options); + } + + async getAdvancedNumberInsight(number, callback = null, params = {}) { + const insightData = { number, ...params }; + if (callback) insightData.callback = callback; + + const options = { + url: this.baseUrl + this.URLs.numberInsightAdvanced, + query: this.addKeySecretAuth(insightData) + }; + return this._get(options); + } + + // ************************** Number Management Methods ********************************** + + async searchNumbers(country, params = {}) { + const options = { + url: this.baseUrl + this.URLs.numbers, + query: this.addKeySecretAuth({ country, ...params }) + }; + return this._get(options); + } + + async getOwnNumbers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.numbersOwn, + query: this.addKeySecretAuth(params) + }; + return this._get(options); + } + + async buyNumber(country, msisdn, params = {}) { + const numberData = { + country, + msisdn, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.numbersBuy, + body: numberData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(numberData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + async cancelNumber(country, msisdn, params = {}) { + const numberData = { + country, + msisdn, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.numbersCancel, + body: numberData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(numberData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + async updateNumber(country, msisdn, params = {}) { + const numberData = { + country, + msisdn, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.numbersUpdate, + body: numberData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(numberData)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + // ************************** Account Methods ********************************** + + async getBalance() { + const options = { + url: this.baseUrl + this.URLs.account, + query: this.addKeySecretAuth() + }; + return this._get(options); + } + + async getPricing(country, params = {}) { + const options = { + url: this.baseUrl + this.URLs.accountPricing, + query: this.addKeySecretAuth({ country, ...params }) + }; + return this._get(options); + } + + async getSMSPricing(country, params = {}) { + const options = { + url: this.baseUrl + this.URLs.accountSmsOutbound, + query: this.addKeySecretAuth({ country, ...params }) + }; + return this._get(options); + } + + async getVoicePricing(country, params = {}) { + const options = { + url: this.baseUrl + this.URLs.accountVoiceOutbound, + query: this.addKeySecretAuth({ country, ...params }) + }; + return this._get(options); + } + + async getAccountSettings() { + const options = { + url: this.baseUrl + this.URLs.accountSettings, + query: this.addKeySecretAuth() + }; + return this._get(options); + } + + async updateAccountSettings(params) { + const options = { + url: this.baseUrl + this.URLs.accountSettings, + body: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const urlEncodedBody = new URLSearchParams(this.addKeySecretAuth(params)).toString(); + options.body = urlEncodedBody; + + return this._post(options, false); + } + + // ************************** Messages v2 Methods ********************************** + + async sendMessage(from, to, message, params = {}) { + const messageData = { + from, + to, + message, + ...params + }; + + const options = { + url: this.baseUrlV2 + this.URLs.messages, + body: messageData + }; + return this._post(options); + } + + async sendTextMessage(from, to, text, params = {}) { + const message = { + content: { + type: 'text', + text + } + }; + return this.sendMessage(from, to, message, params); + } + + async sendImageMessage(from, to, imageUrl, caption = '', params = {}) { + const message = { + content: { + type: 'image', + image: { + url: imageUrl, + caption + } + } + }; + return this.sendMessage(from, to, message, params); + } + + async sendFileMessage(from, to, fileUrl, caption = '', params = {}) { + const message = { + content: { + type: 'file', + file: { + url: fileUrl, + caption + } + } + }; + return this.sendMessage(from, to, message, params); + } + + async sendTemplateMessage(from, to, templateName, parameters = [], params = {}) { + const message = { + content: { + type: 'template', + template: { + name: templateName, + parameters + } + } + }; + return this.sendMessage(from, to, message, params); + } + + // ************************** Webhook Methods ********************************** + + async handleWebhook(body, signature = null) { + // Validate webhook signature if provided + if (signature && this.signature_secret) { + const expectedSignature = crypto.createHmac('sha256', this.signature_secret) + .update(JSON.stringify(body)) + .digest('hex'); + + if (signature !== expectedSignature) { + throw new Error('Invalid webhook signature'); + } + } + + // Process different webhook types + if (body.message_uuid) { + // Voice webhook + return { + type: 'voice', + data: body + }; + } else if (body.messageId || body.message_uuid) { + // SMS delivery receipt + return { + type: 'sms_delivery', + data: body + }; + } else if (body.request_id) { + // Verify webhook + return { + type: 'verify', + data: body + }; + } else if (body.from && body.to) { + // Inbound message + return { + type: 'inbound_message', + data: body + }; + } + + return { + type: 'unknown', + data: body + }; + } + + async verifyWebhookSignature(body, signature) { + if (!this.signature_secret) { + throw new Error('Signature secret not configured'); + } + + const expectedSignature = crypto.createHmac('sha256', this.signature_secret) + .update(typeof body === 'string' ? body : JSON.stringify(body)) + .digest('hex'); + + return signature === expectedSignature; + } + + // ************************** Error Handling ********************************** + + async handleError(error) { + if (error.response && error.response.data) { + const errorData = error.response.data; + return { + message: errorData.error_text || errorData.error || 'Unknown error', + status: errorData.status, + error_code: errorData.error_code + }; + } + return { + message: error.message || 'Unknown error occurred' + }; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/vonage/defaultConfig.json b/packages/vonage/defaultConfig.json new file mode 100644 index 0000000..281dbe4 --- /dev/null +++ b/packages/vonage/defaultConfig.json @@ -0,0 +1,9 @@ +{ + "name": "vonage", + "label": "Vonage", + "productUrl": "https://vonage.com", + "apiDocs": "https://developer.vonage.com", + "logoUrl": "https://friggframework.org/assets/img/vonage-icon.png", + "categories": ["Communication", "Voice", "SMS", "Messaging"], + "description": "Vonage API platform for voice calls, SMS, messaging, and number management." +} \ No newline at end of file diff --git a/packages/vonage/definition.js b/packages/vonage/definition.js new file mode 100644 index 0000000..ba26004 --- /dev/null +++ b/packages/vonage/definition.js @@ -0,0 +1,70 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Vonage', + requiredAuthMethods: { + getToken: async function (api, params) { + // Vonage uses API key/secret and JWT authentication + return { + access_token: api.api_key, + token_type: 'ApiKey' + }; + }, + + getEntityDetails: async function (api, userId) { + const balance = await api.getBalance(); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: api.api_key, + user: userId + }, + details: { + balance: balance.value, + autoReload: balance.autoReload, + api_key: api.api_key, + application_id: api.application_id + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['api_key', 'api_secret', 'private_key', 'signature_secret'], + entity: ['application_id'], + }, + + getCredentialDetails: async function (api, userId) { + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: api.api_key, + user: userId + }, + details: { + api_key: api.api_key, + api_secret: api.api_secret + }, + }; + }, + + testAuthRequest: function (api) { + return api.getBalance(); + }, + }, + env: { + api_key: process.env.VONAGE_API_KEY, + api_secret: process.env.VONAGE_API_SECRET, + application_id: process.env.VONAGE_APPLICATION_ID, + private_key: process.env.VONAGE_PRIVATE_KEY, + signature_secret: process.env.VONAGE_SIGNATURE_SECRET, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/vonage/index.js b/packages/vonage/index.js new file mode 100644 index 0000000..be08f56 --- /dev/null +++ b/packages/vonage/index.js @@ -0,0 +1,5 @@ +const Config = require('./defaultConfig.json'); +const { Definition } = require('./definition.js'); +const { Api } = require('./api.js'); + +module.exports = { Config, Definition, Api }; \ No newline at end of file diff --git a/packages/vonage/package.json b/packages/vonage/package.json new file mode 100644 index 0000000..e6e1494 --- /dev/null +++ b/packages/vonage/package.json @@ -0,0 +1,30 @@ +{ + "name": "@friggframework/api-module-vonage", + "version": "1.0.0", + "description": "Vonage API module for the Frigg framework", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "frigg", + "vonage", + "nexmo", + "voice", + "sms", + "api" + ], + "author": "Frigg Framework", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0", + "jsonwebtoken": "^9.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/packages/whatsapp-business/README.md b/packages/whatsapp-business/README.md new file mode 100644 index 0000000..181770e --- /dev/null +++ b/packages/whatsapp-business/README.md @@ -0,0 +1,118 @@ +# WhatsApp Business API Module + +A comprehensive WhatsApp Business API module for the Frigg framework, providing access to WhatsApp's Business messaging capabilities including templates, media, and customer communication. + +## Features + +- **Message Types**: Text, images, documents, audio, video, location, contacts +- **Templates**: Create and manage message templates for notifications +- **Interactive Messages**: Buttons, lists, quick replies +- **Media Management**: Upload, download, and manage media files +- **Business Profile**: Manage business profile information +- **Phone Numbers**: Manage WhatsApp Business phone numbers +- **Webhook Support**: Handle incoming messages and status updates +- **Analytics**: Message delivery and read receipts + +## Installation + +```bash +npm install @friggframework/api-module-whatsapp-business +``` + +## Environment Variables + +```env +WHATSAPP_ACCESS_TOKEN=your_access_token_here +WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id +WHATSAPP_BUSINESS_ACCOUNT_ID=your_business_account_id +WHATSAPP_API_VERSION=v18.0 +``` + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-whatsapp-business'); + +const whatsappApi = new Api({ + access_token: process.env.WHATSAPP_ACCESS_TOKEN, + phone_number_id: process.env.WHATSAPP_PHONE_NUMBER_ID, + whatsapp_business_account_id: process.env.WHATSAPP_BUSINESS_ACCOUNT_ID +}); + +// Send a text message +await whatsappApi.sendTextMessage('1234567890', 'Hello from WhatsApp!'); + +// Send an image with caption +await whatsappApi.sendImageMessage('1234567890', mediaId, 'Check this out!'); + +// Send a template message +await whatsappApi.sendTemplateMessage( + '1234567890', + 'hello_world', + 'en_US', + [] +); + +// Handle webhooks +const update = await whatsappApi.handleWebhook(webhookBody); +``` + +## Key Methods + +### Messaging +- `sendTextMessage(to, text, options)` - Send text message +- `sendImageMessage(to, imageId, caption, options)` - Send image +- `sendDocumentMessage(to, documentId, filename, caption, options)` - Send document +- `sendTemplateMessage(to, templateName, languageCode, components)` - Send template +- `sendButtonMessage(to, bodyText, buttons)` - Send interactive buttons +- `sendListMessage(to, bodyText, buttonText, sections)` - Send list + +### Media Management +- `uploadMedia(file, type, options)` - Upload media file +- `getMedia(mediaId)` - Get media information +- `downloadMedia(mediaUrl)` - Download media file +- `deleteMedia(mediaId)` - Delete media file + +### Templates +- `getMessageTemplates(options)` - Get all templates +- `createMessageTemplate(name, category, language, components)` - Create template +- `deleteMessageTemplate(templateId)` - Delete template + +### Business Profile +- `getBusinessProfile(fields)` - Get business profile +- `updateBusinessProfile(profileData)` - Update business profile + +### Webhook Management +- `subscribeToWebhooks(callbackUrl, verifyToken, fields)` - Subscribe to webhooks +- `handleWebhook(body)` - Process incoming webhooks +- `verifyWebhook(mode, token, challenge, verifyToken)` - Verify webhook + +### Status Management +- `markMessageAsRead(messageId)` - Mark message as read + +## Authentication + +WhatsApp Business API uses Facebook access tokens. You need: +1. A Facebook App with WhatsApp Business API access +2. A WhatsApp Business Account +3. A phone number registered with WhatsApp Business +4. Access tokens from Facebook Developer Console + +## Webhook Handling + +The module handles various webhook types: +- Incoming messages (text, media, interactive responses) +- Message status updates (sent, delivered, read, failed) +- Account updates + +## Message Templates + +WhatsApp requires pre-approved templates for certain message types. The module supports: +- Text templates with variables +- Media templates +- Interactive templates +- Location templates + +## Error Handling + +Comprehensive error handling with detailed Facebook API error responses including error codes, messages, and trace IDs for debugging. \ No newline at end of file diff --git a/packages/whatsapp-business/api.js b/packages/whatsapp-business/api.js new file mode 100644 index 0000000..778f7b7 --- /dev/null +++ b/packages/whatsapp-business/api.js @@ -0,0 +1,433 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); +const FormData = require('form-data'); +const fs = require('fs'); + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.access_token = get(params, 'access_token', null); + this.phone_number_id = get(params, 'phone_number_id', null); + this.whatsapp_business_account_id = get(params, 'whatsapp_business_account_id', null); + this.api_version = get(params, 'api_version', 'v18.0'); + + this.baseUrl = `https://graph.facebook.com/${this.api_version}`; + + this.URLs = { + // Messages + messages: `/${this.phone_number_id}/messages`, + + // Media + media: `/${this.phone_number_id}/media`, + mediaById: (mediaId) => `/${mediaId}`, + + // Phone Numbers + phoneNumbers: `/${this.whatsapp_business_account_id}/phone_numbers`, + phoneNumberById: (phoneNumberId) => `/${phoneNumberId}`, + + // Message Templates + messageTemplates: `/${this.whatsapp_business_account_id}/message_templates`, + messageTemplateById: (templateId) => `/${templateId}`, + + // Webhooks + webhooks: `/${this.whatsapp_business_account_id}/subscribed_apps`, + + // Business Profile + businessProfile: `/${this.phone_number_id}/whatsapp_business_profile`, + + // Account + account: `/${this.whatsapp_business_account_id}`, + }; + } + + addAuthHeaders(headers = {}) { + if (this.access_token) { + headers.Authorization = `Bearer ${this.access_token}`; + } + return headers; + } + + async _request(url, options = {}) { + options.headers = this.addAuthHeaders(options.headers); + return super._request(url, options); + } + + // ************************** Message Methods ********************************** + + async sendMessage(to, message, params = {}) { + const messageData = { + messaging_product: 'whatsapp', + to, + ...message, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.messages, + body: messageData + }; + return this._post(options); + } + + async sendTextMessage(to, text, params = {}) { + const message = { + type: 'text', + text: { body: text } + }; + return this.sendMessage(to, message, params); + } + + async sendImageMessage(to, imageId, caption = '', params = {}) { + const message = { + type: 'image', + image: { + id: imageId, + caption + } + }; + return this.sendMessage(to, message, params); + } + + async sendDocumentMessage(to, documentId, filename = '', caption = '', params = {}) { + const message = { + type: 'document', + document: { + id: documentId, + filename, + caption + } + }; + return this.sendMessage(to, message, params); + } + + async sendAudioMessage(to, audioId, params = {}) { + const message = { + type: 'audio', + audio: { id: audioId } + }; + return this.sendMessage(to, message, params); + } + + async sendVideoMessage(to, videoId, caption = '', params = {}) { + const message = { + type: 'video', + video: { + id: videoId, + caption + } + }; + return this.sendMessage(to, message, params); + } + + async sendLocationMessage(to, latitude, longitude, name = '', address = '', params = {}) { + const message = { + type: 'location', + location: { + latitude, + longitude, + name, + address + } + }; + return this.sendMessage(to, message, params); + } + + async sendContactMessage(to, contact, params = {}) { + const message = { + type: 'contacts', + contacts: [contact] + }; + return this.sendMessage(to, message, params); + } + + async sendTemplateMessage(to, templateName, languageCode, components = [], params = {}) { + const message = { + type: 'template', + template: { + name: templateName, + language: { code: languageCode }, + components + } + }; + return this.sendMessage(to, message, params); + } + + async sendInteractiveMessage(to, interactive, params = {}) { + const message = { + type: 'interactive', + interactive + }; + return this.sendMessage(to, message, params); + } + + async sendButtonMessage(to, bodyText, buttons, params = {}) { + const interactive = { + type: 'button', + body: { text: bodyText }, + action: { buttons } + }; + return this.sendInteractiveMessage(to, interactive, params); + } + + async sendListMessage(to, bodyText, buttonText, sections, params = {}) { + const interactive = { + type: 'list', + body: { text: bodyText }, + action: { + button: buttonText, + sections + } + }; + return this.sendInteractiveMessage(to, interactive, params); + } + + // ************************** Media Methods ********************************** + + async uploadMedia(file, type, params = {}) { + const form = new FormData(); + form.append('file', fs.createReadStream(file)); + form.append('type', type); + form.append('messaging_product', 'whatsapp'); + + Object.keys(params).forEach(key => { + form.append(key, params[key]); + }); + + const options = { + url: this.baseUrl + this.URLs.media, + body: form, + headers: { + ...form.getHeaders(), + ...this.addAuthHeaders() + } + }; + return this._post(options, false); + } + + async getMedia(mediaId) { + const options = { + url: this.baseUrl + this.URLs.mediaById(mediaId), + }; + return this._get(options); + } + + async deleteMedia(mediaId) { + const options = { + url: this.baseUrl + this.URLs.mediaById(mediaId), + }; + return this._delete(options); + } + + async downloadMedia(mediaUrl) { + const options = { + url: mediaUrl, + headers: this.addAuthHeaders() + }; + return this._get(options); + } + + // ************************** Template Methods ********************************** + + async getMessageTemplates(params = {}) { + const options = { + url: this.baseUrl + this.URLs.messageTemplates, + query: params + }; + return this._get(options); + } + + async createMessageTemplate(name, category, language, components, params = {}) { + const templateData = { + name, + category, + language, + components, + ...params + }; + + const options = { + url: this.baseUrl + this.URLs.messageTemplates, + body: templateData + }; + return this._post(options); + } + + async getMessageTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.messageTemplateById(templateId), + }; + return this._get(options); + } + + async deleteMessageTemplate(templateId) { + const options = { + url: this.baseUrl + this.URLs.messageTemplateById(templateId), + }; + return this._delete(options); + } + + // ************************** Phone Number Methods ********************************** + + async getPhoneNumbers(params = {}) { + const options = { + url: this.baseUrl + this.URLs.phoneNumbers, + query: params + }; + return this._get(options); + } + + async getPhoneNumber(phoneNumberId) { + const options = { + url: this.baseUrl + this.URLs.phoneNumberById(phoneNumberId), + }; + return this._get(options); + } + + async updatePhoneNumber(phoneNumberId, params) { + const options = { + url: this.baseUrl + this.URLs.phoneNumberById(phoneNumberId), + body: params + }; + return this._post(options); + } + + // ************************** Business Profile Methods ********************************** + + async getBusinessProfile(fields = []) { + const options = { + url: this.baseUrl + this.URLs.businessProfile, + query: fields.length > 0 ? { fields: fields.join(',') } : {} + }; + return this._get(options); + } + + async updateBusinessProfile(profileData) { + const options = { + url: this.baseUrl + this.URLs.businessProfile, + body: profileData + }; + return this._post(options); + } + + // ************************** Account Methods ********************************** + + async getAccountInfo(fields = []) { + const options = { + url: this.baseUrl + this.URLs.account, + query: fields.length > 0 ? { fields: fields.join(',') } : {} + }; + return this._get(options); + } + + // ************************** Webhook Methods ********************************** + + async subscribeToWebhooks(callbackUrl, verifyToken, fields = []) { + const subscriptionData = { + object: 'whatsapp_business_account', + callback_url: callbackUrl, + verify_token: verifyToken, + fields: fields.join(',') + }; + + const options = { + url: this.baseUrl + this.URLs.webhooks, + body: subscriptionData + }; + return this._post(options); + } + + async getWebhookSubscriptions() { + const options = { + url: this.baseUrl + this.URLs.webhooks, + }; + return this._get(options); + } + + async unsubscribeFromWebhooks() { + const options = { + url: this.baseUrl + this.URLs.webhooks, + }; + return this._delete(options); + } + + // ************************** Webhook Handling ********************************** + + async handleWebhook(body) { + // Process incoming webhook data + const entry = body.entry?.[0]; + if (!entry) return null; + + const changes = entry.changes?.[0]; + if (!changes) return null; + + const value = changes.value; + if (!value) return null; + + if (value.messages && value.messages.length > 0) { + return { + type: 'message', + data: { + messages: value.messages, + contacts: value.contacts, + metadata: value.metadata + } + }; + } + + if (value.statuses && value.statuses.length > 0) { + return { + type: 'status', + data: { + statuses: value.statuses, + metadata: value.metadata + } + }; + } + + return { + type: 'unknown', + data: value + }; + } + + async verifyWebhook(mode, token, challenge, verifyToken) { + if (mode === 'subscribe' && token === verifyToken) { + return challenge; + } + throw new Error('Webhook verification failed'); + } + + // ************************** Message Status Methods ********************************** + + async markMessageAsRead(messageId) { + const options = { + url: this.baseUrl + this.URLs.messages, + body: { + messaging_product: 'whatsapp', + status: 'read', + message_id: messageId + } + }; + return this._post(options); + } + + // ************************** Error Handling ********************************** + + async handleError(error) { + if (error.response && error.response.data) { + const errorData = error.response.data.error; + return { + code: errorData.code, + message: errorData.message, + type: errorData.type, + error_subcode: errorData.error_subcode, + fbtrace_id: errorData.fbtrace_id + }; + } + return { + message: error.message || 'Unknown error occurred' + }; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/whatsapp-business/defaultConfig.json b/packages/whatsapp-business/defaultConfig.json new file mode 100644 index 0000000..b5408d2 --- /dev/null +++ b/packages/whatsapp-business/defaultConfig.json @@ -0,0 +1,9 @@ +{ + "name": "whatsapp-business", + "label": "WhatsApp Business", + "productUrl": "https://business.whatsapp.com", + "apiDocs": "https://developers.facebook.com/docs/whatsapp", + "logoUrl": "https://friggframework.org/assets/img/whatsapp-icon.png", + "categories": ["Communication", "Messaging", "Business"], + "description": "WhatsApp Business API for messaging, templates, media, and customer communication." +} \ No newline at end of file diff --git a/packages/whatsapp-business/definition.js b/packages/whatsapp-business/definition.js new file mode 100644 index 0000000..8bf74a5 --- /dev/null +++ b/packages/whatsapp-business/definition.js @@ -0,0 +1,68 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'WhatsAppBusiness', + requiredAuthMethods: { + getToken: async function (api, params) { + // WhatsApp Business uses Facebook access tokens + return { + access_token: api.access_token, + token_type: 'Bearer' + }; + }, + + getEntityDetails: async function (api, userId) { + const accountInfo = await api.getAccountInfo(['name', 'id']); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: accountInfo.id, + user: userId + }, + details: { + name: accountInfo.name, + phone_number_id: api.phone_number_id, + business_account_id: api.whatsapp_business_account_id + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['access_token'], + entity: ['phone_number_id', 'whatsapp_business_account_id'], + }, + + getCredentialDetails: async function (api, userId) { + const accountInfo = await api.getAccountInfo(['name', 'id']); + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: accountInfo.id, + user: userId + }, + details: { + access_token: api.access_token + }, + }; + }, + + testAuthRequest: function (api) { + return api.getAccountInfo(['id', 'name']); + }, + }, + env: { + access_token: process.env.WHATSAPP_ACCESS_TOKEN, + phone_number_id: process.env.WHATSAPP_PHONE_NUMBER_ID, + whatsapp_business_account_id: process.env.WHATSAPP_BUSINESS_ACCOUNT_ID, + api_version: process.env.WHATSAPP_API_VERSION || 'v18.0', + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/whatsapp-business/index.js b/packages/whatsapp-business/index.js new file mode 100644 index 0000000..be08f56 --- /dev/null +++ b/packages/whatsapp-business/index.js @@ -0,0 +1,5 @@ +const Config = require('./defaultConfig.json'); +const { Definition } = require('./definition.js'); +const { Api } = require('./api.js'); + +module.exports = { Config, Definition, Api }; \ No newline at end of file diff --git a/packages/whatsapp-business/package.json b/packages/whatsapp-business/package.json new file mode 100644 index 0000000..ad3dd95 --- /dev/null +++ b/packages/whatsapp-business/package.json @@ -0,0 +1,29 @@ +{ + "name": "@friggframework/api-module-whatsapp-business", + "version": "1.0.0", + "description": "WhatsApp Business API module for the Frigg framework", + "main": "index.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "frigg", + "whatsapp", + "business", + "messaging", + "api" + ], + "author": "Frigg Framework", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^1.0.0", + "form-data": "^4.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/packages/wise/api.js b/packages/wise/api.js new file mode 100644 index 0000000..3c29d70 --- /dev/null +++ b/packages/wise/api.js @@ -0,0 +1,294 @@ +const { Requester, get } = require('@friggframework/core'); +const axios = require('axios'); + +class Api extends Requester { + constructor(params = {}) { + super(params); + + this.apiToken = get(params, 'apiToken', null); + this.sandbox = get(params, 'sandbox', true); + this.profileId = get(params, 'profileId', null); + + this.baseUrl = this.sandbox + ? 'https://api.sandbox.transferwise.tech' + : 'https://api.wise.com'; + + this.client = axios.create({ + baseURL: this.baseUrl, + headers: { + 'Authorization': `Bearer ${this.apiToken}`, + 'Content-Type': 'application/json', + }, + }); + } + + // Helper method for API requests + async makeRequest(method, endpoint, data = null, params = null) { + try { + const response = await this.client({ + method, + url: endpoint, + data, + params, + }); + return response.data; + } catch (error) { + throw new Error(`Wise API Error: ${error.response?.data?.message || error.message}`); + } + } + + // Get user profiles + async getProfiles() { + return this.makeRequest('GET', '/v1/profiles'); + } + + // Set active profile + setProfile(profileId) { + this.profileId = profileId; + } + + // Get profile by ID + async getProfile(profileId = null) { + const id = profileId || this.profileId; + return this.makeRequest('GET', `/v1/profiles/${id}`); + } + + // Get multi-currency account balances + async getBalances(profileId = null) { + const id = profileId || this.profileId; + return this.makeRequest('GET', `/v4/profiles/${id}/balances?types=STANDARD`); + } + + // Get balance for specific currency + async getBalance(currency, profileId = null) { + const balances = await this.getBalances(profileId); + return balances.find(b => b.currency === currency); + } + + // Create a quote + async createQuote(params) { + const profileId = params.profileId || this.profileId; + const quoteData = { + sourceCurrency: params.sourceCurrency, + targetCurrency: params.targetCurrency, + sourceAmount: params.sourceAmount || null, + targetAmount: params.targetAmount || null, + profile: profileId, + payOut: params.payOut || 'BALANCE', + preferredPayIn: params.preferredPayIn || 'BALANCE', + }; + + return this.makeRequest('POST', '/v3/profiles/' + profileId + '/quotes', quoteData); + } + + // Get quote by ID + async getQuote(quoteId, profileId = null) { + const id = profileId || this.profileId; + return this.makeRequest('GET', `/v3/profiles/${id}/quotes/${quoteId}`); + } + + // List recipients + async getRecipients(currency = null, profileId = null) { + const id = profileId || this.profileId; + const params = currency ? { currency } : {}; + return this.makeRequest('GET', `/v1/accounts?profile=${id}`, null, params); + } + + // Get recipient by ID + async getRecipient(recipientId) { + return this.makeRequest('GET', `/v1/accounts/${recipientId}`); + } + + // Create recipient + async createRecipient(params) { + const profileId = params.profileId || this.profileId; + const recipientData = { + profile: profileId, + accountHolderName: params.accountHolderName, + currency: params.currency, + type: params.type || params.accountType, + details: params.details, + ownedByCustomer: params.ownedByCustomer !== false, + }; + + return this.makeRequest('POST', '/v1/accounts', recipientData); + } + + // Delete recipient + async deleteRecipient(recipientId) { + return this.makeRequest('DELETE', `/v1/accounts/${recipientId}`); + } + + // Get recipient requirements for a currency/country + async getRecipientRequirements(params) { + const queryParams = { + source: params.sourceCurrency, + target: params.targetCurrency, + sourceAmount: params.sourceAmount || 1000, + }; + + return this.makeRequest('GET', '/v1/account-requirements', null, queryParams); + } + + // Create a transfer + async createTransfer(params) { + const profileId = params.profileId || this.profileId; + const transferData = { + sourceCurrency: params.sourceCurrency, + targetCurrency: params.targetCurrency, + sourceAmount: params.sourceAmount || null, + targetAmount: params.targetAmount || null, + profile: profileId, + targetAccount: params.recipientId || params.targetAccount, + quote: params.quoteId, + customerTransactionId: params.customerTransactionId || `transfer-${Date.now()}`, + details: { + reference: params.reference || '', + transferPurpose: params.transferPurpose, + transferPurposeSubTransferPurpose: params.transferPurposeSubTransferPurpose, + sourceOfFunds: params.sourceOfFunds || 'verification.source.of.funds.other', + }, + }; + + return this.makeRequest('POST', '/v1/transfers', transferData); + } + + // Get transfer by ID + async getTransfer(transferId) { + return this.makeRequest('GET', `/v1/transfers/${transferId}`); + } + + // List transfers + async getTransfers(params = {}, profileId = null) { + const id = profileId || this.profileId; + const queryParams = { + profile: id, + limit: params.limit || 100, + offset: params.offset || 0, + status: params.status, + createdDateStart: params.createdDateStart, + createdDateEnd: params.createdDateEnd, + }; + + // Remove undefined values + Object.keys(queryParams).forEach(key => + queryParams[key] === undefined && delete queryParams[key] + ); + + return this.makeRequest('GET', '/v1/transfers', null, queryParams); + } + + // Cancel transfer + async cancelTransfer(transferId) { + return this.makeRequest('PUT', `/v1/transfers/${transferId}/cancel`); + } + + // Fund transfer (simulate payment in sandbox) + async fundTransfer(transferId, profileId = null) { + const id = profileId || this.profileId; + return this.makeRequest('POST', `/v3/profiles/${id}/transfers/${transferId}/payments`, { + type: 'BALANCE', + }); + } + + // Get transfer delivery time + async getDeliveryTime(params) { + const queryParams = { + sourceCurrency: params.sourceCurrency, + targetCurrency: params.targetCurrency, + payIn: params.payIn || 'BALANCE', + payOut: params.payOut || 'BALANCE', + }; + + return this.makeRequest('GET', '/v1/delivery-estimates', null, queryParams); + } + + // Get exchange rates + async getExchangeRates(source = null, target = null) { + const params = {}; + if (source) params.source = source; + if (target) params.target = target; + + return this.makeRequest('GET', '/v1/rates', null, params); + } + + // Get supported currencies + async getCurrencies() { + return this.makeRequest('GET', '/v1/currencies'); + } + + // Get currency pairs + async getCurrencyPairs() { + return this.makeRequest('GET', '/v1/currency-pairs'); + } + + // Webhook signature verification + verifyWebhookSignature(payload, signature, secret) { + const crypto = require('crypto'); + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(payload) + .digest('hex'); + + return signature === expectedSignature; + } + + // Create webhook subscription + async createWebhookSubscription(params) { + const profileId = params.profileId || this.profileId; + const subscriptionData = { + name: params.name || 'Frigg Webhook', + trigger_on: params.events || 'transfers#state-change', + delivery: { + version: '2.0.0', + url: params.url, + }, + scope: { + profile: profileId, + }, + }; + + return this.makeRequest('POST', '/v3/profiles/' + profileId + '/subscriptions', subscriptionData); + } + + // List webhook subscriptions + async getWebhookSubscriptions(profileId = null) { + const id = profileId || this.profileId; + return this.makeRequest('GET', `/v3/profiles/${id}/subscriptions`); + } + + // Delete webhook subscription + async deleteWebhookSubscription(subscriptionId, profileId = null) { + const id = profileId || this.profileId; + return this.makeRequest('DELETE', `/v3/profiles/${id}/subscriptions/${subscriptionId}`); + } + + // Get transfer wise fees + async getTransferFees(params) { + const queryParams = { + sourceCurrency: params.sourceCurrency, + targetCurrency: params.targetCurrency, + sourceAmount: params.sourceAmount || null, + targetAmount: params.targetAmount || null, + payIn: params.payIn || 'BALANCE', + payOut: params.payOut || 'BANK_TRANSFER', + }; + + // Remove null values + Object.keys(queryParams).forEach(key => + queryParams[key] === null && delete queryParams[key] + ); + + return this.makeRequest('GET', '/v1/quotes/fees', null, queryParams); + } + + // Get bank details requirements + async getBankRequirements(currency, country = null) { + const params = { currency }; + if (country) params.country = country; + + return this.makeRequest('GET', '/v1/bank-requirements', null, params); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/wise/defaultConfig.json b/packages/wise/defaultConfig.json new file mode 100644 index 0000000..940c3c6 --- /dev/null +++ b/packages/wise/defaultConfig.json @@ -0,0 +1,15 @@ +{ + "name": "wise", + "displayName": "Wise", + "description": "International money transfers and multi-currency accounts", + "version": "1.0.0", + "categories": ["finance", "payments", "international-transfers"], + "scopes": [ + "transfers.read", + "transfers.write", + "balances.read", + "recipients.read", + "recipients.write", + "profiles.read" + ] +} \ No newline at end of file diff --git a/packages/wise/definition.js b/packages/wise/definition.js new file mode 100644 index 0000000..a560f30 --- /dev/null +++ b/packages/wise/definition.js @@ -0,0 +1,80 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Wise', + requiredAuthMethods: { + getToken: async function (api, params) { + // Wise uses API tokens, not OAuth + // This would typically be handled during initial setup + const apiToken = get(params.data, 'apiToken'); + return { + apiToken: apiToken, + }; + }, + + getEntityDetails: async function (api, userId) { + const profiles = await api.getProfiles(); + const personalProfile = profiles.find(p => p.type === 'PERSONAL') || profiles[0]; + + if (personalProfile) { + api.setProfile(personalProfile.id); + } + + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: personalProfile ? personalProfile.id : 'unknown', + user: userId + }, + details: { + profiles: profiles.map(p => ({ + id: p.id, + type: p.type, + fullName: p.details.firstName + ' ' + p.details.lastName, + })), + primaryProfileId: personalProfile?.id, + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['apiToken'], + entity: ['profileId', 'profiles'], + }, + + getCredentialDetails: async function (api, userId) { + const profiles = await api.getProfiles(); + const profileId = profiles[0]?.id; + + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: profileId || 'unknown', + user: userId + }, + details: { + profileCount: profiles.length, + isSandbox: api.sandbox, + }, + }; + }, + + testAuthRequest: function (api) { + return api.getProfiles(); + }, + }, + env: { + apiToken: process.env.WISE_API_TOKEN, + sandbox: process.env.WISE_SANDBOX === 'true', + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/wise/index.js b/packages/wise/index.js new file mode 100644 index 0000000..5a1a5bd --- /dev/null +++ b/packages/wise/index.js @@ -0,0 +1,7 @@ +const { Definition } = require('./definition'); +const { Api } = require('./api'); + +module.exports = { + Definition, + Api, +}; \ No newline at end of file diff --git a/packages/wise/package.json b/packages/wise/package.json new file mode 100644 index 0000000..ed1fd29 --- /dev/null +++ b/packages/wise/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/wise", + "version": "1.0.0", + "description": "Wise (TransferWise) international money transfer API module for Frigg Framework", + "main": "index.js", + "scripts": { + "test": "jest" + }, + "dependencies": { + "@friggframework/core": "^1.0.0", + "axios": "^1.6.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "keywords": [ + "wise", + "transferwise", + "international-transfers", + "payments", + "api", + "frigg" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/wise/readme.md b/packages/wise/readme.md new file mode 100644 index 0000000..548e131 --- /dev/null +++ b/packages/wise/readme.md @@ -0,0 +1,24 @@ +# Wise API Module + +This module provides integration with the Wise (formerly TransferWise) API for international money transfers. + +## Features + +- Multi-currency account management +- International transfer creation and tracking +- Recipient management +- Real-time exchange rates +- Transfer quotes and fee calculation +- Webhook support for transfer status updates +- Sandbox environment support + +## Environment Variables + +``` +WISE_API_TOKEN=your_api_token +WISE_SANDBOX=true|false +``` + +## Usage + +See the [Frigg Framework documentation](https://docs.friggframework.org) for usage details. \ No newline at end of file diff --git a/packages/wise/tests/api.test.js b/packages/wise/tests/api.test.js new file mode 100644 index 0000000..7507673 --- /dev/null +++ b/packages/wise/tests/api.test.js @@ -0,0 +1,65 @@ +const { Api } = require('../api'); + +describe('Wise API', () => { + let api; + + beforeEach(() => { + api = new Api({ + apiToken: 'test_api_token', + sandbox: true, + }); + }); + + test('should initialize with proper configuration', () => { + expect(api.apiToken).toBe('test_api_token'); + expect(api.sandbox).toBe(true); + expect(api.baseUrl).toBe('https://api.sandbox.transferwise.tech'); + expect(api.client).toBeDefined(); + }); + + test('should set profile ID', () => { + api.setProfile('profile-123'); + expect(api.profileId).toBe('profile-123'); + }); + + test('should construct proper quote request', async () => { + api.profileId = 'profile-123'; + + // Mock the makeRequest method + api.makeRequest = jest.fn().mockResolvedValue({ + id: 'quote-123', + source: 'USD', + target: 'EUR', + sourceAmount: 1000, + }); + + const quote = await api.createQuote({ + sourceCurrency: 'USD', + targetCurrency: 'EUR', + sourceAmount: 1000, + }); + + expect(api.makeRequest).toHaveBeenCalledWith('POST', '/v3/profiles/profile-123/quotes', { + sourceCurrency: 'USD', + targetCurrency: 'EUR', + sourceAmount: 1000, + targetAmount: null, + profile: 'profile-123', + payOut: 'BALANCE', + preferredPayIn: 'BALANCE', + }); + expect(quote.id).toBe('quote-123'); + }); + + test('should verify webhook signature correctly', () => { + const payload = 'test-payload'; + const secret = 'test-secret'; + const validSignature = require('crypto') + .createHmac('sha256', secret) + .update(payload) + .digest('hex'); + + expect(api.verifyWebhookSignature(payload, validSignature, secret)).toBe(true); + expect(api.verifyWebhookSignature(payload, 'invalid-signature', secret)).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/woocommerce/README.md b/packages/woocommerce/README.md new file mode 100644 index 0000000..d5d328c --- /dev/null +++ b/packages/woocommerce/README.md @@ -0,0 +1,321 @@ +# WooCommerce API Module + +A comprehensive WooCommerce REST API v3 client for the Frigg Framework, supporting all major e-commerce operations including products, orders, customers, webhooks, and inventory management. + +## Features + +- **Products Management**: Create, read, update, delete products and variations +- **Order Processing**: Complete order lifecycle management with notes and refunds +- **Customer Management**: Customer data and download tracking +- **Inventory Control**: Stock quantity management for products and variations +- **Webhook Support**: Event-driven integrations with signature verification +- **Reporting**: Sales reports and top sellers analytics +- **Settings & Configuration**: Store settings management +- **Authentication**: Support for both HTTP (OAuth 1.0a) and HTTPS (Basic Auth) + +## Installation + +```bash +npm install @friggframework/api-module-woocommerce +``` + +## Configuration + +### Environment Variables + +```env +WOOCOMMERCE_CONSUMER_KEY=ck_your_consumer_key_here +WOOCOMMERCE_CONSUMER_SECRET=cs_your_consumer_secret_here +WOOCOMMERCE_BASE_URL=https://your-store.com +``` + +### Authentication Setup + +1. Go to your WooCommerce admin dashboard +2. Navigate to WooCommerce > Settings > Advanced > REST API +3. Click "Add key" +4. Set description and user +5. Select permissions (Read, Write, or Read/Write) +6. Copy the generated Consumer Key and Consumer Secret + +## Usage + +### Basic Setup + +```javascript +const { Api } = require('@friggframework/api-module-woocommerce'); + +const api = new Api({ + baseUrl: 'https://your-store.com', + consumer_key: 'ck_your_consumer_key', + consumer_secret: 'cs_your_consumer_secret' +}); +``` + +### Products + +```javascript +// Create a product +const product = await api.createProduct({ + name: 'Premium T-Shirt', + type: 'simple', + regular_price: '29.99', + description: 'High-quality cotton t-shirt', + short_description: 'Premium cotton tee', + categories: [{ id: 1 }], + manage_stock: true, + stock_quantity: 100 +}); + +// Get all products +const products = await api.listProducts({ + per_page: 20, + status: 'publish' +}); + +// Update product +await api.updateProduct(product.id, { + regular_price: '34.99', + stock_quantity: 85 +}); + +// Update stock quantity only +await api.updateProductStock(product.id, 75); +``` + +### Orders + +```javascript +// Get orders +const orders = await api.listOrders({ + status: 'processing', + per_page: 50 +}); + +// Get specific order +const order = await api.getOrderById(123); + +// Update order status +await api.updateOrder(123, { + status: 'completed' +}); + +// Add order note +await api.createOrderNote(123, { + note: 'Package shipped via FedEx', + customer_note: true +}); + +// Create refund +await api.createOrderRefund(123, { + amount: '15.99', + reason: 'Defective item' +}); +``` + +### Customers + +```javascript +// Create customer +const customer = await api.createCustomer({ + email: 'customer@example.com', + first_name: 'John', + last_name: 'Doe', + billing: { + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '123 Main St', + city: 'New York', + state: 'NY', + postcode: '10001', + country: 'US', + email: 'customer@example.com', + phone: '555-123-4567' + } +}); + +// Get customers +const customers = await api.listCustomers({ + role: 'customer', + orderby: 'registered_date' +}); +``` + +### Webhooks + +```javascript +// Create webhook +const webhook = await api.createWebhook({ + name: 'Order Created', + topic: 'order.created', + delivery_url: 'https://your-app.com/webhooks/woocommerce/order-created', + secret: 'your-webhook-secret' +}); + +// Verify webhook signature (in your webhook handler) +const isValid = api.verifyWebhookSignature( + req.body, + req.headers['x-wc-webhook-signature'], + 'your-webhook-secret' +); + +if (isValid) { + // Process webhook payload + console.log('Valid webhook received:', req.body); +} +``` + +### Inventory Management + +```javascript +// Update product stock +await api.updateProductStock(productId, 50, true); // quantity: 50, manage_stock: true + +// Update variation stock +await api.updateVariationStock(productId, variationId, 25); + +// Bulk update products +await api.batchUpdateProducts({ + create: [ + { name: 'New Product 1', regular_price: '19.99' }, + { name: 'New Product 2', regular_price: '24.99' } + ], + update: [ + { id: 123, stock_quantity: 30 } + ] +}); +``` + +### Reports + +```javascript +// Get sales report +const salesReport = await api.getSalesReport({ + period: 'week', + date_min: '2024-01-01', + date_max: '2024-01-31' +}); + +// Get top sellers +const topSellers = await api.getTopSellersReport({ + period: 'month', + limit: 10 +}); +``` + +## API Methods + +### Products +- `createProduct(productData)` +- `listProducts(params)` +- `getProductById(id)` +- `updateProduct(id, productData)` +- `deleteProduct(id, force)` +- `batchUpdateProducts(data)` +- `updateProductStock(productId, stockQuantity, manageStock)` + +### Product Variations +- `createProductVariation(productId, variationData)` +- `listProductVariations(productId, params)` +- `getProductVariationById(productId, variationId)` +- `updateProductVariation(productId, variationId, variationData)` +- `deleteProductVariation(productId, variationId, force)` +- `updateVariationStock(productId, variationId, stockQuantity, manageStock)` + +### Orders +- `createOrder(orderData)` +- `listOrders(params)` +- `getOrderById(id)` +- `updateOrder(id, orderData)` +- `deleteOrder(id, force)` +- `batchUpdateOrders(data)` + +### Order Notes +- `createOrderNote(orderId, noteData)` +- `listOrderNotes(orderId, params)` +- `getOrderNoteById(orderId, noteId)` +- `deleteOrderNote(orderId, noteId, force)` + +### Order Refunds +- `createOrderRefund(orderId, refundData)` +- `listOrderRefunds(orderId, params)` +- `getOrderRefundById(orderId, refundId)` +- `deleteOrderRefund(orderId, refundId, force)` + +### Customers +- `createCustomer(customerData)` +- `listCustomers(params)` +- `getCustomerById(id)` +- `updateCustomer(id, customerData)` +- `deleteCustomer(id, force)` +- `batchUpdateCustomers(data)` +- `getCustomerDownloads(customerId)` + +### Webhooks +- `createWebhook(webhookData)` +- `listWebhooks(params)` +- `getWebhookById(id)` +- `updateWebhook(id, webhookData)` +- `deleteWebhook(id, force)` +- `getWebhookDeliveries(webhookId)` +- `verifyWebhookSignature(payload, signature, secret)` + +### Reports +- `getSalesReport(params)` +- `getTopSellersReport(params)` + +### Settings +- `getSettings(params)` +- `getSettingsByGroup(groupId)` +- `updateSetting(groupId, settingId, value)` + +### System +- `getSystemStatus()` +- `getSystemStatusTools()` + +## Webhook Events + +WooCommerce supports the following webhook topics: + +- `coupon.created`, `coupon.updated`, `coupon.deleted` +- `customer.created`, `customer.updated`, `customer.deleted` +- `order.created`, `order.updated`, `order.deleted` +- `product.created`, `product.updated`, `product.deleted` +- `action.{hook_name}` - Custom action hooks + +## Error Handling + +```javascript +try { + const product = await api.getProductById(123); +} catch (error) { + if (error.response?.status === 404) { + console.log('Product not found'); + } else { + console.error('API Error:', error.message); + } +} +``` + +## Rate Limiting + +WooCommerce REST API has rate limits: +- **Per minute**: 60 requests for authenticated users +- **Per hour**: 3600 requests for authenticated users + +The module will handle rate limit responses automatically with appropriate backoff strategies. + +## Testing + +```bash +npm test +``` + +## Contributing + +Please read the [contributing guidelines](CONTRIBUTING.md) before submitting pull requests. + +## License + +MIT License - see [LICENSE](LICENSE.md) for details. \ No newline at end of file diff --git a/packages/woocommerce/api.js b/packages/woocommerce/api.js new file mode 100644 index 0000000..323512c --- /dev/null +++ b/packages/woocommerce/api.js @@ -0,0 +1,640 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); +const crypto = require('crypto'); + +// WooCommerce REST API v3 client +// Supports Consumer Key/Secret authentication +// Documentation: https://woocommerce.github.io/woocommerce-rest-api-docs/ + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = get(params, 'baseUrl', null); // WooCommerce site URL + this.consumer_key = get(params, 'consumer_key', null); + this.consumer_secret = get(params, 'consumer_secret', null); + this.version = get(params, 'version', 'v3'); + this.isHttps = this.baseUrl ? this.baseUrl.startsWith('https') : true; + + this.URLs = { + // Products + products: '/products', + productById: (productId) => `/products/${productId}`, + productVariations: (productId) => `/products/${productId}/variations`, + productVariationById: (productId, variationId) => `/products/${productId}/variations/${variationId}`, + productCategories: '/products/categories', + productCategoryById: (categoryId) => `/products/categories/${categoryId}`, + productTags: '/products/tags', + productTagById: (tagId) => `/products/tags/${tagId}`, + productAttributes: '/products/attributes', + productAttributeById: (attributeId) => `/products/attributes/${attributeId}`, + productAttributeTerms: (attributeId) => `/products/attributes/${attributeId}/terms`, + productReviews: '/products/reviews', + productReviewById: (reviewId) => `/products/reviews/${reviewId}`, + + // Orders + orders: '/orders', + orderById: (orderId) => `/orders/${orderId}`, + orderNotes: (orderId) => `/orders/${orderId}/notes`, + orderNoteById: (orderId, noteId) => `/orders/${orderId}/notes/${noteId}`, + orderRefunds: (orderId) => `/orders/${orderId}/refunds`, + orderRefundById: (orderId, refundId) => `/orders/${orderId}/refunds/${refundId}`, + + // Customers + customers: '/customers', + customerById: (customerId) => `/customers/${customerId}`, + customerDownloads: (customerId) => `/customers/${customerId}/downloads`, + + // Coupons + coupons: '/coupons', + couponById: (couponId) => `/coupons/${couponId}`, + + // Reports + reports: '/reports', + reportsSales: '/reports/sales', + reportsTopSellers: '/reports/top_sellers', + + // Tax + taxes: '/taxes', + taxById: (taxId) => `/taxes/${taxId}`, + taxClasses: '/taxes/classes', + + // Shipping + shippingZones: '/shipping/zones', + shippingZoneById: (zoneId) => `/shipping/zones/${zoneId}`, + shippingZoneLocations: (zoneId) => `/shipping/zones/${zoneId}/locations`, + shippingZoneMethods: (zoneId) => `/shipping/zones/${zoneId}/methods`, + + // Settings + settings: '/settings', + settingsByGroup: (groupId) => `/settings/${groupId}`, + settingById: (groupId, settingId) => `/settings/${groupId}/${settingId}`, + + // System Status + systemStatus: '/system_status', + systemStatusTools: '/system_status/tools', + + // Webhooks + webhooks: '/webhooks', + webhookById: (webhookId) => `/webhooks/${webhookId}`, + webhookDeliveries: (webhookId) => `/webhooks/${webhookId}/deliveries`, + }; + + // Set API endpoint + this.apiEndpoint = `${this.baseUrl}/wp-json/wc/${this.version}`; + } + + // Generate OAuth 1.0a signature for WooCommerce + generateOAuthSignature(method, url, params = {}) { + const oauth_params = { + oauth_consumer_key: this.consumer_key, + oauth_nonce: crypto.randomBytes(16).toString('hex'), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.floor(Date.now() / 1000), + oauth_version: '1.0', + ...params + }; + + // Create parameter string + const paramString = Object.keys(oauth_params) + .sort() + .map(key => `${key}=${encodeURIComponent(oauth_params[key])}`) + .join('&'); + + // Create signature base string + const baseString = `${method.toUpperCase()}&${encodeURIComponent(url)}&${encodeURIComponent(paramString)}`; + + // Create signing key + const signingKey = `${encodeURIComponent(this.consumer_secret)}&`; + + // Generate signature + const signature = crypto.createHmac('sha1', signingKey).update(baseString).digest('base64'); + oauth_params.oauth_signature = signature; + + return oauth_params; + } + + // Add authentication to request options + addAuthHeaders(options, method = 'GET') { + if (this.isHttps) { + // For HTTPS, use basic auth with consumer key/secret + const auth = Buffer.from(`${this.consumer_key}:${this.consumer_secret}`).toString('base64'); + options.headers = { + ...options.headers, + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }; + } else { + // For HTTP, use OAuth 1.0a + const oauthParams = this.generateOAuthSignature(method, options.url); + const authHeader = 'OAuth ' + Object.keys(oauthParams) + .map(key => `${key}="${encodeURIComponent(oauthParams[key])}"`) + .join(', '); + + options.headers = { + ...options.headers, + 'Authorization': authHeader, + 'Content-Type': 'application/json', + }; + } + } + + async _get(options) { + options.url = this.apiEndpoint + options.url; + this.addAuthHeaders(options, 'GET'); + return super._get(options); + } + + async _post(options, stringify = true) { + options.url = this.apiEndpoint + options.url; + this.addAuthHeaders(options, 'POST'); + return super._post(options, stringify); + } + + async _put(options, stringify = true) { + options.url = this.apiEndpoint + options.url; + this.addAuthHeaders(options, 'PUT'); + return super._put(options, stringify); + } + + async _patch(options, stringify = true) { + options.url = this.apiEndpoint + options.url; + this.addAuthHeaders(options, 'PATCH'); + return super._patch(options, stringify); + } + + async _delete(options) { + options.url = this.apiEndpoint + options.url; + this.addAuthHeaders(options, 'DELETE'); + return super._delete(options); + } + + // ************************** Products ********************************** + + async createProduct(productData) { + const options = { + url: this.URLs.products, + body: productData, + }; + return this._post(options); + } + + async listProducts(params = {}) { + const options = { + url: this.URLs.products, + query: params + }; + return this._get(options); + } + + async getProductById(id) { + const options = { + url: this.URLs.productById(id), + }; + return this._get(options); + } + + async updateProduct(id, productData) { + const options = { + url: this.URLs.productById(id), + body: productData, + }; + return this._put(options); + } + + async deleteProduct(id, force = false) { + const options = { + url: this.URLs.productById(id), + query: { force } + }; + return this._delete(options); + } + + async batchUpdateProducts(data) { + const options = { + url: this.URLs.products + '/batch', + body: data, + }; + return this._post(options); + } + + // Product Variations + async createProductVariation(productId, variationData) { + const options = { + url: this.URLs.productVariations(productId), + body: variationData, + }; + return this._post(options); + } + + async listProductVariations(productId, params = {}) { + const options = { + url: this.URLs.productVariations(productId), + query: params + }; + return this._get(options); + } + + async getProductVariationById(productId, variationId) { + const options = { + url: this.URLs.productVariationById(productId, variationId), + }; + return this._get(options); + } + + async updateProductVariation(productId, variationId, variationData) { + const options = { + url: this.URLs.productVariationById(productId, variationId), + body: variationData, + }; + return this._put(options); + } + + async deleteProductVariation(productId, variationId, force = false) { + const options = { + url: this.URLs.productVariationById(productId, variationId), + query: { force } + }; + return this._delete(options); + } + + // Product Categories + async createProductCategory(categoryData) { + const options = { + url: this.URLs.productCategories, + body: categoryData, + }; + return this._post(options); + } + + async listProductCategories(params = {}) { + const options = { + url: this.URLs.productCategories, + query: params + }; + return this._get(options); + } + + async getProductCategoryById(id) { + const options = { + url: this.URLs.productCategoryById(id), + }; + return this._get(options); + } + + async updateProductCategory(id, categoryData) { + const options = { + url: this.URLs.productCategoryById(id), + body: categoryData, + }; + return this._put(options); + } + + async deleteProductCategory(id, force = false) { + const options = { + url: this.URLs.productCategoryById(id), + query: { force } + }; + return this._delete(options); + } + + // Product Reviews + async listProductReviews(params = {}) { + const options = { + url: this.URLs.productReviews, + query: params + }; + return this._get(options); + } + + async getProductReviewById(id) { + const options = { + url: this.URLs.productReviewById(id), + }; + return this._get(options); + } + + async updateProductReview(id, reviewData) { + const options = { + url: this.URLs.productReviewById(id), + body: reviewData, + }; + return this._put(options); + } + + async deleteProductReview(id, force = false) { + const options = { + url: this.URLs.productReviewById(id), + query: { force } + }; + return this._delete(options); + } + + // ************************** Orders ********************************** + + async createOrder(orderData) { + const options = { + url: this.URLs.orders, + body: orderData, + }; + return this._post(options); + } + + async listOrders(params = {}) { + const options = { + url: this.URLs.orders, + query: params + }; + return this._get(options); + } + + async getOrderById(id) { + const options = { + url: this.URLs.orderById(id), + }; + return this._get(options); + } + + async updateOrder(id, orderData) { + const options = { + url: this.URLs.orderById(id), + body: orderData, + }; + return this._put(options); + } + + async deleteOrder(id, force = false) { + const options = { + url: this.URLs.orderById(id), + query: { force } + }; + return this._delete(options); + } + + async batchUpdateOrders(data) { + const options = { + url: this.URLs.orders + '/batch', + body: data, + }; + return this._post(options); + } + + // Order Notes + async createOrderNote(orderId, noteData) { + const options = { + url: this.URLs.orderNotes(orderId), + body: noteData, + }; + return this._post(options); + } + + async listOrderNotes(orderId, params = {}) { + const options = { + url: this.URLs.orderNotes(orderId), + query: params + }; + return this._get(options); + } + + async getOrderNoteById(orderId, noteId) { + const options = { + url: this.URLs.orderNoteById(orderId, noteId), + }; + return this._get(options); + } + + async deleteOrderNote(orderId, noteId, force = false) { + const options = { + url: this.URLs.orderNoteById(orderId, noteId), + query: { force } + }; + return this._delete(options); + } + + // Order Refunds + async createOrderRefund(orderId, refundData) { + const options = { + url: this.URLs.orderRefunds(orderId), + body: refundData, + }; + return this._post(options); + } + + async listOrderRefunds(orderId, params = {}) { + const options = { + url: this.URLs.orderRefunds(orderId), + query: params + }; + return this._get(options); + } + + async getOrderRefundById(orderId, refundId) { + const options = { + url: this.URLs.orderRefundById(orderId, refundId), + }; + return this._get(options); + } + + async deleteOrderRefund(orderId, refundId, force = false) { + const options = { + url: this.URLs.orderRefundById(orderId, refundId), + query: { force } + }; + return this._delete(options); + } + + // ************************** Customers ********************************** + + async createCustomer(customerData) { + const options = { + url: this.URLs.customers, + body: customerData, + }; + return this._post(options); + } + + async listCustomers(params = {}) { + const options = { + url: this.URLs.customers, + query: params + }; + return this._get(options); + } + + async getCustomerById(id) { + const options = { + url: this.URLs.customerById(id), + }; + return this._get(options); + } + + async updateCustomer(id, customerData) { + const options = { + url: this.URLs.customerById(id), + body: customerData, + }; + return this._put(options); + } + + async deleteCustomer(id, force = false) { + const options = { + url: this.URLs.customerById(id), + query: { force } + }; + return this._delete(options); + } + + async batchUpdateCustomers(data) { + const options = { + url: this.URLs.customers + '/batch', + body: data, + }; + return this._post(options); + } + + async getCustomerDownloads(customerId) { + const options = { + url: this.URLs.customerDownloads(customerId), + }; + return this._get(options); + } + + // ************************** Webhooks ********************************** + + async createWebhook(webhookData) { + const options = { + url: this.URLs.webhooks, + body: webhookData, + }; + return this._post(options); + } + + async listWebhooks(params = {}) { + const options = { + url: this.URLs.webhooks, + query: params + }; + return this._get(options); + } + + async getWebhookById(id) { + const options = { + url: this.URLs.webhookById(id), + }; + return this._get(options); + } + + async updateWebhook(id, webhookData) { + const options = { + url: this.URLs.webhookById(id), + body: webhookData, + }; + return this._put(options); + } + + async deleteWebhook(id, force = false) { + const options = { + url: this.URLs.webhookById(id), + query: { force } + }; + return this._delete(options); + } + + async getWebhookDeliveries(webhookId) { + const options = { + url: this.URLs.webhookDeliveries(webhookId), + }; + return this._get(options); + } + + // ************************** Inventory ********************************** + + async updateProductStock(productId, stockQuantity, manageStock = true) { + const options = { + url: this.URLs.productById(productId), + body: { + manage_stock: manageStock, + stock_quantity: stockQuantity, + }, + }; + return this._put(options); + } + + async updateVariationStock(productId, variationId, stockQuantity, manageStock = true) { + const options = { + url: this.URLs.productVariationById(productId, variationId), + body: { + manage_stock: manageStock, + stock_quantity: stockQuantity, + }, + }; + return this._put(options); + } + + // ************************** Reports ********************************** + + async getSalesReport(params = {}) { + const options = { + url: this.URLs.reportsSales, + query: params + }; + return this._get(options); + } + + async getTopSellersReport(params = {}) { + const options = { + url: this.URLs.reportsTopSellers, + query: params + }; + return this._get(options); + } + + // ************************** Settings ********************************** + + async getSettings(params = {}) { + const options = { + url: this.URLs.settings, + query: params + }; + return this._get(options); + } + + async getSettingsByGroup(groupId) { + const options = { + url: this.URLs.settingsByGroup(groupId), + }; + return this._get(options); + } + + async updateSetting(groupId, settingId, value) { + const options = { + url: this.URLs.settingById(groupId, settingId), + body: { value }, + }; + return this._put(options); + } + + // ************************** System Status ********************************** + + async getSystemStatus() { + const options = { + url: this.URLs.systemStatus, + }; + return this._get(options); + } + + async getSystemStatusTools() { + const options = { + url: this.URLs.systemStatusTools, + }; + return this._get(options); + } + + // ************************** Webhook Verification ********************************** + + verifyWebhookSignature(payload, signature, secret) { + const hash = crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('base64'); + return hash === signature; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/woocommerce/coverage/api.js.html b/packages/woocommerce/coverage/api.js.html new file mode 100644 index 0000000..32cbe87 --- /dev/null +++ b/packages/woocommerce/coverage/api.js.html @@ -0,0 +1,2002 @@ + + + + + + Code coverage report for api.js + + + + + + + + + +
+
+

All files api.js

+
+ +
+ 18.43% + Statements + 33/179 +
+ + +
+ 12.5% + Branches + 4/32 +
+ + +
+ 12.08% + Functions + 11/91 +
+ + +
+ 18.43% + Lines + 33/179 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +6401x +1x +  +  +  +  +  +  +  +10x +  +10x +10x +10x +10x +10x +  +10x +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +10x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +  +5x +  +  +  +1x +  +  +1x +  +  +1x +1x +  +1x +  +  +  +  +2x +  +1x +1x +  +  +  +  +  +  +1x +1x +6x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x
const { OAuth2Requester, get } = require('@friggframework/core');
+const crypto = require('crypto');
+ 
+// WooCommerce REST API v3 client
+// Supports Consumer Key/Secret authentication
+// Documentation: https://woocommerce.github.io/woocommerce-rest-api-docs/
+ 
+class Api extends OAuth2Requester {
+    constructor(params) {
+        super(params);
+        
+        this.baseUrl = get(params, 'baseUrl', null); // WooCommerce site URL
+        this.consumer_key = get(params, 'consumer_key', null);
+        this.consumer_secret = get(params, 'consumer_secret', null);
+        this.version = get(params, 'version', 'v3');
+        this.isHttps = this.baseUrl ? this.baseUrl.startsWith('https') : true;
+ 
+        this.URLs = {
+            // Products
+            products: '/products',
+            productById: (productId) => `/products/${productId}`,
+            productVariations: (productId) => `/products/${productId}/variations`,
+            productVariationById: (productId, variationId) => `/products/${productId}/variations/${variationId}`,
+            productCategories: '/products/categories',
+            productCategoryById: (categoryId) => `/products/categories/${categoryId}`,
+            productTags: '/products/tags',
+            productTagById: (tagId) => `/products/tags/${tagId}`,
+            productAttributes: '/products/attributes',
+            productAttributeById: (attributeId) => `/products/attributes/${attributeId}`,
+            productAttributeTerms: (attributeId) => `/products/attributes/${attributeId}/terms`,
+            productReviews: '/products/reviews',
+            productReviewById: (reviewId) => `/products/reviews/${reviewId}`,
+ 
+            // Orders
+            orders: '/orders',
+            orderById: (orderId) => `/orders/${orderId}`,
+            orderNotes: (orderId) => `/orders/${orderId}/notes`,
+            orderNoteById: (orderId, noteId) => `/orders/${orderId}/notes/${noteId}`,
+            orderRefunds: (orderId) => `/orders/${orderId}/refunds`,
+            orderRefundById: (orderId, refundId) => `/orders/${orderId}/refunds/${refundId}`,
+ 
+            // Customers
+            customers: '/customers',
+            customerById: (customerId) => `/customers/${customerId}`,
+            customerDownloads: (customerId) => `/customers/${customerId}/downloads`,
+ 
+            // Coupons
+            coupons: '/coupons',
+            couponById: (couponId) => `/coupons/${couponId}`,
+ 
+            // Reports
+            reports: '/reports',
+            reportsSales: '/reports/sales',
+            reportsTopSellers: '/reports/top_sellers',
+ 
+            // Tax
+            taxes: '/taxes',
+            taxById: (taxId) => `/taxes/${taxId}`,
+            taxClasses: '/taxes/classes',
+ 
+            // Shipping
+            shippingZones: '/shipping/zones',
+            shippingZoneById: (zoneId) => `/shipping/zones/${zoneId}`,
+            shippingZoneLocations: (zoneId) => `/shipping/zones/${zoneId}/locations`,
+            shippingZoneMethods: (zoneId) => `/shipping/zones/${zoneId}/methods`,
+ 
+            // Settings
+            settings: '/settings',
+            settingsByGroup: (groupId) => `/settings/${groupId}`,
+            settingById: (groupId, settingId) => `/settings/${groupId}/${settingId}`,
+ 
+            // System Status
+            systemStatus: '/system_status',
+            systemStatusTools: '/system_status/tools',
+ 
+            // Webhooks
+            webhooks: '/webhooks',
+            webhookById: (webhookId) => `/webhooks/${webhookId}`,
+            webhookDeliveries: (webhookId) => `/webhooks/${webhookId}/deliveries`,
+        };
+ 
+        // Set API endpoint
+        this.apiEndpoint = `${this.baseUrl}/wp-json/wc/${this.version}`;
+    }
+ 
+    // Generate OAuth 1.0a signature for WooCommerce
+    generateOAuthSignature(method, url, params = {}) {
+        const oauth_params = {
+            oauth_consumer_key: this.consumer_key,
+            oauth_nonce: crypto.randomBytes(16).toString('hex'),
+            oauth_signature_method: 'HMAC-SHA1',
+            oauth_timestamp: Math.floor(Date.now() / 1000),
+            oauth_version: '1.0',
+            ...params
+        };
+ 
+        // Create parameter string
+        const paramString = Object.keys(oauth_params)
+            .sort()
+            .map(key => `${key}=${encodeURIComponent(oauth_params[key])}`)
+            .join('&');
+ 
+        // Create signature base string
+        const baseString = `${method.toUpperCase()}&${encodeURIComponent(url)}&${encodeURIComponent(paramString)}`;
+ 
+        // Create signing key
+        const signingKey = `${encodeURIComponent(this.consumer_secret)}&`;
+ 
+        // Generate signature
+        const signature = crypto.createHmac('sha1', signingKey).update(baseString).digest('base64');
+        oauth_params.oauth_signature = signature;
+ 
+        return oauth_params;
+    }
+ 
+    // Add authentication to request options
+    addAuthHeaders(options, method = 'GET') {
+        if (this.isHttps) {
+            // For HTTPS, use basic auth with consumer key/secret
+            const auth = Buffer.from(`${this.consumer_key}:${this.consumer_secret}`).toString('base64');
+            options.headers = {
+                ...options.headers,
+                'Authorization': `Basic ${auth}`,
+                'Content-Type': 'application/json',
+            };
+        } else {
+            // For HTTP, use OAuth 1.0a
+            const oauthParams = this.generateOAuthSignature(method, options.url);
+            const authHeader = 'OAuth ' + Object.keys(oauthParams)
+                .map(key => `${key}="${encodeURIComponent(oauthParams[key])}"`)
+                .join(', ');
+            
+            options.headers = {
+                ...options.headers,
+                'Authorization': authHeader,
+                'Content-Type': 'application/json',
+            };
+        }
+    }
+ 
+    async _get(options) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'GET');
+        return super._get(options);
+    }
+ 
+    async _post(options, stringify = true) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'POST');
+        return super._post(options, stringify);
+    }
+ 
+    async _put(options, stringify = true) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'PUT');
+        return super._put(options, stringify);
+    }
+ 
+    async _patch(options, stringify = true) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'PATCH');
+        return super._patch(options, stringify);
+    }
+ 
+    async _delete(options) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'DELETE');
+        return super._delete(options);
+    }
+ 
+    // **************************   Products   **********************************
+ 
+    async createProduct(productData) {
+        const options = {
+            url: this.URLs.products,
+            body: productData,
+        };
+        return this._post(options);
+    }
+ 
+    async listProducts(params = {}) {
+        const options = {
+            url: this.URLs.products,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductById(id) {
+        const options = {
+            url: this.URLs.productById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProduct(id, productData) {
+        const options = {
+            url: this.URLs.productById(id),
+            body: productData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProduct(id, force = false) {
+        const options = {
+            url: this.URLs.productById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async batchUpdateProducts(data) {
+        const options = {
+            url: this.URLs.products + '/batch',
+            body: data,
+        };
+        return this._post(options);
+    }
+ 
+    // Product Variations
+    async createProductVariation(productId, variationData) {
+        const options = {
+            url: this.URLs.productVariations(productId),
+            body: variationData,
+        };
+        return this._post(options);
+    }
+ 
+    async listProductVariations(productId, params = {}) {
+        const options = {
+            url: this.URLs.productVariations(productId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductVariationById(productId, variationId) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProductVariation(productId, variationId, variationData) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+            body: variationData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProductVariation(productId, variationId, force = false) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // Product Categories
+    async createProductCategory(categoryData) {
+        const options = {
+            url: this.URLs.productCategories,
+            body: categoryData,
+        };
+        return this._post(options);
+    }
+ 
+    async listProductCategories(params = {}) {
+        const options = {
+            url: this.URLs.productCategories,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductCategoryById(id) {
+        const options = {
+            url: this.URLs.productCategoryById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProductCategory(id, categoryData) {
+        const options = {
+            url: this.URLs.productCategoryById(id),
+            body: categoryData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProductCategory(id, force = false) {
+        const options = {
+            url: this.URLs.productCategoryById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // Product Reviews
+    async listProductReviews(params = {}) {
+        const options = {
+            url: this.URLs.productReviews,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductReviewById(id) {
+        const options = {
+            url: this.URLs.productReviewById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProductReview(id, reviewData) {
+        const options = {
+            url: this.URLs.productReviewById(id),
+            body: reviewData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProductReview(id, force = false) {
+        const options = {
+            url: this.URLs.productReviewById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Orders   **********************************
+ 
+    async createOrder(orderData) {
+        const options = {
+            url: this.URLs.orders,
+            body: orderData,
+        };
+        return this._post(options);
+    }
+ 
+    async listOrders(params = {}) {
+        const options = {
+            url: this.URLs.orders,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getOrderById(id) {
+        const options = {
+            url: this.URLs.orderById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateOrder(id, orderData) {
+        const options = {
+            url: this.URLs.orderById(id),
+            body: orderData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteOrder(id, force = false) {
+        const options = {
+            url: this.URLs.orderById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async batchUpdateOrders(data) {
+        const options = {
+            url: this.URLs.orders + '/batch',
+            body: data,
+        };
+        return this._post(options);
+    }
+ 
+    // Order Notes
+    async createOrderNote(orderId, noteData) {
+        const options = {
+            url: this.URLs.orderNotes(orderId),
+            body: noteData,
+        };
+        return this._post(options);
+    }
+ 
+    async listOrderNotes(orderId, params = {}) {
+        const options = {
+            url: this.URLs.orderNotes(orderId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getOrderNoteById(orderId, noteId) {
+        const options = {
+            url: this.URLs.orderNoteById(orderId, noteId),
+        };
+        return this._get(options);
+    }
+ 
+    async deleteOrderNote(orderId, noteId, force = false) {
+        const options = {
+            url: this.URLs.orderNoteById(orderId, noteId),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // Order Refunds
+    async createOrderRefund(orderId, refundData) {
+        const options = {
+            url: this.URLs.orderRefunds(orderId),
+            body: refundData,
+        };
+        return this._post(options);
+    }
+ 
+    async listOrderRefunds(orderId, params = {}) {
+        const options = {
+            url: this.URLs.orderRefunds(orderId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getOrderRefundById(orderId, refundId) {
+        const options = {
+            url: this.URLs.orderRefundById(orderId, refundId),
+        };
+        return this._get(options);
+    }
+ 
+    async deleteOrderRefund(orderId, refundId, force = false) {
+        const options = {
+            url: this.URLs.orderRefundById(orderId, refundId),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Customers   **********************************
+ 
+    async createCustomer(customerData) {
+        const options = {
+            url: this.URLs.customers,
+            body: customerData,
+        };
+        return this._post(options);
+    }
+ 
+    async listCustomers(params = {}) {
+        const options = {
+            url: this.URLs.customers,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getCustomerById(id) {
+        const options = {
+            url: this.URLs.customerById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateCustomer(id, customerData) {
+        const options = {
+            url: this.URLs.customerById(id),
+            body: customerData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteCustomer(id, force = false) {
+        const options = {
+            url: this.URLs.customerById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async batchUpdateCustomers(data) {
+        const options = {
+            url: this.URLs.customers + '/batch',
+            body: data,
+        };
+        return this._post(options);
+    }
+ 
+    async getCustomerDownloads(customerId) {
+        const options = {
+            url: this.URLs.customerDownloads(customerId),
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Webhooks   **********************************
+ 
+    async createWebhook(webhookData) {
+        const options = {
+            url: this.URLs.webhooks,
+            body: webhookData,
+        };
+        return this._post(options);
+    }
+ 
+    async listWebhooks(params = {}) {
+        const options = {
+            url: this.URLs.webhooks,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getWebhookById(id) {
+        const options = {
+            url: this.URLs.webhookById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateWebhook(id, webhookData) {
+        const options = {
+            url: this.URLs.webhookById(id),
+            body: webhookData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteWebhook(id, force = false) {
+        const options = {
+            url: this.URLs.webhookById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async getWebhookDeliveries(webhookId) {
+        const options = {
+            url: this.URLs.webhookDeliveries(webhookId),
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Inventory   **********************************
+ 
+    async updateProductStock(productId, stockQuantity, manageStock = true) {
+        const options = {
+            url: this.URLs.productById(productId),
+            body: {
+                manage_stock: manageStock,
+                stock_quantity: stockQuantity,
+            },
+        };
+        return this._put(options);
+    }
+ 
+    async updateVariationStock(productId, variationId, stockQuantity, manageStock = true) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+            body: {
+                manage_stock: manageStock,
+                stock_quantity: stockQuantity,
+            },
+        };
+        return this._put(options);
+    }
+ 
+    // **************************   Reports   **********************************
+ 
+    async getSalesReport(params = {}) {
+        const options = {
+            url: this.URLs.reportsSales,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getTopSellersReport(params = {}) {
+        const options = {
+            url: this.URLs.reportsTopSellers,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Settings   **********************************
+ 
+    async getSettings(params = {}) {
+        const options = {
+            url: this.URLs.settings,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getSettingsByGroup(groupId) {
+        const options = {
+            url: this.URLs.settingsByGroup(groupId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateSetting(groupId, settingId, value) {
+        const options = {
+            url: this.URLs.settingById(groupId, settingId),
+            body: { value },
+        };
+        return this._put(options);
+    }
+ 
+    // **************************   System Status   **********************************
+ 
+    async getSystemStatus() {
+        const options = {
+            url: this.URLs.systemStatus,
+        };
+        return this._get(options);
+    }
+ 
+    async getSystemStatusTools() {
+        const options = {
+            url: this.URLs.systemStatusTools,
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Webhook Verification   **********************************
+ 
+    verifyWebhookSignature(payload, signature, secret) {
+        const hash = crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('base64');
+        return hash === signature;
+    }
+}
+ 
+module.exports = { Api };
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/woocommerce/coverage/base.css b/packages/woocommerce/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/packages/woocommerce/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/packages/woocommerce/coverage/block-navigation.js b/packages/woocommerce/coverage/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/packages/woocommerce/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/packages/woocommerce/coverage/favicon.png b/packages/woocommerce/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/packages/woocommerce/coverage/favicon.png differ diff --git a/packages/woocommerce/coverage/index.html b/packages/woocommerce/coverage/index.html new file mode 100644 index 0000000..ae119ac --- /dev/null +++ b/packages/woocommerce/coverage/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 18.43% + Statements + 33/179 +
+ + +
+ 12.5% + Branches + 4/32 +
+ + +
+ 12.08% + Functions + 11/91 +
+ + +
+ 18.43% + Lines + 33/179 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
api.js +
+
18.43%33/17912.5%4/3212.08%11/9118.43%33/179
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/woocommerce/coverage/lcov-report/api.js.html b/packages/woocommerce/coverage/lcov-report/api.js.html new file mode 100644 index 0000000..f1d9a58 --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/api.js.html @@ -0,0 +1,2002 @@ + + + + + + Code coverage report for api.js + + + + + + + + + +
+
+

All files api.js

+
+ +
+ 18.43% + Statements + 33/179 +
+ + +
+ 12.5% + Branches + 4/32 +
+ + +
+ 12.08% + Functions + 11/91 +
+ + +
+ 18.43% + Lines + 33/179 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +6401x +1x +  +  +  +  +  +  +  +10x +  +10x +10x +10x +10x +10x +  +10x +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +10x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +  +5x +  +  +  +1x +  +  +1x +  +  +1x +1x +  +1x +  +  +  +  +2x +  +1x +1x +  +  +  +  +  +  +1x +1x +6x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x
const { OAuth2Requester, get } = require('@friggframework/core');
+const crypto = require('crypto');
+ 
+// WooCommerce REST API v3 client
+// Supports Consumer Key/Secret authentication
+// Documentation: https://woocommerce.github.io/woocommerce-rest-api-docs/
+ 
+class Api extends OAuth2Requester {
+    constructor(params) {
+        super(params);
+        
+        this.baseUrl = get(params, 'baseUrl', null); // WooCommerce site URL
+        this.consumer_key = get(params, 'consumer_key', null);
+        this.consumer_secret = get(params, 'consumer_secret', null);
+        this.version = get(params, 'version', 'v3');
+        this.isHttps = this.baseUrl ? this.baseUrl.startsWith('https') : true;
+ 
+        this.URLs = {
+            // Products
+            products: '/products',
+            productById: (productId) => `/products/${productId}`,
+            productVariations: (productId) => `/products/${productId}/variations`,
+            productVariationById: (productId, variationId) => `/products/${productId}/variations/${variationId}`,
+            productCategories: '/products/categories',
+            productCategoryById: (categoryId) => `/products/categories/${categoryId}`,
+            productTags: '/products/tags',
+            productTagById: (tagId) => `/products/tags/${tagId}`,
+            productAttributes: '/products/attributes',
+            productAttributeById: (attributeId) => `/products/attributes/${attributeId}`,
+            productAttributeTerms: (attributeId) => `/products/attributes/${attributeId}/terms`,
+            productReviews: '/products/reviews',
+            productReviewById: (reviewId) => `/products/reviews/${reviewId}`,
+ 
+            // Orders
+            orders: '/orders',
+            orderById: (orderId) => `/orders/${orderId}`,
+            orderNotes: (orderId) => `/orders/${orderId}/notes`,
+            orderNoteById: (orderId, noteId) => `/orders/${orderId}/notes/${noteId}`,
+            orderRefunds: (orderId) => `/orders/${orderId}/refunds`,
+            orderRefundById: (orderId, refundId) => `/orders/${orderId}/refunds/${refundId}`,
+ 
+            // Customers
+            customers: '/customers',
+            customerById: (customerId) => `/customers/${customerId}`,
+            customerDownloads: (customerId) => `/customers/${customerId}/downloads`,
+ 
+            // Coupons
+            coupons: '/coupons',
+            couponById: (couponId) => `/coupons/${couponId}`,
+ 
+            // Reports
+            reports: '/reports',
+            reportsSales: '/reports/sales',
+            reportsTopSellers: '/reports/top_sellers',
+ 
+            // Tax
+            taxes: '/taxes',
+            taxById: (taxId) => `/taxes/${taxId}`,
+            taxClasses: '/taxes/classes',
+ 
+            // Shipping
+            shippingZones: '/shipping/zones',
+            shippingZoneById: (zoneId) => `/shipping/zones/${zoneId}`,
+            shippingZoneLocations: (zoneId) => `/shipping/zones/${zoneId}/locations`,
+            shippingZoneMethods: (zoneId) => `/shipping/zones/${zoneId}/methods`,
+ 
+            // Settings
+            settings: '/settings',
+            settingsByGroup: (groupId) => `/settings/${groupId}`,
+            settingById: (groupId, settingId) => `/settings/${groupId}/${settingId}`,
+ 
+            // System Status
+            systemStatus: '/system_status',
+            systemStatusTools: '/system_status/tools',
+ 
+            // Webhooks
+            webhooks: '/webhooks',
+            webhookById: (webhookId) => `/webhooks/${webhookId}`,
+            webhookDeliveries: (webhookId) => `/webhooks/${webhookId}/deliveries`,
+        };
+ 
+        // Set API endpoint
+        this.apiEndpoint = `${this.baseUrl}/wp-json/wc/${this.version}`;
+    }
+ 
+    // Generate OAuth 1.0a signature for WooCommerce
+    generateOAuthSignature(method, url, params = {}) {
+        const oauth_params = {
+            oauth_consumer_key: this.consumer_key,
+            oauth_nonce: crypto.randomBytes(16).toString('hex'),
+            oauth_signature_method: 'HMAC-SHA1',
+            oauth_timestamp: Math.floor(Date.now() / 1000),
+            oauth_version: '1.0',
+            ...params
+        };
+ 
+        // Create parameter string
+        const paramString = Object.keys(oauth_params)
+            .sort()
+            .map(key => `${key}=${encodeURIComponent(oauth_params[key])}`)
+            .join('&');
+ 
+        // Create signature base string
+        const baseString = `${method.toUpperCase()}&${encodeURIComponent(url)}&${encodeURIComponent(paramString)}`;
+ 
+        // Create signing key
+        const signingKey = `${encodeURIComponent(this.consumer_secret)}&`;
+ 
+        // Generate signature
+        const signature = crypto.createHmac('sha1', signingKey).update(baseString).digest('base64');
+        oauth_params.oauth_signature = signature;
+ 
+        return oauth_params;
+    }
+ 
+    // Add authentication to request options
+    addAuthHeaders(options, method = 'GET') {
+        if (this.isHttps) {
+            // For HTTPS, use basic auth with consumer key/secret
+            const auth = Buffer.from(`${this.consumer_key}:${this.consumer_secret}`).toString('base64');
+            options.headers = {
+                ...options.headers,
+                'Authorization': `Basic ${auth}`,
+                'Content-Type': 'application/json',
+            };
+        } else {
+            // For HTTP, use OAuth 1.0a
+            const oauthParams = this.generateOAuthSignature(method, options.url);
+            const authHeader = 'OAuth ' + Object.keys(oauthParams)
+                .map(key => `${key}="${encodeURIComponent(oauthParams[key])}"`)
+                .join(', ');
+            
+            options.headers = {
+                ...options.headers,
+                'Authorization': authHeader,
+                'Content-Type': 'application/json',
+            };
+        }
+    }
+ 
+    async _get(options) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'GET');
+        return super._get(options);
+    }
+ 
+    async _post(options, stringify = true) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'POST');
+        return super._post(options, stringify);
+    }
+ 
+    async _put(options, stringify = true) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'PUT');
+        return super._put(options, stringify);
+    }
+ 
+    async _patch(options, stringify = true) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'PATCH');
+        return super._patch(options, stringify);
+    }
+ 
+    async _delete(options) {
+        options.url = this.apiEndpoint + options.url;
+        this.addAuthHeaders(options, 'DELETE');
+        return super._delete(options);
+    }
+ 
+    // **************************   Products   **********************************
+ 
+    async createProduct(productData) {
+        const options = {
+            url: this.URLs.products,
+            body: productData,
+        };
+        return this._post(options);
+    }
+ 
+    async listProducts(params = {}) {
+        const options = {
+            url: this.URLs.products,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductById(id) {
+        const options = {
+            url: this.URLs.productById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProduct(id, productData) {
+        const options = {
+            url: this.URLs.productById(id),
+            body: productData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProduct(id, force = false) {
+        const options = {
+            url: this.URLs.productById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async batchUpdateProducts(data) {
+        const options = {
+            url: this.URLs.products + '/batch',
+            body: data,
+        };
+        return this._post(options);
+    }
+ 
+    // Product Variations
+    async createProductVariation(productId, variationData) {
+        const options = {
+            url: this.URLs.productVariations(productId),
+            body: variationData,
+        };
+        return this._post(options);
+    }
+ 
+    async listProductVariations(productId, params = {}) {
+        const options = {
+            url: this.URLs.productVariations(productId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductVariationById(productId, variationId) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProductVariation(productId, variationId, variationData) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+            body: variationData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProductVariation(productId, variationId, force = false) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // Product Categories
+    async createProductCategory(categoryData) {
+        const options = {
+            url: this.URLs.productCategories,
+            body: categoryData,
+        };
+        return this._post(options);
+    }
+ 
+    async listProductCategories(params = {}) {
+        const options = {
+            url: this.URLs.productCategories,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductCategoryById(id) {
+        const options = {
+            url: this.URLs.productCategoryById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProductCategory(id, categoryData) {
+        const options = {
+            url: this.URLs.productCategoryById(id),
+            body: categoryData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProductCategory(id, force = false) {
+        const options = {
+            url: this.URLs.productCategoryById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // Product Reviews
+    async listProductReviews(params = {}) {
+        const options = {
+            url: this.URLs.productReviews,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getProductReviewById(id) {
+        const options = {
+            url: this.URLs.productReviewById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateProductReview(id, reviewData) {
+        const options = {
+            url: this.URLs.productReviewById(id),
+            body: reviewData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteProductReview(id, force = false) {
+        const options = {
+            url: this.URLs.productReviewById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Orders   **********************************
+ 
+    async createOrder(orderData) {
+        const options = {
+            url: this.URLs.orders,
+            body: orderData,
+        };
+        return this._post(options);
+    }
+ 
+    async listOrders(params = {}) {
+        const options = {
+            url: this.URLs.orders,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getOrderById(id) {
+        const options = {
+            url: this.URLs.orderById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateOrder(id, orderData) {
+        const options = {
+            url: this.URLs.orderById(id),
+            body: orderData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteOrder(id, force = false) {
+        const options = {
+            url: this.URLs.orderById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async batchUpdateOrders(data) {
+        const options = {
+            url: this.URLs.orders + '/batch',
+            body: data,
+        };
+        return this._post(options);
+    }
+ 
+    // Order Notes
+    async createOrderNote(orderId, noteData) {
+        const options = {
+            url: this.URLs.orderNotes(orderId),
+            body: noteData,
+        };
+        return this._post(options);
+    }
+ 
+    async listOrderNotes(orderId, params = {}) {
+        const options = {
+            url: this.URLs.orderNotes(orderId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getOrderNoteById(orderId, noteId) {
+        const options = {
+            url: this.URLs.orderNoteById(orderId, noteId),
+        };
+        return this._get(options);
+    }
+ 
+    async deleteOrderNote(orderId, noteId, force = false) {
+        const options = {
+            url: this.URLs.orderNoteById(orderId, noteId),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // Order Refunds
+    async createOrderRefund(orderId, refundData) {
+        const options = {
+            url: this.URLs.orderRefunds(orderId),
+            body: refundData,
+        };
+        return this._post(options);
+    }
+ 
+    async listOrderRefunds(orderId, params = {}) {
+        const options = {
+            url: this.URLs.orderRefunds(orderId),
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getOrderRefundById(orderId, refundId) {
+        const options = {
+            url: this.URLs.orderRefundById(orderId, refundId),
+        };
+        return this._get(options);
+    }
+ 
+    async deleteOrderRefund(orderId, refundId, force = false) {
+        const options = {
+            url: this.URLs.orderRefundById(orderId, refundId),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    // **************************   Customers   **********************************
+ 
+    async createCustomer(customerData) {
+        const options = {
+            url: this.URLs.customers,
+            body: customerData,
+        };
+        return this._post(options);
+    }
+ 
+    async listCustomers(params = {}) {
+        const options = {
+            url: this.URLs.customers,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getCustomerById(id) {
+        const options = {
+            url: this.URLs.customerById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateCustomer(id, customerData) {
+        const options = {
+            url: this.URLs.customerById(id),
+            body: customerData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteCustomer(id, force = false) {
+        const options = {
+            url: this.URLs.customerById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async batchUpdateCustomers(data) {
+        const options = {
+            url: this.URLs.customers + '/batch',
+            body: data,
+        };
+        return this._post(options);
+    }
+ 
+    async getCustomerDownloads(customerId) {
+        const options = {
+            url: this.URLs.customerDownloads(customerId),
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Webhooks   **********************************
+ 
+    async createWebhook(webhookData) {
+        const options = {
+            url: this.URLs.webhooks,
+            body: webhookData,
+        };
+        return this._post(options);
+    }
+ 
+    async listWebhooks(params = {}) {
+        const options = {
+            url: this.URLs.webhooks,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getWebhookById(id) {
+        const options = {
+            url: this.URLs.webhookById(id),
+        };
+        return this._get(options);
+    }
+ 
+    async updateWebhook(id, webhookData) {
+        const options = {
+            url: this.URLs.webhookById(id),
+            body: webhookData,
+        };
+        return this._put(options);
+    }
+ 
+    async deleteWebhook(id, force = false) {
+        const options = {
+            url: this.URLs.webhookById(id),
+            query: { force }
+        };
+        return this._delete(options);
+    }
+ 
+    async getWebhookDeliveries(webhookId) {
+        const options = {
+            url: this.URLs.webhookDeliveries(webhookId),
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Inventory   **********************************
+ 
+    async updateProductStock(productId, stockQuantity, manageStock = true) {
+        const options = {
+            url: this.URLs.productById(productId),
+            body: {
+                manage_stock: manageStock,
+                stock_quantity: stockQuantity,
+            },
+        };
+        return this._put(options);
+    }
+ 
+    async updateVariationStock(productId, variationId, stockQuantity, manageStock = true) {
+        const options = {
+            url: this.URLs.productVariationById(productId, variationId),
+            body: {
+                manage_stock: manageStock,
+                stock_quantity: stockQuantity,
+            },
+        };
+        return this._put(options);
+    }
+ 
+    // **************************   Reports   **********************************
+ 
+    async getSalesReport(params = {}) {
+        const options = {
+            url: this.URLs.reportsSales,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getTopSellersReport(params = {}) {
+        const options = {
+            url: this.URLs.reportsTopSellers,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Settings   **********************************
+ 
+    async getSettings(params = {}) {
+        const options = {
+            url: this.URLs.settings,
+            query: params
+        };
+        return this._get(options);
+    }
+ 
+    async getSettingsByGroup(groupId) {
+        const options = {
+            url: this.URLs.settingsByGroup(groupId),
+        };
+        return this._get(options);
+    }
+ 
+    async updateSetting(groupId, settingId, value) {
+        const options = {
+            url: this.URLs.settingById(groupId, settingId),
+            body: { value },
+        };
+        return this._put(options);
+    }
+ 
+    // **************************   System Status   **********************************
+ 
+    async getSystemStatus() {
+        const options = {
+            url: this.URLs.systemStatus,
+        };
+        return this._get(options);
+    }
+ 
+    async getSystemStatusTools() {
+        const options = {
+            url: this.URLs.systemStatusTools,
+        };
+        return this._get(options);
+    }
+ 
+    // **************************   Webhook Verification   **********************************
+ 
+    verifyWebhookSignature(payload, signature, secret) {
+        const hash = crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('base64');
+        return hash === signature;
+    }
+}
+ 
+module.exports = { Api };
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/woocommerce/coverage/lcov-report/base.css b/packages/woocommerce/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/packages/woocommerce/coverage/lcov-report/block-navigation.js b/packages/woocommerce/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/packages/woocommerce/coverage/lcov-report/favicon.png b/packages/woocommerce/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/packages/woocommerce/coverage/lcov-report/favicon.png differ diff --git a/packages/woocommerce/coverage/lcov-report/index.html b/packages/woocommerce/coverage/lcov-report/index.html new file mode 100644 index 0000000..ff29ca0 --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 18.43% + Statements + 33/179 +
+ + +
+ 12.5% + Branches + 4/32 +
+ + +
+ 12.08% + Functions + 11/91 +
+ + +
+ 18.43% + Lines + 33/179 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
api.js +
+
18.43%33/17912.5%4/3212.08%11/9118.43%33/179
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/woocommerce/coverage/lcov-report/prettify.css b/packages/woocommerce/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/packages/woocommerce/coverage/lcov-report/prettify.js b/packages/woocommerce/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/packages/woocommerce/coverage/lcov-report/sort-arrow-sprite.png b/packages/woocommerce/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/packages/woocommerce/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/packages/woocommerce/coverage/lcov-report/sorter.js b/packages/woocommerce/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/packages/woocommerce/coverage/lcov-report/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/packages/woocommerce/coverage/lcov.info b/packages/woocommerce/coverage/lcov.info new file mode 100644 index 0000000..44124de --- /dev/null +++ b/packages/woocommerce/coverage/lcov.info @@ -0,0 +1,402 @@ +TN: +SF:api.js +FN:9,(anonymous_0) +FN:21,(anonymous_1) +FN:22,(anonymous_2) +FN:23,(anonymous_3) +FN:25,(anonymous_4) +FN:27,(anonymous_5) +FN:29,(anonymous_6) +FN:30,(anonymous_7) +FN:32,(anonymous_8) +FN:36,(anonymous_9) +FN:37,(anonymous_10) +FN:38,(anonymous_11) +FN:39,(anonymous_12) +FN:40,(anonymous_13) +FN:44,(anonymous_14) +FN:45,(anonymous_15) +FN:49,(anonymous_16) +FN:58,(anonymous_17) +FN:63,(anonymous_18) +FN:64,(anonymous_19) +FN:65,(anonymous_20) +FN:69,(anonymous_21) +FN:70,(anonymous_22) +FN:78,(anonymous_23) +FN:79,(anonymous_24) +FN:87,(anonymous_25) +FN:100,(anonymous_26) +FN:117,(anonymous_27) +FN:130,(anonymous_28) +FN:141,(anonymous_29) +FN:147,(anonymous_30) +FN:153,(anonymous_31) +FN:159,(anonymous_32) +FN:165,(anonymous_33) +FN:173,(anonymous_34) +FN:181,(anonymous_35) +FN:189,(anonymous_36) +FN:196,(anonymous_37) +FN:204,(anonymous_38) +FN:212,(anonymous_39) +FN:221,(anonymous_40) +FN:229,(anonymous_41) +FN:237,(anonymous_42) +FN:244,(anonymous_43) +FN:252,(anonymous_44) +FN:261,(anonymous_45) +FN:269,(anonymous_46) +FN:277,(anonymous_47) +FN:284,(anonymous_48) +FN:292,(anonymous_49) +FN:301,(anonymous_50) +FN:309,(anonymous_51) +FN:316,(anonymous_52) +FN:324,(anonymous_53) +FN:334,(anonymous_54) +FN:342,(anonymous_55) +FN:350,(anonymous_56) +FN:357,(anonymous_57) +FN:365,(anonymous_58) +FN:373,(anonymous_59) +FN:382,(anonymous_60) +FN:390,(anonymous_61) +FN:398,(anonymous_62) +FN:405,(anonymous_63) +FN:414,(anonymous_64) +FN:422,(anonymous_65) +FN:430,(anonymous_66) +FN:437,(anonymous_67) +FN:447,(anonymous_68) +FN:455,(anonymous_69) +FN:463,(anonymous_70) +FN:470,(anonymous_71) +FN:478,(anonymous_72) +FN:486,(anonymous_73) +FN:494,(anonymous_74) +FN:503,(anonymous_75) +FN:511,(anonymous_76) +FN:519,(anonymous_77) +FN:526,(anonymous_78) +FN:534,(anonymous_79) +FN:542,(anonymous_80) +FN:551,(anonymous_81) +FN:562,(anonymous_82) +FN:575,(anonymous_83) +FN:583,(anonymous_84) +FN:593,(anonymous_85) +FN:601,(anonymous_86) +FN:608,(anonymous_87) +FN:618,(anonymous_88) +FN:625,(anonymous_89) +FN:634,(anonymous_90) +FNF:91 +FNH:11 +FNDA:10,(anonymous_0) +FNDA:1,(anonymous_1) +FNDA:1,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:1,(anonymous_9) +FNDA:1,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:1,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:1,(anonymous_25) +FNDA:5,(anonymous_26) +FNDA:2,(anonymous_27) +FNDA:6,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +FNDA:0,(anonymous_44) +FNDA:0,(anonymous_45) +FNDA:0,(anonymous_46) +FNDA:0,(anonymous_47) +FNDA:0,(anonymous_48) +FNDA:0,(anonymous_49) +FNDA:0,(anonymous_50) +FNDA:0,(anonymous_51) +FNDA:0,(anonymous_52) +FNDA:0,(anonymous_53) +FNDA:0,(anonymous_54) +FNDA:0,(anonymous_55) +FNDA:0,(anonymous_56) +FNDA:0,(anonymous_57) +FNDA:0,(anonymous_58) +FNDA:0,(anonymous_59) +FNDA:0,(anonymous_60) +FNDA:0,(anonymous_61) +FNDA:0,(anonymous_62) +FNDA:0,(anonymous_63) +FNDA:0,(anonymous_64) +FNDA:0,(anonymous_65) +FNDA:0,(anonymous_66) +FNDA:0,(anonymous_67) +FNDA:0,(anonymous_68) +FNDA:0,(anonymous_69) +FNDA:0,(anonymous_70) +FNDA:0,(anonymous_71) +FNDA:0,(anonymous_72) +FNDA:0,(anonymous_73) +FNDA:0,(anonymous_74) +FNDA:0,(anonymous_75) +FNDA:0,(anonymous_76) +FNDA:0,(anonymous_77) +FNDA:0,(anonymous_78) +FNDA:0,(anonymous_79) +FNDA:0,(anonymous_80) +FNDA:0,(anonymous_81) +FNDA:0,(anonymous_82) +FNDA:0,(anonymous_83) +FNDA:0,(anonymous_84) +FNDA:0,(anonymous_85) +FNDA:0,(anonymous_86) +FNDA:0,(anonymous_87) +FNDA:0,(anonymous_88) +FNDA:0,(anonymous_89) +FNDA:1,(anonymous_90) +DA:1,1 +DA:2,1 +DA:10,10 +DA:12,10 +DA:13,10 +DA:14,10 +DA:15,10 +DA:16,10 +DA:18,10 +DA:21,1 +DA:22,1 +DA:23,0 +DA:25,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:36,1 +DA:37,1 +DA:38,0 +DA:39,0 +DA:40,0 +DA:44,0 +DA:45,0 +DA:49,0 +DA:58,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:69,0 +DA:70,0 +DA:78,1 +DA:79,0 +DA:83,10 +DA:88,1 +DA:98,1 +DA:100,5 +DA:104,1 +DA:107,1 +DA:110,1 +DA:111,1 +DA:113,1 +DA:118,2 +DA:120,1 +DA:121,1 +DA:128,1 +DA:129,1 +DA:130,6 +DA:133,1 +DA:142,0 +DA:143,0 +DA:144,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:174,0 +DA:178,0 +DA:182,0 +DA:186,0 +DA:190,0 +DA:193,0 +DA:197,0 +DA:201,0 +DA:205,0 +DA:209,0 +DA:213,0 +DA:217,0 +DA:222,0 +DA:226,0 +DA:230,0 +DA:234,0 +DA:238,0 +DA:241,0 +DA:245,0 +DA:249,0 +DA:253,0 +DA:257,0 +DA:262,0 +DA:266,0 +DA:270,0 +DA:274,0 +DA:278,0 +DA:281,0 +DA:285,0 +DA:289,0 +DA:293,0 +DA:297,0 +DA:302,0 +DA:306,0 +DA:310,0 +DA:313,0 +DA:317,0 +DA:321,0 +DA:325,0 +DA:329,0 +DA:335,0 +DA:339,0 +DA:343,0 +DA:347,0 +DA:351,0 +DA:354,0 +DA:358,0 +DA:362,0 +DA:366,0 +DA:370,0 +DA:374,0 +DA:378,0 +DA:383,0 +DA:387,0 +DA:391,0 +DA:395,0 +DA:399,0 +DA:402,0 +DA:406,0 +DA:410,0 +DA:415,0 +DA:419,0 +DA:423,0 +DA:427,0 +DA:431,0 +DA:434,0 +DA:438,0 +DA:442,0 +DA:448,0 +DA:452,0 +DA:456,0 +DA:460,0 +DA:464,0 +DA:467,0 +DA:471,0 +DA:475,0 +DA:479,0 +DA:483,0 +DA:487,0 +DA:491,0 +DA:495,0 +DA:498,0 +DA:504,0 +DA:508,0 +DA:512,0 +DA:516,0 +DA:520,0 +DA:523,0 +DA:527,0 +DA:531,0 +DA:535,0 +DA:539,0 +DA:543,0 +DA:546,0 +DA:552,0 +DA:559,0 +DA:563,0 +DA:570,0 +DA:576,0 +DA:580,0 +DA:584,0 +DA:588,0 +DA:594,0 +DA:598,0 +DA:602,0 +DA:605,0 +DA:609,0 +DA:613,0 +DA:619,0 +DA:622,0 +DA:626,0 +DA:629,0 +DA:635,1 +DA:636,1 +DA:640,1 +LF:179 +LH:33 +BRDA:16,0,0,10 +BRDA:16,0,1,0 +BRDA:87,1,0,1 +BRDA:117,2,0,0 +BRDA:118,3,0,1 +BRDA:118,3,1,1 +BRDA:147,4,0,0 +BRDA:153,5,0,0 +BRDA:159,6,0,0 +BRDA:181,7,0,0 +BRDA:204,8,0,0 +BRDA:229,9,0,0 +BRDA:252,10,0,0 +BRDA:269,11,0,0 +BRDA:292,12,0,0 +BRDA:301,13,0,0 +BRDA:324,14,0,0 +BRDA:342,15,0,0 +BRDA:365,16,0,0 +BRDA:390,17,0,0 +BRDA:405,18,0,0 +BRDA:422,19,0,0 +BRDA:437,20,0,0 +BRDA:455,21,0,0 +BRDA:478,22,0,0 +BRDA:511,23,0,0 +BRDA:534,24,0,0 +BRDA:551,25,0,0 +BRDA:562,26,0,0 +BRDA:575,27,0,0 +BRDA:583,28,0,0 +BRDA:593,29,0,0 +BRF:32 +BRH:4 +end_of_record diff --git a/packages/woocommerce/coverage/prettify.css b/packages/woocommerce/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/packages/woocommerce/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/packages/woocommerce/coverage/prettify.js b/packages/woocommerce/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/packages/woocommerce/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/packages/woocommerce/coverage/sort-arrow-sprite.png b/packages/woocommerce/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/packages/woocommerce/coverage/sort-arrow-sprite.png differ diff --git a/packages/woocommerce/coverage/sorter.js b/packages/woocommerce/coverage/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/packages/woocommerce/coverage/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/packages/woocommerce/defaultConfig.json b/packages/woocommerce/defaultConfig.json new file mode 100644 index 0000000..a5ce56e --- /dev/null +++ b/packages/woocommerce/defaultConfig.json @@ -0,0 +1,11 @@ +{ + "name": "woocommerce", + "label": "WooCommerce", + "productUrl": "https://woocommerce.com", + "apiDocs": "https://woocommerce.github.io/woocommerce-rest-api-docs/", + "logoUrl": "https://friggframework.org/assets/img/woocommerce-icon.png", + "categories": [ + "E-commerce" + ], + "description": "WooCommerce is a customizable, open-source eCommerce platform built on WordPress that powers millions of online stores worldwide." +} \ No newline at end of file diff --git a/packages/woocommerce/definition.js b/packages/woocommerce/definition.js new file mode 100644 index 0000000..cf25773 --- /dev/null +++ b/packages/woocommerce/definition.js @@ -0,0 +1,80 @@ +require('dotenv').config(); +const {Api} = require('./api'); +const {get} = require("@friggframework/core"); +const config = require('./defaultConfig.json') + +const Definition = { + API: Api, + getName: function () { + return config.name + }, + moduleName: config.name, + modelName: 'WooCommerce', + requiredAuthMethods: { + getToken: async function (api, params) { + // WooCommerce uses Consumer Key/Secret, not OAuth tokens + const consumer_key = get(params.data, 'consumer_key'); + const consumer_secret = get(params.data, 'consumer_secret'); + const baseUrl = get(params.data, 'baseUrl'); + + if (!consumer_key || !consumer_secret || !baseUrl) { + throw new Error('Missing required WooCommerce credentials: consumer_key, consumer_secret, and baseUrl'); + } + + return { + consumer_key, + consumer_secret, + baseUrl, + access_token: consumer_key, // Store as access_token for compatibility + token_type: 'consumer_key' + }; + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + // Get store information to identify the entity + const systemStatus = await api.getSystemStatus(); + const settings = await api.getSettingsByGroup('general'); + + const storeTitle = settings.find(s => s.id === 'woocommerce_store_name')?.value || 'WooCommerce Store'; + const storeUrl = api.baseUrl; + + return { + identifiers: {externalId: storeUrl, user: userId}, + details: { + name: storeTitle, + url: storeUrl, + version: systemStatus.wc_version || 'Unknown' + }, + } + }, + apiPropertiesToPersist: { + credential: [ + 'consumer_key', 'consumer_secret', 'baseUrl', 'access_token' + ], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const systemStatus = await api.getSystemStatus(); + const settings = await api.getSettingsByGroup('general'); + + const storeTitle = settings.find(s => s.id === 'woocommerce_store_name')?.value || 'WooCommerce Store'; + + return { + identifiers: {externalId: api.baseUrl, user: userId}, + details: { + name: storeTitle, + version: systemStatus.wc_version || 'Unknown' + } + }; + }, + testAuthRequest: async function (api) { + return api.getSystemStatus() + }, + }, + env: { + consumer_key: process.env.WOOCOMMERCE_CONSUMER_KEY, + consumer_secret: process.env.WOOCOMMERCE_CONSUMER_SECRET, + baseUrl: process.env.WOOCOMMERCE_BASE_URL, + } +}; + +module.exports = {Definition}; \ No newline at end of file diff --git a/packages/woocommerce/index.js b/packages/woocommerce/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/woocommerce/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/woocommerce/jest.config.js b/packages/woocommerce/jest.config.js new file mode 100644 index 0000000..fa8c051 --- /dev/null +++ b/packages/woocommerce/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEnv: ['/tests/setup.js'] +}; \ No newline at end of file diff --git a/packages/woocommerce/package.json b/packages/woocommerce/package.json new file mode 100644 index 0000000..6267840 --- /dev/null +++ b/packages/woocommerce/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-woocommerce", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "WooCommerce API module that lets the Frigg Framework interact with WooCommerce", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@friggframework/devtools": "^1.1.2", + "@friggframework/test": "^1.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.16" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/woocommerce/tests/api.test.js b/packages/woocommerce/tests/api.test.js new file mode 100644 index 0000000..6d59c93 --- /dev/null +++ b/packages/woocommerce/tests/api.test.js @@ -0,0 +1,89 @@ +const { Api } = require('../api'); + +describe('WooCommerce API', () => { + let api; + + beforeEach(() => { + api = new Api({ + baseUrl: 'https://example.com', + consumer_key: 'test_key', + consumer_secret: 'test_secret' + }); + }); + + describe('Constructor', () => { + test('should initialize with correct base URL and credentials', () => { + expect(api.baseUrl).toBe('https://example.com'); + expect(api.consumer_key).toBe('test_key'); + expect(api.consumer_secret).toBe('test_secret'); + expect(api.apiEndpoint).toBe('https://example.com/wp-json/wc/v3'); + }); + + test('should detect HTTPS correctly', () => { + expect(api.isHttps).toBe(true); + + const httpApi = new Api({ + baseUrl: 'http://example.com', + consumer_key: 'test_key', + consumer_secret: 'test_secret' + }); + expect(httpApi.isHttps).toBe(false); + }); + }); + + describe('Authentication', () => { + test('should add basic auth for HTTPS', () => { + const options = { headers: {} }; + api.addAuthHeaders(options, 'GET'); + + expect(options.headers.Authorization).toContain('Basic'); + expect(options.headers['Content-Type']).toBe('application/json'); + }); + + test('should add OAuth signature for HTTP', () => { + const httpApi = new Api({ + baseUrl: 'http://example.com', + consumer_key: 'test_key', + consumer_secret: 'test_secret' + }); + + const options = { + url: 'http://example.com/wp-json/wc/v3/products', + headers: {} + }; + httpApi.addAuthHeaders(options, 'GET'); + + expect(options.headers.Authorization).toContain('OAuth'); + }); + }); + + describe('URL Construction', () => { + test('should construct product URLs correctly', () => { + expect(api.URLs.products).toBe('/products'); + expect(api.URLs.productById(123)).toBe('/products/123'); + expect(api.URLs.productVariations(123)).toBe('/products/123/variations'); + }); + + test('should construct order URLs correctly', () => { + expect(api.URLs.orders).toBe('/orders'); + expect(api.URLs.orderById(456)).toBe('/orders/456'); + expect(api.URLs.orderNotes(456)).toBe('/orders/456/notes'); + }); + + test('should construct webhook URLs correctly', () => { + expect(api.URLs.webhooks).toBe('/webhooks'); + expect(api.URLs.webhookById(789)).toBe('/webhooks/789'); + }); + }); + + describe('Webhook Verification', () => { + test('should verify webhook signature correctly', () => { + const payload = '{"test": "data"}'; + const secret = 'webhook_secret'; + const signature = 'XM+fUc/+4OMhOaJlEwVK20UqBWLUeHvEBKRRfX8t4OU='; + + const isValid = api.verifyWebhookSignature(payload, signature, secret); + expect(typeof isValid).toBe('boolean'); + }); + }); +}); \ No newline at end of file diff --git a/packages/woocommerce/tests/setup.js b/packages/woocommerce/tests/setup.js new file mode 100644 index 0000000..16f4874 --- /dev/null +++ b/packages/woocommerce/tests/setup.js @@ -0,0 +1,12 @@ +// Test setup file for WooCommerce API module +require('dotenv').config(); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; \ No newline at end of file diff --git a/packages/xero/README.md b/packages/xero/README.md new file mode 100644 index 0000000..bb7f29d --- /dev/null +++ b/packages/xero/README.md @@ -0,0 +1,136 @@ +# Xero API Module + +A comprehensive Node.js module for integrating with Xero's Accounting API, built for the Frigg Framework. + +## Overview + +This module provides seamless integration with Xero accounting software, supporting financial management, invoicing, contact management, and business operations. It handles OAuth2 authentication and provides methods for managing accounting data. + +## Installation + +```bash +npm install @friggframework/api-module-xero +``` + +## Configuration + +### Environment Variables + +```bash +XERO_CLIENT_ID=your_xero_client_id +XERO_CLIENT_SECRET=your_xero_client_secret +XERO_SCOPE=openid profile email accounting.transactions accounting.contacts +REDIRECT_URI=your_redirect_uri_base +``` + +### Xero App Setup + +1. Go to [Xero Developer Portal](https://developer.xero.com/myapps) +2. Create a new app +3. Configure redirect URI: `{REDIRECT_URI}/xero` +4. Set required scopes: + - `openid profile email` - Basic identity + - `accounting.transactions` - Transaction access + - `accounting.contacts` - Contact management + - `accounting.settings` - Organization settings + +## Usage + +### Basic Setup + +```javascript +const { Api, Definition } = require('@friggframework/api-module-xero'); + +const api = new Api({ + client_id: process.env.XERO_CLIENT_ID, + client_secret: process.env.XERO_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/xero`, + scope: 'openid profile email accounting.transactions accounting.contacts' +}); +``` + +### Authentication Flow + +```javascript +// 1. Get authorization URL +const authUrl = api.getAuthUri(); + +// 2. Handle callback +const tokens = await api.getTokenFromCode(authorizationCode); + +// 3. Get tenant information and set tenant ID +const tenants = await api.getTenants(); +api.tenantId = tenants[0].tenantId; + +// 4. Get organization details +const org = await api.getOrganisation(); +``` + +### Core Operations + +```javascript +// Get contacts +const contacts = await api.getContacts(); + +// Create contact +const newContact = await api.createContact({ + Contacts: [{ + Name: 'John Doe', + EmailAddress: 'john@example.com', + ContactStatus: 'ACTIVE' + }] +}); + +// Get invoices +const invoices = await api.getInvoices({ + where: 'Type="ACCREC"', + order: 'Date DESC' +}); + +// Create invoice +const newInvoice = await api.createInvoice({ + Invoices: [{ + Type: 'ACCREC', + Contact: { ContactID: 'contact_id' }, + Date: new Date().toISOString().split('T')[0], + DueDate: new Date(Date.now() + 30*24*60*60*1000).toISOString().split('T')[0], + LineItems: [{ + Description: 'Consulting Services', + Quantity: 1, + UnitAmount: 100.00, + AccountCode: '200' + }] + }] +}); + +// Get accounts +const accounts = await api.getAccounts(); +``` + +## API Reference + +### Core Methods + +#### Authentication & Organization +- `getTenants()` - Get authorized organizations +- `getOrganisation()` - Get organization details + +#### Contacts +- `getContacts(params)` - Get contacts +- `createContact(contactData)` - Create new contact + +#### Invoices +- `getInvoices(params)` - Get invoices +- `createInvoice(invoiceData)` - Create new invoice + +#### Accounts +- `getAccounts(params)` - Get chart of accounts + +## Resources + +- [Xero API Documentation](https://developer.xero.com/documentation/) +- [Xero OAuth2 Guide](https://developer.xero.com/documentation/guides/oauth2/overview) + +## License + +MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/packages/xero/api.js b/packages/xero/api.js new file mode 100644 index 0000000..a97e71d --- /dev/null +++ b/packages/xero/api.js @@ -0,0 +1,149 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// Xero Accounting API +// https://developer.xero.com/documentation/ +// Core resources: contacts, invoices, payments, accounts + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://api.xero.com/api.xro/2.0'; + + this.URLs = { + // Organisation + organisation: '/Organisation', + + // Contacts + contacts: '/Contacts', + contactById: (contactId) => `/Contacts/${contactId}`, + + // Invoices + invoices: '/Invoices', + invoiceById: (invoiceId) => `/Invoices/${invoiceId}`, + + // Payments + payments: '/Payments', + + // Accounts + accounts: '/Accounts', + + // Items + items: '/Items', + }; + + this.authorizationUri = encodeURI( + `https://login.xero.com/identity/connect/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${this.scope}&state=${this.state}` + ); + this.tokenUri = 'https://identity.xero.com/connect/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + this.tenantId = get(params, 'tenantId', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri, + }, + }; + + const response = await this._post(options); + await this.setTokens(response); + return response; + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + this.refresh_token = get(params, 'refresh_token'); + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + async getTenants() { + const options = { + url: 'https://api.xero.com/connections', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json', + }, + }; + return this._get(options); + } + + addAuthHeaders(options) { + const authHeaders = { + 'Authorization': `Bearer ${this.access_token}`, + 'Accept': 'application/json', + 'Xero-tenant-id': this.tenantId, + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + async getOrganisation() { + const options = { + url: this.baseUrl + this.URLs.organisation, + }; + return this._get(options); + } + + async getContacts(params = {}) { + const options = { + url: this.baseUrl + this.URLs.contacts, + query: params, + }; + return this._get(options); + } + + async createContact(body) { + const options = { + url: this.baseUrl + this.URLs.contacts, + body: body, + }; + return this._post(options); + } + + async getInvoices(params = {}) { + const options = { + url: this.baseUrl + this.URLs.invoices, + query: params, + }; + return this._get(options); + } + + async createInvoice(body) { + const options = { + url: this.baseUrl + this.URLs.invoices, + body: body, + }; + return this._post(options); + } + + async getAccounts(params = {}) { + const options = { + url: this.baseUrl + this.URLs.accounts, + query: params, + }; + return this._get(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/xero/defaultConfig.json b/packages/xero/defaultConfig.json new file mode 100644 index 0000000..058d0d2 --- /dev/null +++ b/packages/xero/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "xero", + "label": "Xero", + "productUrl": "https://xero.com", + "apiDocs": "https://developer.xero.com/documentation/", + "logoUrl": "https://www.xero.com/content/dam/xero/images/logos/xero-logo-blue.svg", + "categories": [ + "Accounting Software", + "Financial Management", + "Invoicing", + "Business Intelligence" + ], + "description": "Xero is cloud-based accounting software designed for small and medium-sized businesses to manage finances and operations." +} \ No newline at end of file diff --git a/packages/xero/definition.js b/packages/xero/definition.js new file mode 100644 index 0000000..26572d2 --- /dev/null +++ b/packages/xero/definition.js @@ -0,0 +1,50 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Xero', + requiredAuthMethods: { + getToken: async (api, params) => { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const tenants = await api.getTenants(); + if (tenants.length > 0) { + api.tenantId = tenants[0].tenantId; + } + const orgDetails = await api.getOrganisation(); + const org = orgDetails.Organisations[0]; + return { + identifiers: { externalId: org.OrganisationID, user: userId }, + details: { name: org.Name, tenantId: api.tenantId }, + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token'], + entity: ['tenantId'], + }, + getCredentialDetails: async (api, userId) => { + const orgDetails = await api.getOrganisation(); + const org = orgDetails.Organisations[0]; + return { + identifiers: { externalId: org.OrganisationID, user: userId }, + details: {}, + }; + }, + testAuthRequest: async (api) => api.getOrganisation(), + }, + env: { + client_id: process.env.XERO_CLIENT_ID, + client_secret: process.env.XERO_CLIENT_SECRET, + scope: process.env.XERO_SCOPE || 'openid profile email accounting.transactions accounting.contacts', + redirect_uri: `${process.env.REDIRECT_URI}/xero`, + }, +}; + +module.exports = { Definition }; diff --git a/packages/xero/index.js b/packages/xero/index.js new file mode 100644 index 0000000..002e1fc --- /dev/null +++ b/packages/xero/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; diff --git a/packages/needs-updating/yotpo/.env.example b/packages/yotpo/.env.example similarity index 100% rename from packages/needs-updating/yotpo/.env.example rename to packages/yotpo/.env.example diff --git a/packages/needs-updating/yotpo/CHANGELOG.md b/packages/yotpo/CHANGELOG.md similarity index 100% rename from packages/needs-updating/yotpo/CHANGELOG.md rename to packages/yotpo/CHANGELOG.md diff --git a/packages/v1-ready/ironclad/LICENSE.md b/packages/yotpo/LICENSE.md similarity index 100% rename from packages/v1-ready/ironclad/LICENSE.md rename to packages/yotpo/LICENSE.md diff --git a/packages/needs-updating/yotpo/README.md b/packages/yotpo/README.md similarity index 100% rename from packages/needs-updating/yotpo/README.md rename to packages/yotpo/README.md diff --git a/packages/needs-updating/yotpo/api/UGCApi.js b/packages/yotpo/api/UGCApi.js similarity index 100% rename from packages/needs-updating/yotpo/api/UGCApi.js rename to packages/yotpo/api/UGCApi.js diff --git a/packages/needs-updating/yotpo/api/api.js b/packages/yotpo/api/api.js similarity index 100% rename from packages/needs-updating/yotpo/api/api.js rename to packages/yotpo/api/api.js diff --git a/packages/needs-updating/yotpo/api/appDeveloperApi.js b/packages/yotpo/api/appDeveloperApi.js similarity index 100% rename from packages/needs-updating/yotpo/api/appDeveloperApi.js rename to packages/yotpo/api/appDeveloperApi.js diff --git a/packages/needs-updating/yotpo/api/coreApi.js b/packages/yotpo/api/coreApi.js similarity index 100% rename from packages/needs-updating/yotpo/api/coreApi.js rename to packages/yotpo/api/coreApi.js diff --git a/packages/needs-updating/yotpo/api/loyaltyApi.js b/packages/yotpo/api/loyaltyApi.js similarity index 100% rename from packages/needs-updating/yotpo/api/loyaltyApi.js rename to packages/yotpo/api/loyaltyApi.js diff --git a/packages/needs-updating/yotpo/authFields.js b/packages/yotpo/authFields.js similarity index 100% rename from packages/needs-updating/yotpo/authFields.js rename to packages/yotpo/authFields.js diff --git a/packages/needs-updating/yotpo/credential.js b/packages/yotpo/credential.js similarity index 100% rename from packages/needs-updating/yotpo/credential.js rename to packages/yotpo/credential.js diff --git a/packages/needs-updating/yotpo/custom-jest-env.js b/packages/yotpo/custom-jest-env.js similarity index 100% rename from packages/needs-updating/yotpo/custom-jest-env.js rename to packages/yotpo/custom-jest-env.js diff --git a/packages/needs-updating/yotpo/defaultConfig.json b/packages/yotpo/defaultConfig.json similarity index 100% rename from packages/needs-updating/yotpo/defaultConfig.json rename to packages/yotpo/defaultConfig.json diff --git a/packages/yotpo/definition.js b/packages/yotpo/definition.js new file mode 100644 index 0000000..497ee08 --- /dev/null +++ b/packages/yotpo/definition.js @@ -0,0 +1,237 @@ +const { IntegrationBase, debug, get, ModuleConstants } = require('@friggframework/core'); +const { Api } = require('./api/api'); +const { Entity } = require('./entity'); +const { Credential } = require('./credential'); +const AuthFields = require('./authFields'); + +class YotpoIntegration extends IntegrationBase { + static Definition = { + name: 'yotpo', + version: '1.0.0', + display: { + label: 'Yotpo', + description: 'Yotpo', + imageURL: 'https://friggframework.org/assets/img/yotpo-icon.png', + icon: '', + category: 'Marketing', + }, + modules: { + api: Api, + credential: Credential, + entity: Entity, + }, + }; + + static AuthFields = AuthFields; + + async findOrCreateCredential(params) { + const store_id = get(params.data, 'store_id', null); + const secret = get(params.data, 'secret', null); + + const search = await this.Manager.Credential.find({ + user: this.userId, + store_id, + secret, + }); + + if (search.length === 0) { + const createObj = { + user: this.userId, + store_id, + secret, + }; + this.credential = await this.Manager.Credential.create(createObj); + } else if (search.length === 1) { + this.credential = search[0]; + } else { + debug( + 'Multiple credentials found with the same Client ID', + store_id, + secret + ); + } + } + + async findOrCreateEntity(params) { + const store_id = get(params.data, 'store_id', null); + const name = get(params, 'name', null); + + const search = await this.Manager.Entity.find({ + user: this.userId, + externalId: store_id, + }); + if (search.length === 0) { + const createObj = { + credential: this.credential.id, + user: this.userId, + name, + externalId: store_id, + }; + this.entity = await this.Manager.Entity.create(createObj); + } else if (search.length === 1) { + this.entity = search[0]; + } else { + debug( + 'Multiple entities found with the same external ID:', + store_id + ); + this.throwException(''); + } + } + + async getAuthorizationRequirements() { + return { + url: this.api.appDeveloperApi.authorizationUri, + type: ModuleConstants.authType.oauth2, + data: { + jsonSchema: AuthFields.jsonSchema, + uiSchema: AuthFields.uiSchema, + }, + }; + } + + async testAuth() { + let validAuth = false; + const authRequests = [ + this.api.appDeveloperApi.listOrders(), + this.api.coreApi.listOrders(), + ]; + if ( + this.api.loyaltyApi.API_KEY_VALUE || + this.credential.loyalty_api_key + ) + authRequests.push(this.api.loyaltyApi.listActiveCampaigns()); + try { + await Promise.all(authRequests); + validAuth = true; + } catch (e) { + debug(e); + } + return validAuth; + } + + async receiveNotification(notifier, delegateString, object = null) { + if (delegateString === this.api.appDeveloperApi.DLGT_TOKEN_UPDATE) { + const updatedToken = { + user: this.userId.toString(), + access_token: this.api.appDeveloperApi.access_token, + refresh_token: this.api.appDeveloperApi.refresh_token, + auth_is_valid: true, + store_id: this.api.coreApi.store_id, + secret: this.api.coreApi.secret, + coreApiAccessToken: this.api.coreApi.API_KEY_VALUE, + appKey: this.api.appDeveloperApi.appKey, + loyalty_api_key: this.api.loyaltyApi.API_KEY_VALUE, + loyalty_guid: this.api.loyaltyApi.GUID, + }; + + Object.keys(updatedToken).forEach( + (k) => updatedToken[k] == null && delete updatedToken[k] + ); + // TODO-new globally... multiple credentials should be allowed, this is 1:1 + if (!this.credential) { + let credentialSearch = await this.Manager.Credential.find({ + user: this.userId.toString(), + }); + if (credentialSearch.length === 0) { + this.credential = await this.Manager.Credential.create(updatedToken); + } else if (credentialSearch.length === 1) { + this.credential = await this.Manager.Credential.findOneAndUpdate( + {_id: credentialSearch[0]}, + {$set: updatedToken}, + {useFindAndModify: true, new: true} + ); + } else { + // Handling multiple credentials found with an error for the time being + debug( + 'Multiple credentials found with the same client ID:' + ); + } + } else { + this.credential = await this.Manager.Credential.findOneAndUpdate( + {_id: this.credential}, + {$set: updatedToken}, + {useFindAndModify: true, new: true} + ); + } + } + if ( + delegateString === this.api.appDeveloperApi.DLGT_TOKEN_DEAUTHORIZED + ) { + await this.deauthorize(); + } + if (delegateString === this.api.appDeveloperApi.DLGT_INVALID_AUTH) { + return this.markCredentialsInvalid(); + } + } + + async processAuthorizationCallback(params) { + const store_id = get(params.data, 'store_id', null); + const secret = get(params.data, 'secret', null); + const code = get(params.data, 'code', null); + const loyalty_api_key = get(params.data, 'loyalty_api_key', null); + const loyalty_guid = get(params.data, 'loyalty_guid', null); + // const appKey = get(params.data, 'app_key', null); + // vv TDOO temporary for specific implementation override. Don't do this at home. + const appKey = get(params.data, 'store_id', null); + this.api.coreApi.store_id = store_id; + this.api.coreApi.apiKeySecret = secret; + this.api.appDeveloperApi.appKey = appKey; + if (loyalty_api_key) this.api.loyaltyApi.setApiKey(loyalty_api_key); + if (loyalty_guid) this.api.loyaltyApi.setGuid(loyalty_guid); + await this.api.coreApi.getToken(); + await this.api.appDeveloperApi.getTokenFromCode(code); + const authRes = await this.testAuth(); + if (!authRes) throw new Error('Authentication failed'); + + await this.findOrCreateEntity({ + store_id, + secret, + }); + return { + credential_id: this.credential.id, + entity_id: this.entity.id, + type: YotpoIntegration.Definition.name, + }; + } + + async deauthorize() { + // wipe api connection + this.api = new Api(); + + // delete credentials from the database + const entity = await this.entityMO.getByUserId(this.userId); + if (entity.credential) { + await this.credentialMO.delete(entity.credential); + entity.credential = undefined; + await entity.save(); + } + } + + async getApiObject() { + let apiParams = { + client_id: process.env.YOTPO_CLIENT_ID, + client_secret: process.env.YOTPO_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/yotpo`, + delegate: this, + }; + + if (this.credential) { + apiParams = { + ...apiParams, + ...this.credential.toObject(), + }; + apiParams.API_KEY_VALUE = apiParams.coreApiAccessToken; + } + + const api = new Api(apiParams); + if (apiParams.loyalty_api_key) { + api.loyaltyApi.setApiKey(apiParams.loyalty_api_key); + api.loyaltyApi.setGuid(apiParams.loyalty_guid); + } + + return api; + } +} + +module.exports = YotpoIntegration; \ No newline at end of file diff --git a/packages/needs-updating/yotpo/entity.js b/packages/yotpo/entity.js similarity index 100% rename from packages/needs-updating/yotpo/entity.js rename to packages/yotpo/entity.js diff --git a/packages/needs-updating/yotpo/fixtures/responses/authResponse.json b/packages/yotpo/fixtures/responses/authResponse.json similarity index 100% rename from packages/needs-updating/yotpo/fixtures/responses/authResponse.json rename to packages/yotpo/fixtures/responses/authResponse.json diff --git a/packages/needs-updating/yotpo/fixtures/responses/createOrderFulfillmentResponse.json b/packages/yotpo/fixtures/responses/createOrderFulfillmentResponse.json similarity index 100% rename from packages/needs-updating/yotpo/fixtures/responses/createOrderFulfillmentResponse.json rename to packages/yotpo/fixtures/responses/createOrderFulfillmentResponse.json diff --git a/packages/yotpo/index.js b/packages/yotpo/index.js new file mode 100644 index 0000000..a0eac7a --- /dev/null +++ b/packages/yotpo/index.js @@ -0,0 +1,3 @@ +const Definition = require('./definition'); + +module.exports = { Definition }; diff --git a/packages/needs-updating/yotpo/jest.config.js b/packages/yotpo/jest.config.js similarity index 100% rename from packages/needs-updating/yotpo/jest.config.js rename to packages/yotpo/jest.config.js diff --git a/packages/needs-updating/yotpo/test/api.test.js b/packages/yotpo/test/api.test.js similarity index 100% rename from packages/needs-updating/yotpo/test/api.test.js rename to packages/yotpo/test/api.test.js diff --git a/packages/needs-updating/yotpo/test/loyaltyApi.test.js b/packages/yotpo/test/loyaltyApi.test.js similarity index 100% rename from packages/needs-updating/yotpo/test/loyaltyApi.test.js rename to packages/yotpo/test/loyaltyApi.test.js diff --git a/packages/needs-updating/yotpo/test/manager.test.js b/packages/yotpo/test/manager.test.js similarity index 100% rename from packages/needs-updating/yotpo/test/manager.test.js rename to packages/yotpo/test/manager.test.js diff --git a/packages/needs-updating/yotpo/test/recorded-requests/.loyaltyApi.json.backup b/packages/yotpo/test/recorded-requests/.loyaltyApi.json.backup similarity index 100% rename from packages/needs-updating/yotpo/test/recorded-requests/.loyaltyApi.json.backup rename to packages/yotpo/test/recorded-requests/.loyaltyApi.json.backup diff --git a/packages/youtube/README.md b/packages/youtube/README.md new file mode 100644 index 0000000..0d829de --- /dev/null +++ b/packages/youtube/README.md @@ -0,0 +1,124 @@ +# YouTube API Module + +A comprehensive Node.js module for integrating with YouTube's Data API v3, built for the Frigg Framework. + +## Overview + +This module provides seamless integration with YouTube, supporting video management, channel operations, playlist management, and content discovery. It handles Google OAuth2 authentication and provides methods for managing YouTube resources. + +## Installation + +```bash +npm install @friggframework/api-module-youtube +``` + +## Configuration + +### Environment Variables + +```bash +YOUTUBE_CLIENT_ID=your_google_client_id +YOUTUBE_CLIENT_SECRET=your_google_client_secret +YOUTUBE_SCOPE=https://www.googleapis.com/auth/youtube +REDIRECT_URI=your_redirect_uri_base +``` + +### Google Cloud Console Setup + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select existing one +3. Enable YouTube Data API v3 +4. Create OAuth2 credentials +5. Configure redirect URI: `{REDIRECT_URI}/youtube` +6. Set required scopes: + - `https://www.googleapis.com/auth/youtube` - Full YouTube access + - `https://www.googleapis.com/auth/youtube.readonly` - Read-only access + - `https://www.googleapis.com/auth/youtube.upload` - Upload videos + +## Usage + +### Basic Setup + +```javascript +const { Api, Definition } = require('@friggframework/api-module-youtube'); + +const api = new Api({ + client_id: process.env.YOUTUBE_CLIENT_ID, + client_secret: process.env.YOUTUBE_CLIENT_SECRET, + redirect_uri: `${process.env.REDIRECT_URI}/youtube`, + scope: 'https://www.googleapis.com/auth/youtube' +}); +``` + +### Authentication Flow + +```javascript +// 1. Get authorization URL +const authUrl = api.getAuthUri(); + +// 2. Handle callback +const tokens = await api.getTokenFromCode(authorizationCode); + +// 3. Get channel details +const channel = await api.getMyChannel(); +``` + +### Core Operations + +```javascript +// Get my channel +const myChannel = await api.getMyChannel(); + +// Search videos +const searchResults = await api.search({ + q: 'nodejs tutorial', + type: 'video', + maxResults: 25 +}); + +// Get videos by ID +const videos = await api.getVideos({ + id: 'video_id_1,video_id_2' +}); + +// Get playlists +const playlists = await api.getPlaylists({ + channelId: 'channel_id', + maxResults: 50 +}); + +// Subscribe to a channel +await api.subscribe('channel_id'); +``` + +## API Reference + +### Core Methods + +#### Authentication & Channels +- `getMyChannel()` - Get authenticated user's channel +- `getChannels(params)` - Get channel information + +#### Videos +- `getVideos(params)` - Get video details +- `uploadVideo(videoData)` - Upload video (requires multipart handling) + +#### Search & Discovery +- `search(params)` - Search YouTube content + +#### Playlists +- `getPlaylists(params)` - Get playlist information +- `createPlaylist(playlistData)` - Create new playlist + +#### Subscriptions +- `getSubscriptions(params)` - Get user subscriptions +- `subscribe(channelId)` - Subscribe to channel + +## Resources + +- [YouTube Data API Documentation](https://developers.google.com/youtube/v3) +- [Google OAuth2 Guide](https://developers.google.com/identity/protocols/oauth2) + +## License + +MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/packages/youtube/api.js b/packages/youtube/api.js new file mode 100644 index 0000000..b99a847 --- /dev/null +++ b/packages/youtube/api.js @@ -0,0 +1,179 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); + +// YouTube Data API v3 +// https://developers.google.com/youtube/v3 +// Core resources: channels, videos, playlists, search, subscriptions + +class Api extends OAuth2Requester { + constructor(params) { + super(params); + + this.baseUrl = 'https://www.googleapis.com/youtube/v3'; + + this.URLs = { + // Channels + channels: '/channels', + + // Videos + videos: '/videos', + + // Playlists + playlists: '/playlists', + playlistItems: '/playlistItems', + + // Search + search: '/search', + + // Subscriptions + subscriptions: '/subscriptions', + + // Comments + comments: '/comments', + commentThreads: '/commentThreads', + }; + + this.authorizationUri = encodeURI( + `https://accounts.google.com/o/oauth2/v2/auth?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${this.scope}&response_type=code&state=${this.state}&access_type=offline` + ); + this.tokenUri = 'https://oauth2.googleapis.com/token'; + + this.access_token = get(params, 'access_token', null); + this.refresh_token = get(params, 'refresh_token', null); + } + + getAuthUri() { + return this.authorizationUri; + } + + async getTokenFromCode(code) { + const options = { + url: this.tokenUri, + form: { + grant_type: 'authorization_code', + client_id: this.client_id, + client_secret: this.client_secret, + code: code, + redirect_uri: this.redirect_uri, + }, + }; + + const response = await this._post(options); + await this.setTokens(response); + return response; + } + + async setTokens(params) { + this.access_token = get(params, 'access_token'); + this.refresh_token = get(params, 'refresh_token'); + + const accessExpiresIn = get(params, 'expires_in', null); + if (accessExpiresIn) { + this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000); + } + + await this.notify(this.DLGT_TOKEN_UPDATE); + } + + addAuthHeaders(options) { + const authHeaders = { + 'Authorization': `Bearer ${this.access_token}`, + 'Accept': 'application/json', + }; + options.headers = { + ...authHeaders, + ...options.headers, + }; + } + + // ************************** Channels ********************************** + + async getChannels(params = {}) { + const options = { + url: this.baseUrl + this.URLs.channels, + query: { part: 'snippet,contentDetails,statistics', ...params }, + }; + return this._get(options); + } + + async getMyChannel() { + return this.getChannels({ mine: true }); + } + + // ************************** Videos ********************************** + + async getVideos(params = {}) { + const options = { + url: this.baseUrl + this.URLs.videos, + query: { part: 'snippet,contentDetails,statistics', ...params }, + }; + return this._get(options); + } + + async uploadVideo(body) { + // Note: Video uploads require multipart/form-data and resumable uploads + // This is a simplified version - full implementation would handle file uploads + const options = { + url: this.baseUrl + this.URLs.videos, + query: { part: 'snippet,status' }, + body: body, + }; + return this._post(options); + } + + // ************************** Playlists ********************************** + + async getPlaylists(params = {}) { + const options = { + url: this.baseUrl + this.URLs.playlists, + query: { part: 'snippet,contentDetails', ...params }, + }; + return this._get(options); + } + + async createPlaylist(body) { + const options = { + url: this.baseUrl + this.URLs.playlists, + query: { part: 'snippet,status' }, + body: body, + }; + return this._post(options); + } + + // ************************** Search ********************************** + + async search(params = {}) { + const options = { + url: this.baseUrl + this.URLs.search, + query: { part: 'snippet', ...params }, + }; + return this._get(options); + } + + // ************************** Subscriptions ********************************** + + async getSubscriptions(params = {}) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + query: { part: 'snippet,contentDetails', ...params }, + }; + return this._get(options); + } + + async subscribe(channelId) { + const options = { + url: this.baseUrl + this.URLs.subscriptions, + query: { part: 'snippet' }, + body: { + snippet: { + resourceId: { + kind: 'youtube#channel', + channelId: channelId + } + } + }, + }; + return this._post(options); + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/youtube/defaultConfig.json b/packages/youtube/defaultConfig.json new file mode 100644 index 0000000..c9f441a --- /dev/null +++ b/packages/youtube/defaultConfig.json @@ -0,0 +1,14 @@ +{ + "name": "youtube", + "label": "YouTube", + "productUrl": "https://youtube.com", + "apiDocs": "https://developers.google.com/youtube/v3", + "logoUrl": "https://upload.wikimedia.org/wikipedia/commons/b/b8/YouTube_Logo_2017.svg", + "categories": [ + "Video Platform", + "Content Management", + "Social Media", + "Google Services" + ], + "description": "YouTube is Google's video sharing platform that allows users to upload, view, and share videos worldwide." +} \ No newline at end of file diff --git a/packages/youtube/definition.js b/packages/youtube/definition.js new file mode 100644 index 0000000..f84b2cb --- /dev/null +++ b/packages/youtube/definition.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'YouTube', + requiredAuthMethods: { + getToken: async function (api, params) { + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + }, + getEntityDetails: async function (api, callbackParams, tokenResponse, userId) { + const channel = await api.getMyChannel(); + const channelData = channel.items[0]; + return { + identifiers: { externalId: channelData.id, user: userId }, + details: { + name: channelData.snippet.title, + description: channelData.snippet.description + }, + }; + }, + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token'], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + const channel = await api.getMyChannel(); + const channelData = channel.items[0]; + return { + identifiers: { externalId: channelData.id, user: userId }, + details: {}, + }; + }, + testAuthRequest: async function (api) { + return api.getMyChannel(); + }, + }, + env: { + client_id: process.env.YOUTUBE_CLIENT_ID, + client_secret: process.env.YOUTUBE_CLIENT_SECRET, + scope: process.env.YOUTUBE_SCOPE || 'https://www.googleapis.com/auth/youtube', + redirect_uri: `${process.env.REDIRECT_URI}/youtube`, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/youtube/index.js b/packages/youtube/index.js new file mode 100644 index 0000000..c23eb7f --- /dev/null +++ b/packages/youtube/index.js @@ -0,0 +1,9 @@ +const {Api} = require('./api'); +const {Definition} = require('./definition'); +const Config = require('./defaultConfig'); + +module.exports = { + Api, + Config, + Definition +}; \ No newline at end of file diff --git a/packages/zendesk/api.js b/packages/zendesk/api.js new file mode 100644 index 0000000..d92c83e --- /dev/null +++ b/packages/zendesk/api.js @@ -0,0 +1,425 @@ +const { OAuth2Requester, get } = require('@friggframework/core'); +const axios = require('axios'); + +class Api extends OAuth2Requester { + constructor(params = {}) { + super(params); + + this.subdomain = get(params, 'subdomain', null); + this.email = get(params, 'email', null); + this.apiToken = get(params, 'apiToken', null); + this.clientId = get(params, 'clientId', null); + this.clientSecret = get(params, 'clientSecret', null); + this.redirectUri = get(params, 'redirectUri', null); + + this.baseUrl = `https://${this.subdomain}.zendesk.com`; + this.apiUrl = `${this.baseUrl}/api/v2`; + + // OAuth2 endpoints + this.authorizationUri = `${this.baseUrl}/oauth/authorizations/new`; + this.tokenUri = `${this.baseUrl}/oauth/tokens`; + + this.scope = get(params, 'scope', 'read write').replace(/,/g, ' '); + + this.client = axios.create({ + baseURL: this.apiUrl, + }); + + // Add request interceptor for authentication + this.client.interceptors.request.use((config) => { + if (this.access_token) { + // OAuth2 authentication + config.headers['Authorization'] = `Bearer ${this.access_token}`; + } else if (this.email && this.apiToken) { + // API token authentication + const token = Buffer.from(`${this.email}/token:${this.apiToken}`).toString('base64'); + config.headers['Authorization'] = `Basic ${token}`; + } + config.headers['Content-Type'] = 'application/json'; + return config; + }); + } + + // OAuth2 Methods + async getAuthorizationUri() { + const params = new URLSearchParams({ + response_type: 'code', + client_id: this.clientId, + redirect_uri: this.redirectUri, + scope: this.scope, + state: Math.random().toString(36).substring(7), + }); + + return `${this.authorizationUri}?${params.toString()}`; + } + + async getTokenFromCode(code) { + const data = { + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri, + scope: this.scope, + }; + + const response = await axios.post(this.tokenUri, data); + await this.setTokens(response.data); + return response.data; + } + + async refreshAccessToken(refreshToken) { + const data = { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: this.clientId, + client_secret: this.clientSecret, + }; + + const response = await axios.post(this.tokenUri, data); + await this.setTokens(response.data); + return response.data; + } + + // Helper method for API requests + async makeRequest(method, endpoint, data = null, params = null) { + try { + const response = await this.client({ + method, + url: endpoint, + data, + params, + }); + return response.data; + } catch (error) { + throw new Error(`Zendesk API Error: ${error.response?.data?.error || error.message}`); + } + } + + // Ticket endpoints + async getTickets(params = {}) { + return this.makeRequest('GET', '/tickets.json', null, params); + } + + async getTicket(ticketId, params = {}) { + return this.makeRequest('GET', `/tickets/${ticketId}.json`, null, params); + } + + async createTicket(ticket) { + return this.makeRequest('POST', '/tickets.json', { ticket }); + } + + async updateTicket(ticketId, ticket) { + return this.makeRequest('PUT', `/tickets/${ticketId}.json`, { ticket }); + } + + async deleteTicket(ticketId) { + return this.makeRequest('DELETE', `/tickets/${ticketId}.json`); + } + + async bulkDeleteTickets(ticketIds) { + const ids = ticketIds.join(','); + return this.makeRequest('DELETE', `/tickets/destroy_many.json?ids=${ids}`); + } + + // Ticket comments + async getTicketComments(ticketId, params = {}) { + return this.makeRequest('GET', `/tickets/${ticketId}/comments.json`, null, params); + } + + async addTicketComment(ticketId, comment) { + const ticket = { + comment: comment + }; + return this.makeRequest('PUT', `/tickets/${ticketId}.json`, { ticket }); + } + + // Ticket fields + async getTicketFields(params = {}) { + return this.makeRequest('GET', '/ticket_fields.json', null, params); + } + + async getTicketField(fieldId) { + return this.makeRequest('GET', `/ticket_fields/${fieldId}.json`); + } + + async createTicketField(ticketField) { + return this.makeRequest('POST', '/ticket_fields.json', { ticket_field: ticketField }); + } + + async updateTicketField(fieldId, ticketField) { + return this.makeRequest('PUT', `/ticket_fields/${fieldId}.json`, { ticket_field: ticketField }); + } + + // User endpoints + async getUsers(params = {}) { + return this.makeRequest('GET', '/users.json', null, params); + } + + async getUser(userId) { + return this.makeRequest('GET', `/users/${userId}.json`); + } + + async getCurrentUser() { + return this.makeRequest('GET', '/users/me.json'); + } + + async createUser(user) { + return this.makeRequest('POST', '/users.json', { user }); + } + + async createOrUpdateUser(user) { + return this.makeRequest('POST', '/users/create_or_update.json', { user }); + } + + async updateUser(userId, user) { + return this.makeRequest('PUT', `/users/${userId}.json`, { user }); + } + + async deleteUser(userId) { + return this.makeRequest('DELETE', `/users/${userId}.json`); + } + + async suspendUser(userId) { + return this.makeRequest('PUT', `/users/${userId}.json`, { + user: { suspended: true } + }); + } + + async searchUsers(query, params = {}) { + params.query = query; + return this.makeRequest('GET', '/users/search.json', null, params); + } + + // Organization endpoints + async getOrganizations(params = {}) { + return this.makeRequest('GET', '/organizations.json', null, params); + } + + async getOrganization(organizationId) { + return this.makeRequest('GET', `/organizations/${organizationId}.json`); + } + + async createOrganization(organization) { + return this.makeRequest('POST', '/organizations.json', { organization }); + } + + async updateOrganization(organizationId, organization) { + return this.makeRequest('PUT', `/organizations/${organizationId}.json`, { organization }); + } + + async deleteOrganization(organizationId) { + return this.makeRequest('DELETE', `/organizations/${organizationId}.json`); + } + + async searchOrganizations(query, params = {}) { + return this.makeRequest('GET', '/organizations/autocomplete.json', null, { ...params, name: query }); + } + + // Group endpoints + async getGroups(params = {}) { + return this.makeRequest('GET', '/groups.json', null, params); + } + + async getGroup(groupId) { + return this.makeRequest('GET', `/groups/${groupId}.json`); + } + + async createGroup(group) { + return this.makeRequest('POST', '/groups.json', { group }); + } + + async updateGroup(groupId, group) { + return this.makeRequest('PUT', `/groups/${groupId}.json`, { group }); + } + + async deleteGroup(groupId) { + return this.makeRequest('DELETE', `/groups/${groupId}.json`); + } + + // Brand endpoints + async getBrands(params = {}) { + return this.makeRequest('GET', '/brands.json', null, params); + } + + async getBrand(brandId) { + return this.makeRequest('GET', `/brands/${brandId}.json`); + } + + // View endpoints + async getViews(params = {}) { + return this.makeRequest('GET', '/views.json', null, params); + } + + async getView(viewId) { + return this.makeRequest('GET', `/views/${viewId}.json`); + } + + async executeView(viewId, params = {}) { + return this.makeRequest('GET', `/views/${viewId}/tickets.json`, null, params); + } + + async getViewCount(viewId) { + return this.makeRequest('GET', `/views/${viewId}/count.json`); + } + + // Macro endpoints + async getMacros(params = {}) { + return this.makeRequest('GET', '/macros.json', null, params); + } + + async getMacro(macroId) { + return this.makeRequest('GET', `/macros/${macroId}.json`); + } + + async applyMacro(ticketId, macroId) { + return this.makeRequest('PUT', `/tickets/${ticketId}/macros/${macroId}/apply.json`); + } + + // Automation endpoints + async getAutomations(params = {}) { + return this.makeRequest('GET', '/automations.json', null, params); + } + + async getAutomation(automationId) { + return this.makeRequest('GET', `/automations/${automationId}.json`); + } + + // Trigger endpoints + async getTriggers(params = {}) { + return this.makeRequest('GET', '/triggers.json', null, params); + } + + async getTrigger(triggerId) { + return this.makeRequest('GET', `/triggers/${triggerId}.json`); + } + + // Search endpoint + async search(query, params = {}) { + params.query = query; + return this.makeRequest('GET', '/search.json', null, params); + } + + // Satisfaction ratings + async getSatisfactionRatings(params = {}) { + return this.makeRequest('GET', '/satisfaction_ratings.json', null, params); + } + + async getSatisfactionRating(ratingId) { + return this.makeRequest('GET', `/satisfaction_ratings/${ratingId}.json`); + } + + // Tags + async getTags(params = {}) { + return this.makeRequest('GET', '/tags.json', null, params); + } + + async setTags(type, id, tags) { + return this.makeRequest('PUT', `/${type}/${id}/tags.json`, { tags }); + } + + async addTags(type, id, tags) { + return this.makeRequest('PUT', `/${type}/${id}/tags.json`, { tags, safe_update: true }); + } + + async deleteTags(type, id, tags) { + return this.makeRequest('DELETE', `/${type}/${id}/tags.json`, { tags }); + } + + // Help Center endpoints + async getCategories(params = {}) { + return this.makeRequest('GET', '/help_center/categories.json', null, params); + } + + async getCategory(categoryId) { + return this.makeRequest('GET', `/help_center/categories/${categoryId}.json`); + } + + async createCategory(category) { + return this.makeRequest('POST', '/help_center/categories.json', { category }); + } + + async getSections(params = {}) { + return this.makeRequest('GET', '/help_center/sections.json', null, params); + } + + async getSection(sectionId) { + return this.makeRequest('GET', `/help_center/sections/${sectionId}.json`); + } + + async createSection(section) { + return this.makeRequest('POST', '/help_center/sections.json', { section }); + } + + async getArticles(params = {}) { + return this.makeRequest('GET', '/help_center/articles.json', null, params); + } + + async getArticle(articleId) { + return this.makeRequest('GET', `/help_center/articles/${articleId}.json`); + } + + async createArticle(article) { + return this.makeRequest('POST', '/help_center/articles.json', { article }); + } + + async updateArticle(articleId, article) { + return this.makeRequest('PUT', `/help_center/articles/${articleId}.json`, { article }); + } + + async searchArticles(query, params = {}) { + params.query = query; + return this.makeRequest('GET', '/help_center/articles/search.json', null, params); + } + + // Custom objects + async getCustomObjects(params = {}) { + return this.makeRequest('GET', '/custom_objects.json', null, params); + } + + async getCustomObject(key) { + return this.makeRequest('GET', `/custom_objects/${key}.json`); + } + + async getCustomObjectRecords(key, params = {}) { + return this.makeRequest('GET', `/custom_objects/${key}/records.json`, null, params); + } + + async createCustomObjectRecord(key, record) { + return this.makeRequest('POST', `/custom_objects/${key}/records.json`, { record }); + } + + // Webhooks + async getWebhooks(params = {}) { + return this.makeRequest('GET', '/webhooks.json', null, params); + } + + async getWebhook(webhookId) { + return this.makeRequest('GET', `/webhooks/${webhookId}.json`); + } + + async createWebhook(webhook) { + return this.makeRequest('POST', '/webhooks.json', { webhook }); + } + + async updateWebhook(webhookId, webhook) { + return this.makeRequest('PUT', `/webhooks/${webhookId}.json`, { webhook }); + } + + async deleteWebhook(webhookId) { + return this.makeRequest('DELETE', `/webhooks/${webhookId}.json`); + } + + // Webhook signature verification + verifyWebhookSignature(payload, signature, signingSecret) { + const crypto = require('crypto'); + const expectedSignature = crypto + .createHmac('sha256', signingSecret) + .update(payload) + .digest('base64'); + + return signature === expectedSignature; + } +} + +module.exports = { Api }; \ No newline at end of file diff --git a/packages/zendesk/defaultConfig.json b/packages/zendesk/defaultConfig.json new file mode 100644 index 0000000..007e3aa --- /dev/null +++ b/packages/zendesk/defaultConfig.json @@ -0,0 +1,19 @@ +{ + "name": "zendesk", + "displayName": "Zendesk", + "description": "Customer service and support ticketing platform", + "version": "1.0.0", + "categories": ["customer-support", "helpdesk", "crm"], + "scopes": [ + "read", + "write", + "tickets:read", + "tickets:write", + "users:read", + "users:write", + "organizations:read", + "organizations:write", + "hc:read", + "hc:write" + ] +} \ No newline at end of file diff --git a/packages/zendesk/definition.js b/packages/zendesk/definition.js new file mode 100644 index 0000000..1f5d65d --- /dev/null +++ b/packages/zendesk/definition.js @@ -0,0 +1,91 @@ +require('dotenv').config(); +const { Api } = require('./api.js'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Zendesk', + requiredAuthMethods: { + getToken: async function (api, params) { + if (params.data.code) { + // OAuth2 flow + const code = get(params.data, 'code'); + return api.getTokenFromCode(code); + } else { + // API token flow + return { + email: params.data.email, + apiToken: params.data.apiToken, + subdomain: params.data.subdomain, + }; + } + }, + + getEntityDetails: async function (api, userId) { + const userResponse = await api.getCurrentUser(); + const user = userResponse.user; + + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: user.id.toString(), + user: userId + }, + details: { + name: user.name, + email: user.email, + role: user.role, + organizationId: user.organization_id, + timeZone: user.time_zone, + locale: user.locale, + subdomain: api.subdomain, + }, + }; + }, + + apiPropertiesToPersist: { + credential: ['access_token', 'refresh_token', 'email', 'apiToken', 'subdomain'], + entity: ['role', 'organization_id', 'subdomain'], + }, + + getCredentialDetails: async function (api, userId) { + const userResponse = await api.getCurrentUser(); + const user = userResponse.user; + + if (userId.userId) userId = userId.userId; + return { + identifiers: { + externalId: user.id.toString(), + user: userId + }, + details: { + createdAt: user.created_at, + updatedAt: user.updated_at, + verified: user.verified, + active: user.active, + }, + }; + }, + + testAuthRequest: function (api) { + return api.getCurrentUser(); + }, + }, + env: { + // OAuth2 configuration + clientId: process.env.ZENDESK_CLIENT_ID, + clientSecret: process.env.ZENDESK_CLIENT_SECRET, + redirectUri: `${process.env.REDIRECT_URI}/zendesk`, + subdomain: process.env.ZENDESK_SUBDOMAIN, + // API token configuration + email: process.env.ZENDESK_EMAIL, + apiToken: process.env.ZENDESK_API_TOKEN, + }, +}; + +module.exports = { Definition }; \ No newline at end of file diff --git a/packages/zendesk/index.js b/packages/zendesk/index.js new file mode 100644 index 0000000..5a1a5bd --- /dev/null +++ b/packages/zendesk/index.js @@ -0,0 +1,7 @@ +const { Definition } = require('./definition'); +const { Api } = require('./api'); + +module.exports = { + Definition, + Api, +}; \ No newline at end of file diff --git a/packages/zendesk/package.json b/packages/zendesk/package.json new file mode 100644 index 0000000..0668350 --- /dev/null +++ b/packages/zendesk/package.json @@ -0,0 +1,26 @@ +{ + "name": "@friggframework/zendesk", + "version": "1.0.0", + "description": "Zendesk customer service platform API module for Frigg Framework", + "main": "index.js", + "scripts": { + "test": "jest" + }, + "dependencies": { + "@friggframework/core": "^1.0.0", + "axios": "^1.6.0" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "keywords": [ + "zendesk", + "customer-support", + "helpdesk", + "tickets", + "api", + "frigg" + ], + "author": "Frigg Framework", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/zendesk/readme.md b/packages/zendesk/readme.md new file mode 100644 index 0000000..89adb65 --- /dev/null +++ b/packages/zendesk/readme.md @@ -0,0 +1,36 @@ +# Zendesk API Module + +This module provides integration with the Zendesk customer service platform API. + +## Features + +- OAuth2 and API token authentication +- Ticket management (create, read, update, delete) +- User and organization management +- Help Center articles and knowledge base +- Macros, triggers, and automations +- Custom fields and objects +- Satisfaction ratings +- Webhooks support +- Search functionality + +## Environment Variables + +For OAuth2: +``` +ZENDESK_CLIENT_ID=your_client_id +ZENDESK_CLIENT_SECRET=your_client_secret +ZENDESK_SUBDOMAIN=your_subdomain +REDIRECT_URI=your_app_redirect_uri +``` + +For API Token authentication: +``` +ZENDESK_EMAIL=your_email +ZENDESK_API_TOKEN=your_api_token +ZENDESK_SUBDOMAIN=your_subdomain +``` + +## Usage + +See the [Frigg Framework documentation](https://docs.friggframework.org) for usage details. \ No newline at end of file diff --git a/packages/zendesk/tests/api.test.js b/packages/zendesk/tests/api.test.js new file mode 100644 index 0000000..c547af1 --- /dev/null +++ b/packages/zendesk/tests/api.test.js @@ -0,0 +1,96 @@ +const { Api } = require('../api'); + +describe('Zendesk API', () => { + let api; + + beforeEach(() => { + api = new Api({ + subdomain: 'test-company', + clientId: 'test_client_id', + clientSecret: 'test_client_secret', + redirectUri: 'https://example.com/callback', + }); + }); + + test('should initialize with OAuth2 configuration', () => { + expect(api.subdomain).toBe('test-company'); + expect(api.clientId).toBe('test_client_id'); + expect(api.clientSecret).toBe('test_client_secret'); + expect(api.baseUrl).toBe('https://test-company.zendesk.com'); + expect(api.apiUrl).toBe('https://test-company.zendesk.com/api/v2'); + expect(api.client).toBeDefined(); + }); + + test('should initialize with API token configuration', () => { + const apiTokenAuth = new Api({ + subdomain: 'test-company', + email: 'user@example.com', + apiToken: 'test_api_token', + }); + + expect(apiTokenAuth.email).toBe('user@example.com'); + expect(apiTokenAuth.apiToken).toBe('test_api_token'); + }); + + test('should generate authorization URI', async () => { + const authUri = await api.getAuthorizationUri(); + + expect(authUri).toContain('https://test-company.zendesk.com/oauth/authorizations/new'); + expect(authUri).toContain('client_id=test_client_id'); + expect(authUri).toContain('response_type=code'); + expect(authUri).toContain('redirect_uri='); + }); + + test('should construct create ticket request', async () => { + api.access_token = 'test_access_token'; + + // Mock the makeRequest method + api.makeRequest = jest.fn().mockResolvedValue({ + ticket: { + id: 123, + subject: 'Test ticket', + status: 'new', + }, + }); + + const result = await api.createTicket({ + subject: 'Test ticket', + comment: { body: 'Test description' }, + priority: 'normal', + }); + + expect(api.makeRequest).toHaveBeenCalledWith('POST', '/tickets.json', { + ticket: { + subject: 'Test ticket', + comment: { body: 'Test description' }, + priority: 'normal', + }, + }); + expect(result.ticket.id).toBe(123); + }); + + test('should add tags to resources', async () => { + api.access_token = 'test_access_token'; + + api.makeRequest = jest.fn().mockResolvedValue({ tags: ['tag1', 'tag2'] }); + + await api.addTags('tickets', 123, ['tag1', 'tag2']); + + expect(api.makeRequest).toHaveBeenCalledWith('PUT', '/tickets/123/tags.json', { + tags: ['tag1', 'tag2'], + safe_update: true, + }); + }); + + test('should verify webhook signature correctly', () => { + const payload = 'test-payload'; + const signingSecret = 'test-secret'; + const validSignature = require('crypto') + .createHmac('sha256', signingSecret) + .update(payload) + .digest('base64'); + + expect(api.verifyWebhookSignature(payload, validSignature, signingSecret)).toBe(true); + expect(api.verifyWebhookSignature(payload, 'invalid-signature', signingSecret)).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/v1-ready/zoho-crm/.env.example b/packages/zoho-crm/.env.example similarity index 100% rename from packages/v1-ready/zoho-crm/.env.example rename to packages/zoho-crm/.env.example diff --git a/packages/v1-ready/zoho-crm/CHANGELOG.md b/packages/zoho-crm/CHANGELOG.md similarity index 100% rename from packages/v1-ready/zoho-crm/CHANGELOG.md rename to packages/zoho-crm/CHANGELOG.md diff --git a/packages/v1-ready/zoho-crm/README.md b/packages/zoho-crm/README.md similarity index 100% rename from packages/v1-ready/zoho-crm/README.md rename to packages/zoho-crm/README.md diff --git a/packages/v1-ready/zoho-crm/api.js b/packages/zoho-crm/api.js similarity index 100% rename from packages/v1-ready/zoho-crm/api.js rename to packages/zoho-crm/api.js diff --git a/packages/v1-ready/zoho-crm/defaultConfig.json b/packages/zoho-crm/defaultConfig.json similarity index 100% rename from packages/v1-ready/zoho-crm/defaultConfig.json rename to packages/zoho-crm/defaultConfig.json diff --git a/packages/v1-ready/zoho-crm/definition.js b/packages/zoho-crm/definition.js similarity index 100% rename from packages/v1-ready/zoho-crm/definition.js rename to packages/zoho-crm/definition.js diff --git a/packages/v1-ready/zoho-crm/fenestra/platform.fenestra.yaml b/packages/zoho-crm/fenestra/platform.fenestra.yaml similarity index 100% rename from packages/v1-ready/zoho-crm/fenestra/platform.fenestra.yaml rename to packages/zoho-crm/fenestra/platform.fenestra.yaml diff --git a/packages/v1-ready/zoho-crm/fenestra/schemas/zoho-crm-validation.json b/packages/zoho-crm/fenestra/schemas/zoho-crm-validation.json similarity index 100% rename from packages/v1-ready/zoho-crm/fenestra/schemas/zoho-crm-validation.json rename to packages/zoho-crm/fenestra/schemas/zoho-crm-validation.json diff --git a/packages/v1-ready/zoho-crm/images/image-1.jpg b/packages/zoho-crm/images/image-1.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-1.jpg rename to packages/zoho-crm/images/image-1.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-10.jpg b/packages/zoho-crm/images/image-10.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-10.jpg rename to packages/zoho-crm/images/image-10.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-11.jpg b/packages/zoho-crm/images/image-11.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-11.jpg rename to packages/zoho-crm/images/image-11.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-12.jpg b/packages/zoho-crm/images/image-12.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-12.jpg rename to packages/zoho-crm/images/image-12.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-2.jpg b/packages/zoho-crm/images/image-2.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-2.jpg rename to packages/zoho-crm/images/image-2.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-3.jpg b/packages/zoho-crm/images/image-3.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-3.jpg rename to packages/zoho-crm/images/image-3.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-5.jpg b/packages/zoho-crm/images/image-5.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-5.jpg rename to packages/zoho-crm/images/image-5.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-6.jpg b/packages/zoho-crm/images/image-6.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-6.jpg rename to packages/zoho-crm/images/image-6.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-7.jpg b/packages/zoho-crm/images/image-7.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-7.jpg rename to packages/zoho-crm/images/image-7.jpg diff --git a/packages/v1-ready/zoho-crm/images/image-9.jpg b/packages/zoho-crm/images/image-9.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image-9.jpg rename to packages/zoho-crm/images/image-9.jpg diff --git a/packages/v1-ready/zoho-crm/images/image.jpg b/packages/zoho-crm/images/image.jpg similarity index 100% rename from packages/v1-ready/zoho-crm/images/image.jpg rename to packages/zoho-crm/images/image.jpg diff --git a/packages/v1-ready/zoho-crm/index.js b/packages/zoho-crm/index.js similarity index 100% rename from packages/v1-ready/zoho-crm/index.js rename to packages/zoho-crm/index.js diff --git a/packages/v1-ready/zoho-crm/jest.config.js b/packages/zoho-crm/jest.config.js similarity index 100% rename from packages/v1-ready/zoho-crm/jest.config.js rename to packages/zoho-crm/jest.config.js diff --git a/packages/v1-ready/zoho-crm/package.json b/packages/zoho-crm/package.json similarity index 100% rename from packages/v1-ready/zoho-crm/package.json rename to packages/zoho-crm/package.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/README.md b/packages/zoho-crm/specs/openAPI/v8.0/README.md similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/README.md rename to packages/zoho-crm/specs/openAPI/v8.0/README.md diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/apis.json b/packages/zoho-crm/specs/openAPI/v8.0/apis.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/apis.json rename to packages/zoho-crm/specs/openAPI/v8.0/apis.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/appointment_preference.json b/packages/zoho-crm/specs/openAPI/v8.0/appointment_preference.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/appointment_preference.json rename to packages/zoho-crm/specs/openAPI/v8.0/appointment_preference.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/assignment_rules.json b/packages/zoho-crm/specs/openAPI/v8.0/assignment_rules.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/assignment_rules.json rename to packages/zoho-crm/specs/openAPI/v8.0/assignment_rules.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/associate_email.json b/packages/zoho-crm/specs/openAPI/v8.0/associate_email.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/associate_email.json rename to packages/zoho-crm/specs/openAPI/v8.0/associate_email.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/attachments.json b/packages/zoho-crm/specs/openAPI/v8.0/attachments.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/attachments.json rename to packages/zoho-crm/specs/openAPI/v8.0/attachments.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/audit_log_export.json b/packages/zoho-crm/specs/openAPI/v8.0/audit_log_export.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/audit_log_export.json rename to packages/zoho-crm/specs/openAPI/v8.0/audit_log_export.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/available_currencies.json b/packages/zoho-crm/specs/openAPI/v8.0/available_currencies.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/available_currencies.json rename to packages/zoho-crm/specs/openAPI/v8.0/available_currencies.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/backup.json b/packages/zoho-crm/specs/openAPI/v8.0/backup.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/backup.json rename to packages/zoho-crm/specs/openAPI/v8.0/backup.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/blueprint.json b/packages/zoho-crm/specs/openAPI/v8.0/blueprint.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/blueprint.json rename to packages/zoho-crm/specs/openAPI/v8.0/blueprint.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/bulk_read.json b/packages/zoho-crm/specs/openAPI/v8.0/bulk_read.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/bulk_read.json rename to packages/zoho-crm/specs/openAPI/v8.0/bulk_read.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/bulk_write.json b/packages/zoho-crm/specs/openAPI/v8.0/bulk_write.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/bulk_write.json rename to packages/zoho-crm/specs/openAPI/v8.0/bulk_write.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/business_hours.json b/packages/zoho-crm/specs/openAPI/v8.0/business_hours.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/business_hours.json rename to packages/zoho-crm/specs/openAPI/v8.0/business_hours.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/cadences.json b/packages/zoho-crm/specs/openAPI/v8.0/cadences.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/cadences.json rename to packages/zoho-crm/specs/openAPI/v8.0/cadences.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/cadences_execution.json b/packages/zoho-crm/specs/openAPI/v8.0/cadences_execution.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/cadences_execution.json rename to packages/zoho-crm/specs/openAPI/v8.0/cadences_execution.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/call_preferences.json b/packages/zoho-crm/specs/openAPI/v8.0/call_preferences.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/call_preferences.json rename to packages/zoho-crm/specs/openAPI/v8.0/call_preferences.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/cancel_meetings.json b/packages/zoho-crm/specs/openAPI/v8.0/cancel_meetings.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/cancel_meetings.json rename to packages/zoho-crm/specs/openAPI/v8.0/cancel_meetings.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/change_owner.json b/packages/zoho-crm/specs/openAPI/v8.0/change_owner.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/change_owner.json rename to packages/zoho-crm/specs/openAPI/v8.0/change_owner.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/common.json b/packages/zoho-crm/specs/openAPI/v8.0/common.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/common.json rename to packages/zoho-crm/specs/openAPI/v8.0/common.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/contact_roles.json b/packages/zoho-crm/specs/openAPI/v8.0/contact_roles.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/contact_roles.json rename to packages/zoho-crm/specs/openAPI/v8.0/contact_roles.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/conversion_option.json b/packages/zoho-crm/specs/openAPI/v8.0/conversion_option.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/conversion_option.json rename to packages/zoho-crm/specs/openAPI/v8.0/conversion_option.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/convert_lead.json b/packages/zoho-crm/specs/openAPI/v8.0/convert_lead.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/convert_lead.json rename to packages/zoho-crm/specs/openAPI/v8.0/convert_lead.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/coql.json b/packages/zoho-crm/specs/openAPI/v8.0/coql.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/coql.json rename to packages/zoho-crm/specs/openAPI/v8.0/coql.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/currencies.json b/packages/zoho-crm/specs/openAPI/v8.0/currencies.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/currencies.json rename to packages/zoho-crm/specs/openAPI/v8.0/currencies.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/custom_views.json b/packages/zoho-crm/specs/openAPI/v8.0/custom_views.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/custom_views.json rename to packages/zoho-crm/specs/openAPI/v8.0/custom_views.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/download_attachments.json b/packages/zoho-crm/specs/openAPI/v8.0/download_attachments.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/download_attachments.json rename to packages/zoho-crm/specs/openAPI/v8.0/download_attachments.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/download_inline_images.json b/packages/zoho-crm/specs/openAPI/v8.0/download_inline_images.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/download_inline_images.json rename to packages/zoho-crm/specs/openAPI/v8.0/download_inline_images.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/duplicate_check_preference.json b/packages/zoho-crm/specs/openAPI/v8.0/duplicate_check_preference.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/duplicate_check_preference.json rename to packages/zoho-crm/specs/openAPI/v8.0/duplicate_check_preference.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_compose.json b/packages/zoho-crm/specs/openAPI/v8.0/email_compose.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_compose.json rename to packages/zoho-crm/specs/openAPI/v8.0/email_compose.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_drafts.json b/packages/zoho-crm/specs/openAPI/v8.0/email_drafts.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_drafts.json rename to packages/zoho-crm/specs/openAPI/v8.0/email_drafts.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_related_records.json b/packages/zoho-crm/specs/openAPI/v8.0/email_related_records.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_related_records.json rename to packages/zoho-crm/specs/openAPI/v8.0/email_related_records.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_sharing_details.json b/packages/zoho-crm/specs/openAPI/v8.0/email_sharing_details.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_sharing_details.json rename to packages/zoho-crm/specs/openAPI/v8.0/email_sharing_details.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_templates.json b/packages/zoho-crm/specs/openAPI/v8.0/email_templates.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/email_templates.json rename to packages/zoho-crm/specs/openAPI/v8.0/email_templates.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/entity_scores.json b/packages/zoho-crm/specs/openAPI/v8.0/entity_scores.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/entity_scores.json rename to packages/zoho-crm/specs/openAPI/v8.0/entity_scores.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/features.json b/packages/zoho-crm/specs/openAPI/v8.0/features.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/features.json rename to packages/zoho-crm/specs/openAPI/v8.0/features.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/field_attachments.json b/packages/zoho-crm/specs/openAPI/v8.0/field_attachments.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/field_attachments.json rename to packages/zoho-crm/specs/openAPI/v8.0/field_attachments.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/field_map_dependency.json b/packages/zoho-crm/specs/openAPI/v8.0/field_map_dependency.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/field_map_dependency.json rename to packages/zoho-crm/specs/openAPI/v8.0/field_map_dependency.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/fields.json b/packages/zoho-crm/specs/openAPI/v8.0/fields.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/fields.json rename to packages/zoho-crm/specs/openAPI/v8.0/fields.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/files.json b/packages/zoho-crm/specs/openAPI/v8.0/files.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/files.json rename to packages/zoho-crm/specs/openAPI/v8.0/files.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/find_and_merge.json b/packages/zoho-crm/specs/openAPI/v8.0/find_and_merge.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/find_and_merge.json rename to packages/zoho-crm/specs/openAPI/v8.0/find_and_merge.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/fiscal_year.json b/packages/zoho-crm/specs/openAPI/v8.0/fiscal_year.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/fiscal_year.json rename to packages/zoho-crm/specs/openAPI/v8.0/fiscal_year.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/from_addresses.json b/packages/zoho-crm/specs/openAPI/v8.0/from_addresses.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/from_addresses.json rename to packages/zoho-crm/specs/openAPI/v8.0/from_addresses.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/global_picklists.json b/packages/zoho-crm/specs/openAPI/v8.0/global_picklists.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/global_picklists.json rename to packages/zoho-crm/specs/openAPI/v8.0/global_picklists.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/holidays.json b/packages/zoho-crm/specs/openAPI/v8.0/holidays.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/holidays.json rename to packages/zoho-crm/specs/openAPI/v8.0/holidays.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/inventory_convert.json b/packages/zoho-crm/specs/openAPI/v8.0/inventory_convert.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/inventory_convert.json rename to packages/zoho-crm/specs/openAPI/v8.0/inventory_convert.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/inventory_mass_convert.json b/packages/zoho-crm/specs/openAPI/v8.0/inventory_mass_convert.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/inventory_mass_convert.json rename to packages/zoho-crm/specs/openAPI/v8.0/inventory_mass_convert.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/inventory_templates.json b/packages/zoho-crm/specs/openAPI/v8.0/inventory_templates.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/inventory_templates.json rename to packages/zoho-crm/specs/openAPI/v8.0/inventory_templates.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/layouts.json b/packages/zoho-crm/specs/openAPI/v8.0/layouts.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/layouts.json rename to packages/zoho-crm/specs/openAPI/v8.0/layouts.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mail_merge.json b/packages/zoho-crm/specs/openAPI/v8.0/mail_merge.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mail_merge.json rename to packages/zoho-crm/specs/openAPI/v8.0/mail_merge.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mass_change_owner.json b/packages/zoho-crm/specs/openAPI/v8.0/mass_change_owner.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mass_change_owner.json rename to packages/zoho-crm/specs/openAPI/v8.0/mass_change_owner.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mass_convert.json b/packages/zoho-crm/specs/openAPI/v8.0/mass_convert.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mass_convert.json rename to packages/zoho-crm/specs/openAPI/v8.0/mass_convert.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mass_delete_tags.json b/packages/zoho-crm/specs/openAPI/v8.0/mass_delete_tags.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/mass_delete_tags.json rename to packages/zoho-crm/specs/openAPI/v8.0/mass_delete_tags.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/modules.json b/packages/zoho-crm/specs/openAPI/v8.0/modules.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/modules.json rename to packages/zoho-crm/specs/openAPI/v8.0/modules.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/notes.json b/packages/zoho-crm/specs/openAPI/v8.0/notes.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/notes.json rename to packages/zoho-crm/specs/openAPI/v8.0/notes.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/notifications.json b/packages/zoho-crm/specs/openAPI/v8.0/notifications.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/notifications.json rename to packages/zoho-crm/specs/openAPI/v8.0/notifications.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/org.json b/packages/zoho-crm/specs/openAPI/v8.0/org.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/org.json rename to packages/zoho-crm/specs/openAPI/v8.0/org.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/pick_list_values.json b/packages/zoho-crm/specs/openAPI/v8.0/pick_list_values.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/pick_list_values.json rename to packages/zoho-crm/specs/openAPI/v8.0/pick_list_values.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/pipeline.json b/packages/zoho-crm/specs/openAPI/v8.0/pipeline.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/pipeline.json rename to packages/zoho-crm/specs/openAPI/v8.0/pipeline.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portal_invite.json b/packages/zoho-crm/specs/openAPI/v8.0/portal_invite.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portal_invite.json rename to packages/zoho-crm/specs/openAPI/v8.0/portal_invite.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portal_user_type.json b/packages/zoho-crm/specs/openAPI/v8.0/portal_user_type.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portal_user_type.json rename to packages/zoho-crm/specs/openAPI/v8.0/portal_user_type.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portals.json b/packages/zoho-crm/specs/openAPI/v8.0/portals.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portals.json rename to packages/zoho-crm/specs/openAPI/v8.0/portals.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portals_meta.json b/packages/zoho-crm/specs/openAPI/v8.0/portals_meta.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/portals_meta.json rename to packages/zoho-crm/specs/openAPI/v8.0/portals_meta.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/profiles.json b/packages/zoho-crm/specs/openAPI/v8.0/profiles.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/profiles.json rename to packages/zoho-crm/specs/openAPI/v8.0/profiles.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/python/sample_api_runner.py b/packages/zoho-crm/specs/openAPI/v8.0/python/sample_api_runner.py similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/python/sample_api_runner.py rename to packages/zoho-crm/specs/openAPI/v8.0/python/sample_api_runner.py diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record.json b/packages/zoho-crm/specs/openAPI/v8.0/record.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record.json rename to packages/zoho-crm/specs/openAPI/v8.0/record.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record_locking.json b/packages/zoho-crm/specs/openAPI/v8.0/record_locking.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record_locking.json rename to packages/zoho-crm/specs/openAPI/v8.0/record_locking.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record_locking_configuration.json b/packages/zoho-crm/specs/openAPI/v8.0/record_locking_configuration.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record_locking_configuration.json rename to packages/zoho-crm/specs/openAPI/v8.0/record_locking_configuration.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record_share_email.json b/packages/zoho-crm/specs/openAPI/v8.0/record_share_email.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/record_share_email.json rename to packages/zoho-crm/specs/openAPI/v8.0/record_share_email.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/recycle_bin.json b/packages/zoho-crm/specs/openAPI/v8.0/recycle_bin.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/recycle_bin.json rename to packages/zoho-crm/specs/openAPI/v8.0/recycle_bin.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/related_lists.json b/packages/zoho-crm/specs/openAPI/v8.0/related_lists.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/related_lists.json rename to packages/zoho-crm/specs/openAPI/v8.0/related_lists.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/related_records.json b/packages/zoho-crm/specs/openAPI/v8.0/related_records.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/related_records.json rename to packages/zoho-crm/specs/openAPI/v8.0/related_records.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/reschedule_history.json b/packages/zoho-crm/specs/openAPI/v8.0/reschedule_history.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/reschedule_history.json rename to packages/zoho-crm/specs/openAPI/v8.0/reschedule_history.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/roles.json b/packages/zoho-crm/specs/openAPI/v8.0/roles.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/roles.json rename to packages/zoho-crm/specs/openAPI/v8.0/roles.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/scoring_rules.json b/packages/zoho-crm/specs/openAPI/v8.0/scoring_rules.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/scoring_rules.json rename to packages/zoho-crm/specs/openAPI/v8.0/scoring_rules.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/send_mail.json b/packages/zoho-crm/specs/openAPI/v8.0/send_mail.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/send_mail.json rename to packages/zoho-crm/specs/openAPI/v8.0/send_mail.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/service_preference.json b/packages/zoho-crm/specs/openAPI/v8.0/service_preference.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/service_preference.json rename to packages/zoho-crm/specs/openAPI/v8.0/service_preference.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/share_records.json b/packages/zoho-crm/specs/openAPI/v8.0/share_records.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/share_records.json rename to packages/zoho-crm/specs/openAPI/v8.0/share_records.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/shift_hours.json b/packages/zoho-crm/specs/openAPI/v8.0/shift_hours.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/shift_hours.json rename to packages/zoho-crm/specs/openAPI/v8.0/shift_hours.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/tags.json b/packages/zoho-crm/specs/openAPI/v8.0/tags.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/tags.json rename to packages/zoho-crm/specs/openAPI/v8.0/tags.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/territories.json b/packages/zoho-crm/specs/openAPI/v8.0/territories.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/territories.json rename to packages/zoho-crm/specs/openAPI/v8.0/territories.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/territory_users.json b/packages/zoho-crm/specs/openAPI/v8.0/territory_users.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/territory_users.json rename to packages/zoho-crm/specs/openAPI/v8.0/territory_users.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/timelines.json b/packages/zoho-crm/specs/openAPI/v8.0/timelines.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/timelines.json rename to packages/zoho-crm/specs/openAPI/v8.0/timelines.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/unblock_email.json b/packages/zoho-crm/specs/openAPI/v8.0/unblock_email.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/unblock_email.json rename to packages/zoho-crm/specs/openAPI/v8.0/unblock_email.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/unsubscribe_links.json b/packages/zoho-crm/specs/openAPI/v8.0/unsubscribe_links.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/unsubscribe_links.json rename to packages/zoho-crm/specs/openAPI/v8.0/unsubscribe_links.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/user_groups.json b/packages/zoho-crm/specs/openAPI/v8.0/user_groups.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/user_groups.json rename to packages/zoho-crm/specs/openAPI/v8.0/user_groups.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/user_type_users.json b/packages/zoho-crm/specs/openAPI/v8.0/user_type_users.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/user_type_users.json rename to packages/zoho-crm/specs/openAPI/v8.0/user_type_users.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users.json b/packages/zoho-crm/specs/openAPI/v8.0/users.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users.json rename to packages/zoho-crm/specs/openAPI/v8.0/users.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users_territories.json b/packages/zoho-crm/specs/openAPI/v8.0/users_territories.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users_territories.json rename to packages/zoho-crm/specs/openAPI/v8.0/users_territories.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users_transfer_delete.json b/packages/zoho-crm/specs/openAPI/v8.0/users_transfer_delete.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users_transfer_delete.json rename to packages/zoho-crm/specs/openAPI/v8.0/users_transfer_delete.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users_unavailability.json b/packages/zoho-crm/specs/openAPI/v8.0/users_unavailability.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/users_unavailability.json rename to packages/zoho-crm/specs/openAPI/v8.0/users_unavailability.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/variable_groups.json b/packages/zoho-crm/specs/openAPI/v8.0/variable_groups.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/variable_groups.json rename to packages/zoho-crm/specs/openAPI/v8.0/variable_groups.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/variables.json b/packages/zoho-crm/specs/openAPI/v8.0/variables.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/variables.json rename to packages/zoho-crm/specs/openAPI/v8.0/variables.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/wizards.json b/packages/zoho-crm/specs/openAPI/v8.0/wizards.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/wizards.json rename to packages/zoho-crm/specs/openAPI/v8.0/wizards.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/zia_org_enrichment.json b/packages/zoho-crm/specs/openAPI/v8.0/zia_org_enrichment.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/zia_org_enrichment.json rename to packages/zoho-crm/specs/openAPI/v8.0/zia_org_enrichment.json diff --git a/packages/v1-ready/zoho-crm/specs/openAPI/v8.0/zia_people_enrichment.json b/packages/zoho-crm/specs/openAPI/v8.0/zia_people_enrichment.json similarity index 100% rename from packages/v1-ready/zoho-crm/specs/openAPI/v8.0/zia_people_enrichment.json rename to packages/zoho-crm/specs/openAPI/v8.0/zia_people_enrichment.json diff --git a/packages/v1-ready/zoho-crm/tests/api.test.js b/packages/zoho-crm/tests/api.test.js similarity index 100% rename from packages/v1-ready/zoho-crm/tests/api.test.js rename to packages/zoho-crm/tests/api.test.js diff --git a/packages/v1-ready/zoom/.env.example b/packages/zoom/.env.example similarity index 100% rename from packages/v1-ready/zoom/.env.example rename to packages/zoom/.env.example diff --git a/packages/v1-ready/unbabel-projects/.eslintrc.json b/packages/zoom/.eslintrc.json similarity index 100% rename from packages/v1-ready/unbabel-projects/.eslintrc.json rename to packages/zoom/.eslintrc.json diff --git a/packages/v1-ready/zoom/CHANGELOG.md b/packages/zoom/CHANGELOG.md similarity index 100% rename from packages/v1-ready/zoom/CHANGELOG.md rename to packages/zoom/CHANGELOG.md diff --git a/packages/v1-ready/microsoft-teams/LICENSE.md b/packages/zoom/LICENSE.md similarity index 100% rename from packages/v1-ready/microsoft-teams/LICENSE.md rename to packages/zoom/LICENSE.md diff --git a/packages/v1-ready/zoom/README.md b/packages/zoom/README.md similarity index 100% rename from packages/v1-ready/zoom/README.md rename to packages/zoom/README.md diff --git a/packages/v1-ready/zoom/api.js b/packages/zoom/api.js similarity index 100% rename from packages/v1-ready/zoom/api.js rename to packages/zoom/api.js diff --git a/packages/v1-ready/zoom/defaultConfig.json b/packages/zoom/defaultConfig.json similarity index 100% rename from packages/v1-ready/zoom/defaultConfig.json rename to packages/zoom/defaultConfig.json diff --git a/packages/v1-ready/zoom/definition.js b/packages/zoom/definition.js similarity index 100% rename from packages/v1-ready/zoom/definition.js rename to packages/zoom/definition.js diff --git a/packages/v1-ready/zoom/index.js b/packages/zoom/index.js similarity index 100% rename from packages/v1-ready/zoom/index.js rename to packages/zoom/index.js diff --git a/packages/v1-ready/pipedrive/jest.config.js b/packages/zoom/jest.config.js similarity index 100% rename from packages/v1-ready/pipedrive/jest.config.js rename to packages/zoom/jest.config.js diff --git a/packages/v1-ready/zoom/package.json b/packages/zoom/package.json similarity index 100% rename from packages/v1-ready/zoom/package.json rename to packages/zoom/package.json diff --git a/packages/v1-ready/zoom/tests/api.test.js b/packages/zoom/tests/api.test.js similarity index 100% rename from packages/v1-ready/zoom/tests/api.test.js rename to packages/zoom/tests/api.test.js diff --git a/packages/v1-ready/zoom/tests/auther.test.js b/packages/zoom/tests/auther.test.js similarity index 100% rename from packages/v1-ready/zoom/tests/auther.test.js rename to packages/zoom/tests/auther.test.js diff --git a/reports/swarm-auto-centralized-1750900273293.json b/reports/swarm-auto-centralized-1750900273293.json new file mode 100644 index 0000000..9994f0c --- /dev/null +++ b/reports/swarm-auto-centralized-1750900273293.json @@ -0,0 +1,13 @@ +{ + "objective": "Please make sure we're referencing SAFLA for any stored best practices and then updating as we go. Also be sure to check the updated refactor work that Daniel is doing in https://github.com/friggframework/frigg/pull/397 to inform an updates we might want to make for the following ask... your goal: update all packages so that they're 1- Clean and only have the files that are needed for the newest frigg versions, 2- are all 'v1 ready' which really just means using frigg core and hte new definition approach, 3- have a local copy of any open API spec we can find, 4- have any other spec that seems relevant... fenestra spec, arazzo spec, async api spec, 5- have some beginnings of documentation, 6- have links to their developer docs, portal, partnership stuff, etc., 7- note any other oddities about developing against their api, 8- reflect whether they're a part of a larger ecosystem of packages that are related to one another. Maybe other things to include? But either way, at the end, the library should be cleaner, all of the packages should be ready to run, and the navigation in the repo should be clean and easy.", + "strategy": "auto", + "mode": "centralized", + "maxAgents": 5, + "timeout": 60, + "parallel": false, + "monitor": false, + "output": "json", + "outputDir": "./reports", + "timestamp": "2025-06-26T01:11:13.289Z", + "id": "swarm-auto-centralized-1750900273293" +} \ No newline at end of file diff --git a/reports/swarm-auto-centralized-1750952453215.json b/reports/swarm-auto-centralized-1750952453215.json new file mode 100644 index 0000000..e60a961 --- /dev/null +++ b/reports/swarm-auto-centralized-1750952453215.json @@ -0,0 +1,13 @@ +{ + "objective": "seems like we missed on a few in the agent batches for generation, and then cursor closed. Can we take it easier with just 3 agents now and have htem work through those agent batches and confirm or deny that the modules were created fully/correctly? if they have been, move them to the /packages folder, and update our inventory to record their creation. Then move on to the next batch.", + "strategy": "auto", + "mode": "centralized", + "maxAgents": 5, + "timeout": 60, + "parallel": false, + "monitor": false, + "output": "json", + "outputDir": "./reports", + "timestamp": "2025-06-26T15:40:53.214Z", + "id": "swarm-auto-centralized-1750952453215" +} \ No newline at end of file diff --git a/scripts/api-inventory-manager.js b/scripts/api-inventory-manager.js new file mode 100755 index 0000000..58a2002 --- /dev/null +++ b/scripts/api-inventory-manager.js @@ -0,0 +1,303 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const readline = require('readline'); +const path = require('path'); + +class APIInventoryManager { + constructor(baseDir = '.') { + this.inventoryPath = path.join(baseDir, 'api-inventory.jsonl'); + this.indexPath = path.join(baseDir, 'api-index.json'); + this.cache = new Map(); + this.index = null; + } + + async init() { + // Load index + if (fs.existsSync(this.indexPath)) { + this.index = JSON.parse(fs.readFileSync(this.indexPath, 'utf8')); + } else { + this.index = this.createEmptyIndex(); + } + } + + createEmptyIndex() { + return { + version: "1.0.0", + last_updated: new Date().toISOString(), + stats: { + total: 0, + implemented: 0, + with_openapi: 0, + with_fenestra: 0, + with_frigg: 0, + by_category: {}, + by_auth: {} + }, + quick_lookup: {} + }; + } + + // Add or update an API + async upsertAPI(api) { + const apis = await this.loadAll(); + apis.set(api.id, api); + await this.saveAll(apis); + await this.rebuildIndex(); + return api; + } + + // Batch insert for efficiency + async batchUpsert(apis) { + const existing = await this.loadAll(); + for (const api of apis) { + existing.set(api.id, api); + } + await this.saveAll(existing); + await this.rebuildIndex(); + } + + // Find APIs by criteria + async find(criteria) { + const apis = await this.loadAll(); + const results = []; + + for (const [id, api] of apis) { + let match = true; + for (const [key, value] of Object.entries(criteria)) { + if (api[key] !== value) { + match = false; + break; + } + } + if (match) results.push(api); + } + + return results; + } + + // Quick lookup by ID using index + async getById(id) { + if (this.index.quick_lookup[id] !== undefined) { + // Use line number from index for O(1) lookup + return await this.getByLineNumber(this.index.quick_lookup[id]); + } + return null; + } + + // Get API by line number (fast for large files) + async getByLineNumber(lineNum) { + return new Promise((resolve, reject) => { + const stream = fs.createReadStream(this.inventoryPath); + const rl = readline.createInterface({ input: stream }); + let currentLine = 0; + + rl.on('line', (line) => { + if (currentLine === lineNum) { + rl.close(); + stream.close(); + resolve(JSON.parse(line)); + } + currentLine++; + }); + + rl.on('close', () => resolve(null)); + rl.on('error', reject); + }); + } + + // Load all APIs (with caching) + async loadAll() { + if (this.cache.size > 0) return this.cache; + + const apis = new Map(); + if (!fs.existsSync(this.inventoryPath)) return apis; + + const stream = fs.createReadStream(this.inventoryPath); + const rl = readline.createInterface({ input: stream }); + + return new Promise((resolve, reject) => { + rl.on('line', (line) => { + try { + const api = JSON.parse(line); + apis.set(api.id, api); + } catch (e) { + console.error('Invalid JSON line:', line); + } + }); + + rl.on('close', () => { + this.cache = apis; + resolve(apis); + }); + + rl.on('error', reject); + }); + } + + // Save all APIs + async saveAll(apis) { + const lines = []; + for (const [id, api] of apis) { + lines.push(JSON.stringify(api)); + } + fs.writeFileSync(this.inventoryPath, lines.join('\n')); + this.cache = apis; // Update cache + } + + // Rebuild index for fast lookups + async rebuildIndex() { + const apis = await this.loadAll(); + const index = this.createEmptyIndex(); + + let lineNum = 0; + for (const [id, api] of apis) { + // Quick lookup by ID + index.quick_lookup[id] = lineNum++; + + // Update stats + index.stats.total++; + if (api.implemented) index.stats.implemented++; + if (api.openapi) index.stats.with_openapi++; + if (api.fenestra) index.stats.with_fenestra++; + if (api.frigg) index.stats.with_frigg++; + + // Category stats + index.stats.by_category[api.cat] = (index.stats.by_category[api.cat] || 0) + 1; + index.stats.by_auth[api.auth] = (index.stats.by_auth[api.auth] || 0) + 1; + } + + index.last_updated = new Date().toISOString(); + fs.writeFileSync(this.indexPath, JSON.stringify(index, null, 2)); + this.index = index; + } + + // Import from Lefthook + async importFromLefthook(url = 'https://admin.lefthook.com/api/apis') { + const fetch = require('node-fetch'); + const response = await fetch(url); + const data = await response.json(); + + const apis = await this.loadAll(); + let added = 0; + + for (const item of data.items) { + const id = item.slug; + if (!apis.has(id)) { + apis.set(id, { + id: id, + name: item.name, + implemented: item.status === 'Built', + openapi: false, + fenestra: false, + frigg: false, + auth: 'Unknown', + cat: this.normalizeCategory(item.category), + subcat: item.category, + notes: `Lefthook: ${item.status}`, + lefthook: item.status, + updated: new Date().toISOString() + }); + added++; + } + } + + await this.saveAll(apis); + await this.rebuildIndex(); + return { added, total: data.items.length }; + } + + normalizeCategory(category) { + const mapping = { + 'CRM (Customer Relationship Management)': 'CRM', + 'HR Talent & Recruitment': 'HR', + 'Project Management': 'Productivity', + 'Marketing Automation': 'Marketing', + 'Email Newsletters': 'Communication', + 'Phone and SMS': 'Communication', + 'Video Conferencing': 'Communication', + 'File Management and Storage': 'Productivity', + 'Forms and Surveys': 'Productivity', + 'eCommerce': 'E-commerce', + 'Developer Tools': 'Developer', + 'Social Media Accounts': 'Social', + 'Social Media Marketing': 'Marketing', + 'Website Builders': 'Developer', + 'Task Management': 'Productivity', + 'Team Chat': 'Communication', + 'Team Collaboration': 'Productivity', + 'Customer Support': 'CRM', + 'Event Management': 'Marketing', + 'Online Courses': 'Education', + 'Scheduling and Booking': 'Productivity', + 'Security and Identity Tools': 'Security', + 'Databases': 'Developer', + 'Documents': 'Productivity', + 'Signatures': 'Productivity', + 'Proposal and Invoice Management': 'Finance', + 'Accounting': 'Finance', + 'Amazon': 'Developer', + 'Google': 'Productivity' + }; + + return mapping[category] || category; + } + + // Generate reports + async generateReport() { + const stats = this.index.stats; + console.log('\n=== API Inventory Report ==='); + console.log(`Total APIs: ${stats.total}`); + console.log(`Implemented: ${stats.implemented} (${(stats.implemented/stats.total*100).toFixed(1)}%)`); + console.log(`With OpenAPI: ${stats.with_openapi} (${(stats.with_openapi/stats.implemented*100).toFixed(1)}%)`); + console.log(`With Fenestra: ${stats.with_fenestra} (${(stats.with_fenestra/stats.implemented*100).toFixed(1)}%)`); + console.log('\nBy Category:'); + Object.entries(stats.by_category) + .sort(([,a], [,b]) => b - a) + .forEach(([cat, count]) => console.log(` ${cat}: ${count}`)); + console.log('\nBy Auth Type:'); + Object.entries(stats.by_auth) + .sort(([,a], [,b]) => b - a) + .forEach(([auth, count]) => console.log(` ${auth}: ${count}`)); + } +} + +// CLI Interface +if (require.main === module) { + const manager = new APIInventoryManager(); + const command = process.argv[2]; + + (async () => { + await manager.init(); + + switch (command) { + case 'import': + console.log('Importing from Lefthook...'); + const result = await manager.importFromLefthook(); + console.log(`Added ${result.added} new APIs from ${result.total} total`); + break; + + case 'report': + await manager.generateReport(); + break; + + case 'rebuild': + console.log('Rebuilding index...'); + await manager.rebuildIndex(); + console.log('Index rebuilt successfully'); + break; + + case 'find': + const criteria = JSON.parse(process.argv[3] || '{}'); + const results = await manager.find(criteria); + console.log(`Found ${results.length} APIs:`); + results.forEach(api => console.log(` ${api.id}: ${api.name}`)); + break; + + default: + console.log('Usage: api-inventory-manager.js [import|report|rebuild|find]'); + } + })(); +} + +module.exports = APIInventoryManager; \ No newline at end of file diff --git a/scripts/api-inventory.ts b/scripts/api-inventory.ts new file mode 100644 index 0000000..b2a674d --- /dev/null +++ b/scripts/api-inventory.ts @@ -0,0 +1,166 @@ +// API Inventory Types and Manager for 4000+ API scale + +export interface APIEntry { + id: string; // Unique identifier (slug) + name: string; // Display name + implemented: boolean; // Is it implemented in our library? + openapi: boolean; // Has OpenAPI/Swagger spec? + fenestra: boolean; // Has Fenestra UI spec? + frigg: boolean; // Has Frigg extensions? + auth: AuthType; // Authentication method + cat: Category; // Main category + subcat?: string; // Subcategory + notes?: string; // Additional notes + priority?: Priority; // Implementation priority + lefthook?: string; // Lefthook status + updated: string; // ISO date string + + // Optional extended fields for scale + version?: string; // API version we support + deprecated?: boolean; // Is this API deprecated? + alternatives?: string[]; // Alternative API IDs + dependencies?: string[]; // Other APIs this depends on + enterprise?: boolean; // Enterprise-only API? + region?: string[]; // Geographic restrictions + pricing?: PricingTier; // Free, Paid, Enterprise + rateLimit?: RateLimit; // Rate limiting info +} + +export type AuthType = + | 'OAuth2' + | 'OAuth1' + | 'API Key' + | 'Bearer Token' + | 'Basic Auth' + | 'JWT' + | 'Custom' + | 'None' + | 'Unknown'; + +export type Category = + | 'AI/ML' + | 'Analytics' + | 'Communication' + | 'CRM' + | 'Developer' + | 'E-commerce' + | 'Education' + | 'Finance' + | 'Gaming' + | 'Healthcare' + | 'HR' + | 'Legal' + | 'Marketing' + | 'Media' + | 'Productivity' + | 'Real Estate' + | 'Security' + | 'Social' + | 'Travel' + | 'Other'; + +export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; +export type PricingTier = 'FREE' | 'FREEMIUM' | 'PAID' | 'ENTERPRISE'; + +export interface RateLimit { + requests: number; + window: string; // e.g., "1h", "1d" + tier?: string; +} + +export interface APIIndex { + version: string; + last_updated: string; + stats: { + total: number; + implemented: number; + with_openapi: number; + with_fenestra: number; + with_frigg: number; + by_category: Record; + by_auth: Record; + by_priority: Record; + }; + categories: Record; + quick_lookup: Record; // id -> line number + + // Bloom filter for fast existence checks at scale + bloom_filter?: { + size: number; + hash_count: number; + bits: string; // Base64 encoded bit array + }; +} + +// Optimized storage strategies for 4000+ APIs +export interface StorageStrategy { + // Primary storage: JSONL for streaming and append-only operations + primary: 'jsonl'; + + // Index storage: JSON with compressed options + index: 'json' | 'msgpack' | 'protobuf'; + + // Cache strategy + cache: { + type: 'memory' | 'redis' | 'sqlite'; + max_items: number; + ttl: number; // seconds + }; + + // Sharding strategy for massive scale + sharding?: { + enabled: boolean; + strategy: 'alphabetical' | 'hash' | 'category'; + shards: number; + }; +} + +// Query interface for efficient lookups +export interface QueryOptions { + // Filters + implemented?: boolean; + category?: Category | Category[]; + auth?: AuthType | AuthType[]; + hasOpenAPI?: boolean; + priority?: Priority | Priority[]; + + // Pagination + offset?: number; + limit?: number; + + // Sorting + sortBy?: keyof APIEntry; + sortOrder?: 'asc' | 'desc'; + + // Performance hints + useCache?: boolean; + parallel?: boolean; +} + +// Batch operations for efficiency +export interface BatchOperation { + type: 'upsert' | 'delete' | 'update'; + apis: Partial[]; + options?: { + validate?: boolean; + atomic?: boolean; + skipIndex?: boolean; // Rebuild index after all ops + }; +} + +// Migration utilities for converting from other formats +export interface MigrationSource { + type: 'markdown' | 'csv' | 'json' | 'lefthook' | 'postman'; + path?: string; + url?: string; + mapping?: Record; +} + +// Export formats for different use cases +export interface ExportOptions { + format: 'json' | 'jsonl' | 'csv' | 'markdown' | 'sql' | 'parquet'; + filters?: QueryOptions; + fields?: (keyof APIEntry)[]; + pretty?: boolean; + compress?: boolean; +} \ No newline at end of file diff --git a/scripts/migrate-to-jsonl.js b/scripts/migrate-to-jsonl.js new file mode 100644 index 0000000..c5446e0 --- /dev/null +++ b/scripts/migrate-to-jsonl.js @@ -0,0 +1,205 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// Our current implemented APIs with their specs status +const IMPLEMENTED_APIS = [ + // Analytics (6) + {id:'amplitude',name:'Amplitude',cat:'Analytics',auth:'API Key'}, + {id:'fathom',name:'Fathom',cat:'Analytics',auth:'API Key'}, + {id:'google-analytics-4',name:'Google Analytics 4',cat:'Analytics',auth:'OAuth2'}, + {id:'mixpanel',name:'Mixpanel',cat:'Analytics',auth:'Service Account'}, + {id:'posthog',name:'PostHog',cat:'Analytics',auth:'API Key'}, + {id:'segment',name:'Segment',cat:'Analytics',auth:'Write Key'}, + + // AI/ML (5) + {id:'anthropic',name:'Anthropic',cat:'AI/ML',auth:'API Key'}, + {id:'cohere',name:'Cohere',cat:'AI/ML',auth:'API Key'}, + {id:'huggingface',name:'Hugging Face',cat:'AI/ML',auth:'API Token'}, + {id:'openai',name:'OpenAI',cat:'AI/ML',auth:'API Key'}, + {id:'replicate',name:'Replicate',cat:'AI/ML',auth:'API Token'}, + + // Communication (12) + {id:'discord',name:'Discord',cat:'Communication',auth:'OAuth2',openapi:true}, + {id:'intercom',name:'Intercom',cat:'Communication',auth:'OAuth2'}, + {id:'microsoft-teams',name:'Microsoft Teams',cat:'Communication',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'onesignal',name:'OneSignal',cat:'Communication',auth:'API Key'}, + {id:'pusher',name:'Pusher',cat:'Communication',auth:'App Key/Secret'}, + {id:'sendgrid',name:'SendGrid',cat:'Communication',auth:'API Key'}, + {id:'slack',name:'Slack',cat:'Communication',auth:'OAuth2',openapi:true}, + {id:'telegram',name:'Telegram',cat:'Communication',auth:'Bot Token'}, + {id:'twilio',name:'Twilio',cat:'Communication',auth:'API Key/Secret',openapi:true}, + {id:'vonage',name:'Vonage',cat:'Communication',auth:'API Key/Secret'}, + {id:'whatsapp-business',name:'WhatsApp Business',cat:'Communication',auth:'Access Token'}, + {id:'zoom',name:'Zoom',cat:'Communication',auth:'OAuth2'}, + + // CRM (11) + {id:'activecampaign',name:'ActiveCampaign',cat:'CRM',auth:'API Key'}, + {id:'attentive',name:'Attentive',cat:'CRM',auth:'OAuth2'}, + {id:'attio',name:'Attio',cat:'CRM',auth:'OAuth2'}, + {id:'crossbeam',name:'Crossbeam',cat:'CRM',auth:'API Key'}, + {id:'hubspot',name:'HubSpot',cat:'CRM',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'marketo',name:'Marketo',cat:'CRM',auth:'API Key'}, + {id:'outreach',name:'Outreach',cat:'CRM',auth:'OAuth2'}, + {id:'pipedrive',name:'Pipedrive',cat:'CRM',auth:'OAuth2',fenestra:true}, + {id:'salesforce',name:'Salesforce',cat:'CRM',auth:'OAuth2',fenestra:true}, + {id:'salesloft',name:'Salesloft',cat:'CRM',auth:'OAuth2'}, + {id:'zoho-crm',name:'Zoho CRM',cat:'CRM',auth:'OAuth2',fenestra:true}, + + // Customer Support (3) + {id:'freshdesk',name:'Freshdesk',cat:'Support',auth:'API Key'}, + {id:'helpscout',name:'Help Scout',cat:'Support',auth:'OAuth2'}, + {id:'zendesk',name:'Zendesk',cat:'Support',auth:'OAuth2/API Key'}, + + // Design (2) + {id:'canva',name:'Canva',cat:'Design',auth:'OAuth2',fenestra:true}, + {id:'figma',name:'Figma',cat:'Design',auth:'OAuth2',fenestra:true}, + + // Developer (4) + {id:'github',name:'GitHub',cat:'Developer',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'gitlab',name:'GitLab',cat:'Developer',auth:'OAuth2',openapi:true}, + {id:'linear',name:'Linear',cat:'Developer',auth:'OAuth2'}, + {id:'rollworks',name:'Rollworks',cat:'Developer',auth:'API Key'}, + + // E-commerce (11) + {id:'42matters',name:'42matters',cat:'E-commerce',auth:'API Key'}, + {id:'bigcommerce',name:'BigCommerce',cat:'E-commerce',auth:'OAuth2'}, + {id:'clyde',name:'Clyde',cat:'E-commerce',auth:'Client Key/Secret'}, + {id:'etsy',name:'Etsy',cat:'E-commerce',auth:'OAuth2'}, + {id:'fastspring-iq',name:'FastSpring',cat:'E-commerce',auth:'OAuth2'}, + {id:'gorgias',name:'Gorgias',cat:'E-commerce',auth:'OAuth2',fenestra:true}, + {id:'payjunction',name:'PayJunction',cat:'E-commerce',auth:'API Key'}, + {id:'recharge',name:'Recharge',cat:'E-commerce',auth:'API Key'}, + {id:'shopify',name:'Shopify',cat:'E-commerce',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'woocommerce',name:'WooCommerce',cat:'E-commerce',auth:'Consumer Key/Secret'}, + {id:'yotpo',name:'Yotpo',cat:'E-commerce',auth:'API Key'}, + + // Email Marketing (1) + {id:'mailchimp',name:'Mailchimp',cat:'Marketing',auth:'OAuth2'}, + + // File Storage (4) + {id:'box',name:'Box',cat:'Storage',auth:'OAuth2',openapi:true}, + {id:'dropbox',name:'Dropbox',cat:'Storage',auth:'OAuth2'}, + {id:'google-drive',name:'Google Drive',cat:'Storage',auth:'OAuth2'}, + {id:'sharepoint',name:'SharePoint',cat:'Storage',auth:'OAuth2'}, + + // Finance (10) + {id:'airwallex',name:'Airwallex',cat:'Finance',auth:'OAuth2'}, + {id:'coinbase',name:'Coinbase',cat:'Finance',auth:'OAuth2/API Key'}, + {id:'freshbooks',name:'FreshBooks',cat:'Finance',auth:'OAuth2'}, + {id:'paypal',name:'PayPal',cat:'Finance',auth:'OAuth2',openapi:true}, + {id:'plaid',name:'Plaid',cat:'Finance',auth:'Client ID/Secret'}, + {id:'qbo',name:'QuickBooks Online',cat:'Finance',auth:'OAuth2'}, + {id:'square',name:'Square',cat:'Finance',auth:'OAuth2',openapi:true}, + {id:'stripe',name:'Stripe',cat:'Finance',auth:'API Key',openapi:true}, + {id:'wise',name:'Wise',cat:'Finance',auth:'API Token'}, + {id:'xero',name:'Xero',cat:'Finance',auth:'OAuth2'}, + + // HR (3) + {id:'deel',name:'Deel',cat:'HR',auth:'OAuth2'}, + {id:'huggg',name:'Huggg',cat:'HR',auth:'Username/Password'}, + {id:'personio',name:'Personio',cat:'HR',auth:'API Key'}, + + // Legal (2) + {id:'docusign',name:'DocuSign',cat:'Legal',auth:'OAuth2'}, + {id:'ironclad',name:'Ironclad',cat:'Legal',auth:'OAuth2'}, + + // Other (5) + {id:'connectwise',name:'ConnectWise',cat:'Other',auth:'API Key'}, + {id:'netx',name:'Netx',cat:'Other',auth:'OAuth2'}, + {id:'revio',name:'Revio',cat:'Other',auth:'API Key'}, + {id:'terminus',name:'Terminus',cat:'Other',auth:'API Key'}, + {id:'unbabel-projects',name:'Unbabel Projects',cat:'Other',auth:'API Key'}, + + // Phone (1) + {id:'openphone',name:'OpenPhone',cat:'Phone',auth:'API Key'}, + + // Productivity (13) + {id:'airtable',name:'Airtable',cat:'Productivity',auth:'API Key',fenestra:true}, + {id:'asana',name:'Asana',cat:'Productivity',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'calendly',name:'Calendly',cat:'Productivity',auth:'OAuth2'}, + {id:'clickup',name:'ClickUp',cat:'Productivity',auth:'OAuth2'}, + {id:'evernote',name:'Evernote',cat:'Productivity',auth:'OAuth 1.0a'}, + {id:'google-calendar',name:'Google Calendar',cat:'Productivity',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'google-workspace',name:'Google Workspace',cat:'Productivity',auth:'OAuth2',fenestra:true}, + {id:'miro',name:'Miro',cat:'Productivity',auth:'OAuth2'}, + {id:'monday',name:'Monday.com',cat:'Productivity',auth:'OAuth2',fenestra:true}, + {id:'notion',name:'Notion',cat:'Productivity',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'todoist',name:'Todoist',cat:'Productivity',auth:'API Token/OAuth2'}, + {id:'trello',name:'Trello',cat:'Productivity',auth:'OAuth2',openapi:true,fenestra:true}, + {id:'typeform',name:'Typeform',cat:'Productivity',auth:'OAuth2'}, + + // Social (3) + {id:'linkedin',name:'LinkedIn',cat:'Social',auth:'OAuth2'}, + {id:'reddit',name:'Reddit',cat:'Social',auth:'OAuth2'}, + {id:'youtube',name:'YouTube',cat:'Social',auth:'OAuth2'}, + + // System Integration (3) + {id:'frontify',name:'Frontify',cat:'System',auth:'OAuth2'}, + {id:'front',name:'Front',cat:'System',auth:'OAuth2',fenestra:true}, + {id:'jira',name:'Jira',cat:'System',auth:'OAuth2'}, + + // Content (3) + {id:'contentful',name:'Contentful',cat:'Content',auth:'API Key'}, + {id:'contentstack',name:'Contentstack',cat:'Content',auth:'API Key'}, + {id:'unbabel',name:'Unbabel',cat:'Content',auth:'API Key'}, +]; + +// Create JSONL entries +const entries = IMPLEMENTED_APIS.map(api => ({ + id: api.id, + name: api.name, + implemented: true, + openapi: api.openapi || false, + fenestra: api.fenestra || false, + frigg: false, // None have Frigg extensions yet + auth: api.auth, + cat: api.cat, + subcat: api.subcat || '', + notes: api.notes || '', + updated: new Date().toISOString() +})); + +// Write JSONL file +const jsonlPath = path.join(__dirname, '..', 'api-inventory.jsonl'); +const jsonlContent = entries.map(e => JSON.stringify(e)).join('\n'); +fs.writeFileSync(jsonlPath, jsonlContent); + +console.log(`✅ Migrated ${entries.length} APIs to JSONL format`); + +// Create index +const index = { + version: '1.0.0', + last_updated: new Date().toISOString(), + stats: { + total: entries.length, + implemented: entries.filter(e => e.implemented).length, + with_openapi: entries.filter(e => e.openapi).length, + with_fenestra: entries.filter(e => e.fenestra).length, + with_frigg: 0, + by_category: {}, + by_auth: {} + }, + quick_lookup: {} +}; + +// Build stats +entries.forEach((entry, idx) => { + index.quick_lookup[entry.id] = idx; + index.stats.by_category[entry.cat] = (index.stats.by_category[entry.cat] || 0) + 1; + index.stats.by_auth[entry.auth] = (index.stats.by_auth[entry.auth] || 0) + 1; +}); + +// Write index +const indexPath = path.join(__dirname, '..', 'api-index.json'); +fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); + +console.log('✅ Created index with stats:'); +console.log(` Total: ${index.stats.total}`); +console.log(` With OpenAPI: ${index.stats.with_openapi}`); +console.log(` With Fenestra: ${index.stats.with_fenestra}`); +console.log('\\n📊 By Category:'); +Object.entries(index.stats.by_category) + .sort(([,a], [,b]) => b - a) + .forEach(([cat, count]) => console.log(` ${cat}: ${count}`)); \ No newline at end of file