From ba7e6d38e2eb4c1e04e99b058ca43e4f7110edea Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 28 Apr 2025 22:35:36 +0300 Subject: [PATCH 01/27] import collections --- .../dashboard/src/app/(layout)/services/import/parsePostman.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dashboard/src/app/(layout)/services/import/parsePostman.ts b/packages/dashboard/src/app/(layout)/services/import/parsePostman.ts index 095e70e..f6aa918 100644 --- a/packages/dashboard/src/app/(layout)/services/import/parsePostman.ts +++ b/packages/dashboard/src/app/(layout)/services/import/parsePostman.ts @@ -143,7 +143,6 @@ function createServiceObject(postman: PostmanCollection): Tables<'services'> { function determineBaseUrl(postman: PostmanCollection): string { // Check for common base URL variable names const baseUrlVarNames = ['base_url', 'baseUrl', 'baseURL', 'BASE_URL', 'apiUrl', 'api_url', 'url', 'host']; - let baseUrl = null; // First check collection variables if (postman.variable && postman.variable.length > 0) { @@ -468,6 +467,7 @@ function processItems(items: PostmanItem[], endpoints: Tables<'endpoints'>[], ba const urlObj = new URL(urlWithProtocol); path = urlObj.pathname; } catch (error) { + console.error(error) // If URL parsing fails, try to extract path with regex const pathMatch = request.url.raw.match(/https?:\/\/[^\/]+(\/[^?#]*)/); if (pathMatch && pathMatch[1]) { @@ -570,6 +570,7 @@ function processItems(items: PostmanItem[], endpoints: Tables<'endpoints'>[], ba }); } } catch (e) { + console.error(e); parameters.push({ name: 'body', in: 'body', From 3b157820aeb2db13b04749a7aac3f2f10e4d4d6e Mon Sep 17 00:00:00 2001 From: Yevhen Oliinyk Date: Fri, 9 May 2025 20:26:23 +0300 Subject: [PATCH 02/27] Add github workflow. --- .github/workflows/containers.yaml | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/containers.yaml diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml new file mode 100644 index 0000000..81eb316 --- /dev/null +++ b/.github/workflows/containers.yaml @@ -0,0 +1,32 @@ +name: Build and Push Docker Images + +on: + push: + branches: ["main"] + +jobs: + docker-build-push: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Build and push backend + uses: docker/build-push-action@v5 + with: + context: ./packages/backend + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest + + - name: Build and push frontend + uses: docker/build-push-action@v5 + with: + context: ./packages/frontend + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/frontend:latest From a6ffdf9e2dc1919ad12f1bf4179bcce1a80f2253 Mon Sep 17 00:00:00 2001 From: Yevhen Oliinyk Date: Fri, 9 May 2025 20:29:11 +0300 Subject: [PATCH 03/27] Fix workflow. --- .github/workflows/containers.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml index 81eb316..0d5046b 100644 --- a/.github/workflows/containers.yaml +++ b/.github/workflows/containers.yaml @@ -27,6 +27,6 @@ jobs: - name: Build and push frontend uses: docker/build-push-action@v5 with: - context: ./packages/frontend + context: ./packages/dashboard push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/frontend:latest From a1979b96adc5d75f0907418e918d96b87861b5ed Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Wed, 14 May 2025 22:16:05 +0300 Subject: [PATCH 04/27] payments screen --- .../app/(layout)/components/AppSidebar.tsx | 11 +- .../components/PlansComparison.tsx | 143 ++++++++++++++++++ .../components/SubscriptionCard.tsx | 98 ++++++++++++ .../src/app/(layout)/subscription/page.tsx | 42 +++++ packages/dashboard/src/config/features.ts | 1 + 5 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx create mode 100644 packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx create mode 100644 packages/dashboard/src/app/(layout)/subscription/page.tsx diff --git a/packages/dashboard/src/app/(layout)/components/AppSidebar.tsx b/packages/dashboard/src/app/(layout)/components/AppSidebar.tsx index 51c84c5..875687e 100644 --- a/packages/dashboard/src/app/(layout)/components/AppSidebar.tsx +++ b/packages/dashboard/src/app/(layout)/components/AppSidebar.tsx @@ -1,7 +1,7 @@ "use client" import Link from "next/link" import { usePathname } from "next/navigation" -import { AlertCircleIcon, Book, Globe, KeyRound, Layers } from 'lucide-react' +import {AlertCircleIcon, Book, CreditCard, Globe, KeyRound, Layers} from 'lucide-react' import Image from "next/image" import { Sidebar, @@ -102,6 +102,15 @@ export function AppSidebar() { + {FEATURES.SIDEBAR.SHOW_USAGE && + + + + Subscription + + + } + diff --git a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx new file mode 100644 index 0000000..3473a56 --- /dev/null +++ b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx @@ -0,0 +1,143 @@ +"use client"; + +import {useState} from "react"; +import {Button} from "@/components/ui/button"; +import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from "@/components/ui/card"; +import {CheckCircle, XCircle} from "lucide-react"; + +export default function PlansComparison({initialSubscription}) { + const [subscription, setSubscription] = useState(initialSubscription); + const [billingCycle, setBillingCycle] = useState(initialSubscription.billingCycle); + + // Handle upgrade + const handleUpgrade = async () => { + try { + // In a real app, you would call an API endpoint + // await fetch('/api/subscription/upgrade', { + // method: 'POST', + // body: JSON.stringify({ plan: 'pro', billingCycle }) + // }); + + setSubscription({ + ...subscription, + type: "pro", + requests: { + used: subscription.requests.used, + total: 10000 + }, + billingCycle: billingCycle, + renewalDate: billingCycle === "monthly" ? "June 14, 2025" : "May 14, 2026" + }); + } catch (error) { + console.error("Error upgrading subscription:", error); + } + }; + + // Toggle billing cycle + const changeBillingCycle = (cycle) => { + setBillingCycle(cycle); + }; + + return ( + <> +
+
+ + +
+
+ +
+ {/* Free Plan */} + + + Free Plan + Basic features for starters + + +
$0/month
+
    +
  • + + 100 API requests per month +
  • +
  • + + Basic API gateway functionality +
  • +
  • + + Endpoints to MCP +
  • +
  • + + Incidents alerts +
  • +
+
+ + + +
+ + + + Pro Plan + Advanced features for professionals + + +
+
+ {billingCycle === "monthly" ? "$20" : "$200"} + + /{billingCycle === "monthly" ? "month" : "year"} + +
+ {billingCycle === "yearly" && ( +
2 months free
+ )} +
+
    +
  • + + 10,000 API requests per month +
  • +
  • + + Advanced API Gateway functionality +
  • +
  • + + Priority support +
  • +
  • + + Incident alerts +
  • +
+
+ + {subscription.type === "pro" ? ( + + ) : ( + + )} + +
+
+ + ); +} diff --git a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx new file mode 100644 index 0000000..a333dfc --- /dev/null +++ b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function SubscriptionCard({ initialSubscription }) { + const [subscription, setSubscription] = useState(initialSubscription); + + // Calculate usage percentage + const usagePercentage = Math.round((subscription.requests.used / subscription.requests.total) * 100); + + // Handle upgrade + const handleUpgrade = async () => { + try { + // In a real app, you would call an API endpoint + // await fetch('/api/subscription/upgrade', { method: 'POST' }); + + setSubscription({ + ...subscription, + type: "pro", + requests: { + used: subscription.requests.used, + total: 10000 + }, + renewalDate: "June 14, 2025", + billingCycle: "monthly" + }); + } catch (error) { + console.error("Error upgrading subscription:", error); + } + }; + + // Handle cancel + const handleCancel = async () => { + if (window.confirm("Are you sure you want to cancel your subscription?")) { + try { + // In a real app, you would call an API endpoint + // await fetch('/api/subscription/cancel', { method: 'POST' }); + + setSubscription({ + ...subscription, + type: "free", + requests: { + used: subscription.requests.used, + total: 100 + } + }); + } catch (error) { + console.error("Error canceling subscription:", error); + } + } + }; + + return ( + + +
+ +
+ {subscription.type === "pro" ? "Pro Plan" : "Free Plan"} +
+
+ {subscription.type === "pro" && ( + Active + )} +
+
+ + {subscription.type === "pro" + ? `Renews on ${subscription.renewalDate} (${subscription.billingCycle === "monthly" ? "Monthly" : "Yearly"})` + : "Limited features"} + +
+
+ {subscription.type === "pro" ? ( + + ) : ( + + )} +
+
+ +
+
API Requests
+
+ {subscription.requests.used} / {subscription.requests.total} +
+
+ +
+
+ ); +} diff --git a/packages/dashboard/src/app/(layout)/subscription/page.tsx b/packages/dashboard/src/app/(layout)/subscription/page.tsx new file mode 100644 index 0000000..3e77cc7 --- /dev/null +++ b/packages/dashboard/src/app/(layout)/subscription/page.tsx @@ -0,0 +1,42 @@ +import { Separator } from "@/components/ui/separator"; +import SubscriptionCard from "./components/SubscriptionCard"; +import PlansComparison from "./components/PlansComparison"; + +export default async function SubscriptionPage() { + // In a real app, you would fetch this data server-side + const userData = { + subscription: { + type: "free", // "free" or "pro" + requests: { + used: 2345, + total: 10000 + }, + renewalDate: "June 14, 2025", + billingCycle: "yearly" // "monthly" or "yearly" + } + }; + + return ( +
+
+
+

Manage Subscription

+
+
+ +
+ {/* Current Subscription Section */} +
+

Current Subscription

+ +
+ + {/* Plans Comparison Section */} +
+

Plans Comparison

+ +
+
+
+ ); +} diff --git a/packages/dashboard/src/config/features.ts b/packages/dashboard/src/config/features.ts index 58f8c31..19cc77f 100644 --- a/packages/dashboard/src/config/features.ts +++ b/packages/dashboard/src/config/features.ts @@ -7,6 +7,7 @@ const FEATURES = { SIDEBAR: { SHOW_USAGE: !isSelfHosted, SHOW_REGION: !isSelfHosted, + SHOW_SUBSCRIPTIONS: !isSelfHosted, }, ANALYTICS: { ENABLE_SENTRY: !isSelfHosted, From 3387f3b339fff778281c73371ff3b11977244c41 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Thu, 15 May 2025 21:05:04 +0300 Subject: [PATCH 05/27] payments screen --- packages/dashboard/package.json | 2 + packages/dashboard/pnpm-lock.yaml | 17 ++++++ .../components/PlansComparison.tsx | 45 +++++++++------- .../src/app/(layout)/subscription/page.tsx | 7 ++- .../src/app/api/subscription/route.ts | 31 +++++++++++ packages/dashboard/src/hooks/usePaddle.ts | 46 ++++++++++++++++ .../src/utils/paddle/get-paddle-instance.ts | 15 ++++++ .../src/utils/paddle/process-webhook.ts | 54 +++++++++++++++++++ 8 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 packages/dashboard/src/app/api/subscription/route.ts create mode 100644 packages/dashboard/src/hooks/usePaddle.ts create mode 100644 packages/dashboard/src/utils/paddle/get-paddle-instance.ts create mode 100644 packages/dashboard/src/utils/paddle/process-webhook.ts diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 40150fa..b2bd1ba 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -14,6 +14,8 @@ "@hookform/resolvers": "^3.10.0", "@keyv/redis": "^4.3.2", "@monaco-editor/react": "^4.7.0", + "@paddle/paddle-js": "^1.4.1", + "@paddle/paddle-node-sdk": "^2.7.1", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-avatar": "^1.1.3", diff --git a/packages/dashboard/pnpm-lock.yaml b/packages/dashboard/pnpm-lock.yaml index ce06fb9..b522546 100644 --- a/packages/dashboard/pnpm-lock.yaml +++ b/packages/dashboard/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@paddle/paddle-js': + specifier: ^1.4.1 + version: 1.4.1 + '@paddle/paddle-node-sdk': + specifier: ^2.7.1 + version: 2.7.1 '@radix-ui/react-accordion': specifier: ^1.2.3 version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -748,6 +754,13 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@paddle/paddle-js@1.4.1': + resolution: {integrity: sha512-GKuXVnUAIGq4H1AxrPRRMZXl+pTSGiKMStpRlvF6+dv03BwhkqbyHJJZ39e6bMquVbYSa33/9cu6fuW8pie8aQ==} + + '@paddle/paddle-node-sdk@2.7.1': + resolution: {integrity: sha512-7XWmXzm7VeH/aU/uyPrHZCbWCt/AUvVsWVZsj1XCNjXhNXdzgoA6kbGe97KUJcoELftge+DAMGBNWxv0kT1opQ==} + engines: {node: '>=18'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -4818,6 +4831,10 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@paddle/paddle-js@1.4.1': {} + + '@paddle/paddle-node-sdk@2.7.1': {} + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx index 3473a56..8c363b2 100644 --- a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx @@ -4,37 +4,42 @@ import {useState} from "react"; import {Button} from "@/components/ui/button"; import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from "@/components/ui/card"; import {CheckCircle, XCircle} from "lucide-react"; +import {usePaddle} from "@/hooks/usePaddle"; +import {createClient} from "@/utils/supabase/server"; + +const pro_monthly_id = "pri_01jv5ewwzjg4ab4dzzgm5xc1d5" +const pro_yearly_id = "pri_01jv5ey9ahq6xb8es0v14z741p" + +type Props = { + initialSubscription: any + customerData: { + email: string + } +} + +export default async function PlansComparison({initialSubscription, customerData}:Props) { -export default function PlansComparison({initialSubscription}) { const [subscription, setSubscription] = useState(initialSubscription); const [billingCycle, setBillingCycle] = useState(initialSubscription.billingCycle); + const {paddle, error, openCheckout} = usePaddle(); // Handle upgrade - const handleUpgrade = async () => { + const handleUpgrade = async (priceId: string) => { try { - // In a real app, you would call an API endpoint - // await fetch('/api/subscription/upgrade', { - // method: 'POST', - // body: JSON.stringify({ plan: 'pro', billingCycle }) - // }); - - setSubscription({ - ...subscription, - type: "pro", - requests: { - used: subscription.requests.used, - total: 10000 - }, - billingCycle: billingCycle, - renewalDate: billingCycle === "monthly" ? "June 14, 2025" : "May 14, 2026" - }); + openCheckout({ + customer: customerData, + items: [{ + quantity:1, + priceId + }] + }) } catch (error) { console.error("Error upgrading subscription:", error); } }; // Toggle billing cycle - const changeBillingCycle = (cycle) => { + const changeBillingCycle = (cycle: any) => { setBillingCycle(cycle); }; @@ -133,7 +138,7 @@ export default function PlansComparison({initialSubscription}) { {subscription.type === "pro" ? ( ) : ( - + )} diff --git a/packages/dashboard/src/app/(layout)/subscription/page.tsx b/packages/dashboard/src/app/(layout)/subscription/page.tsx index 3e77cc7..9e1e627 100644 --- a/packages/dashboard/src/app/(layout)/subscription/page.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/page.tsx @@ -1,8 +1,11 @@ import { Separator } from "@/components/ui/separator"; import SubscriptionCard from "./components/SubscriptionCard"; import PlansComparison from "./components/PlansComparison"; +import {createClient} from "@/utils/supabase/server"; export default async function SubscriptionPage() { + const supabase = await createClient() + const {data: {user}} = await supabase.auth.getUser() // In a real app, you would fetch this data server-side const userData = { subscription: { @@ -34,7 +37,9 @@ export default async function SubscriptionPage() { {/* Plans Comparison Section */}

Plans Comparison

- +
diff --git a/packages/dashboard/src/app/api/subscription/route.ts b/packages/dashboard/src/app/api/subscription/route.ts new file mode 100644 index 0000000..c5a3da0 --- /dev/null +++ b/packages/dashboard/src/app/api/subscription/route.ts @@ -0,0 +1,31 @@ +import { NextRequest } from 'next/server'; +import { ProcessWebhook } from '@/utils/paddle/process-webhook'; +import { getPaddleInstance } from '@/utils/paddle/get-paddle-instance'; +import { env } from 'next-runtime-env'; + +const webhookProcessor = new ProcessWebhook(); + +export async function POST(request: NextRequest) { + const signature = request.headers.get('paddle-signature') || ''; + const rawRequestBody = await request.text(); + const privateKey = env('PADDLE_NOTIFICATION_WEBHOOK_SECRET') || ''; + + try { + if (!signature || !rawRequestBody) { + return Response.json({ error: 'Missing signature from header' }, { status: 400 }); + } + + const paddle = getPaddleInstance(); + const eventData = await paddle.webhooks.unmarshal(rawRequestBody, privateKey, signature); + const eventName = eventData?.eventType ?? 'Unknown event'; + + if (eventData) { + await webhookProcessor.processEvent(eventData); + } + + return Response.json({ status: 200, eventName }); + } catch (e) { + console.log(e); + return Response.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/packages/dashboard/src/hooks/usePaddle.ts b/packages/dashboard/src/hooks/usePaddle.ts new file mode 100644 index 0000000..8f0085d --- /dev/null +++ b/packages/dashboard/src/hooks/usePaddle.ts @@ -0,0 +1,46 @@ +import { env } from "next-runtime-env"; +import {useCallback, useEffect, useState } from "react"; +import {initializePaddle, Paddle} from "@paddle/paddle-js"; + + +const environment = env('NEXT_PUBLIC_PADDLE_ENV') as "sandbox"; +const token = env('NEXT_PUBLIC_PADDLE_CLIENT_TOKEN')!; + +export function usePaddle() { + const [paddle, setPaddle] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + + initializePaddle({ environment, token }) + .then((instance) => { + if (isMounted && instance) { + setPaddle(instance); + } + }) + .catch((err) => { + if (isMounted) { + setError(err as Error); + console.error('Failed to initialize Paddle:', err); + } + }); + + return () => { + isMounted = false; + }; + }, []); + + const openCheckout = useCallback( + (options: Parameters[0]) => { + if (!paddle) { + console.warn('Paddle not initialized yet'); + return; + } + paddle.Checkout.open(options); + }, + [paddle] + ); + + return { paddle, openCheckout, error } as const; +} diff --git a/packages/dashboard/src/utils/paddle/get-paddle-instance.ts b/packages/dashboard/src/utils/paddle/get-paddle-instance.ts new file mode 100644 index 0000000..daf57cb --- /dev/null +++ b/packages/dashboard/src/utils/paddle/get-paddle-instance.ts @@ -0,0 +1,15 @@ +import { Environment, LogLevel, Paddle, PaddleOptions } from '@paddle/paddle-node-sdk'; +import {env} from "next-runtime-env"; + +export function getPaddleInstance() { + const paddleOptions: PaddleOptions = { + environment: (env('NEXT_PUBLIC_PADDLE_ENV') as Environment) ?? Environment.sandbox, + logLevel: LogLevel.error, + }; + + if (!process.env.PADDLE_API_KEY) { + console.error('Paddle API key is missing'); + } + + return new Paddle(process.env.PADDLE_API_KEY!, paddleOptions); +} diff --git a/packages/dashboard/src/utils/paddle/process-webhook.ts b/packages/dashboard/src/utils/paddle/process-webhook.ts new file mode 100644 index 0000000..ac7d28d --- /dev/null +++ b/packages/dashboard/src/utils/paddle/process-webhook.ts @@ -0,0 +1,54 @@ +import { + CustomerCreatedEvent, + CustomerUpdatedEvent, + EventEntity, + EventName, + SubscriptionCreatedEvent, + SubscriptionUpdatedEvent, +} from '@paddle/paddle-node-sdk'; +import { createClient } from '../supabase/server'; + +export class ProcessWebhook { + async processEvent(eventData: EventEntity) { + switch (eventData.eventType) { + case EventName.SubscriptionCreated: + case EventName.SubscriptionUpdated: + await this.updateSubscriptionData(eventData); + break; + case EventName.CustomerCreated: + case EventName.CustomerUpdated: + await this.updateCustomerData(eventData); + break; + } + } + + private async updateSubscriptionData(eventData: SubscriptionCreatedEvent | SubscriptionUpdatedEvent) { + const supabase = await createClient(); + const { error } = await supabase + .from('subscriptions') + .upsert({ + subscription_id: eventData.data.id, + subscription_status: eventData.data.status, + price_id: eventData.data.items[0].price?.id ?? '', + product_id: eventData.data.items[0].price?.productId ?? '', + scheduled_change: eventData.data.scheduledChange?.effectiveAt, + customer_id: eventData.data.customerId, + }) + .select(); + + if (error) throw error; + } + + private async updateCustomerData(eventData: CustomerCreatedEvent | CustomerUpdatedEvent) { + const supabase = await createClient(); + const { error } = await supabase + .from('customers') + .upsert({ + customer_id: eventData.data.id, + email: eventData.data.email, + }) + .select(); + + if (error) throw error; + } +} From c1799b97a7b9f5c61706260bb429febb4c33afd4 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Thu, 15 May 2025 21:24:57 +0300 Subject: [PATCH 06/27] payments screen --- packages/dashboard/src/utils/paddle/process-webhook.ts | 6 +++--- packages/dashboard/src/utils/supabase/admin.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 packages/dashboard/src/utils/supabase/admin.ts diff --git a/packages/dashboard/src/utils/paddle/process-webhook.ts b/packages/dashboard/src/utils/paddle/process-webhook.ts index ac7d28d..c07f3a4 100644 --- a/packages/dashboard/src/utils/paddle/process-webhook.ts +++ b/packages/dashboard/src/utils/paddle/process-webhook.ts @@ -6,7 +6,7 @@ import { SubscriptionCreatedEvent, SubscriptionUpdatedEvent, } from '@paddle/paddle-node-sdk'; -import { createClient } from '../supabase/server'; +import { UNSAFE_createAdminClient } from '../supabase/admin'; export class ProcessWebhook { async processEvent(eventData: EventEntity) { @@ -23,7 +23,7 @@ export class ProcessWebhook { } private async updateSubscriptionData(eventData: SubscriptionCreatedEvent | SubscriptionUpdatedEvent) { - const supabase = await createClient(); + const supabase = await UNSAFE_createAdminClient(); const { error } = await supabase .from('subscriptions') .upsert({ @@ -40,7 +40,7 @@ export class ProcessWebhook { } private async updateCustomerData(eventData: CustomerCreatedEvent | CustomerUpdatedEvent) { - const supabase = await createClient(); + const supabase = await UNSAFE_createAdminClient(); const { error } = await supabase .from('customers') .upsert({ diff --git a/packages/dashboard/src/utils/supabase/admin.ts b/packages/dashboard/src/utils/supabase/admin.ts new file mode 100644 index 0000000..d264406 --- /dev/null +++ b/packages/dashboard/src/utils/supabase/admin.ts @@ -0,0 +1,10 @@ +import { createClient } from '@supabase/supabase-js' +import {env} from "next-runtime-env"; + +export async function UNSAFE_createAdminClient() { + + return createClient( + env('NEXT_PUBLIC_SUPABASE_URL')!, + process.env.SUPABASE_SERVICE_ROLE!, + ) +} From 749b8808fadb4c1a5109ffa11462d515fb592405 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Thu, 15 May 2025 21:45:20 +0300 Subject: [PATCH 07/27] payments screen --- packages/dashboard/src/utils/data/plans.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/dashboard/src/utils/data/plans.ts diff --git a/packages/dashboard/src/utils/data/plans.ts b/packages/dashboard/src/utils/data/plans.ts new file mode 100644 index 0000000..1d8bce7 --- /dev/null +++ b/packages/dashboard/src/utils/data/plans.ts @@ -0,0 +1,8 @@ +export const Plans = { + free: { + requests: 100 + }, + pro: { + requests: 10000, + } +} as const; From d6116f7e288b028dd9d8223ef1786b7a01e3412e Mon Sep 17 00:00:00 2001 From: Yevhen Oliinyk Date: Thu, 15 May 2025 21:51:01 +0300 Subject: [PATCH 08/27] Add check for the subsciption on the backend. --- .../backend/src/modules/base/checkUsage.ts | 15 +++- packages/backend/src/utils/constants.ts | 10 ++- packages/backend/src/utils/database.types.ts | 69 +++++++++++++++++++ 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/modules/base/checkUsage.ts b/packages/backend/src/modules/base/checkUsage.ts index c4fd14e..d747a01 100644 --- a/packages/backend/src/modules/base/checkUsage.ts +++ b/packages/backend/src/modules/base/checkUsage.ts @@ -1,12 +1,21 @@ +import { PLANS } from '@utils/constants'; import { supabase } from '../../utils/supabase'; -import { BASIC_PLAN_MAX_REQUESTS } from '../../utils/constants'; export async function checkUsage( userId: string, ): Promise<{ error?: string; status?: number }> { + + const { data: proSubscriptionExists, error: subscriptionCheckError } = await supabase.rpc('check_subscription', { p_user_id: userId }); + const maxRequests = proSubscriptionExists ? PLANS.PRO.REQUESTS_PER_MONTH : PLANS.BASIC.REQUESTS_PER_MONTH; + + if (subscriptionCheckError) { + console.error('Error checking subscription:', subscriptionCheckError); + return { error: 'API200 Error: Internal server error', status: 500 }; + } + const { data, error } = await supabase.rpc('increment_usage', { p_user_id: userId, - p_max_requests: BASIC_PLAN_MAX_REQUESTS, + p_max_requests: maxRequests, }); if (error) { @@ -19,7 +28,7 @@ export async function checkUsage( if (!result?.allowed) { return { - error: `API200 Error: Monthly usage limit exceeded (${result?.c_count}/${BASIC_PLAN_MAX_REQUESTS})`, + error: `API200 Error: Monthly usage limit exceeded (${result?.c_count}/${maxRequests})`, status: 429, }; } diff --git a/packages/backend/src/utils/constants.ts b/packages/backend/src/utils/constants.ts index 6106a8f..54c6ed3 100644 --- a/packages/backend/src/utils/constants.ts +++ b/packages/backend/src/utils/constants.ts @@ -1,2 +1,10 @@ -export const BASIC_PLAN_MAX_REQUESTS = 1000; export const DAY = 24 * 60 * 60 * 1000; + +export const PLANS = { + BASIC: { + REQUESTS_PER_MONTH: 100 + }, + PRO: { + REQUESTS_PER_MONTH: 10000 + } +} as const; diff --git a/packages/backend/src/utils/database.types.ts b/packages/backend/src/utils/database.types.ts index 5979d16..28dfb98 100644 --- a/packages/backend/src/utils/database.types.ts +++ b/packages/backend/src/utils/database.types.ts @@ -30,6 +30,27 @@ export type Database = { } Relationships: [] } + customers: { + Row: { + created_at: string + customer_id: string + email: string + updated_at: string + } + Insert: { + created_at?: string + customer_id: string + email: string + updated_at?: string + } + Update: { + created_at?: string + customer_id?: string + email?: string + updated_at?: string + } + Relationships: [] + } endpoints: { Row: { cache_enabled: boolean @@ -314,23 +335,67 @@ export type Database = { } Relationships: [] } + subscriptions: { + Row: { + created_at: string + customer_id: string + price_id: string | null + product_id: string | null + scheduled_change: string | null + subscription_id: string + subscription_status: string + updated_at: string + } + Insert: { + created_at?: string + customer_id: string + price_id?: string | null + product_id?: string | null + scheduled_change?: string | null + subscription_id: string + subscription_status: string + updated_at?: string + } + Update: { + created_at?: string + customer_id?: string + price_id?: string | null + product_id?: string | null + scheduled_change?: string | null + subscription_id?: string + subscription_status?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "subscriptions_customer_id_fkey" + columns: ["customer_id"] + isOneToOne: false + referencedRelation: "customers" + referencedColumns: ["customer_id"] + }, + ] + } usages: { Row: { billing_started_at: string calls_count: number id: number + updated_at: string | null user_id: string } Insert: { billing_started_at?: string calls_count?: number id?: number + updated_at?: string | null user_id: string } Update: { billing_started_at?: string calls_count?: number id?: number + updated_at?: string | null user_id?: string } Relationships: [] @@ -340,6 +405,10 @@ export type Database = { [_ in never]: never } Functions: { + check_subscription: { + Args: { p_user_id: string } + Returns: boolean + } get_route_data: { Args: { p_service_name: string From d264be3141ca97a9cfb91f786d256e84459fb506 Mon Sep 17 00:00:00 2001 From: Yevhen Oliinyk Date: Fri, 16 May 2025 21:31:47 +0300 Subject: [PATCH 09/27] Update usage bar. --- .../(layout)/components/UsageProgressBar.tsx | 21 +++++++++++++------ packages/dashboard/src/utils/constants.ts | 11 +++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/dashboard/src/app/(layout)/components/UsageProgressBar.tsx b/packages/dashboard/src/app/(layout)/components/UsageProgressBar.tsx index 6934dd2..bb69118 100644 --- a/packages/dashboard/src/app/(layout)/components/UsageProgressBar.tsx +++ b/packages/dashboard/src/app/(layout)/components/UsageProgressBar.tsx @@ -1,32 +1,41 @@ import { Progress } from "@/components/ui/progress" import { useEffect, useState } from "react"; import { createClient } from "@/utils/supabase/client"; - -const MAX_REQUESTS_PER_MONTH = 1000 +import { PLANS } from '@/utils/constants'; +import { Badge } from '@/components/ui/badge'; export function UsageProgressBar() { const [usages, setUsages] = useState(0) + const [percentage, setPercentage] = useState(0) + const [maxRequestsPerMonth, setMaxRequestsPerMonth] = useState(0) + const [isPro, setIsPro] = useState(false) const supabase = createClient() - useEffect(() => { const fetchData = async () => { const { data } = await supabase.from('usages').select().maybeSingle() setUsages(data?.calls_count ?? 0) + + const user = await supabase.auth.getUser() + const { data: proSubscriptionExists } = await supabase.rpc('check_subscription', { p_user_id: user.data.user?.id }) + + setIsPro(proSubscriptionExists) + setMaxRequestsPerMonth(proSubscriptionExists ? PLANS.PRO.REQUESTS_PER_MONTH : PLANS.BASIC.REQUESTS_PER_MONTH) + setPercentage(Math.min((usages / maxRequestsPerMonth) * 100, 100)) } fetchData() - }, [supabase]) // Added supabase to the dependency array - const percentage = Math.min((usages / MAX_REQUESTS_PER_MONTH) * 100, 100) + }, []) return (
Requests this month - {usages} / {MAX_REQUESTS_PER_MONTH} + {usages} / {maxRequestsPerMonth}
+ {isPro && Pro}
) } diff --git a/packages/dashboard/src/utils/constants.ts b/packages/dashboard/src/utils/constants.ts index 2602cb0..e56eae7 100644 --- a/packages/dashboard/src/utils/constants.ts +++ b/packages/dashboard/src/utils/constants.ts @@ -1,4 +1,13 @@ export const DAY_MS = 24 * 60 * 60 * 1000; -export const EXTERNAL_ENDPOINTS_SOURCES = ['openapi', 'postman']; \ No newline at end of file +export const EXTERNAL_ENDPOINTS_SOURCES = ['openapi', 'postman']; + +export const PLANS = { + BASIC: { + REQUESTS_PER_MONTH: 100 + }, + PRO: { + REQUESTS_PER_MONTH: 10000 + } +} as const; \ No newline at end of file From fb230de83d059596c6a5fcdacc7b6debc425aa60 Mon Sep 17 00:00:00 2001 From: Yevhen Oliinyk Date: Fri, 16 May 2025 21:32:09 +0300 Subject: [PATCH 10/27] Save next_billed_at to db. --- .../src/utils/paddle/process-webhook.ts | 1 + .../src/utils/supabase/database.types.ts | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/dashboard/src/utils/paddle/process-webhook.ts b/packages/dashboard/src/utils/paddle/process-webhook.ts index c07f3a4..716c9c3 100644 --- a/packages/dashboard/src/utils/paddle/process-webhook.ts +++ b/packages/dashboard/src/utils/paddle/process-webhook.ts @@ -33,6 +33,7 @@ export class ProcessWebhook { product_id: eventData.data.items[0].price?.productId ?? '', scheduled_change: eventData.data.scheduledChange?.effectiveAt, customer_id: eventData.data.customerId, + next_billed_at: eventData.data.nextBilledAt, }) .select(); diff --git a/packages/dashboard/src/utils/supabase/database.types.ts b/packages/dashboard/src/utils/supabase/database.types.ts index 5979d16..c7a4e28 100644 --- a/packages/dashboard/src/utils/supabase/database.types.ts +++ b/packages/dashboard/src/utils/supabase/database.types.ts @@ -30,6 +30,27 @@ export type Database = { } Relationships: [] } + customers: { + Row: { + created_at: string + customer_id: string + email: string + updated_at: string + } + Insert: { + created_at?: string + customer_id: string + email: string + updated_at?: string + } + Update: { + created_at?: string + customer_id?: string + email?: string + updated_at?: string + } + Relationships: [] + } endpoints: { Row: { cache_enabled: boolean @@ -314,23 +335,70 @@ export type Database = { } Relationships: [] } + subscriptions: { + Row: { + created_at: string + customer_id: string + next_billed_at: string + price_id: string | null + product_id: string | null + scheduled_change: string | null + subscription_id: string + subscription_status: string + updated_at: string + } + Insert: { + created_at?: string + customer_id: string + next_billed_at: string + price_id?: string | null + product_id?: string | null + scheduled_change?: string | null + subscription_id: string + subscription_status: string + updated_at?: string + } + Update: { + created_at?: string + customer_id?: string + next_billed_at?: string + price_id?: string | null + product_id?: string | null + scheduled_change?: string | null + subscription_id?: string + subscription_status?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "subscriptions_customer_id_fkey" + columns: ["customer_id"] + isOneToOne: false + referencedRelation: "customers" + referencedColumns: ["customer_id"] + }, + ] + } usages: { Row: { billing_started_at: string calls_count: number id: number + updated_at: string | null user_id: string } Insert: { billing_started_at?: string calls_count?: number id?: number + updated_at?: string | null user_id: string } Update: { billing_started_at?: string calls_count?: number id?: number + updated_at?: string | null user_id?: string } Relationships: [] @@ -340,6 +408,10 @@ export type Database = { [_ in never]: never } Functions: { + check_subscription: { + Args: { p_user_id: string } + Returns: boolean + } get_route_data: { Args: { p_service_name: string From 98b859b3ea78d93c473d7f42a11b9d64469710f2 Mon Sep 17 00:00:00 2001 From: Yevhen Oliinyk Date: Sun, 18 May 2025 19:32:27 +0300 Subject: [PATCH 11/27] Add saving billing_cycle to db. --- packages/backend/src/utils/database.types.ts | 6 ++++++ packages/dashboard/src/utils/paddle/process-webhook.ts | 1 + packages/dashboard/src/utils/supabase/database.types.ts | 3 +++ 3 files changed, 10 insertions(+) diff --git a/packages/backend/src/utils/database.types.ts b/packages/backend/src/utils/database.types.ts index 28dfb98..942333b 100644 --- a/packages/backend/src/utils/database.types.ts +++ b/packages/backend/src/utils/database.types.ts @@ -337,8 +337,10 @@ export type Database = { } subscriptions: { Row: { + billing_cycle: string created_at: string customer_id: string + next_billed_at: string price_id: string | null product_id: string | null scheduled_change: string | null @@ -347,8 +349,10 @@ export type Database = { updated_at: string } Insert: { + billing_cycle: string created_at?: string customer_id: string + next_billed_at: string price_id?: string | null product_id?: string | null scheduled_change?: string | null @@ -357,8 +361,10 @@ export type Database = { updated_at?: string } Update: { + billing_cycle?: string created_at?: string customer_id?: string + next_billed_at?: string price_id?: string | null product_id?: string | null scheduled_change?: string | null diff --git a/packages/dashboard/src/utils/paddle/process-webhook.ts b/packages/dashboard/src/utils/paddle/process-webhook.ts index 716c9c3..35d595d 100644 --- a/packages/dashboard/src/utils/paddle/process-webhook.ts +++ b/packages/dashboard/src/utils/paddle/process-webhook.ts @@ -34,6 +34,7 @@ export class ProcessWebhook { scheduled_change: eventData.data.scheduledChange?.effectiveAt, customer_id: eventData.data.customerId, next_billed_at: eventData.data.nextBilledAt, + billing_cycle: eventData.data.billingCycle.interval }) .select(); diff --git a/packages/dashboard/src/utils/supabase/database.types.ts b/packages/dashboard/src/utils/supabase/database.types.ts index c7a4e28..942333b 100644 --- a/packages/dashboard/src/utils/supabase/database.types.ts +++ b/packages/dashboard/src/utils/supabase/database.types.ts @@ -337,6 +337,7 @@ export type Database = { } subscriptions: { Row: { + billing_cycle: string created_at: string customer_id: string next_billed_at: string @@ -348,6 +349,7 @@ export type Database = { updated_at: string } Insert: { + billing_cycle: string created_at?: string customer_id: string next_billed_at: string @@ -359,6 +361,7 @@ export type Database = { updated_at?: string } Update: { + billing_cycle?: string created_at?: string customer_id?: string next_billed_at?: string From 5a310df3af8c0f4a13eddeaea45afdd4fb708181 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 18 May 2025 20:28:58 +0300 Subject: [PATCH 12/27] payments screen --- .../components/PlansComparison.tsx | 38 +++------- .../components/SubscriptionCard.tsx | 75 +++++++------------ .../src/app/(layout)/subscription/page.tsx | 25 +++---- packages/dashboard/src/hooks/usePaddle.ts | 25 ++++++- packages/dashboard/src/utils/data/plans.ts | 8 -- .../src/utils/paddle/getSubscription.ts | 44 +++++++++++ .../src/utils/supabase/database.types.ts | 3 + 7 files changed, 120 insertions(+), 98 deletions(-) delete mode 100644 packages/dashboard/src/utils/data/plans.ts create mode 100644 packages/dashboard/src/utils/paddle/getSubscription.ts diff --git a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx index 8c363b2..85ac74b 100644 --- a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx @@ -4,39 +4,22 @@ import {useState} from "react"; import {Button} from "@/components/ui/button"; import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from "@/components/ui/card"; import {CheckCircle, XCircle} from "lucide-react"; -import {usePaddle} from "@/hooks/usePaddle"; -import {createClient} from "@/utils/supabase/server"; +import {pro_monthly_id, pro_yearly_id, usePaddle} from "@/hooks/usePaddle"; +import {Tables} from "@/utils/supabase/database.types"; -const pro_monthly_id = "pri_01jv5ewwzjg4ab4dzzgm5xc1d5" -const pro_yearly_id = "pri_01jv5ey9ahq6xb8es0v14z741p" type Props = { - initialSubscription: any + subscription: (Tables<'subscriptions'> & Tables<'customers'>) | null; customerData: { email: string } } -export default async function PlansComparison({initialSubscription, customerData}:Props) { +export default function PlansComparison({subscription, customerData}: Props) { - const [subscription, setSubscription] = useState(initialSubscription); - const [billingCycle, setBillingCycle] = useState(initialSubscription.billingCycle); - const {paddle, error, openCheckout} = usePaddle(); + const [billingCycle, setBillingCycle] = useState("monthly"); + const {paddle, error, handleUpgrade} = usePaddle(); - // Handle upgrade - const handleUpgrade = async (priceId: string) => { - try { - openCheckout({ - customer: customerData, - items: [{ - quantity:1, - priceId - }] - }) - } catch (error) { - console.error("Error upgrading subscription:", error); - } - }; // Toggle billing cycle const changeBillingCycle = (cycle: any) => { @@ -65,7 +48,6 @@ export default async function PlansComparison({initialSubscription, customerData
- {/* Free Plan */} Free Plan @@ -118,7 +100,7 @@ export default async function PlansComparison({initialSubscription, customerData
  • - 10,000 API requests per month + 10,000 API requests per month
  • @@ -135,10 +117,12 @@ export default async function PlansComparison({initialSubscription, customerData
- {subscription.type === "pro" ? ( + {subscription?.subscription_status === "active" ? ( ) : ( - + )}
diff --git a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx index a333dfc..a135620 100644 --- a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx @@ -5,52 +5,35 @@ import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import {Tables} from "@/utils/supabase/database.types"; +import {PLANS} from "@/utils/constants"; +import {format} from "date-fns"; +import {pro_monthly_id, pro_yearly_id, usePaddle} from "@/hooks/usePaddle"; -export default function SubscriptionCard({ initialSubscription }) { - const [subscription, setSubscription] = useState(initialSubscription); +type Props = { + subscription: (Tables<'subscriptions'> & Tables<'customers'>) | null; + usages: number + customerData: { + email: string + } +} - // Calculate usage percentage - const usagePercentage = Math.round((subscription.requests.used / subscription.requests.total) * 100); +export default function SubscriptionCard({ subscription, usages, customerData }: Props) { + const {paddle, error, handleUpgrade} = usePaddle(); - // Handle upgrade - const handleUpgrade = async () => { - try { - // In a real app, you would call an API endpoint - // await fetch('/api/subscription/upgrade', { method: 'POST' }); + const isPro = subscription?.subscription_status === "active"; + const maxRequestsPerMonth = isPro ? PLANS.PRO.REQUESTS_PER_MONTH : PLANS.BASIC.REQUESTS_PER_MONTH - setSubscription({ - ...subscription, - type: "pro", - requests: { - used: subscription.requests.used, - total: 10000 - }, - renewalDate: "June 14, 2025", - billingCycle: "monthly" - }); - } catch (error) { - console.error("Error upgrading subscription:", error); - } - }; + const usagePercentage = Math.min((usages / maxRequestsPerMonth) * 100, 100) // Handle cancel const handleCancel = async () => { - if (window.confirm("Are you sure you want to cancel your subscription?")) { - try { - // In a real app, you would call an API endpoint - // await fetch('/api/subscription/cancel', { method: 'POST' }); - - setSubscription({ - ...subscription, - type: "free", - requests: { - used: subscription.requests.used, - total: 100 - } - }); - } catch (error) { - console.error("Error canceling subscription:", error); - } + try { + paddle?.Retain.initCancellationFlow({ + subscriptionId: subscription?.subscription_id!, + }) + } catch (error) { + console.error("Error canceling subscription:", error); } }; @@ -60,27 +43,27 @@ export default function SubscriptionCard({ initialSubscription }) {
- {subscription.type === "pro" ? "Pro Plan" : "Free Plan"} + {isPro ? "Pro Plan" : "Free Plan"}
- {subscription.type === "pro" && ( + {isPro && ( Active )}
- {subscription.type === "pro" - ? `Renews on ${subscription.renewalDate} (${subscription.billingCycle === "monthly" ? "Monthly" : "Yearly"})` + {isPro + ? `Renews on ${format(new Date(subscription?.next_billed_at), "dd MMM yyyy")} (${subscription?.billing_cycle ? "Monthly" : "Yearly"})` : "Limited features"}
- {subscription.type === "pro" ? ( + {isPro ? ( ) : ( - + )}
@@ -88,7 +71,7 @@ export default function SubscriptionCard({ initialSubscription }) {
API Requests
- {subscription.requests.used} / {subscription.requests.total} + {usages} / {maxRequestsPerMonth}
diff --git a/packages/dashboard/src/app/(layout)/subscription/page.tsx b/packages/dashboard/src/app/(layout)/subscription/page.tsx index 9e1e627..b852b9f 100644 --- a/packages/dashboard/src/app/(layout)/subscription/page.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/page.tsx @@ -2,22 +2,15 @@ import { Separator } from "@/components/ui/separator"; import SubscriptionCard from "./components/SubscriptionCard"; import PlansComparison from "./components/PlansComparison"; import {createClient} from "@/utils/supabase/server"; +import { getSubscription } from "@/utils/paddle/getSubscription"; export default async function SubscriptionPage() { const supabase = await createClient() const {data: {user}} = await supabase.auth.getUser() - // In a real app, you would fetch this data server-side - const userData = { - subscription: { - type: "free", // "free" or "pro" - requests: { - used: 2345, - total: 10000 - }, - renewalDate: "June 14, 2025", - billingCycle: "yearly" // "monthly" or "yearly" - } - }; + const { data } = await supabase.from('usages').select().eq('user_id',user?.id!).maybeSingle() + const usages = data?.calls_count + const subscription = await getSubscription() + return (
@@ -28,16 +21,16 @@ export default async function SubscriptionPage() {
- {/* Current Subscription Section */}

Current Subscription

- +
- {/* Plans Comparison Section */}

Plans Comparison

-
diff --git a/packages/dashboard/src/hooks/usePaddle.ts b/packages/dashboard/src/hooks/usePaddle.ts index 8f0085d..cf9ddd5 100644 --- a/packages/dashboard/src/hooks/usePaddle.ts +++ b/packages/dashboard/src/hooks/usePaddle.ts @@ -6,6 +6,10 @@ import {initializePaddle, Paddle} from "@paddle/paddle-js"; const environment = env('NEXT_PUBLIC_PADDLE_ENV') as "sandbox"; const token = env('NEXT_PUBLIC_PADDLE_CLIENT_TOKEN')!; +export const pro_monthly_id = "pri_01jvj6fb5bmpvyke0c6v23sv2a" +export const pro_yearly_id = "pri_01jvj6gh0wenwg2drzdk3dd6y2" + + export function usePaddle() { const [paddle, setPaddle] = useState(null); const [error, setError] = useState(null); @@ -42,5 +46,24 @@ export function usePaddle() { [paddle] ); - return { paddle, openCheckout, error } as const; + const handleUpgrade = async (priceId: string, email: string) => { + try { + openCheckout({ + customer: { + email + }, + items: [{ + quantity: 1, + priceId + }], + settings:{ + successUrl: window.location.href + } + }) + } catch (error) { + console.error("Error upgrading subscription:", error); + } + }; + + return { paddle, handleUpgrade, error } as const; } diff --git a/packages/dashboard/src/utils/data/plans.ts b/packages/dashboard/src/utils/data/plans.ts deleted file mode 100644 index 1d8bce7..0000000 --- a/packages/dashboard/src/utils/data/plans.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const Plans = { - free: { - requests: 100 - }, - pro: { - requests: 10000, - } -} as const; diff --git a/packages/dashboard/src/utils/paddle/getSubscription.ts b/packages/dashboard/src/utils/paddle/getSubscription.ts new file mode 100644 index 0000000..3b5cdeb --- /dev/null +++ b/packages/dashboard/src/utils/paddle/getSubscription.ts @@ -0,0 +1,44 @@ +import {createClient} from "@/utils/supabase/server"; + +export async function getSubscription() { + // Create a Supabase client + const supabase = await createClient() + + // Get the current authenticated user + const { data: { user } } = await supabase.auth.getUser(); + + // If no user is found, return null + if (!user) { + return null; + } + + // First get the customer that matches the user's email + const { data: customer } = await supabase + .from('customers') + .select('*') + .eq('email', user.email) + .single(); + + // If no customer is found, return null + if (!customer) { + return null; + } + + // Get the subscription for this customer + const { data: subscription } = await supabase + .from('subscriptions') + .select('*') + .eq('customer_id', customer.customer_id) + .maybeSingle(); + + // If no subscription is found, return null + if (!subscription) { + return null; + } + + // Return the subscription with the customer data + return { + ...subscription, + customer, + }; +} diff --git a/packages/dashboard/src/utils/supabase/database.types.ts b/packages/dashboard/src/utils/supabase/database.types.ts index c7a4e28..942333b 100644 --- a/packages/dashboard/src/utils/supabase/database.types.ts +++ b/packages/dashboard/src/utils/supabase/database.types.ts @@ -337,6 +337,7 @@ export type Database = { } subscriptions: { Row: { + billing_cycle: string created_at: string customer_id: string next_billed_at: string @@ -348,6 +349,7 @@ export type Database = { updated_at: string } Insert: { + billing_cycle: string created_at?: string customer_id: string next_billed_at: string @@ -359,6 +361,7 @@ export type Database = { updated_at?: string } Update: { + billing_cycle?: string created_at?: string customer_id?: string next_billed_at?: string From 4979974958193ef8c8f99f202eb38cb528f47bbc Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 18 May 2025 20:39:36 +0300 Subject: [PATCH 13/27] build fix --- .../components/PlansComparison.tsx | 2 +- .../components/SubscriptionCard.tsx | 19 +++++++++---------- .../src/app/(layout)/subscription/page.tsx | 6 +++--- packages/dashboard/src/hooks/usePaddle.ts | 3 +++ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx index 85ac74b..d6bb80c 100644 --- a/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/components/PlansComparison.tsx @@ -18,7 +18,7 @@ type Props = { export default function PlansComparison({subscription, customerData}: Props) { const [billingCycle, setBillingCycle] = useState("monthly"); - const {paddle, error, handleUpgrade} = usePaddle(); + const {handleUpgrade} = usePaddle(); // Toggle billing cycle diff --git a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx index a135620..f9b0fe1 100644 --- a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx @@ -1,14 +1,13 @@ "use client"; -import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; -import { Progress } from "@/components/ui/progress"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import {Badge} from "@/components/ui/badge"; +import {Progress} from "@/components/ui/progress"; +import {Button} from "@/components/ui/button"; +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"; import {Tables} from "@/utils/supabase/database.types"; import {PLANS} from "@/utils/constants"; import {format} from "date-fns"; -import {pro_monthly_id, pro_yearly_id, usePaddle} from "@/hooks/usePaddle"; +import {pro_monthly_id, usePaddle} from "@/hooks/usePaddle"; type Props = { subscription: (Tables<'subscriptions'> & Tables<'customers'>) | null; @@ -18,8 +17,8 @@ type Props = { } } -export default function SubscriptionCard({ subscription, usages, customerData }: Props) { - const {paddle, error, handleUpgrade} = usePaddle(); +export default function SubscriptionCard({subscription, usages, customerData}: Props) { + const {paddle, handleUpgrade} = usePaddle(); const isPro = subscription?.subscription_status === "active"; const maxRequestsPerMonth = isPro ? PLANS.PRO.REQUESTS_PER_MONTH : PLANS.BASIC.REQUESTS_PER_MONTH @@ -30,7 +29,7 @@ export default function SubscriptionCard({ subscription, usages, customerData }: const handleCancel = async () => { try { paddle?.Retain.initCancellationFlow({ - subscriptionId: subscription?.subscription_id!, + subscriptionId: subscription?.subscription_id as string, }) } catch (error) { console.error("Error canceling subscription:", error); @@ -74,7 +73,7 @@ export default function SubscriptionCard({ subscription, usages, customerData }: {usages} / {maxRequestsPerMonth}
- + ); diff --git a/packages/dashboard/src/app/(layout)/subscription/page.tsx b/packages/dashboard/src/app/(layout)/subscription/page.tsx index b852b9f..a185405 100644 --- a/packages/dashboard/src/app/(layout)/subscription/page.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/page.tsx @@ -7,7 +7,7 @@ import { getSubscription } from "@/utils/paddle/getSubscription"; export default async function SubscriptionPage() { const supabase = await createClient() const {data: {user}} = await supabase.auth.getUser() - const { data } = await supabase.from('usages').select().eq('user_id',user?.id!).maybeSingle() + const { data } = await supabase.from('usages').select().eq('user_id',user?.id as string).maybeSingle() const usages = data?.calls_count const subscription = await getSubscription() @@ -24,14 +24,14 @@ export default async function SubscriptionPage() {

Current Subscription

Plans Comparison

diff --git a/packages/dashboard/src/hooks/usePaddle.ts b/packages/dashboard/src/hooks/usePaddle.ts index cf9ddd5..204c090 100644 --- a/packages/dashboard/src/hooks/usePaddle.ts +++ b/packages/dashboard/src/hooks/usePaddle.ts @@ -1,6 +1,7 @@ import { env } from "next-runtime-env"; import {useCallback, useEffect, useState } from "react"; import {initializePaddle, Paddle} from "@paddle/paddle-js"; +import { captureException } from "@sentry/nextjs" const environment = env('NEXT_PUBLIC_PADDLE_ENV') as "sandbox"; @@ -24,6 +25,7 @@ export function usePaddle() { } }) .catch((err) => { + captureException(err); if (isMounted) { setError(err as Error); console.error('Failed to initialize Paddle:', err); @@ -61,6 +63,7 @@ export function usePaddle() { } }) } catch (error) { + captureException(error); console.error("Error upgrading subscription:", error); } }; From 2d776b0578efdee691f0fa5ddc9d8c42156bf304 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Tue, 20 May 2025 21:46:28 +0300 Subject: [PATCH 14/27] add links to subscription page --- .../components/SubscriptionFooter.tsx | 38 ++++++++++++++++ .../src/app/(layout)/subscription/page.tsx | 43 +++++++++++-------- 2 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 packages/dashboard/src/app/(layout)/subscription/components/SubscriptionFooter.tsx diff --git a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionFooter.tsx b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionFooter.tsx new file mode 100644 index 0000000..c0a41c3 --- /dev/null +++ b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionFooter.tsx @@ -0,0 +1,38 @@ +import Link from "next/link" + +import { cn } from "@/lib/utils" + +export function SubscriptionFooter({ className }: { className?: string }) { + return ( +
+
+
+ + Terms of service + + + Privacy policy + + + Refund policy + +
+
+
+ ) +} diff --git a/packages/dashboard/src/app/(layout)/subscription/page.tsx b/packages/dashboard/src/app/(layout)/subscription/page.tsx index a185405..05a4b62 100644 --- a/packages/dashboard/src/app/(layout)/subscription/page.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/page.tsx @@ -3,6 +3,7 @@ import SubscriptionCard from "./components/SubscriptionCard"; import PlansComparison from "./components/PlansComparison"; import {createClient} from "@/utils/supabase/server"; import { getSubscription } from "@/utils/paddle/getSubscription"; +import { SubscriptionFooter } from "./components/SubscriptionFooter"; export default async function SubscriptionPage() { const supabase = await createClient() @@ -13,28 +14,32 @@ export default async function SubscriptionPage() { return ( -
-
-
-

Manage Subscription

-
-
- -
-
-

Current Subscription

- +
+
+
+
+

Manage Subscription

+
+ +
+
+

Current Subscription

+ +
-
-

Plans Comparison

- +
+

Plans Comparison

+ +
+
- ); + +); } From a37818a36cce145bd54a3d21ef1537525a20a576 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Tue, 20 May 2025 21:50:31 +0300 Subject: [PATCH 15/27] fix usages null state --- .../app/(layout)/subscription/components/SubscriptionCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx index f9b0fe1..88f0bc8 100644 --- a/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx +++ b/packages/dashboard/src/app/(layout)/subscription/components/SubscriptionCard.tsx @@ -23,7 +23,7 @@ export default function SubscriptionCard({subscription, usages, customerData}: P const isPro = subscription?.subscription_status === "active"; const maxRequestsPerMonth = isPro ? PLANS.PRO.REQUESTS_PER_MONTH : PLANS.BASIC.REQUESTS_PER_MONTH - const usagePercentage = Math.min((usages / maxRequestsPerMonth) * 100, 100) + const usagePercentage = Math.min(((usages || 0) / maxRequestsPerMonth) * 100, 100) // Handle cancel const handleCancel = async () => { @@ -70,7 +70,7 @@ export default function SubscriptionCard({subscription, usages, customerData}: P
API Requests
- {usages} / {maxRequestsPerMonth} + {usages || 0} / {maxRequestsPerMonth}
From 664183a94d930a08e738a6b5143c50585b7b9990 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Fri, 23 May 2025 23:45:57 +0300 Subject: [PATCH 16/27] sdk base --- packages/js-sdk/.gitignore | 2 + packages/js-sdk/README.md | 223 ++++++++++++++++++++ packages/js-sdk/src/index.ts | 374 ++++++++++++++++++++++++++++++++++ packages/js-sdk/tsconfig.json | 25 +++ 4 files changed, 624 insertions(+) create mode 100644 packages/js-sdk/.gitignore create mode 100644 packages/js-sdk/README.md create mode 100644 packages/js-sdk/src/index.ts create mode 100644 packages/js-sdk/tsconfig.json diff --git a/packages/js-sdk/.gitignore b/packages/js-sdk/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/packages/js-sdk/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md new file mode 100644 index 0000000..3374ecc --- /dev/null +++ b/packages/js-sdk/README.md @@ -0,0 +1,223 @@ +# API200 SDK Generator + +[![npm version](https://badge.fury.io/js/api200-sdk-generator.svg)](https://badge.fury.io/js/api200-sdk-generator) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A powerful CLI tool that automatically generates a TypeScript SDK for your [API 200](https://api200.co) services. Transform your API endpoints into a fully-typed, easy-to-use SDK with a single command. + +## šŸš€ Features + +- **šŸ”§ Zero Configuration** - Just provide your API token and go +- **šŸ“ Full TypeScript Support** - Complete type safety with IntelliSense +- **šŸŽÆ Intuitive API** - Clean, predictable method naming +- **⚔ Fast Generation** - Generates SDK in seconds +- **šŸ”„ Automatic Updates** - Re-run to sync with API changes +- **šŸ“¦ No Dependencies** - Generated code has zero runtime dependencies + +## šŸ“„ Installation + +### Global Installation +```bash +npm install -g api200-sdk-generator +``` + +### One-time Usage (Recommended) +```bash +npx api200-generate-sdk -t your_api_token +``` + +## šŸ› ļø Usage + +### Basic Usage +```bash +npx api200-generate-sdk --token YOUR_API_TOKEN +``` + +### Custom Output Directory +```bash +npx api200-generate-sdk -t YOUR_API_TOKEN -o ./src/api +``` + +### Custom Base URL +```bash +npx api200-generate-sdk -t YOUR_API_TOKEN -u https://custom.api200.co/api +``` + +### All Options +```bash +api200-generate-sdk [options] + +Options: + -t, --token API200 user token (required) + -u, --base-url Base API URL (default: "https://eu.api200.co/api") + -o, --output Output directory (default: "./lib/api200") + -h, --help Display help for command +``` + +## šŸ“ Generated Structure + +The generator creates a clean, organized SDK structure: + +``` +lib/api200/ +ā”œā”€ā”€ index.ts # Main SDK export +ā”œā”€ā”€ types.ts # TypeScript interfaces +ā”œā”€ā”€ users.ts # Users service methods +ā”œā”€ā”€ orders.ts # Orders service methods +└── payments.ts # Payments service methods +``` + +## šŸ’» Usage Examples + +### Basic Import and Usage +```typescript +import { api200 } from './lib/api200'; + +// GET request with path parameter +const user = await api200.users.getUserById.get({ id: "123" }); +console.log(user.data); + +// GET request with query parameters +const users = await api200.users.getUsers.get({ + page: 1, + limit: 10, + status: "active" +}); + +// POST request with body +const newUser = await api200.users.createUser.post({ + requestBody: { + name: "John Doe", + email: "john@example.com" + } +}); +``` + +### Error Handling +```typescript +import { api200 } from './lib/api200'; +import type { ApiError } from './lib/api200/types'; + +try { + const result = await api200.users.getUserById.get({ id: "123" }); + console.log('Success:', result.data); +} catch (error) { + const apiError = error as ApiError; + console.error('Error:', apiError.message); + console.error('Status:', apiError.status); +} +``` + +### Type Safety +```typescript +import { api200 } from './lib/api200'; +import type { GetUsersByIdParams, PostUsersParams } from './lib/api200/types'; + +// Fully typed parameters +const params: GetUsersByIdParams = { id: "123" }; +const user = await api200.users.getUserById.get(params); + +// TypeScript will catch type errors +const createParams: PostUsersParams = { + requestBody: { + name: "John", + email: "john@example.com", + // age: "30" // āŒ TypeScript error if age should be number + age: 30 // āœ… Correct type + } +}; +``` + +## šŸ”„ Updating Your SDK + +When your API changes, simply re-run the generator to update your SDK: + +```bash +npx api200-generate-sdk -t YOUR_API_TOKEN +``` + +This will overwrite the existing SDK files with the latest API definitions. + +## 🌟 Method Naming Convention + +The generator creates clean, predictable method names: + +| API Endpoint | Generated Method | +|--------------|------------------| +| `GET /users` | `api200.users.getUsers.get()` | +| `GET /users/{id}` | `api200.users.getUsersById.get()` | +| `POST /users` | `api200.users.postUsers.post()` | +| `PUT /users/{id}` | `api200.users.putUsersById.put()` | +| `DELETE /users/{id}` | `api200.users.deleteUsersById.delete()` | + +## šŸ”§ Configuration + +### Environment Variables +You can also use environment variables instead of command-line options: + +```bash +export API200_TOKEN=your_token_here +export API200_BASE_URL=https://eu.api200.co/api +export API200_OUTPUT=./lib/api200 + +npx api200-generate-sdk +``` + +### Project Integration +Add generation to your package.json scripts: + +```json +{ + "scripts": { + "generate-sdk": "api200-generate-sdk -t $API200_TOKEN", + "build": "npm run generate-sdk && tsc", + "dev": "npm run generate-sdk && npm run build -- --watch" + } +} +``` + +## šŸ› Troubleshooting + +### Common Issues + +**"Failed to fetch services" Error** +- Verify your API token is correct +- Check if the base URL is accessible +- Ensure you have internet connectivity + +**TypeScript Compilation Errors** +- Make sure you have TypeScript installed: `npm install -D typescript` +- Check that your tsconfig.json includes the generated files + +**Permission Errors** +- Ensure you have write permissions to the output directory +- Try running with elevated permissions if needed + +### Getting Help +- Check the [API 200](https://api200.co) documentation +- Open an issue on GitHub +- Contact support through the API 200 website + +## šŸ“„ License + +MIT License - see the [LICENSE](LICENSE) file for details. + +## šŸ”— Links + +- **[API 200 Website](https://api200.co)** - Main service website +- **[API Documentation](https://api200.co/docs)** - Full API documentation +- **[GitHub Repository](https://github.com/your-org/api200-sdk-generator)** - Source code and issues + +## šŸ¤ Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +--- + +**Made with ā¤ļø for [API 200](https://api200.co) developers** diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts new file mode 100644 index 0000000..77727e4 --- /dev/null +++ b/packages/js-sdk/src/index.ts @@ -0,0 +1,374 @@ +#!/usr/bin/env node +import { Command } from 'commander'; +import fs from 'fs-extra'; +import path from 'path'; +import { z } from 'zod'; + +const envSchema = z.object({ + token: z.string().min(1), + baseUrl: z.string().url().default("https://eu.api200.co/api"), + output: z.string().optional() +}); + +interface ServiceEndpoint { + name: string; + method: string; + description?: string; + schema?: { + parameters?: Array<{ + name: string; + type: string; + in: 'path' | 'query' | 'header' | 'body'; + required?: boolean; + description?: string; + }>; + requestBody?: { + content: { + [contentType: string]: { + schema: any; + }; + }; + }; + }; +} + +interface Service { + name: string; + description?: string; + endpoints: ServiceEndpoint[]; +} + +const program = new Command(); + +program + .name('api200-generate-sdk') + .description('Generate TypeScript SDK for API200 services') + .requiredOption('-t, --token ', 'API200 user token') + .option('-u, --base-url ', 'Base API URL', 'https://eu.api200.co/api') + .option('-o, --output ', 'Output directory') + .action(async (options) => { + try { + const config = envSchema.parse({ + token: options.token, + baseUrl: options.baseUrl, + output: options.output + }); + + // Determine output directory + const outputDir = determineOutputDirectory(config.output); + + await generateSDK(config.token, config.baseUrl, outputDir); + } catch (error) { + console.error('Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); + +function determineOutputDirectory(providedOutput?: string): string { + if (providedOutput) { + return providedOutput; + } + + const currentDir = process.cwd(); + const srcExists = fs.existsSync(path.join(currentDir, 'src')); + + if (srcExists) { + return './src/lib/api200'; + } else { + return './lib/api200'; + } +} + +async function generateSDK(userKey: string, baseApiUrl: string, outputDir: string) { + console.log('šŸš€ Generating API200 SDK...'); + + const baseUrl = baseApiUrl.replace(/\/api$/, "/"); + + // Fetch services data + const response = await fetch(`${baseUrl}/user/mcp-services`, { + headers: { + "x-api-key": userKey + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch services: ${response.status} ${response.statusText}`); + } + + const services: Service[] = await response.json(); + console.log(`šŸ“” Found ${services.length} services`); + + // Create output directory + await fs.ensureDir(outputDir); + + // Generate types file + await generateTypesFile(services, outputDir); + + // Generate api200 client file + await generateApi200ClientFile(outputDir); + + // Generate service files + const serviceExports: string[] = []; + for (const service of services) { + const serviceName = toCamelCase(service.name); + await generateServiceFile(service, outputDir); + serviceExports.push(`export { ${serviceName} } from './${service.name}';`); + } + + // Generate main index file + await generateIndexFile(services, serviceExports, outputDir); + + console.log('āœ… SDK generated successfully!'); + console.log(`šŸ“ Output directory: ${outputDir}`); + console.log('\nšŸ“– Usage example:'); + console.log('```typescript'); + console.log(`import { createAPI200Client, api200 } from '${outputDir.startsWith('./src') ? outputDir.replace('./src/', './') : outputDir}';`); + console.log(''); + console.log('// Initialize the client with your credentials'); + console.log("createAPI200Client('https://eu.api200.co/api', 'your-api-key');"); + console.log(''); + console.log('// Use the API'); + console.log('const result = await api200.users.getUserById.get({ id: "123" });'); + console.log('```'); +} + +function toCamelCase(str: string): string { + return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +} + +function toPascalCase(str: string): string { + return str.charAt(0).toUpperCase() + toCamelCase(str).slice(1); +} + +function generateTypeFromSchema(schema: any): string { + if (!schema) return 'any'; + + if (schema.type === 'array' && schema.items) { + return `Array<${generateTypeFromSchema(schema.items)}>`; + } + + if (schema.type === 'object' || (!schema.type && schema.properties)) { + const props: string[] = []; + if (schema.properties) { + Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => { + const isRequired = schema.required?.includes(propName); + const propType = generateTypeFromSchema(propSchema); + props.push(` ${propName}${isRequired ? '' : '?'}: ${propType};`); + }); + } + return `{\n${props.join('\n')}\n}`; + } + + switch (schema.type) { + case 'string': return 'string'; + case 'integer': + case 'number': return 'number'; + case 'boolean': return 'boolean'; + default: return 'any'; + } +} + +async function generateTypesFile(services: Service[], outputDir: string) { + const typeDefinitions: string[] = []; + + services.forEach(service => { + service.endpoints.forEach(endpoint => { + const methodName = `${endpoint.method.toLowerCase()}_${endpoint.name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); + + // Parameters interface + const paramTypes: string[] = []; + if (endpoint.schema?.parameters) { + endpoint.schema.parameters.forEach(param => { + const isRequired = param.required !== false; + let paramType = 'string'; + switch (param.type) { + case 'integer': + case 'number': + paramType = 'number'; + break; + case 'boolean': + paramType = 'boolean'; + break; + } + paramTypes.push(` ${param.name}${isRequired ? '' : '?'}: ${paramType};`); + }); + } + + // Request body type + let requestBodyType = ''; + if (['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase())) { + if (endpoint.schema?.requestBody?.content) { + const jsonContent = endpoint.schema.requestBody.content['application/json']; + if (jsonContent?.schema) { + requestBodyType = generateTypeFromSchema(jsonContent.schema); + } else { + requestBodyType = 'any'; + } + paramTypes.push(` requestBody?: ${requestBodyType};`); + } else { + paramTypes.push(` requestBody?: any;`); + } + } + + if (paramTypes.length > 0) { + typeDefinitions.push(`export interface ${toPascalCase(methodName)}Params {\n${paramTypes.join('\n')}\n}`); + } + }); + }); + + const typesContent = `// Auto-generated types for API200 SDK +${typeDefinitions.join('\n\n')} + +export interface ApiResponse { + data: T; + status: number; + statusText: string; +} + +export interface ApiError { + message: string; + status?: number; + statusText?: string; +} +`; + + await fs.writeFile(path.join(outputDir, 'types.ts'), typesContent); +} + +async function generateServiceFile(service: Service, outputDir: string) { + const serviceName = toCamelCase(service.name); + const methods: string[] = []; + + service.endpoints.forEach(endpoint => { + const methodName = `${endpoint.method.toLowerCase()}_${endpoint.name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); + const hasParams = endpoint.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase()); + const paramsType = hasParams ? `${toPascalCase(methodName)}Params` : '{}'; + + methods.push(` + ${methodName}: { + async ${endpoint.method.toLowerCase()}(params${hasParams ? `: ${paramsType}` : '?'}: ${paramsType} = {} as ${paramsType}): Promise { + return makeRequest('${service.name}', '${endpoint.name}', '${endpoint.method}', params); + } + }`); + }); + + const importTypes = service.endpoints + .map(e => { + const methodName = `${e.method.toLowerCase()}_${e.name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); + const hasParams = e.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(e.method.toUpperCase()); + return hasParams ? `${toPascalCase(methodName)}Params` : ''; + }) + .filter(Boolean); + + const serviceContent = `// Auto-generated service: ${service.name} +import { ApiResponse, ApiError${importTypes.length > 0 ? ', ' + importTypes.join(', ') : ''} } from './types'; +import { makeRequest } from './api200'; + +export const ${serviceName} = {${methods.join(',')} +}; +`; + + await fs.writeFile(path.join(outputDir, `${service.name}.ts`), serviceContent); +} + +async function generateApi200ClientFile(outputDir: string) { + const clientContent = `// Auto-generated API200 client +import { ApiResponse, ApiError } from './types'; + +let config: { baseUrl: string; userKey: string } | null = null; + +export function createAPI200Client(baseUrl: string, userKey: string) { + config = { baseUrl, userKey }; +} + +export async function makeRequest(serviceName: string, endpointPath: string, method: string, params: any = {}): Promise { + if (!config) { + throw new Error('API200 client not initialized. Call createAPI200Client(baseUrl, userKey) first.'); + } + + try { + // Handle path parameters + let processedPath = endpointPath; + const queryParams: string[] = []; + + if (params) { + // Replace path parameters + Object.keys(params).forEach(key => { + if (processedPath.includes(\`{\${key}}\`)) { + processedPath = processedPath.replace(\`{\${key}}\`, params[key]); + } else if (key !== 'requestBody') { + // Add as query parameter + queryParams.push(\`\${key}=\${encodeURIComponent(params[key])}\`); + } + }); + } + + const fullUrl = \`\${config.baseUrl}/\${serviceName}\${processedPath}\${queryParams.length ? '?' + queryParams.join('&') : ''}\`; + + const requestOptions: RequestInit = { + method, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'x-api-key': config.userKey + } + }; + + if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && params.requestBody) { + requestOptions.body = JSON.stringify(params.requestBody); + } + + const response = await fetch(fullUrl, requestOptions); + const data = await response.json(); + + return { + data, + status: response.status, + statusText: response.statusText + }; + } catch (error) { + throw { + message: error instanceof Error ? error.message : String(error), + status: 0, + statusText: 'Network Error' + } as ApiError; + } +} +`; + + await fs.writeFile(path.join(outputDir, 'api200.ts'), clientContent); +} + +async function generateIndexFile(services: Service[], serviceExports: string[], outputDir: string) { + const serviceImports = services.map(service => { + const serviceName = toCamelCase(service.name); + return `import { ${serviceName} } from './${service.name}';`; + }).join('\n'); + + const apiObjectProperties = services.map(service => { + const serviceName = toCamelCase(service.name); + return ` ${serviceName}`; + }).join(',\n'); + + const indexContent = `// Auto-generated API200 SDK +import { createAPI200Client } from './api200'; +${serviceImports} + +export * from './types'; +export { createAPI200Client } from './api200'; +${serviceExports.join('\n')} + +// Initialize the client - users should call this with their credentials +// createAPI200Client('https://eu.api200.co/api', 'your-api-key'); + +export const api200 = { +${apiObjectProperties} +}; + +export default api200; +`; + + await fs.writeFile(path.join(outputDir, 'index.ts'), indexContent); +} + +program.parse(); diff --git a/packages/js-sdk/tsconfig.json b/packages/js-sdk/tsconfig.json new file mode 100644 index 0000000..a1b2cdf --- /dev/null +++ b/packages/js-sdk/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} From 9b73420b97c5e9884ad0aeeef9230b5778220ca8 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Fri, 23 May 2025 23:55:17 +0300 Subject: [PATCH 17/27] sdk base --- packages/js-sdk/.gitignore | 1 + packages/js-sdk/package.json | 42 + packages/js-sdk/pnpm-lock.yaml | 1229 +++++++++++++++++ .../js-sdk/src/generators/client-generator.ts | 71 + .../js-sdk/src/generators/index-generator.ts | 41 + packages/js-sdk/src/generators/index.ts | 55 + .../src/generators/service-generator.ts | 41 + .../js-sdk/src/generators/type-generator.ts | 99 ++ packages/js-sdk/src/index.ts | 347 +---- packages/js-sdk/src/utils/file-utils.ts | 18 + packages/js-sdk/src/utils/schema.ts | 9 + packages/js-sdk/src/utils/string-utils.ts | 12 + packages/js-sdk/src/utils/types.ts | 27 + 13 files changed, 1648 insertions(+), 344 deletions(-) create mode 100644 packages/js-sdk/package.json create mode 100644 packages/js-sdk/pnpm-lock.yaml create mode 100644 packages/js-sdk/src/generators/client-generator.ts create mode 100644 packages/js-sdk/src/generators/index-generator.ts create mode 100644 packages/js-sdk/src/generators/index.ts create mode 100644 packages/js-sdk/src/generators/service-generator.ts create mode 100644 packages/js-sdk/src/generators/type-generator.ts create mode 100644 packages/js-sdk/src/utils/file-utils.ts create mode 100644 packages/js-sdk/src/utils/schema.ts create mode 100644 packages/js-sdk/src/utils/string-utils.ts create mode 100644 packages/js-sdk/src/utils/types.ts diff --git a/packages/js-sdk/.gitignore b/packages/js-sdk/.gitignore index f06235c..9c97bbd 100644 --- a/packages/js-sdk/.gitignore +++ b/packages/js-sdk/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.env diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json new file mode 100644 index 0000000..49a1299 --- /dev/null +++ b/packages/js-sdk/package.json @@ -0,0 +1,42 @@ +{ + "type": "module", + "name": "api200-sdk-generator", + "version": "1.0.0", + "description": "CLI tool to generate TypeScript SDK for API 200 services", + "main": "dist/index.js", + "bin": { + "api200-generate-sdk": "./dist/index.js" + }, + "scripts": { + "build": "tsup src/index.ts --format esm --dts", + "dev": "tsup src/index.ts --format esm --watch", + "start": "node dist/index.js", + "prepare": "pnpm run build", + }, + "keywords": [ + "api200", + "sdk", + "generator", + "typescript", + "cli" + ], + "author": "Maksym Budnyk", + "dependencies": { + "commander": "^11.0.0", + "fs-extra": "^11.0.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^22.14.1", + "tsup": "^8.4.0", + "typescript": "^5.3.3" + }, + "files": [ + "dist", + "README.md" + ], + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/js-sdk/pnpm-lock.yaml b/packages/js-sdk/pnpm-lock.yaml new file mode 100644 index 0000000..a9c8399 --- /dev/null +++ b/packages/js-sdk/pnpm-lock.yaml @@ -0,0 +1,1229 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + commander: + specifier: ^11.0.0 + version: 11.1.0 + fs-extra: + specifier: ^11.0.0 + version: 11.3.0 + zod: + specifier: ^3.22.0 + version: 3.25.26 + devDependencies: + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/node': + specifier: ^22.14.1 + version: 22.15.21 + tsup: + specifier: ^8.4.0 + version: 8.5.0(typescript@5.8.3) + typescript: + specifier: ^5.3.3 + version: 5.8.3 + +packages: + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.41.0': + resolution: {integrity: sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.41.0': + resolution: {integrity: sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.41.0': + resolution: {integrity: sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.41.0': + resolution: {integrity: sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.41.0': + resolution: {integrity: sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.41.0': + resolution: {integrity: sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.41.0': + resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.41.0': + resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.41.0': + resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.41.0': + resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.41.0': + resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': + resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.41.0': + resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.41.0': + resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.41.0': + resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.41.0': + resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.41.0': + resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.41.0': + resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.41.0': + resolution: {integrity: sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.41.0': + resolution: {integrity: sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/node@22.15.21': + resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.41.0: + resolution: {integrity: sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + zod@3.25.26: + resolution: {integrity: sha512-UNDqvDmpFigmuN0Lmcaydt2WWRLH63+TLHFzPSWtLnlfPxwm1rp+FP0uWcxA8iZRjLuDF4oxQ7CSWAddzEN26A==} + +snapshots: + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.41.0': + optional: true + + '@rollup/rollup-android-arm64@4.41.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.41.0': + optional: true + + '@rollup/rollup-darwin-x64@4.41.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.41.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.41.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.41.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.41.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.41.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.41.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.41.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.41.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.41.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.41.0': + optional: true + + '@types/estree@1.0.7': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.15.21 + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.15.21 + + '@types/node@22.15.21': + dependencies: + undici-types: 6.21.0 + + acorn@8.14.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + bundle-require@5.1.0(esbuild@0.25.4): + dependencies: + esbuild: 0.25.4 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@11.1.0: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.17 + mlly: 1.7.4 + rollup: 4.41.0 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + graceful-fs@4.2.11: {} + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + joycon@3.1.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + lodash.sortby@4.7.0: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + mlly@1.7.4: + dependencies: + acorn: 8.14.1 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + object-assign@4.1.1: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.2: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + + punycode@2.3.1: {} + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.41.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.41.0 + '@rollup/rollup-android-arm64': 4.41.0 + '@rollup/rollup-darwin-arm64': 4.41.0 + '@rollup/rollup-darwin-x64': 4.41.0 + '@rollup/rollup-freebsd-arm64': 4.41.0 + '@rollup/rollup-freebsd-x64': 4.41.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.41.0 + '@rollup/rollup-linux-arm-musleabihf': 4.41.0 + '@rollup/rollup-linux-arm64-gnu': 4.41.0 + '@rollup/rollup-linux-arm64-musl': 4.41.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.41.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.41.0 + '@rollup/rollup-linux-riscv64-gnu': 4.41.0 + '@rollup/rollup-linux-riscv64-musl': 4.41.0 + '@rollup/rollup-linux-s390x-gnu': 4.41.0 + '@rollup/rollup-linux-x64-gnu': 4.41.0 + '@rollup/rollup-linux-x64-musl': 4.41.0 + '@rollup/rollup-win32-arm64-msvc': 4.41.0 + '@rollup/rollup-win32-ia32-msvc': 4.41.0 + '@rollup/rollup-win32-x64-msvc': 4.41.0 + fsevents: 2.3.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.0(typescript@5.8.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.4) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1 + esbuild: 0.25.4 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.41.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + webidl-conversions@4.0.2: {} + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + zod@3.25.26: {} diff --git a/packages/js-sdk/src/generators/client-generator.ts b/packages/js-sdk/src/generators/client-generator.ts new file mode 100644 index 0000000..1e9c0ee --- /dev/null +++ b/packages/js-sdk/src/generators/client-generator.ts @@ -0,0 +1,71 @@ +// generators/client-generator.ts +import fs from 'fs-extra'; +import path from 'path'; + +export async function generateApi200ClientFile(outputDir: string) { + const clientContent = `// Auto-generated API200 client +import { ApiResponse, ApiError } from './types'; + +let config: { baseUrl: string; userKey: string } | null = null; + +export function createAPI200Client(baseUrl: string, userKey: string) { + config = { baseUrl, userKey }; +} + +export async function makeRequest(serviceName: string, endpointPath: string, method: string, params: any = {}): Promise { + if (!config) { + throw new Error('API200 client not initialized. Call createAPI200Client(baseUrl, userKey) first.'); + } + + try { + // Handle path parameters + let processedPath = endpointPath; + const queryParams: string[] = []; + + if (params) { + // Replace path parameters + Object.keys(params).forEach(key => { + if (processedPath.includes(\`{\${key}}\`)) { + processedPath = processedPath.replace(\`{\${key}}\`, params[key]); + } else if (key !== 'requestBody') { + // Add as query parameter + queryParams.push(\`\${key}=\${encodeURIComponent(params[key])}\`); + } + }); + } + + const fullUrl = \`\${config.baseUrl}/\${serviceName}\${processedPath}\${queryParams.length ? '?' + queryParams.join('&') : ''}\`; + + const requestOptions: RequestInit = { + method, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'x-api-key': config.userKey + } + }; + + if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && params.requestBody) { + requestOptions.body = JSON.stringify(params.requestBody); + } + + const response = await fetch(fullUrl, requestOptions); + const data = await response.json(); + + return { + data, + status: response.status, + statusText: response.statusText + }; + } catch (error) { + throw { + message: error instanceof Error ? error.message : String(error), + status: 0, + statusText: 'Network Error' + } as ApiError; + } +} +`; + + await fs.writeFile(path.join(outputDir, 'api200.ts'), clientContent); +} diff --git a/packages/js-sdk/src/generators/index-generator.ts b/packages/js-sdk/src/generators/index-generator.ts new file mode 100644 index 0000000..87b4b1d --- /dev/null +++ b/packages/js-sdk/src/generators/index-generator.ts @@ -0,0 +1,41 @@ +// generators/index-generator.ts +import fs from 'fs-extra'; +import path from 'path'; +import { Service } from '../utils/types'; +import { toCamelCase } from '../utils/string-utils'; + +export async function generateIndexFile(services: Service[], outputDir: string) { + const serviceImports = services.map(service => { + const serviceName = toCamelCase(service.name); + return `import { ${serviceName} } from './${service.name}';`; + }).join('\n'); + + const serviceExports = services.map(service => { + return `export { ${toCamelCase(service.name)} } from './${service.name}';`; + }).join('\n'); + + const apiObjectProperties = services.map(service => { + const serviceName = toCamelCase(service.name); + return ` ${serviceName}`; + }).join(',\n'); + + const indexContent = `// Auto-generated API200 SDK +import { createAPI200Client } from './api200'; +${serviceImports} + +export * from './types'; +export { createAPI200Client } from './api200'; +${serviceExports} + +// Initialize the client - users should call this with their credentials +// createAPI200Client('https://eu.api200.co/api', 'your-api-key'); + +export const api200 = { +${apiObjectProperties} +}; + +export default api200; +`; + + await fs.writeFile(path.join(outputDir, 'index.ts'), indexContent); +} diff --git a/packages/js-sdk/src/generators/index.ts b/packages/js-sdk/src/generators/index.ts new file mode 100644 index 0000000..71204c3 --- /dev/null +++ b/packages/js-sdk/src/generators/index.ts @@ -0,0 +1,55 @@ +// generators/index.ts +import fs from 'fs-extra'; +import { Service } from '../utils/types'; +import { generateTypesFile } from './type-generator'; +import { generateServiceFile } from './service-generator'; +import { generateApi200ClientFile } from './client-generator'; +import { generateIndexFile } from './index-generator'; + +export async function generateSDK(userKey: string, baseApiUrl: string, outputDir: string) { + console.log('šŸš€ Generating API200 SDK...'); + + const baseUrl = baseApiUrl.replace(/\/api$/, "/"); + + // Fetch services data + const response = await fetch(`${baseUrl}/user/mcp-services`, { + headers: { + "x-api-key": userKey + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch services: ${response.status} ${response.statusText}`); + } + + const services: Service[] = await response.json(); + console.log(`šŸ“” Found ${services.length} services`); + + // Create output directory + await fs.ensureDir(outputDir); + + // Generate all files + await generateTypesFile(services, outputDir); + await generateApi200ClientFile(outputDir); + + // Generate service files + for (const service of services) { + await generateServiceFile(service, outputDir); + } + + // Generate main index file + await generateIndexFile(services, outputDir); + + console.log('āœ… SDK generated successfully!'); + console.log(`šŸ“ Output directory: ${outputDir}`); + console.log('\nšŸ“– Usage example:'); + console.log('```typescript'); + console.log(`import { createAPI200Client, api200 } from '${outputDir.startsWith('./src') ? outputDir.replace('./src/', './') : outputDir}';`); + console.log(''); + console.log('// Initialize the client with your credentials'); + console.log("createAPI200Client('https://eu.api200.co/api', 'your-api-key');"); + console.log(''); + console.log('// Use the API'); + console.log('const result = await api200.users.getUserById.get({ id: "123" });'); + console.log('```'); +} diff --git a/packages/js-sdk/src/generators/service-generator.ts b/packages/js-sdk/src/generators/service-generator.ts new file mode 100644 index 0000000..018132a --- /dev/null +++ b/packages/js-sdk/src/generators/service-generator.ts @@ -0,0 +1,41 @@ +// generators/service-generator.ts +import fs from 'fs-extra'; +import path from 'path'; +import { Service } from '../utils/types'; +import { toCamelCase, toPascalCase, generateMethodName } from '../utils/string-utils'; + +export async function generateServiceFile(service: Service, outputDir: string) { + const serviceName = toCamelCase(service.name); + const methods: string[] = []; + + service.endpoints.forEach(endpoint => { + const methodName = generateMethodName(endpoint.method, endpoint.name); + const hasParams = endpoint.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase()); + const paramsType = hasParams ? `${toPascalCase(methodName)}Params` : '{}'; + + methods.push(` + ${methodName}: { + async ${endpoint.method.toLowerCase()}(params${hasParams ? `: ${paramsType}` : '?'} = {} as ${paramsType}): Promise { + return makeRequest('${service.name}', '${endpoint.name}', '${endpoint.method}', params); + } + }`); + }); + + const importTypes = service.endpoints + .map(e => { + const methodName = generateMethodName(e.method, e.name); + const hasParams = e.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(e.method.toUpperCase()); + return hasParams ? `${toPascalCase(methodName)}Params` : ''; + }) + .filter(Boolean); + + const serviceContent = `// Auto-generated service: ${service.name} +import { ApiResponse, ApiError${importTypes.length > 0 ? ', ' + importTypes.join(', ') : ''} } from './types'; +import { makeRequest } from './api200'; + +export const ${serviceName} = {${methods.join(',')} +}; +`; + + await fs.writeFile(path.join(outputDir, `${service.name}.ts`), serviceContent); +} diff --git a/packages/js-sdk/src/generators/type-generator.ts b/packages/js-sdk/src/generators/type-generator.ts new file mode 100644 index 0000000..f1826c3 --- /dev/null +++ b/packages/js-sdk/src/generators/type-generator.ts @@ -0,0 +1,99 @@ +// generators/type-generator.ts +import fs from 'fs-extra'; +import path from 'path'; +import { Service } from '../utils/types'; +import { toPascalCase, generateMethodName } from '../utils/string-utils.js'; + +export function generateTypeFromSchema(schema: any): string { + if (!schema) return 'any'; + + if (schema.type === 'array' && schema.items) { + return `Array<${generateTypeFromSchema(schema.items)}>`; + } + + if (schema.type === 'object' || (!schema.type && schema.properties)) { + const props: string[] = []; + if (schema.properties) { + Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => { + const isRequired = schema.required?.includes(propName); + const propType = generateTypeFromSchema(propSchema); + props.push(` ${propName}${isRequired ? '' : '?'}: ${propType};`); + }); + } + return `{\n${props.join('\n')}\n}`; + } + + switch (schema.type) { + case 'string': return 'string'; + case 'integer': + case 'number': return 'number'; + case 'boolean': return 'boolean'; + default: return 'any'; + } +} + +export async function generateTypesFile(services: Service[], outputDir: string) { + const typeDefinitions: string[] = []; + + services.forEach(service => { + service.endpoints.forEach(endpoint => { + const methodName = generateMethodName(endpoint.method, endpoint.name); + + // Parameters interface + const paramTypes: string[] = []; + if (endpoint.schema?.parameters) { + endpoint.schema.parameters.forEach(param => { + const isRequired = param.required !== false; + let paramType = 'string'; + switch (param.type) { + case 'integer': + case 'number': + paramType = 'number'; + break; + case 'boolean': + paramType = 'boolean'; + break; + } + paramTypes.push(` ${param.name}${isRequired ? '' : '?'}: ${paramType};`); + }); + } + + // Request body type + if (['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase())) { + if (endpoint.schema?.requestBody?.content) { + const jsonContent = endpoint.schema.requestBody.content['application/json']; + if (jsonContent?.schema) { + const requestBodyType = generateTypeFromSchema(jsonContent.schema); + paramTypes.push(` requestBody?: ${requestBodyType};`); + } else { + paramTypes.push(` requestBody?: any;`); + } + } else { + paramTypes.push(` requestBody?: any;`); + } + } + + if (paramTypes.length > 0) { + typeDefinitions.push(`export interface ${toPascalCase(methodName)}Params {\n${paramTypes.join('\n')}\n}`); + } + }); + }); + + const typesContent = `// Auto-generated types for API200 SDK +${typeDefinitions.join('\n\n')} + +export interface ApiResponse { + data: T; + status: number; + statusText: string; +} + +export interface ApiError { + message: string; + status?: number; + statusText?: string; +} +`; + + await fs.writeFile(path.join(outputDir, 'types.ts'), typesContent); +} diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 77727e4..5a12ddc 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -1,42 +1,8 @@ #!/usr/bin/env node import { Command } from 'commander'; -import fs from 'fs-extra'; -import path from 'path'; -import { z } from 'zod'; - -const envSchema = z.object({ - token: z.string().min(1), - baseUrl: z.string().url().default("https://eu.api200.co/api"), - output: z.string().optional() -}); - -interface ServiceEndpoint { - name: string; - method: string; - description?: string; - schema?: { - parameters?: Array<{ - name: string; - type: string; - in: 'path' | 'query' | 'header' | 'body'; - required?: boolean; - description?: string; - }>; - requestBody?: { - content: { - [contentType: string]: { - schema: any; - }; - }; - }; - }; -} - -interface Service { - name: string; - description?: string; - endpoints: ServiceEndpoint[]; -} +import { generateSDK } from './generators'; +import { envSchema } from './utils/schema'; +import { determineOutputDirectory } from './utils/file-utils'; const program = new Command(); @@ -64,311 +30,4 @@ program } }); -function determineOutputDirectory(providedOutput?: string): string { - if (providedOutput) { - return providedOutput; - } - - const currentDir = process.cwd(); - const srcExists = fs.existsSync(path.join(currentDir, 'src')); - - if (srcExists) { - return './src/lib/api200'; - } else { - return './lib/api200'; - } -} - -async function generateSDK(userKey: string, baseApiUrl: string, outputDir: string) { - console.log('šŸš€ Generating API200 SDK...'); - - const baseUrl = baseApiUrl.replace(/\/api$/, "/"); - - // Fetch services data - const response = await fetch(`${baseUrl}/user/mcp-services`, { - headers: { - "x-api-key": userKey - } - }); - - if (!response.ok) { - throw new Error(`Failed to fetch services: ${response.status} ${response.statusText}`); - } - - const services: Service[] = await response.json(); - console.log(`šŸ“” Found ${services.length} services`); - - // Create output directory - await fs.ensureDir(outputDir); - - // Generate types file - await generateTypesFile(services, outputDir); - - // Generate api200 client file - await generateApi200ClientFile(outputDir); - - // Generate service files - const serviceExports: string[] = []; - for (const service of services) { - const serviceName = toCamelCase(service.name); - await generateServiceFile(service, outputDir); - serviceExports.push(`export { ${serviceName} } from './${service.name}';`); - } - - // Generate main index file - await generateIndexFile(services, serviceExports, outputDir); - - console.log('āœ… SDK generated successfully!'); - console.log(`šŸ“ Output directory: ${outputDir}`); - console.log('\nšŸ“– Usage example:'); - console.log('```typescript'); - console.log(`import { createAPI200Client, api200 } from '${outputDir.startsWith('./src') ? outputDir.replace('./src/', './') : outputDir}';`); - console.log(''); - console.log('// Initialize the client with your credentials'); - console.log("createAPI200Client('https://eu.api200.co/api', 'your-api-key');"); - console.log(''); - console.log('// Use the API'); - console.log('const result = await api200.users.getUserById.get({ id: "123" });'); - console.log('```'); -} - -function toCamelCase(str: string): string { - return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); -} - -function toPascalCase(str: string): string { - return str.charAt(0).toUpperCase() + toCamelCase(str).slice(1); -} - -function generateTypeFromSchema(schema: any): string { - if (!schema) return 'any'; - - if (schema.type === 'array' && schema.items) { - return `Array<${generateTypeFromSchema(schema.items)}>`; - } - - if (schema.type === 'object' || (!schema.type && schema.properties)) { - const props: string[] = []; - if (schema.properties) { - Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => { - const isRequired = schema.required?.includes(propName); - const propType = generateTypeFromSchema(propSchema); - props.push(` ${propName}${isRequired ? '' : '?'}: ${propType};`); - }); - } - return `{\n${props.join('\n')}\n}`; - } - - switch (schema.type) { - case 'string': return 'string'; - case 'integer': - case 'number': return 'number'; - case 'boolean': return 'boolean'; - default: return 'any'; - } -} - -async function generateTypesFile(services: Service[], outputDir: string) { - const typeDefinitions: string[] = []; - - services.forEach(service => { - service.endpoints.forEach(endpoint => { - const methodName = `${endpoint.method.toLowerCase()}_${endpoint.name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); - - // Parameters interface - const paramTypes: string[] = []; - if (endpoint.schema?.parameters) { - endpoint.schema.parameters.forEach(param => { - const isRequired = param.required !== false; - let paramType = 'string'; - switch (param.type) { - case 'integer': - case 'number': - paramType = 'number'; - break; - case 'boolean': - paramType = 'boolean'; - break; - } - paramTypes.push(` ${param.name}${isRequired ? '' : '?'}: ${paramType};`); - }); - } - - // Request body type - let requestBodyType = ''; - if (['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase())) { - if (endpoint.schema?.requestBody?.content) { - const jsonContent = endpoint.schema.requestBody.content['application/json']; - if (jsonContent?.schema) { - requestBodyType = generateTypeFromSchema(jsonContent.schema); - } else { - requestBodyType = 'any'; - } - paramTypes.push(` requestBody?: ${requestBodyType};`); - } else { - paramTypes.push(` requestBody?: any;`); - } - } - - if (paramTypes.length > 0) { - typeDefinitions.push(`export interface ${toPascalCase(methodName)}Params {\n${paramTypes.join('\n')}\n}`); - } - }); - }); - - const typesContent = `// Auto-generated types for API200 SDK -${typeDefinitions.join('\n\n')} - -export interface ApiResponse { - data: T; - status: number; - statusText: string; -} - -export interface ApiError { - message: string; - status?: number; - statusText?: string; -} -`; - - await fs.writeFile(path.join(outputDir, 'types.ts'), typesContent); -} - -async function generateServiceFile(service: Service, outputDir: string) { - const serviceName = toCamelCase(service.name); - const methods: string[] = []; - - service.endpoints.forEach(endpoint => { - const methodName = `${endpoint.method.toLowerCase()}_${endpoint.name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); - const hasParams = endpoint.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase()); - const paramsType = hasParams ? `${toPascalCase(methodName)}Params` : '{}'; - - methods.push(` - ${methodName}: { - async ${endpoint.method.toLowerCase()}(params${hasParams ? `: ${paramsType}` : '?'}: ${paramsType} = {} as ${paramsType}): Promise { - return makeRequest('${service.name}', '${endpoint.name}', '${endpoint.method}', params); - } - }`); - }); - - const importTypes = service.endpoints - .map(e => { - const methodName = `${e.method.toLowerCase()}_${e.name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); - const hasParams = e.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(e.method.toUpperCase()); - return hasParams ? `${toPascalCase(methodName)}Params` : ''; - }) - .filter(Boolean); - - const serviceContent = `// Auto-generated service: ${service.name} -import { ApiResponse, ApiError${importTypes.length > 0 ? ', ' + importTypes.join(', ') : ''} } from './types'; -import { makeRequest } from './api200'; - -export const ${serviceName} = {${methods.join(',')} -}; -`; - - await fs.writeFile(path.join(outputDir, `${service.name}.ts`), serviceContent); -} - -async function generateApi200ClientFile(outputDir: string) { - const clientContent = `// Auto-generated API200 client -import { ApiResponse, ApiError } from './types'; - -let config: { baseUrl: string; userKey: string } | null = null; - -export function createAPI200Client(baseUrl: string, userKey: string) { - config = { baseUrl, userKey }; -} - -export async function makeRequest(serviceName: string, endpointPath: string, method: string, params: any = {}): Promise { - if (!config) { - throw new Error('API200 client not initialized. Call createAPI200Client(baseUrl, userKey) first.'); - } - - try { - // Handle path parameters - let processedPath = endpointPath; - const queryParams: string[] = []; - - if (params) { - // Replace path parameters - Object.keys(params).forEach(key => { - if (processedPath.includes(\`{\${key}}\`)) { - processedPath = processedPath.replace(\`{\${key}}\`, params[key]); - } else if (key !== 'requestBody') { - // Add as query parameter - queryParams.push(\`\${key}=\${encodeURIComponent(params[key])}\`); - } - }); - } - - const fullUrl = \`\${config.baseUrl}/\${serviceName}\${processedPath}\${queryParams.length ? '?' + queryParams.join('&') : ''}\`; - - const requestOptions: RequestInit = { - method, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'x-api-key': config.userKey - } - }; - - if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && params.requestBody) { - requestOptions.body = JSON.stringify(params.requestBody); - } - - const response = await fetch(fullUrl, requestOptions); - const data = await response.json(); - - return { - data, - status: response.status, - statusText: response.statusText - }; - } catch (error) { - throw { - message: error instanceof Error ? error.message : String(error), - status: 0, - statusText: 'Network Error' - } as ApiError; - } -} -`; - - await fs.writeFile(path.join(outputDir, 'api200.ts'), clientContent); -} - -async function generateIndexFile(services: Service[], serviceExports: string[], outputDir: string) { - const serviceImports = services.map(service => { - const serviceName = toCamelCase(service.name); - return `import { ${serviceName} } from './${service.name}';`; - }).join('\n'); - - const apiObjectProperties = services.map(service => { - const serviceName = toCamelCase(service.name); - return ` ${serviceName}`; - }).join(',\n'); - - const indexContent = `// Auto-generated API200 SDK -import { createAPI200Client } from './api200'; -${serviceImports} - -export * from './types'; -export { createAPI200Client } from './api200'; -${serviceExports.join('\n')} - -// Initialize the client - users should call this with their credentials -// createAPI200Client('https://eu.api200.co/api', 'your-api-key'); - -export const api200 = { -${apiObjectProperties} -}; - -export default api200; -`; - - await fs.writeFile(path.join(outputDir, 'index.ts'), indexContent); -} - program.parse(); diff --git a/packages/js-sdk/src/utils/file-utils.ts b/packages/js-sdk/src/utils/file-utils.ts new file mode 100644 index 0000000..ee534ca --- /dev/null +++ b/packages/js-sdk/src/utils/file-utils.ts @@ -0,0 +1,18 @@ +// utils/file-utils.ts +import fs from 'fs-extra'; +import path from 'path'; + +export function determineOutputDirectory(providedOutput?: string): string { + if (providedOutput) { + return providedOutput; + } + + const currentDir = process.cwd(); + const srcExists = fs.existsSync(path.join(currentDir, 'src')); + + if (srcExists) { + return './src/lib/api200'; + } else { + return './lib/api200'; + } +} diff --git a/packages/js-sdk/src/utils/schema.ts b/packages/js-sdk/src/utils/schema.ts new file mode 100644 index 0000000..692853f --- /dev/null +++ b/packages/js-sdk/src/utils/schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const envSchema = z.object({ + token: z.string().min(1), + baseUrl: z.string().url().default("https://eu.api200.co/api"), + output: z.string().optional() +}); + +export type Config = z.infer; diff --git a/packages/js-sdk/src/utils/string-utils.ts b/packages/js-sdk/src/utils/string-utils.ts new file mode 100644 index 0000000..ee7209c --- /dev/null +++ b/packages/js-sdk/src/utils/string-utils.ts @@ -0,0 +1,12 @@ +// utils/string-utils.ts +export function toCamelCase(str: string): string { + return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +} + +export function toPascalCase(str: string): string { + return str.charAt(0).toUpperCase() + toCamelCase(str).slice(1); +} + +export function generateMethodName(method: string, name: string): string { + return `${method.toLowerCase()}_${name.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); +} diff --git a/packages/js-sdk/src/utils/types.ts b/packages/js-sdk/src/utils/types.ts new file mode 100644 index 0000000..84b59a9 --- /dev/null +++ b/packages/js-sdk/src/utils/types.ts @@ -0,0 +1,27 @@ +export interface ServiceEndpoint { + name: string; + method: string; + description?: string; + schema?: { + parameters?: Array<{ + name: string; + type: string; + in: 'path' | 'query' | 'header' | 'body'; + required?: boolean; + description?: string; + }>; + requestBody?: { + content: { + [contentType: string]: { + schema: any; + }; + }; + }; + }; +} + +export interface Service { + name: string; + description?: string; + endpoints: ServiceEndpoint[]; +} From 2af9f23883d83dc575a46a42964eeb92d60c79eb Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 25 May 2025 23:29:23 +0300 Subject: [PATCH 18/27] sdk improvements --- .../js-sdk/src/generators/client-generator.ts | 69 +++++++++++++------ .../js-sdk/src/generators/index-generator.ts | 33 ++------- packages/js-sdk/src/generators/index.ts | 22 +++--- .../src/generators/service-generator.ts | 40 +++++++---- .../js-sdk/src/generators/type-generator.ts | 21 +++--- 5 files changed, 99 insertions(+), 86 deletions(-) diff --git a/packages/js-sdk/src/generators/client-generator.ts b/packages/js-sdk/src/generators/client-generator.ts index 1e9c0ee..3327558 100644 --- a/packages/js-sdk/src/generators/client-generator.ts +++ b/packages/js-sdk/src/generators/client-generator.ts @@ -1,34 +1,41 @@ // generators/client-generator.ts import fs from 'fs-extra'; import path from 'path'; +import { Service } from '../utils/types'; +import { toCamelCase, toPascalCase } from '../utils/string-utils'; -export async function generateApi200ClientFile(outputDir: string) { - const clientContent = `// Auto-generated API200 client -import { ApiResponse, ApiError } from './types'; +export async function generateApi200ClientFile(services: Service[], outputDir: string) { + const serviceImports = services.map(service => { + const serviceName = toCamelCase(service.name); + return `import { create${toPascalCase(service.name)}Service } from './${service.name}';`; + }).join('\n'); -let config: { baseUrl: string; userKey: string } | null = null; + const apiObjectProperties = services.map(service => { + const serviceName = toCamelCase(service.name); + return ` ${serviceName}: create${toPascalCase(service.name)}Service(config)`; + }).join(',\n'); -export function createAPI200Client(baseUrl: string, userKey: string) { - config = { baseUrl, userKey }; -} + const clientContent = `import { API200Config } from './types'; +${serviceImports} -export async function makeRequest(serviceName: string, endpointPath: string, method: string, params: any = {}): Promise { - if (!config) { - throw new Error('API200 client not initialized. Call createAPI200Client(baseUrl, userKey) first.'); - } +export interface API200Client { +${services.map(service => { + const serviceName = toCamelCase(service.name); + return ` ${serviceName}: ReturnType;`; + }).join('\n')} +} + +export async function makeRequest(config: API200Config, serviceName: string, endpointPath: string, method: string, params: any = {}): Promise<{ data: any; error: any }> { try { - // Handle path parameters let processedPath = endpointPath; const queryParams: string[] = []; if (params) { - // Replace path parameters Object.keys(params).forEach(key => { if (processedPath.includes(\`{\${key}}\`)) { processedPath = processedPath.replace(\`{\${key}}\`, params[key]); } else if (key !== 'requestBody') { - // Add as query parameter queryParams.push(\`\${key}=\${encodeURIComponent(params[key])}\`); } }); @@ -52,19 +59,39 @@ export async function makeRequest(serviceName: string, endpointPath: string, met const response = await fetch(fullUrl, requestOptions); const data = await response.json(); + if (!response.ok) { + return { + data: null, + error: { + message: data.error || \`HTTP \${response.status}: \${response.statusText}\`, + status: response.status, + details: data.details + } + }; + } + return { data, - status: response.status, - statusText: response.statusText + error: null }; } catch (error) { - throw { - message: error instanceof Error ? error.message : String(error), - status: 0, - statusText: 'Network Error' - } as ApiError; + return { + data: null, + error: { + message: error instanceof Error ? error.message : String(error), + status: 0 + } + }; } } + +export function createAPI200Client(userKey: string, baseUrl: string = 'https://app.api200.co/api'): API200Client { + const config: API200Config = { baseUrl, userKey }; + + return { +${apiObjectProperties} + }; +} `; await fs.writeFile(path.join(outputDir, 'api200.ts'), clientContent); diff --git a/packages/js-sdk/src/generators/index-generator.ts b/packages/js-sdk/src/generators/index-generator.ts index 87b4b1d..2e67495 100644 --- a/packages/js-sdk/src/generators/index-generator.ts +++ b/packages/js-sdk/src/generators/index-generator.ts @@ -2,39 +2,16 @@ import fs from 'fs-extra'; import path from 'path'; import { Service } from '../utils/types'; -import { toCamelCase } from '../utils/string-utils'; -export async function generateIndexFile(services: Service[], outputDir: string) { - const serviceImports = services.map(service => { - const serviceName = toCamelCase(service.name); - return `import { ${serviceName} } from './${service.name}';`; - }).join('\n'); +export async function generateIndexFile(services: Service[], outputDir: string, userKey: string) { + const indexContent = `import { createAPI200Client } from './api200'; - const serviceExports = services.map(service => { - return `export { ${toCamelCase(service.name)} } from './${service.name}';`; - }).join('\n'); - - const apiObjectProperties = services.map(service => { - const serviceName = toCamelCase(service.name); - return ` ${serviceName}`; - }).join(',\n'); - - const indexContent = `// Auto-generated API200 SDK -import { createAPI200Client } from './api200'; -${serviceImports} +// TODO: Move this to environment variables (process.env.API200_KEY) +const api200 = createAPI200Client('${userKey}', 'https://app.api200.co/api'); +export default api200; export * from './types'; export { createAPI200Client } from './api200'; -${serviceExports} - -// Initialize the client - users should call this with their credentials -// createAPI200Client('https://eu.api200.co/api', 'your-api-key'); - -export const api200 = { -${apiObjectProperties} -}; - -export default api200; `; await fs.writeFile(path.join(outputDir, 'index.ts'), indexContent); diff --git a/packages/js-sdk/src/generators/index.ts b/packages/js-sdk/src/generators/index.ts index 71204c3..836d29e 100644 --- a/packages/js-sdk/src/generators/index.ts +++ b/packages/js-sdk/src/generators/index.ts @@ -11,7 +11,6 @@ export async function generateSDK(userKey: string, baseApiUrl: string, outputDir const baseUrl = baseApiUrl.replace(/\/api$/, "/"); - // Fetch services data const response = await fetch(`${baseUrl}/user/mcp-services`, { headers: { "x-api-key": userKey @@ -25,31 +24,28 @@ export async function generateSDK(userKey: string, baseApiUrl: string, outputDir const services: Service[] = await response.json(); console.log(`šŸ“” Found ${services.length} services`); - // Create output directory await fs.ensureDir(outputDir); - // Generate all files await generateTypesFile(services, outputDir); - await generateApi200ClientFile(outputDir); + await generateApi200ClientFile(services, outputDir); - // Generate service files for (const service of services) { await generateServiceFile(service, outputDir); } - // Generate main index file - await generateIndexFile(services, outputDir); + await generateIndexFile(services, outputDir, userKey); console.log('āœ… SDK generated successfully!'); console.log(`šŸ“ Output directory: ${outputDir}`); console.log('\nšŸ“– Usage example:'); console.log('```typescript'); - console.log(`import { createAPI200Client, api200 } from '${outputDir.startsWith('./src') ? outputDir.replace('./src/', './') : outputDir}';`); + console.log(`import api200 from '${outputDir.startsWith('./src') ? outputDir.replace('./src/', './') : outputDir}';`); console.log(''); - console.log('// Initialize the client with your credentials'); - console.log("createAPI200Client('https://eu.api200.co/api', 'your-api-key');"); - console.log(''); - console.log('// Use the API'); - console.log('const result = await api200.users.getUserById.get({ id: "123" });'); + console.log('const { data, error } = await api200.users.getUserById.get({ id: "123" });'); + console.log('if (error) {'); + console.log(' console.error(error.message);'); + console.log('} else {'); + console.log(' console.log(data);'); + console.log('}'); console.log('```'); } diff --git a/packages/js-sdk/src/generators/service-generator.ts b/packages/js-sdk/src/generators/service-generator.ts index 018132a..b07b72a 100644 --- a/packages/js-sdk/src/generators/service-generator.ts +++ b/packages/js-sdk/src/generators/service-generator.ts @@ -8,34 +8,46 @@ export async function generateServiceFile(service: Service, outputDir: string) { const serviceName = toCamelCase(service.name); const methods: string[] = []; - service.endpoints.forEach(endpoint => { + service.endpoints.forEach((endpoint) => { const methodName = generateMethodName(endpoint.method, endpoint.name); - const hasParams = endpoint.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase()); + const hasParams = + (endpoint.schema?.parameters?.length ?? 0) > 0 || + ['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase()); const paramsType = hasParams ? `${toPascalCase(methodName)}Params` : '{}'; methods.push(` - ${methodName}: { - async ${endpoint.method.toLowerCase()}(params${hasParams ? `: ${paramsType}` : '?'} = {} as ${paramsType}): Promise { - return makeRequest('${service.name}', '${endpoint.name}', '${endpoint.method}', params); - } - }`); + ${methodName}: { + async ${endpoint.method.toLowerCase()}(params${ + hasParams ? `: ${paramsType}` : '?' + } = {} as ${paramsType}) { + return makeRequest(config, '${service.name}', '${endpoint.name}', '${endpoint.method}', params); + } + }`); }); const importTypes = service.endpoints - .map(e => { + .map((e) => { const methodName = generateMethodName(e.method, e.name); - const hasParams = e.schema?.parameters?.length! > 0 || ['POST', 'PUT', 'PATCH'].includes(e.method.toUpperCase()); + const hasParams = + (e.schema?.parameters?.length ?? 0) > 0 || + ['POST', 'PUT', 'PATCH'].includes(e.method.toUpperCase()); return hasParams ? `${toPascalCase(methodName)}Params` : ''; }) .filter(Boolean); - const serviceContent = `// Auto-generated service: ${service.name} -import { ApiResponse, ApiError${importTypes.length > 0 ? ', ' + importTypes.join(', ') : ''} } from './types'; + const serviceContent = `import { + ${importTypes.length > 0 ? importTypes.join(', ') + ', ' : ''}API200Config +} from './types'; import { makeRequest } from './api200'; -export const ${serviceName} = {${methods.join(',')} -}; +export function create${toPascalCase(service.name)}Service(config: API200Config) { + return {${methods.join(',')} + }; +} `; - await fs.writeFile(path.join(outputDir, `${service.name}.ts`), serviceContent); + await fs.writeFile( + path.join(outputDir, `${service.name}.ts`), + serviceContent.trimStart() + ); } diff --git a/packages/js-sdk/src/generators/type-generator.ts b/packages/js-sdk/src/generators/type-generator.ts index f1826c3..bcbe6c8 100644 --- a/packages/js-sdk/src/generators/type-generator.ts +++ b/packages/js-sdk/src/generators/type-generator.ts @@ -39,7 +39,6 @@ export async function generateTypesFile(services: Service[], outputDir: string) service.endpoints.forEach(endpoint => { const methodName = generateMethodName(endpoint.method, endpoint.name); - // Parameters interface const paramTypes: string[] = []; if (endpoint.schema?.parameters) { endpoint.schema.parameters.forEach(param => { @@ -58,7 +57,6 @@ export async function generateTypesFile(services: Service[], outputDir: string) }); } - // Request body type if (['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase())) { if (endpoint.schema?.requestBody?.content) { const jsonContent = endpoint.schema.requestBody.content['application/json']; @@ -79,19 +77,22 @@ export async function generateTypesFile(services: Service[], outputDir: string) }); }); - const typesContent = `// Auto-generated types for API200 SDK -${typeDefinitions.join('\n\n')} + const typesContent = `${typeDefinitions.join('\n\n')} -export interface ApiResponse { - data: T; - status: number; - statusText: string; +export interface API200Config { + baseUrl: string; + userKey: string; } -export interface ApiError { +export interface API200Response { + data: T | null; + error: API200Error | null; +} + +export interface API200Error { message: string; status?: number; - statusText?: string; + details?: any; } `; From 450d00acc724cfb65666237b297b726979566b50 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 25 May 2025 23:50:11 +0300 Subject: [PATCH 19/27] sdk finish --- packages/js-sdk/src/generators/client-generator.ts | 2 +- packages/js-sdk/src/generators/index-generator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js-sdk/src/generators/client-generator.ts b/packages/js-sdk/src/generators/client-generator.ts index 3327558..c7417a8 100644 --- a/packages/js-sdk/src/generators/client-generator.ts +++ b/packages/js-sdk/src/generators/client-generator.ts @@ -85,7 +85,7 @@ export async function makeRequest(config: API200Config, serviceName: string, end } } -export function createAPI200Client(userKey: string, baseUrl: string = 'https://app.api200.co/api'): API200Client { +export function createAPI200Client(userKey: string, baseUrl: string = 'https://eu.api200.co/api'): API200Client { const config: API200Config = { baseUrl, userKey }; return { diff --git a/packages/js-sdk/src/generators/index-generator.ts b/packages/js-sdk/src/generators/index-generator.ts index 2e67495..f6d89f0 100644 --- a/packages/js-sdk/src/generators/index-generator.ts +++ b/packages/js-sdk/src/generators/index-generator.ts @@ -7,7 +7,7 @@ export async function generateIndexFile(services: Service[], outputDir: string, const indexContent = `import { createAPI200Client } from './api200'; // TODO: Move this to environment variables (process.env.API200_KEY) -const api200 = createAPI200Client('${userKey}', 'https://app.api200.co/api'); +const api200 = createAPI200Client('${userKey}', 'https://eu.api200.co/api'); export default api200; export * from './types'; From 1aea5fb8eaf3e9f7c2b6221e3a41dc2ba1748bc4 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 00:00:17 +0300 Subject: [PATCH 20/27] update sdk readme --- packages/js-sdk/README.md | 252 ++++++++++++-------------------------- 1 file changed, 80 insertions(+), 172 deletions(-) diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index 3374ecc..deb9515 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -1,223 +1,131 @@ # API200 SDK Generator -[![npm version](https://badge.fury.io/js/api200-sdk-generator.svg)](https://badge.fury.io/js/api200-sdk-generator) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +A TypeScript SDK generator that automatically creates a fully-typed client library for your API200 services. Generate type-safe API clients with zero configuration and full IDE support. -A powerful CLI tool that automatically generates a TypeScript SDK for your [API 200](https://api200.co) services. Transform your API endpoints into a fully-typed, easy-to-use SDK with a single command. +## Features -## šŸš€ Features +- **Automatic Type Generation**: Creates TypeScript interfaces from your API schemas +- **Full Type Safety**: Compile-time error checking for API calls +- **Zero Configuration**: Works out of the box with sensible defaults +- **Path & Query Parameters**: Handles both path variables and query strings +- **Request Body Support**: Type-safe request body handling for POST/PUT/PATCH +- **Error Handling**: Structured error responses with status codes +- **IDE Support**: Full IntelliSense and autocomplete support -- **šŸ”§ Zero Configuration** - Just provide your API token and go -- **šŸ“ Full TypeScript Support** - Complete type safety with IntelliSense -- **šŸŽÆ Intuitive API** - Clean, predictable method naming -- **⚔ Fast Generation** - Generates SDK in seconds -- **šŸ”„ Automatic Updates** - Re-run to sync with API changes -- **šŸ“¦ No Dependencies** - Generated code has zero runtime dependencies +## Prerequisites -## šŸ“„ Installation +1. **Register with API200**: Sign up at [API200](https://api200.co) and set up your API proxy platform +2. **Import/Create Services**: Add your API services to the API200 platform +3. **Get API Token**: Obtain your user token from the API200 dashboard -### Global Installation -```bash -npm install -g api200-sdk-generator -``` +## Installation + +Generate your SDK using npx (no installation required): -### One-time Usage (Recommended) ```bash -npx api200-generate-sdk -t your_api_token +npx api200-generate-sdk -t YOUR_API_TOKEN ``` -## šŸ› ļø Usage +## Command Options -### Basic Usage -```bash -npx api200-generate-sdk --token YOUR_API_TOKEN -``` +| Option | Short | Description | Default | +|--------|-------|-------------|---------| +| `--token` | `-t` | **Required.** Your API200 user token || +| `--base-url` | `-u` | Base API URL | `https://eu.api200.co/api` | +| `--output` | `-o` | Output directory | `./src/lib/api200` or `./lib/api200` | -### Custom Output Directory -```bash -npx api200-generate-sdk -t YOUR_API_TOKEN -o ./src/api -``` +### Examples -### Custom Base URL ```bash -npx api200-generate-sdk -t YOUR_API_TOKEN -u https://custom.api200.co/api -``` +# Basic usage +npx api200-generate-sdk -t your_token_here -### All Options -```bash -api200-generate-sdk [options] +# Custom output directory +npx api200-generate-sdk -t your_token_here -o ./src/api -Options: - -t, --token API200 user token (required) - -u, --base-url Base API URL (default: "https://eu.api200.co/api") - -o, --output Output directory (default: "./lib/api200") - -h, --help Display help for command +# Different API region +npx api200-generate-sdk -t your_token_here -u https://us.api200.co/api ``` -## šŸ“ Generated Structure +## Generated Structure -The generator creates a clean, organized SDK structure: +The SDK generator creates the following file structure: ``` lib/api200/ -ā”œā”€ā”€ index.ts # Main SDK export -ā”œā”€ā”€ types.ts # TypeScript interfaces -ā”œā”€ā”€ users.ts # Users service methods -ā”œā”€ā”€ orders.ts # Orders service methods -└── payments.ts # Payments service methods +ā”œā”€ā”€ index.ts # Main export file with configured client +ā”œā”€ā”€ api200.ts # Core client and request handling +ā”œā”€ā”€ types.ts # TypeScript type definitions +└── [service-name].ts # Individual service files ``` -## šŸ’» Usage Examples +## Usage Example + +### Usage -### Basic Import and Usage ```typescript -import { api200 } from './lib/api200'; +import api200 from './lib/api200'; // GET request with path parameter -const user = await api200.users.getUserById.get({ id: "123" }); -console.log(user.data); - -// GET request with query parameters -const users = await api200.users.getUsers.get({ - page: 1, - limit: 10, - status: "active" +const { data, error } = await api200.users.getUserById.get({ + id: "123" }); -// POST request with body -const newUser = await api200.users.createUser.post({ - requestBody: { - name: "John Doe", - email: "john@example.com" - } -}); -``` - -### Error Handling -```typescript -import { api200 } from './lib/api200'; -import type { ApiError } from './lib/api200/types'; - -try { - const result = await api200.users.getUserById.get({ id: "123" }); - console.log('Success:', result.data); -} catch (error) { - const apiError = error as ApiError; - console.error('Error:', apiError.message); - console.error('Status:', apiError.status); +if (error) { + console.error('API Error:', error.message); +} else { + console.log('User data:', data); } ``` -### Type Safety -```typescript -import { api200 } from './lib/api200'; -import type { GetUsersByIdParams, PostUsersParams } from './lib/api200/types'; - -// Fully typed parameters -const params: GetUsersByIdParams = { id: "123" }; -const user = await api200.users.getUserById.get(params); - -// TypeScript will catch type errors -const createParams: PostUsersParams = { - requestBody: { - name: "John", - email: "john@example.com", - // age: "30" // āŒ TypeScript error if age should be number - age: 30 // āœ… Correct type - } -}; -``` +### Method Naming Convention -## šŸ”„ Updating Your SDK +Methods are generated using the pattern: `[httpMethod][EndpointPath]` -When your API changes, simply re-run the generator to update your SDK: +Examples: +- `GET /users/{id}` → `getUserById.get()` +- `POST /users` → `createUser.post()` +- `PUT /users/{id}` → `updateUserById.put()` +- `DELETE /orders/{orderId}` → `deleteOrderByOrderId.delete()` -```bash -npx api200-generate-sdk -t YOUR_API_TOKEN -``` - -This will overwrite the existing SDK files with the latest API definitions. - -## 🌟 Method Naming Convention - -The generator creates clean, predictable method names: - -| API Endpoint | Generated Method | -|--------------|------------------| -| `GET /users` | `api200.users.getUsers.get()` | -| `GET /users/{id}` | `api200.users.getUsersById.get()` | -| `POST /users` | `api200.users.postUsers.post()` | -| `PUT /users/{id}` | `api200.users.putUsersById.put()` | -| `DELETE /users/{id}` | `api200.users.deleteUsersById.delete()` | - -## šŸ”§ Configuration - -### Environment Variables -You can also use environment variables instead of command-line options: - -```bash -export API200_TOKEN=your_token_here -export API200_BASE_URL=https://eu.api200.co/api -export API200_OUTPUT=./lib/api200 +## Error Handling -npx api200-generate-sdk -``` +All API methods return a consistent response structure: -### Project Integration -Add generation to your package.json scripts: +```typescript +interface API200Response { + data: T | null; + error: API200Error | null; +} -```json -{ - "scripts": { - "generate-sdk": "api200-generate-sdk -t $API200_TOKEN", - "build": "npm run generate-sdk && tsc", - "dev": "npm run generate-sdk && npm run build -- --watch" - } +interface API200Error { + message: string; + status?: number; + details?: any; } ``` -## šŸ› Troubleshooting - -### Common Issues - -**"Failed to fetch services" Error** -- Verify your API token is correct -- Check if the base URL is accessible -- Ensure you have internet connectivity - -**TypeScript Compilation Errors** -- Make sure you have TypeScript installed: `npm install -D typescript` -- Check that your tsconfig.json includes the generated files - -**Permission Errors** -- Ensure you have write permissions to the output directory -- Try running with elevated permissions if needed - -### Getting Help -- Check the [API 200](https://api200.co) documentation -- Open an issue on GitHub -- Contact support through the API 200 website - -## šŸ“„ License +## Types -MIT License - see the [LICENSE](LICENSE) file for details. +The generator creates comprehensive TypeScript types for all your APIs: -## šŸ”— Links +## Updating Your SDK -- **[API 200 Website](https://api200.co)** - Main service website -- **[API Documentation](https://api200.co/docs)** - Full API documentation -- **[GitHub Repository](https://github.com/your-org/api200-sdk-generator)** - Source code and issues +When you add new services or modify existing ones in API200: -## šŸ¤ Contributing +1. **Regenerate the SDK**: + ```bash + npx api200-generate-sdk -t YOUR_API_TOKEN + ``` -Contributions are welcome! Please feel free to submit a Pull Request. +2. **The generator will**: + - Fetch the latest service definitions + - Update all type definitions + - Add new service methods + - Preserve your existing configuration -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +3. **No breaking changes**: Existing code continues to work while new features become available ---- +## Support -**Made with ā¤ļø for [API 200](https://api200.co) developers** +- [GitHub](https://github.com/API-200/api200/discussions) From 456b8ff86c486da9511f677595ebd30ac9985712 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 00:30:08 +0300 Subject: [PATCH 21/27] Update code snippets to sdk (for js) in dashboard --- .../[endpointId]/components/CodeExample.tsx | 118 +++++++++++++----- .../dashboard/src/utils/getCodeExample.ts | 85 ++++++++++--- 2 files changed, 154 insertions(+), 49 deletions(-) diff --git a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx index 9a51854..0ffc57d 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx @@ -2,9 +2,9 @@ import { FC, useState } from "react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Card, CardContent } from "@/components/ui/card" +import {Card, CardContent, CardDescription, CardTitle} from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { Copy, CopyCheck } from "lucide-react" +import { Copy, CopyCheck, Download } from "lucide-react" import {getCodeExample, Language, Method} from "@/utils/getCodeExample"; interface ApiExampleProps { @@ -12,10 +12,10 @@ interface ApiExampleProps { method: Method } - export const ApiExample: FC = ({ url, method }) => { const [language, setLanguage] = useState("js") const [isCopied, setIsCopied] = useState(false) + const [isSdkCopied, setIsSdkCopied] = useState(false) const handleCopy = async () => { try { @@ -27,38 +27,88 @@ export const ApiExample: FC = ({ url, method }) => { } } + const handleCopyInstallCommand = async () => { + try { + await navigator.clipboard.writeText("npx api200-generate-sdk -t your_token_here") + setIsSdkCopied(true) + setTimeout(() => setIsSdkCopied(false), 2000) + } catch (err) { + console.error('Failed to copy install command:', err) + } + } + return ( - - -
- - -
-
-          {getCodeExample(language, url, method)}
-        
-
-
+
+ {/* SDK Installation Instructions for JavaScript */} + {language === "js" && ( + + +
+
+ + šŸ“¦ SDK Installation + + + Generate your personalized SDK with your API token + +
+ +
+
+ + npx api200-generate-sdk -t your_token_here + +
+
+
+ )} + + {/* Main Code Example */} + + +
+ + +
+
+                        {getCodeExample(language, url, method)}
+                    
+
+
+
) } diff --git a/packages/dashboard/src/utils/getCodeExample.ts b/packages/dashboard/src/utils/getCodeExample.ts index eccfc14..e2e7862 100644 --- a/packages/dashboard/src/utils/getCodeExample.ts +++ b/packages/dashboard/src/utils/getCodeExample.ts @@ -1,28 +1,83 @@ - export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" export type Language = "js" | "python" | "curl" | "php" | "go" | "rust" | "java" | "csharp" +// Helper function to generate SDK method name (matching your SDK generator logic) +function generateSDKMethodName(method: string, path: string): string { + return `${method.toLowerCase()}_${path.replace(/^\//, '').replace(/\//g, '_')}`.replace(/{([^}]+)}/g, 'by_$1'); +} + +// Helper function to convert service name to camelCase (matching your SDK generator logic) +function toCamelCase(str: string): string { + return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +} + +// Helper function to extract service name and endpoint path from URL +function parseApiUrl(url: string): { serviceName: string; endpointPath: string } { + // Extract from URL like: https://eu.api200.co/api/users/profile/{id} + const urlParts = url.split('/api/'); + if (urlParts.length < 2) { + return { serviceName: 'service', endpointPath: '/endpoint' }; + } + + const pathParts = urlParts[1].split('/'); + const serviceName = pathParts[0] || 'service'; + const endpointPath = '/' + pathParts.slice(1).join('/'); + + return { serviceName, endpointPath }; +} + +// Helper function to generate parameter example based on URL path +function generateSDKParams(url: string, method: Method): string { + const pathParams = url.match(/{([^}]+)}/g); + const hasRequestBody = ['POST', 'PUT', 'PATCH'].includes(method); + + if (!pathParams && !hasRequestBody) { + return ''; + } + const params: string[] = []; + + // Add path parameters + if (pathParams) { + pathParams.forEach(param => { + const paramName = param.replace(/[{}]/g, ''); + params.push(` ${paramName}: "your_${paramName}_value"`); + }); + } + + // Add request body for POST, PUT, PATCH + if (hasRequestBody) { + params.push(` requestBody: { + // Your request data here + name: "example", + value: "data" + }`); + } + + return params.length > 0 ? `{\n${params.join(',\n')}\n }` : ''; +} export const getCodeExample = (language: Language, url: string, method: Method): string => { const headers = '{"x-api-key": "YOUR_API_KEY"}' - const examples = { - js: `const fetchData = async () => { - try { - const response = await fetch("${url}", { - method: "${method}", - headers: ${headers} - }); - const data = await response.json(); - console.log(data); - } catch (error) { - console.error('Error:', error); - } + // For JavaScript, show SDK usage instead of fetch + if (language === 'js') { + const { serviceName, endpointPath } = parseApiUrl(url); + const camelCaseServiceName = toCamelCase(serviceName); + const methodName = generateSDKMethodName(method, endpointPath); + const params = generateSDKParams(url, method); + + return ` +import api200 from '@lib/api200'; + +const fetchData = async () => { + const { data, error } = await api200.${camelCaseServiceName}.${methodName}.${method.toLowerCase()}(${params ? params : ''}); }; -fetchData();`, +fetchData();`; + } + const examples = { python: `import requests url = "${url}" @@ -138,5 +193,5 @@ class Program }` } - return examples[language] || examples.js + return examples[language] } From 74a0b6d6694d4678c29f3ec5f686022dffafd9d6 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 00:33:25 +0300 Subject: [PATCH 22/27] small fix --- .../[id]/endpoints/[endpointId]/components/CodeExample.tsx | 2 +- packages/js-sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx index 0ffc57d..c515314 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx @@ -64,7 +64,7 @@ export const ApiExample: FC = ({ url, method }) => {
- npx api200-generate-sdk -t your_token_here + npx api200-generate-sdk -t YOUR_API_TOKEN
diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 49a1299..1994fd9 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -11,7 +11,7 @@ "build": "tsup src/index.ts --format esm --dts", "dev": "tsup src/index.ts --format esm --watch", "start": "node dist/index.js", - "prepare": "pnpm run build", + "prepare": "pnpm run build" }, "keywords": [ "api200", From c30ec4cdc064e524fa060e4f091ea6335ad6a5b4 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 00:35:34 +0300 Subject: [PATCH 23/27] cope update --- .../[id]/endpoints/[endpointId]/components/CodeExample.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx index c515314..e0e60f7 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx @@ -29,7 +29,7 @@ export const ApiExample: FC = ({ url, method }) => { const handleCopyInstallCommand = async () => { try { - await navigator.clipboard.writeText("npx api200-generate-sdk -t your_token_here") + await navigator.clipboard.writeText("npx api200-generate-sdk -t YOUR_API_TOKEN") setIsSdkCopied(true) setTimeout(() => setIsSdkCopied(false), 2000) } catch (err) { From 5b6d577b664063f717d5b0e8fec2a589a68e7db3 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 00:37:33 +0300 Subject: [PATCH 24/27] build fix --- .../[id]/endpoints/[endpointId]/components/CodeExample.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx index e0e60f7..b917b42 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx @@ -4,7 +4,7 @@ import { FC, useState } from "react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import {Card, CardContent, CardDescription, CardTitle} from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { Copy, CopyCheck, Download } from "lucide-react" +import { Copy, CopyCheck } from "lucide-react" import {getCodeExample, Language, Method} from "@/utils/getCodeExample"; interface ApiExampleProps { From 42c68aca1d857dfb5547f74e72c748f6918532d4 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 22:41:44 +0300 Subject: [PATCH 25/27] sdk fix for non mcp services --- packages/backend/src/user-router.ts | 25 ++++++++++++++++++++++++- packages/js-sdk/src/generators/index.ts | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/user-router.ts b/packages/backend/src/user-router.ts index ccaaedb..bd93468 100644 --- a/packages/backend/src/user-router.ts +++ b/packages/backend/src/user-router.ts @@ -29,5 +29,28 @@ export const createUserRouter = () => { ctx.body = mcpServices.data; }); + router.get('/all-services', async (ctx) => { + const keyData = await validateApiKey(ctx); + if (!keyData) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const services = await supabase + .from('services') + .select('*, endpoints(*)') + .eq('user_id', keyData.user_id); + + if (services.error) { + ctx.status = 500; + ctx.body = { error: 'Failed to fetch services' }; + return; + } + + ctx.status = 200; + ctx.body = services.data; + }); + return router; -} \ No newline at end of file +} diff --git a/packages/js-sdk/src/generators/index.ts b/packages/js-sdk/src/generators/index.ts index 836d29e..00cbd0a 100644 --- a/packages/js-sdk/src/generators/index.ts +++ b/packages/js-sdk/src/generators/index.ts @@ -11,7 +11,7 @@ export async function generateSDK(userKey: string, baseApiUrl: string, outputDir const baseUrl = baseApiUrl.replace(/\/api$/, "/"); - const response = await fetch(`${baseUrl}/user/mcp-services`, { + const response = await fetch(`${baseUrl}/user/all-services`, { headers: { "x-api-key": userKey } From e37e9ebe87b1f66e413007d713c8bc530fef3071 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 22:43:04 +0300 Subject: [PATCH 26/27] publish js sdk fix --- packages/js-sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 1994fd9..d829ebb 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "api200-sdk-generator", - "version": "1.0.0", + "version": "1.0.1", "description": "CLI tool to generate TypeScript SDK for API 200 services", "main": "dist/index.js", "bin": { From ca6c7d6dfdd551344984f8200b53368f67aab080 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Mon, 26 May 2025 23:19:46 +0300 Subject: [PATCH 27/27] typo fix --- .../[id]/endpoints/[endpointId]/components/CodeExample.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx index b917b42..e547959 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/endpoints/[endpointId]/components/CodeExample.tsx @@ -64,7 +64,7 @@ export const ApiExample: FC = ({ url, method }) => {
- npx api200-generate-sdk -t YOUR_API_TOKEN + npx api200-sdk-generator -t YOUR_API_TOKEN