From c8a659845bfcfc6acd9a94abf5887dd83183a8dc Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Fri, 6 Feb 2026 13:17:23 +0530 Subject: [PATCH 01/15] Update SDK to support v1 API with model parameter and stream field accumulation - Migrate all endpoints to v1 paths (/v1/generate, /v1/edit, /v1/convert/ai-vectorize) - Add response envelope unwrapping (success/data/metadata) and v1 error mapping - Add model parameter support for Generate and Edit (mutually exclusive with quality) - Add storage parameter for persistent SVG URLs across all endpoints - Restructure convert namespace (client.convert.aiVectorize) for future extensibility - Update style params, aspect ratios, and types to match v1 API spec - Fix streaming to accumulate fields across events (svgUrl from generated event now carries through to complete event) - Add manual test script and SDK test commands for all 17 test scenarios --- docs/v1-api.md | 2467 +++++++++++++++++ src/clients/BaseClient.ts | 113 +- src/clients/ConvertClient.ts | 293 +- src/clients/EditClient.ts | 160 +- src/clients/GenerateClient.ts | 98 +- src/clients/convert/AIVectorizeClient.ts | 322 +++ src/clients/convert/index.ts | 1 + src/core/SVGMakerClient.ts | 13 +- src/errors/CustomErrors.ts | 35 +- src/index.ts | 4 +- src/types/api.ts | 152 +- src/utils/httpClient.ts | 115 +- tests/SVGMakerClient.test.ts | 5 +- tests/manual-test.ts | 851 +++--- tests/quality-aspectratio-constraints.test.ts | 145 +- tests/sdk-test-commands.md | 181 ++ 16 files changed, 3964 insertions(+), 991 deletions(-) create mode 100644 docs/v1-api.md create mode 100644 src/clients/convert/AIVectorizeClient.ts create mode 100644 src/clients/convert/index.ts create mode 100644 tests/sdk-test-commands.md diff --git a/docs/v1-api.md b/docs/v1-api.md new file mode 100644 index 0000000..67e19b2 --- /dev/null +++ b/docs/v1-api.md @@ -0,0 +1,2467 @@ +# SVGMaker API v1 Documentation + +## Overview + +The SVGMaker API v1 provides a RESTful interface for generating, editing, and converting images to SVG format using advanced AI technology. All endpoints require authentication via API key and use a consistent response format. + +**Base URL:** `https://svgmaker.io/api` + +**API Version:** v1 + +--- + +## Authentication + +All API endpoints require authentication using an API key passed in the `x-api-key` header. + +### Header Format + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +### Getting Your API Key + +1. Sign up or log in to your SVGMaker account +2. Navigate to your account settings +3. Generate your API key +4. Keep your API key secure and never share it publicly + +### Security Best Practices + +- Store API keys in environment variables, not in your code +- Rotate your API keys regularly for enhanced security +- Monitor your API usage to detect any unauthorized access +- Never commit API keys to version control + +--- + +## Response Format + +All API responses follow a consistent structure: + +### Success Response + +```json +{ + "success": true, + "data": { + // Response data here + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 2, + "creditsRemaining": 105.5 + } +} +``` + +### Error Response + +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "status": 400, + "message": "Human-readable error message", + "details": {} + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Note:** The `status` field in the error object matches the HTTP status code in the response header. Both are included for convenience - clients can use either the HTTP status code or the `status` field in the error object. + +--- + +## Credit System + +Credits are consumed based on the operation type and quality level: + +| Operation | Quality | Credits | Description | +|-----------|---------|---------|-------------| +| Generate | Low | 1 | Basic quality SVG generation | +| Generate | Medium | 2 | Standard quality SVG generation | +| Generate | High | 3 | Premium quality SVG generation | +| Edit | Low | 2 | Basic quality image/SVG editing | +| Edit | Medium | 3 | Standard quality image/SVG editing | +| Edit | High | 5 | Premium quality image/SVG editing | +| Convert (AI Vectorize) | - | 1 | Convert raster image to SVG using AI | +| Convert (Trace) | - | 0.5 | Trace raster image to SVG using VTracer | +| Convert (SVG to Vector) | - | 0.5 | Convert SVG to vector format (PDF/EPS/DXF/AI/PS) | +| Convert (Raster to Raster) | - | 0.25 | Convert between raster formats | +| Convert (Batch) | - | 0.5/0.25 | Batch convert files (0.5 per vector, 0.25 per raster) | +| Enhance Prompt | - | 0.5 | Enhance text prompts using AI | +| Optimize SVG | - | 0.5 | Optimize SVG files using SVGO. Use compress=true for SVGZ. | + +### Model-Based Credits + +When using the `model` parameter instead of `quality`, credits are charged based on the specific model selected. The `model` and `quality` parameters are **mutually exclusive** — providing both will return an error. + +**Note:** Pro tier models require a paid account. Free tier models are available to all users. + +#### Available Models + +| Model ID | Tier | Generate Credits | Edit Credits | Description | +|----------|------|-----------------|--------------|-------------| +| `gpt-image-1-mini` | Free | 1 | 2 | Fast, lightweight image generation | +| `flux-1-dev` | Free | 1 | - | Painterly renders from Black Forest Labs | +| `flux-2-dev` | Free | 1 | 2 | Next-gen Flux model with edit support | +| `z-image-turbo` | Free | 1 | - | Fast turbo generation | +| `qwen-image` | Free | 2 | - | Alibaba's image generation model | +| `qwen-image-edit-plus` | Free | - | 2 | Alibaba's image editing model | +| `flux-kontext-dev` | Free | - | 1 | Context-aware editing model | +| `gpt-image-1` | Pro | 2 | 3 | OpenAI's standard image model | +| `gpt-image-1.5` | Pro | 2 | 3 | OpenAI's enhanced image model | +| `nano-banana` | Pro | 2 | 3 | Google's Gemini image model | +| `nano-banana-pro` | Pro | 5 | 5 | Google's premium Gemini model | +| `imagen-4` | Pro | 2 | - | Google's Imagen 4 model | +| `imagen-4-ultra` | Pro | 3 | - | Google's Imagen 4 Ultra model | +| `seedream-4.5` | Pro | 2 | 3 | ByteDance's Seedream model | + +**"-"** indicates the model does not support that operation. + +--- + +## Endpoints + +### Generate SVG + +Generate SVG graphics from text descriptions using AI. + +**Endpoint:** `POST /v1/generate` + +**Request Format:** JSON + +**Credits:** 1-3 credits (depending on quality) + +#### Headers + +``` +Content-Type: application/json +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `prompt` | string | Yes | - | Text description of the SVG to generate | +| `quality` | string | No | `medium` | Quality level: `low`, `medium`, or `high` | +| `aspectRatio` | string | No | `auto` | Aspect ratio: `auto`, `portrait`, `landscape`, or `square` | +| `background` | string | No | `auto` | Background type: `auto`, `transparent`, or `opaque` | +| `storage` | boolean | No | `false` | Whether to save files to cloud storage. When `false`, returns proxy URLs immediately (files not persisted). When `true`, saves files to Firebase Storage. `generationId` is always returned. | +| `stream` | boolean | No | `false` | Enable streaming response (Server-Sent Events) | +| `base64Png` | boolean | No | `false` | Include base64-encoded PNG preview in response | +| `svgText` | boolean | No | `false` | Include raw SVG source code as text in response | +| `styleParams` | object | No | - | Style configuration object (see Style Parameters section below) | +| `model` | string | No | - | Specific AI model ID to use. Cannot be combined with `quality`. Credits are charged based on the model. See Available Models section. | + +#### Style Parameters + +The `styleParams` object can include: + +```json +{ + "style": "flat|line_art|engraving|linocut|silhouette|isometric|cartoon|ghibli", + "color_mode": "full_color|monochrome|few_colors", + "image_complexity": "icon|illustration|scene", + "text": "only_title|embedded_text", + "composition": "centered_object|repeating_pattern|full_scene|objects_in_grid" +} +``` + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/generate \ + -H "Content-Type: application/json" \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -d '{ + "prompt": "A minimalist mountain landscape with geometric shapes", + "quality": "high", + "aspectRatio": "landscape", + "background": "transparent", + "storage": false, + "base64Png": true, + "svgText": true, + "styleParams": { + "style": "flat", + "color_mode": "monochrome", + "composition": "centered_object" + } + }' +``` + +#### Example Request (with model) + +```bash +curl -X POST https://svgmaker.io/api/v1/generate \ + -H "Content-Type: application/json" \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -d '{ + "prompt": "A minimalist mountain landscape with geometric shapes", + "model": "flux-1-dev", + "background": "transparent", + "svgText": true + }' +``` + +#### Response (storage: false) + +```json +{ + "success": true, + "data": { + "creditCost": 3, + "quality": "high", + "message": "Image generated successfully", + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h", + "base64Png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "svgText": "..." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 3, + "creditsRemaining": 102.5 + } +} +``` + +#### Response (storage: true) + +```json +{ + "success": true, + "data": { + "generationId": "eEwY0EcnHiy1c8nZKd3S", + "creditCost": 3, + "quality": "high", + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h", + "message": "Files stored to cloud successfully", + "base64Png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "svgText": "..." + }, + "metadata": { + "requestId": "req_4tOpoNLjmSsU", + "creditsUsed": 3, + "creditsRemaining": 100.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `creditCost` | number | Credits charged for this operation | +| `quality` | string | Quality level used (`low`, `medium`, `high`) | +| `message` | string | Status message (`Image generated successfully` when `storage: false`, `Files stored to cloud successfully` when `storage: true`) | +| `svgUrl` | string | URL to access the generated SVG (proxy URL or persistent URL) | +| `svgUrlExpiresIn` | string | Expiration time for proxy URLs (e.g., "12h"). Only present for proxy URLs. | +| `generationId` | string | Unique identifier for the generation. Always present. | +| `base64Png` | string | Base64-encoded PNG preview. Only present when `base64Png: true`. | +| `svgText` | string | Raw SVG source code. Only present when `svgText: true`. | + +--- + +### Edit Image/SVG + +Edit existing images or SVGs with AI-powered modifications. + +**Endpoint:** `POST /v1/edit` + +**Request Format:** multipart/form-data + +**Credits:** 2-5 credits (depending on quality) + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `image` | file | Yes | - | Image file to edit (PNG, JPEG, WebP, SVG, etc.) | +| `prompt` | string | Yes | - | Edit instructions as a text string | +| `quality` | string | No | `medium` | Quality level: `low`, `medium`, or `high` | +| `aspectRatio` | string | No | `auto` | Aspect ratio: `auto`, `portrait`, `landscape`, or `square` | +| `background` | string | No | `auto` | Background type: `auto`, `transparent`, or `opaque` | +| `storage` | string | No | `false` | Whether to save to cloud storage. Pass `"true"` or `"false"` as a string. | +| `stream` | string | No | `false` | Enable streaming response. Pass `"true"` or `"false"` as a string. | +| `base64Png` | string | No | `false` | Include base64-encoded PNG preview. Pass `"true"` or `"false"` as a string. | +| `svgText` | string | No | `false` | Include raw SVG source code. Pass `"true"` or `"false"` as a string. | +| `styleParams` | string | No | - | JSON string containing style parameters (see Style Parameters section) | +| `model` | string | No | - | Specific AI model ID to use. Cannot be combined with `quality`. Credits are charged based on the model. Pass as a string in form data. See Available Models section. | + +\* Either `prompt` or `styleParams` must be provided. + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/edit \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "image=@input-image.png" \ + -F "prompt=Add a golden frame around this image" \ + -F "quality=medium" \ + -F "aspectRatio=square" \ + -F "background=transparent" \ + -F "storage=false" \ + -F "base64Png=true" \ + -F "svgText=true" \ + -F "styleParams={\"style\":\"flat\",\"color_mode\":\"monochrome\"}" +``` + +#### Example Request (with model) + +```bash +curl -X POST https://svgmaker.io/api/v1/edit \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "image=@input-image.png" \ + -F "prompt=Add a golden frame around this image" \ + -F "model=gpt-image-1-mini" \ + -F "background=transparent" \ + -F "svgText=true" +``` + +#### Response (storage: false) + +```json +{ + "success": true, + "data": { + "creditCost": 3, + "quality": "medium", + "message": "Image edited successfully", + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h", + "base64Png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "svgText": "..." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 3, + "creditsRemaining": 97.5 + } +} +``` + +#### Response (storage: true) + +```json +{ + "success": true, + "data": { + "generationId": "eEwY0EcnHiy1c8nZKd3S", + "creditCost": 3, + "quality": "medium", + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h", + "message": "Files stored to cloud successfully", + "base64Png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "svgText": "..." + }, + "metadata": { + "requestId": "req_4tOpoNLjmSsU", + "creditsUsed": 3, + "creditsRemaining": 94.5 + } +} +``` + +#### Response Fields + +Same as Generate endpoint (see above). + +--- + +### Enhance Prompt + +Enhance a text prompt using AI to make it more detailed, specific, and effective for generating high-quality SVG images. + +**Endpoint:** `POST /v1/enhance-prompt` + +**Request Format:** JSON + +**Credits:** 0.5 credits + +#### Headers + +``` +Content-Type: application/json +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `prompt` | string | Yes | - | The text prompt to enhance | + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/enhance-prompt \ + -H "Content-Type: application/json" \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -d '{ + "prompt": "a cat" + }' +``` + +#### Response + +```json +{ + "success": true, + "data": { + "enhancedPrompt": "A stylized vector illustration of a cat, featuring clean lines and geometric shapes, rendered in a modern flat design style with vibrant colors and minimalist composition." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0.5, + "creditsRemaining": 99.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `enhancedPrompt` | string | The AI-enhanced version of the input prompt, optimized for SVG image generation | + +#### Error Responses + +**Missing Prompt:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "A valid prompt is required." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Invalid Request Body:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "Invalid request body." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### AI Vectorize Image + +Convert raster images (PNG, JPEG, WebP, etc.) to SVG format using AI-powered vectorization. + +**Endpoint:** `POST /v1/convert/ai-vectorize` + +**Request Format:** multipart/form-data + +**Credits:** 1 credit + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file` | file | Yes | - | Image file to convert (PNG, JPEG, WebP, TIFF, etc.). SVG files are not accepted as they are already in vector format. | +| `storage` | string | No | `false` | Whether to save to cloud storage. Pass `"true"` or `"false"` as a string. | +| `stream` | string | No | `false` | Enable streaming response. Pass `"true"` or `"false"` as a string. | +| `svgText` | string | No | `false` | Include raw SVG source code. Pass `"true"` or `"false"` as a string. | + +**Note:** The `base64Png` parameter is not supported for this endpoint since it converts raster images to SVG. + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/convert/ai-vectorize \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image-to-convert.png" \ + -F "storage=false" \ + -F "svgText=true" \ + -F "stream=false" +``` + +#### Response (storage: false) + +```json +{ + "success": true, + "data": { + "creditCost": 1, + "message": "Image converted successfully", + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h", + "svgText": "..." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 1, + "creditsRemaining": 99.5 + } +} +``` + +#### Response (storage: true) + +```json +{ + "success": true, + "data": { + "generationId": "eEwY0EcnHiy1c8nZKd3S", + "creditCost": 1, + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h", + "message": "Files stored to cloud successfully", + "svgText": "..." + }, + "metadata": { + "requestId": "req_4tOpoNLjmSsU", + "creditsUsed": 1, + "creditsRemaining": 98.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `creditCost` | number | Credits charged for this operation (always 1) | +| `message` | string | Status message (`Image converted successfully` when `storage: false`, `Files stored to cloud successfully` when `storage: true`) | +| `svgUrl` | string | URL to access the converted SVG (proxy URL or persistent URL) | +| `svgUrlExpiresIn` | string | Expiration time for proxy URLs (e.g., "12h"). Only present for proxy URLs. | +| `generationId` | string | Unique identifier for the generation. Always present. | +| `svgText` | string | Raw SVG source code. Only present when `svgText: true`. | + +#### Error Responses + +**SVG File Provided:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "SVG files are already in vector format and do not need conversion." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**No File Provided:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "No image file provided." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**File Too Large:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "File size exceeds 100 MB limit." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### Trace to SVG + +Trace raster images to SVG format using tracing algorithms. Currently supports VTracer, with Potrace and Coltrace coming soon. + +**Endpoint:** `POST /v1/convert/trace` + +**Request Format:** multipart/form-data + +**Credits:** 0.5 credits + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file` | file | Yes | - | Raster image file to trace (PNG, JPG, WEBP, TIFF) | +| `algorithm` | string | No | `vtracer` | Tracing algorithm: `vtracer` (currently supported), `potrace` (coming soon), or `coltrace` (coming soon). Can be passed as a query parameter or form data field. | +| `preset` | string | No | `poster` | Tracing preset: `bw`, `poster`, or `photo` | +| `mode` | string | No | `spline` | Tracing mode: `pixel`, `polygon`, or `spline` | +| `hierarchical` | string | No | `stacked` | Hierarchical mode: `stacked` or `cutout` | +| `detail` | number | No | 50 | Detail level (0-100) | +| `smoothness` | number | No | 50 | Smoothness level (0-100) | +| `corners` | number | No | 50 | Corner detection level (0-100) | +| `reduceNoise` | number | No | 4 | Noise reduction level | + +**Note:** The `toFormat` parameter is automatically set to `SVG` for this endpoint. + +#### Example Request + +```bash +# Using vtracer (default) +curl -X POST https://svgmaker.io/api/v1/convert/trace \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.png" \ + -F "preset=poster" \ + -F "mode=spline" \ + -F "detail=60" \ + -F "smoothness=50" \ + -F "corners=50" + +# Explicitly specifying algorithm as query parameter +curl -X POST "https://svgmaker.io/api/v1/convert/trace?algorithm=vtracer" \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.png" \ + -F "preset=poster" + +# Or as form data field +curl -X POST https://svgmaker.io/api/v1/convert/trace \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.png" \ + -F "algorithm=vtracer" \ + -F "preset=poster" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "results": [ + { + "filename": "image.png", + "success": true, + "url": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "urlExpiresIn": "24h", + "format": "svg" + } + ], + "summary": { + "total": 1, + "successful": 1, + "failed": 0 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0.5, + "creditsRemaining": 99.5 + } +} +``` + +**Note:** URLs expire after 12 hours. Files are automatically deleted after 24 hours. + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `results` | array | Array of conversion results | +| `results[].filename` | string | Original filename | +| `results[].success` | boolean | Whether conversion succeeded | +| `results[].url` | string | Proxy URL to download the converted file (expires after 12h) | +| `results[].urlExpiresIn` | string | URL expiration time (e.g., "12h") | +| `results[].format` | string | Output file extension | +| `results[].error` | string | Error message (if failed) | +| `summary` | object | Summary statistics | +| `summary.total` | number | Total number of files processed | +| `summary.successful` | number | Number of successful conversions | +| `summary.failed` | number | Number of failed conversions | + +#### Error Responses + +**Invalid Algorithm:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Invalid algorithm 'invalid-algo'. Supported: vtracer. potrace and coltrace coming soon." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Algorithm Coming Soon (Potrace/Coltrace):** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Algorithm 'potrace' is coming soon. Currently only 'vtracer' is supported." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### SVG to Vector + +Convert SVG files to other vector formats (PDF, EPS, DXF, AI, PS). + +**Endpoint:** `POST /v1/convert/svg-to-vector` + +**Request Format:** multipart/form-data + +**Credits:** 0.5 credits + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file` | file | Yes | - | SVG file to convert | +| `toFormat` | string | Yes | - | Target format: `PDF`, `EPS`, `DXF`, `AI`, or `PS` | +| `textToPath` | string | No | `false` | Convert text to paths. Pass `"true"` or `"false"` as a string. | +| `dxfVersion` | string | No | `R14` | DXF version (only for DXF format): `R12` or `R14` | + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/convert/svg-to-vector \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.svg" \ + -F "toFormat=PDF" \ + -F "textToPath=true" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "results": [ + { + "filename": "image.svg", + "success": true, + "url": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "urlExpiresIn": "24h", + "format": "pdf" + } + ], + "summary": { + "total": 1, + "successful": 1, + "failed": 0 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0.5, + "creditsRemaining": 99.5 + } +} +``` + +**Note:** URLs expire after 12 hours. Files are automatically deleted after 24 hours. + +#### Response Fields + +Same structure as Trace to SVG endpoint (see above). + +--- + +### Raster to Raster + +Convert between raster image formats (PNG, JPG, WebP, TIFF, GIF, AVIF). + +**Endpoint:** `POST /v1/convert/raster-to-raster` + +**Request Format:** multipart/form-data + +**Credits:** 0.25 credits + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file` | file | Yes | - | Raster image file to convert (PNG, JPG, WEBP, TIFF, GIF, AVIF) | +| `toFormat` | string | Yes | - | Target format: `PNG`, `JPG`, `WEBP`, `TIFF`, `GIF`, or `AVIF` | +| `quality` | number | No | - | Output quality (1-100). Only applies to lossy formats like JPG and WebP. | +| `width` | number | No | - | Output width in pixels. Maintains aspect ratio if only width is specified. | +| `height` | number | No | - | Output height in pixels. Maintains aspect ratio if only height is specified. | + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/convert/raster-to-raster \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.png" \ + -F "toFormat=JPG" \ + -F "quality=90" \ + -F "width=1920" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "results": [ + { + "filename": "image.png", + "success": true, + "url": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "urlExpiresIn": "24h", + "format": "jpg" + } + ], + "summary": { + "total": 1, + "successful": 1, + "failed": 0 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0.25, + "creditsRemaining": 99.75 + } +} +``` + +**Note:** URLs expire after 12 hours. Files are automatically deleted after 24 hours. + +#### Response Fields + +Same structure as Trace to SVG endpoint (see above). + +--- + +### Batch Convert + +Convert multiple files (up to 10) in a single request. Supports all conversion types. + +**Endpoint:** `POST /v1/convert/batch` + +**Request Format:** multipart/form-data + +**Credits:** 0.5 credits per vector output, 0.25 credits per raster output + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file` | file | Yes | - | File(s) to convert. Can include multiple files (up to 10). Use `file` as the field name for each file. | +| `toFormat` | string | Yes | - | Target format (e.g., `SVG`, `PDF`, `PNG`, `JPG`, etc.) | +| `textToPath` | string | No | `false` | Convert text to paths (for SVG to vector). Pass `"true"` or `"false"` as a string. | +| `dxfVersion` | string | No | `R14` | DXF version (only for DXF format): `R12` or `R14` | +| `preset` | string | No | `poster` | Tracing preset (for raster to SVG): `bw`, `poster`, or `photo` | +| `mode` | string | No | `spline` | Tracing mode (for raster to SVG): `pixel`, `polygon`, or `spline` | +| `hierarchical` | string | No | `stacked` | Hierarchical mode (for raster to SVG): `stacked` or `cutout` | +| `detail` | number | No | 50 | Detail level (0-100, for raster to SVG) | +| `smoothness` | number | No | 50 | Smoothness level (0-100, for raster to SVG) | +| `corners` | number | No | 50 | Corner detection level (0-100, for raster to SVG) | +| `quality` | number | No | - | Output quality (1-100, for raster conversions) | +| `width` | number | No | - | Output width in pixels (for raster conversions) | +| `height` | number | No | - | Output height in pixels (for raster conversions) | + +#### Example Request + +```bash +curl -X POST https://svgmaker.io/api/v1/convert/batch \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image1.png" \ + -F "file=@image2.jpg" \ + -F "toFormat=SVG" \ + -F "preset=poster" \ + -F "mode=spline" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "results": [ + { + "filename": "image1.png", + "success": true, + "url": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "urlExpiresIn": "24h", + "format": "svg" + }, + { + "filename": "image2.jpg", + "success": true, + "url": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "urlExpiresIn": "24h", + "format": "svg" + } + ], + "summary": { + "total": 2, + "successful": 2, + "failed": 0 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 1.0, + "creditsRemaining": 99.0 + } +} +``` + +**Note:** URLs expire after 12 hours. Files are automatically deleted after 24 hours. + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `results` | array | Array of conversion results | +| `results[].filename` | string | Original filename | +| `results[].success` | boolean | Whether conversion succeeded | +| `results[].url` | string | Proxy URL to download the converted file (expires after 12h) | +| `results[].urlExpiresIn` | string | URL expiration time (e.g., "12h") | +| `results[].format` | string | Output file extension | +| `results[].error` | string | Error message (if failed) | +| `summary` | object | Summary statistics | +| `summary.total` | number | Total number of files processed | +| `summary.successful` | number | Number of successful conversions | +| `summary.failed` | number | Number of failed conversions | + +#### Error Responses + +**Too Many Files:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "This endpoint accepts maximum 10 file. Use /v1/convert/batch for multiple files." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Invalid Format:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Invalid output format \"PNG\". Allowed: PDF, EPS, DXF, AI, PS" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Missing toFormat:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Target format (toFormat) is required" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### Optimize SVG + +Optimize SVG files using SVGO to reduce file size by 10-30%. + +**Endpoint:** `POST /v1/svg/optimize` + +**Request Format:** multipart/form-data + +**Credits:** 0.5 credits + +#### Headers + +``` +Content-Type: multipart/form-data +x-api-key: svgmaker-io{your-api-key} +``` + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file` | file | Yes | - | SVG file to optimize | +| `compress` | string | No | `false` | Compress to SVGZ format after optimization. Pass `"true"` or `"false"` as a string. When true, returns svgzUrl instead of svgUrl. | + +#### Example Request + +```bash +# Optimize only +curl -X POST https://svgmaker.io/api/v1/svg/optimize \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.svg" + +# Optimize and compress to SVGZ +curl -X POST https://svgmaker.io/api/v1/svg/optimize \ + -H "x-api-key: svgmaker-io{your-api-key}" \ + -F "file=@image.svg" \ + -F "compress=true" +``` + +#### Response (compress: false) + +```json +{ + "success": true, + "data": { + "svgUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "svgUrlExpiresIn": "12h" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0.5, + "creditsRemaining": 99.5 + } +} +``` + +**Note:** URLs expire after 24 hours. Files are automatically deleted after 24 hours. + +#### Response (compress: true) + +```json +{ + "success": true, + "data": { + "svgzUrl": "https://svgmaker.io/api/files/eyJ1cmwiOi...", + "svgzUrlExpiresIn": "24h", + "filename": "optimized-image.svgz", + "compressedSize": 12345 + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0.5, + "creditsRemaining": 99.5 + } +} +``` + +#### Response Fields (compress: false) + +| Field | Type | Description | +|-------|------|-------------| +| `svgUrl` | string | Proxy URL to download the optimized SVG (expires after 24h) | +| `svgUrlExpiresIn` | string | URL expiration time ("24h") | + +#### Response Fields (compress: true) + +| Field | Type | Description | +|-------|------|-------------| +| `svgzUrl` | string | Proxy URL to download the SVGZ file (expires after 24h) | +| `svgzUrlExpiresIn` | string | URL expiration time ("24h") | +| `filename` | string | Filename of the compressed file | +| `compressedSize` | number | Size of the compressed file in bytes | + +**Note:** URLs expire after 24 hours. Files are automatically deleted after 24 hours. + +#### Error Responses + +**No File Provided:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "SVG file is required" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Invalid File Type:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Only SVG files are accepted" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +## Generations Management + +### List Generations + +List your own generations with pagination and filters. + +**Endpoint:** `GET /v1/generations` + +**Request Format:** Query parameters + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `page` | number | No | 1 | Page number (1-indexed) | +| `limit` | number | No | 20 | Number of items per page (1-100) | +| `type` | string | No | - | Filter by type. Can specify multiple: `type=generate&type=edit` | +| `hashtags` | string | No | - | Filter by hashtag. Can specify multiple: `hashtags=vector&hashtags=logo` | +| `categories` | string | No | - | Filter by category. Can specify multiple: `categories=flat&categories=icon` | +| `query` | string | No | - | Search query for prompt/description | + +#### Example Requests + +```bash +# Basic pagination +curl -X GET "https://svgmaker.io/api/v1/generations?page=1&limit=20" \ + -H "x-api-key: svgmaker-io{your-api-key}" | jq + +# Filter by type +curl -X GET "https://svgmaker.io/api/v1/generations?type=generate&limit=10" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Multiple types (OR condition) +curl -X GET "https://svgmaker.io/api/v1/generations?type=generate&type=edit" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Search by query (searches prompt/description) +curl -X GET "https://svgmaker.io/api/v1/generations?query=mountain%20landscape" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Filter by hashtags +curl -X GET "https://svgmaker.io/api/v1/generations?hashtags=vector&hashtags=logo" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Filter by categories (AND condition - all must match) +curl -X GET "https://svgmaker.io/api/v1/generations?categories=flat&categories=icon" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Combined search: query + type + hashtags +curl -X GET "https://svgmaker.io/api/v1/generations?query=minimalist&type=generate&hashtags=vector&page=1&limit=20" \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "items": [ + "eEwY0EcnHiy1c8nZKd3S", + "abc123xyz456", + "def789ghi012" + ], + "pagination": { + "page": 1, + "limit": 20, + "totalItems": 156, + "totalPages": 8, + "hasNextPage": true, + "hasPrevPage": false + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 100.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `items` | string[] | Array of generation IDs. Fetch individual details using GET /v1/generations/{id} | +| `pagination.page` | number | Current page number (1-indexed) | +| `pagination.limit` | number | Items per page | +| `pagination.totalItems` | number | Total number of generations | +| `pagination.totalPages` | number | Total number of pages | +| `pagination.hasNextPage` | boolean | Whether there is a next page | +| `pagination.hasPrevPage` | boolean | Whether there is a previous page | + +--- + +### Get Generation + +Get details of a single generation by ID. + +**Endpoint:** `GET /v1/generations/{id}` + +**Request Format:** Path parameter + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string | Yes | Generation ID | + +#### Example Request + +Replace `{generation_id}` with your actual generation ID. + +```bash +curl -X GET https://svgmaker.io/api/v1/generations/{generation_id} \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "id": "eEwY0EcnHiy1c8nZKd3S", + "prompt": "A minimalist mountain landscape", + "type": "generate", + "quality": "high", + "isPublic": false, + "metadata": { + "hashTags": ["vector", "landscape"], + "category": ["illustration"] + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 100.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Generation ID | +| `prompt` | string | The prompt used to generate the image | +| `type` | string | Generation type: `generate`, `edit`, `convert`, or `crafted` | +| `quality` | string | Quality level: `low`, `medium`, `high`, or `manual` | +| `isPublic` | boolean | Whether the generation is public | +| `metadata.hashTags` | string[] | Array of hashtags | +| `metadata.category` | string[] | Array of categories | + +#### Error Responses + +**Not Found:** +```json +{ + "success": false, + "error": { + "code": "NOT_FOUND", + "status": 404, + "message": "Generation not found." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Not Authorized:** +```json +{ + "success": false, + "error": { + "code": "FORBIDDEN", + "status": 403, + "message": "Not authorized to access this generation." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### Delete Generation + +Delete a generation and its associated files. **Paid users only.** + +**Endpoint:** `DELETE /v1/generations/{id}` + +**Request Format:** Path parameter + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string | Yes | Generation ID | + +#### Example Request + +Replace `{generation_id}` with your actual generation ID. + +```bash +curl -X DELETE https://svgmaker.io/api/v1/generations/{generation_id} \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "message": "Generation and associated files deleted successfully" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 100.5 + } +} +``` + +#### Error Responses + +**Not Paid User:** +```json +{ + "success": false, + "error": { + "code": "FORBIDDEN", + "status": 403, + "message": "Only paid members can delete generations." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### Share Generation + +Make a generation public (share it to the gallery). + +**Endpoint:** `POST /v1/generations/{id}/share` + +**Request Format:** Path parameter + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string | Yes | Generation ID | + +#### Example Request + +Replace `{generation_id}` with your actual generation ID. + +```bash +curl -X POST https://svgmaker.io/api/v1/generations/{generation_id}/share \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "message": "Generation is now public", + "isPublic": true, + "shareUrl": "https://svgmaker.io/share/{generation_id}" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 100.5 + } +} +``` + +#### Response (Already Public) + +```json +{ + "success": true, + "data": { + "message": "Already public", + "isPublic": true, + "shareUrl": "https://svgmaker.io/share/{generation_id}" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 100.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `message` | string | Status message indicating the result | +| `isPublic` | boolean | Whether the generation is now public | +| `shareUrl` | string | The public share URL for the generation (e.g., `https://svgmaker.io/share/{generation_id}`) | + +#### Error Responses + +**Not Authorized:** +```json +{ + "success": false, + "error": { + "code": "FORBIDDEN", + "status": 403, + "message": "You do not have permission to share this generation." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### Download Generation + +Download your own generation in various formats. **Paid users only.** + +**Endpoint:** `GET /v1/generations/{id}/download` + +**Request Format:** Path parameter + query parameters + +**Credits:** 0 credits (free for own generations) + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string | Yes | Generation ID | + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `format` | string | No | `webp` | Output format: `svg`, `webp`, `png`, `svg-optimized`, or `svgz` | +| `optimize` | string | No | `false` | Optimize before compressing (only for `svgz` format). Pass `"true"` or `"false"` as a string. | + +#### Example Request + +Replace `{generation_id}` with your actual generation ID. + +```bash +# Download as SVG +curl -X GET "https://svgmaker.io/api/v1/generations/{generation_id}/download?format=svg" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Download as SVGZ with optimization +curl -X GET "https://svgmaker.io/api/v1/generations/{generation_id}/download?format=svgz&optimize=true" \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +```json +{ + "success": true, + "data": { + "id": "eEwY0EcnHiy1c8nZKd3S", + "url": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "urlExpiresIn": "24h", + "format": "svg", + "filename": "svgmaker-eEwY0EcnHiy1c8nZKd3S.svg" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 100.5 + } +} +``` + +**Note:** URLs expire after 12 hours. Files are automatically deleted after 24 hours. + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Generation ID | +| `url` | string | Proxy URL to download the file (expires after 12h) | +| `urlExpiresIn` | string | URL expiration time (e.g., "12h") | +| `format` | string | File format requested | +| `filename` | string | Suggested filename for the downloaded file | + +#### Error Responses + +**Not Paid User:** +```json +{ + "success": false, + "error": { + "code": "FORBIDDEN", + "status": 403, + "message": "Download feature is only available for paid users." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Invalid Format:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "Invalid format. Must be one of: svg, webp, png, svg-optimized, svgz" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +## Gallery + +### Browse Gallery + +Browse the public gallery with pagination and filters. + +**Endpoint:** `GET /v1/gallery` + +**Request Format:** Query parameters + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `page` | number | No | 1 | Page number (1-indexed) | +| `limit` | number | No | 20 | Number of items per page (1-100) | +| `type` | string | No | - | Filter by type. Can specify multiple: `type=generate&type=edit` | +| `hashtags` | string | No | - | Filter by hashtag. Can specify multiple: `hashtags=vector&hashtags=logo` | +| `categories` | string | No | - | Filter by category. Can specify multiple: `categories=flat&categories=icon` | +| `query` | string | No | - | Search query for prompt/description | +| `pro` | string | No | - | Filter for pro images. Pass `"true"` as a string. | +| `gold` | string | No | - | Filter for gold images. Pass `"true"` as a string. | + +#### Example Requests + +```bash +# Basic pagination +curl -X GET "https://svgmaker.io/api/v1/gallery?page=1&limit=20" \ + -H "x-api-key: svgmaker-io{your-api-key}" | jq + +# Filter by type +curl -X GET "https://svgmaker.io/api/v1/gallery?type=generate&limit=10" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Multiple types (OR condition) +curl -X GET "https://svgmaker.io/api/v1/gallery?type=generate&type=edit" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Search by query (searches prompt/description) +curl -X GET "https://svgmaker.io/api/v1/gallery?query=mountain%20landscape" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Filter by hashtags +curl -X GET "https://svgmaker.io/api/v1/gallery?hashtags=vector&hashtags=logo" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Filter by categories (AND condition - all must match) +curl -X GET "https://svgmaker.io/api/v1/gallery?categories=flat&categories=icon" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Filter for pro images only +curl -X GET "https://svgmaker.io/api/v1/gallery?pro=true&limit=20" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Filter for gold images only +curl -X GET "https://svgmaker.io/api/v1/gallery?gold=true&limit=20" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Combined search: query + type + pro filter +curl -X GET "https://svgmaker.io/api/v1/gallery?query=minimalist&type=generate&pro=true&page=1&limit=20" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Combined search: gold images with specific hashtags +curl -X GET "https://svgmaker.io/api/v1/gallery?gold=true&hashtags=vector&hashtags=premium&page=1" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Complex search: pro images, specific type, query, and categories +curl -X GET "https://svgmaker.io/api/v1/gallery?pro=true&type=generate&query=landscape&categories=illustration&page=1&limit=30" \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +Same structure as List Generations endpoint (see above). + +--- + +### Get Gallery Item + +Get details of a single public gallery item by ID. + +**Endpoint:** `GET /v1/gallery/{id}` + +**Request Format:** Path parameter + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string | Yes | Gallery item ID | + +#### Example Request + +Replace `{generation_id}` with your actual gallery item ID. + +```bash +curl -X GET https://svgmaker.io/api/v1/gallery/{generation_id} \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +Same structure as Get Generation endpoint (see above). + +--- + +### Download Gallery Item + +Download a public gallery item in various formats. + +**Endpoint:** `GET /v1/gallery/{id}/download` + +**Request Format:** Path parameter + query parameters + +**Credits:** 1 credit for SVG formats, 0 credits for WebP/PNG + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string | Yes | Gallery item ID | + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `format` | string | No | `webp` | Output format: `svg`, `webp`, `png`, `svg-optimized`, or `svgz` | +| `optimize` | string | No | `false` | Optimize before compressing (only for `svgz` format). Pass `"true"` or `"false"` as a string. | + +#### Example Request + +Replace `{generation_id}` with your actual gallery item ID. + +```bash +# Download as SVG +curl -X GET "https://svgmaker.io/api/v1/gallery/{generation_id}/download?format=svg" \ + -H "x-api-key: svgmaker-io{your-api-key}" + +# Download as SVGZ with optimization +curl -X GET "https://svgmaker.io/api/v1/gallery/{generation_id}/download?format=svgz&optimize=true" \ + -H "x-api-key: svgmaker-io{your-api-key}" +``` + +#### Response + +Same structure as Download Generation endpoint (see above). + +#### Credit Costs + +| Format | Credits | +|--------|---------| +| `svg`, `svg-optimized`, `svgz` | 1 credit | +| `webp`, `png` | 0 credits (free) | + +#### Error Responses + +**Pro Image - Not Paid:** +```json +{ + "success": false, + "error": { + "code": "FORBIDDEN", + "status": 403, + "message": "Pro images require a paid account. Please upgrade to download." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Insufficient Credits (for SVG formats):** +```json +{ + "success": false, + "error": { + "code": "INSUFFICIENT_CREDITS", + "status": 402, + "message": "Insufficient credits. This download requires 1 credit.", + "details": { + "creditsRequired": 1 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +## Account Management + +### Get Account Information + +Get your account information including email, display name, account type, and credit balance. + +**Endpoint:** `GET /v1/account` + +**Request Format:** Headers only + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Example Request + +```bash +curl -X GET https://svgmaker.io/api/v1/account \ + -H "x-api-key: svgmaker-io{your-api-key}" | jq +``` + +#### Response + +```json +{ + "success": true, + "data": { + "email": "user@example.com", + "displayName": "John Doe", + "accountType": "premium", + "credits": 150.5 + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 150.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `email` | string | User's email address | +| `displayName` | string | User's display name | +| `accountType` | string | Account type: `"free"` or `"premium"` | +| `credits` | number | Current credit balance | + +#### Error Responses + +**User Not Found:** +```json +{ + "success": false, + "error": { + "code": "NOT_FOUND", + "status": 404, + "message": "User not found." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +### Get Usage Statistics + +Get API usage statistics for your account. Returns all-time totals by default, or daily breakdowns for a specified period. + +**Endpoint:** `GET /v1/account/usage` + +**Request Format:** Query parameters (optional) + +**Credits:** 0 credits + +#### Headers + +``` +x-api-key: svgmaker-io{your-api-key} +``` + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `days` | number | No | Number of days to look back (must be positive integer). Cannot be used with `start`/`end`. | +| `start` | string | No | Start date in YYYY-MM-DD format. Must be used with `end`. Cannot be used with `days`. | +| `end` | string | No | End date in YYYY-MM-DD format. Must be used with `start`. Cannot be used with `days`. | + +**Note:** If no parameters are provided, returns all-time totals. If `days` or `start`/`end` are provided, returns daily breakdown for that period. + +#### Example Requests + +```bash +# Get all-time usage statistics +curl -X GET https://svgmaker.io/api/v1/account/usage \ + -H "x-api-key: svgmaker-io{your-api-key}" | jq + +# Get usage for last 7 days +curl -X GET "https://svgmaker.io/api/v1/account/usage?days=7" \ + -H "x-api-key: svgmaker-io{your-api-key}" | jq + +# Get usage for a date range +curl -X GET "https://svgmaker.io/api/v1/account/usage?start=2024-01-01&end=2024-01-31" \ + -H "x-api-key: svgmaker-io{your-api-key}" | jq +``` + +#### Response (All-Time) + +```json +{ + "success": true, + "data": { + "period": { + "type": "all" + }, + "summary": { + "requests": 1250, + "creditsUsed": 3420.5, + "successCount": 1200, + "errorCount": 50, + "successRate": 0.96 + }, + "byCategory": { + "generate": { + "requests": 500, + "credits": 1200 + }, + "edit": { + "requests": 300, + "credits": 750 + }, + "convert": { + "total": { + "requests": 400, + "credits": 1200 + }, + "endpoints": { + "convert/ai-vectorize": { + "requests": 200, + "credits": 600 + }, + "convert/trace": { + "requests": 150, + "credits": 450 + }, + "convert/svg-to-vector": { + "requests": 50, + "credits": 150 + } + } + }, + "gallery": { + "total": { + "requests": 50, + "credits": 270.5 + }, + "endpoints": { + "gallery/download": { + "requests": 50, + "credits": 270.5 + } + } + } + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 150.5 + } +} +``` + +#### Response (With Date Range) + +```json +{ + "success": true, + "data": { + "period": { + "type": "days", + "days": 7, + "from": "2024-01-24", + "to": "2024-01-31" + }, + "summary": { + "requests": 150, + "creditsUsed": 420.5, + "successCount": 145, + "errorCount": 5, + "successRate": 0.967 + }, + "byCategory": { + "generate": { + "requests": 60, + "credits": 150 + }, + "edit": { + "requests": 40, + "credits": 100 + }, + "convert": { + "total": { + "requests": 45, + "credits": 135 + }, + "endpoints": { + "convert/ai-vectorize": { + "requests": 25, + "credits": 75 + }, + "convert/trace": { + "requests": 20, + "credits": 60 + } + } + }, + "gallery": { + "total": { + "requests": 5, + "credits": 35.5 + }, + "endpoints": { + "gallery/download": { + "requests": 5, + "credits": 35.5 + } + } + } + }, + "daily": [ + { + "date": "2024-01-24", + "requests": 20, + "credits": 55.5 + }, + { + "date": "2024-01-25", + "requests": 25, + "credits": 70 + }, + { + "date": "2024-01-26", + "requests": 15, + "credits": 40 + }, + { + "date": "2024-01-27", + "requests": 30, + "credits": 85 + }, + { + "date": "2024-01-28", + "requests": 22, + "credits": 60 + }, + { + "date": "2024-01-29", + "requests": 18, + "credits": 50 + }, + { + "date": "2024-01-30", + "requests": 20, + "credits": 55 + } + ], + "allTime": { + "requests": 1250, + "creditsUsed": 3420.5 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 0, + "creditsRemaining": 150.5 + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `period.type` | string | Period type: `"all"`, `"days"`, or `"range"` | +| `period.days` | number | Number of days (only present when `type` is `"days"`) | +| `period.from` | string | Start date in YYYY-MM-DD format (only present when `type` is `"days"` or `"range"`) | +| `period.to` | string | End date in YYYY-MM-DD format (only present when `type` is `"days"` or `"range"`) | +| `summary.requests` | number | Total number of API requests | +| `summary.creditsUsed` | number | Total credits used | +| `summary.successCount` | number | Number of successful requests | +| `summary.errorCount` | number | Number of failed requests | +| `summary.successRate` | number | Success rate (0-1) | +| `byCategory` | object | Usage statistics grouped by category. Single-endpoint categories (e.g., `generate`, `edit`) have `requests` and `credits`. Multi-endpoint categories (e.g., `convert`, `gallery`) have `total` and `endpoints` objects. | +| `daily` | array | Daily breakdown (only present when date range is specified). Each item contains `date`, `requests`, and `credits`. | +| `allTime` | object | All-time totals (only present when date range is specified). Contains `requests` and `creditsUsed`. | + +#### Error Responses + +**Invalid Parameters:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "Use either days or start/end, not both." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Invalid Date Format:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "Invalid date. Dates must be valid and in YYYY-MM-DD format." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Start Date After End Date:** +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "Start date must be before or equal to end date." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +## Error Handling + +### Common Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `INVALID_API_KEY` | 401 | Invalid or missing API key | +| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for the operation | +| `INVALID_REQUEST` | 400 | Invalid request parameters | +| `VALIDATION_ERROR` | 400 | Request validation failed | +| `METHOD_NOT_ALLOWED` | 405 | HTTP method not allowed for this endpoint | +| `ENDPOINT_NOT_FOUND` | 404 | Endpoint does not exist | +| `CONTENT_POLICY` | 422 | Content policy violation | +| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests, rate limit exceeded | +| `SERVER_ERROR` | 500 | Internal server error | +| `ENDPOINT_DISABLED` | 503 | Endpoint is currently disabled | + +### Example Error Response + +```json +{ + "success": false, + "error": { + "code": "INSUFFICIENT_CREDITS", + "status": 402, + "message": "Insufficient credits. This operation requires 3 credits.", + "details": { + "creditsRequired": 3 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +### Error Examples + +#### Missing API Key + +```json +{ + "success": false, + "error": { + "code": "INVALID_API_KEY", + "status": 401, + "message": "API key required. Pass x-api-key header." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +#### Invalid Request + +```json +{ + "success": false, + "error": { + "code": "INVALID_REQUEST", + "status": 400, + "message": "Invalid request" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +#### Insufficient Credits + +```json +{ + "success": false, + "error": { + "code": "INSUFFICIENT_CREDITS", + "status": 402, + "message": "Insufficient credits. This operation requires 3 credits.", + "details": { + "creditsRequired": 3 + } + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +#### Validation Error + +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Quality must be one of: low, medium, high, manual." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +#### Model Validation Errors + +**Model and Quality Both Specified:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Cannot specify both 'model' and 'quality'. Use 'model' for specific model selection or 'quality' for auto mode." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Unknown Model:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 400, + "message": "Unknown model: \"nonexistent-model\"." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +**Pro Model Without Paid Account:** +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "status": 403, + "message": "Pro models require upgrade: imagen-4-ultra" + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_" + } +} +``` + +--- + +## Rate Limiting + +All API endpoints are rate limited to prevent abuse and ensure fair usage. Rate limits are applied per API key. + +### Rate Limit Headers + +All responses include rate limit information in the headers: + +| Header | Description | +|--------|-------------| +| `x-ratelimit-limit-requests` | Maximum number of requests allowed in the current window | +| `x-ratelimit-remaining-requests` | Number of requests remaining in the current window | +| `x-ratelimit-reset-requests` | Unix timestamp (in seconds) when the rate limit window resets | + +### Rate Limits by Endpoint + +#### Computation Endpoints (Credits > 0) + +| Endpoint | Limit | Window | +|----------|-------|--------| +| `/v1/generate` | 3 requests | 60 seconds | +| `/v1/edit` | 3 requests | 60 seconds | +| `/v1/convert/ai-vectorize` | 5 requests | 60 seconds | +| `/v1/convert/trace` | 5 requests | 60 seconds | +| `/v1/convert/svg-to-vector` | 5 requests | 60 seconds | +| `/v1/convert/raster-to-raster` | 5 requests | 60 seconds | +| `/v1/convert/batch` | 5 requests | 60 seconds | +| `/v1/enhance-prompt` | 10 requests | 60 seconds | +| `/v1/svg/optimize` | 10 requests | 60 seconds | +| `/v1/gallery/{id}/download` | 5 requests | 60 seconds | + +#### Free Endpoints (Credits = 0) + +| Endpoint | Limit | Window | +|----------|-------|--------| +| `/v1/generations` | 1000 requests | 60 seconds | +| `/v1/generations/{id}` | 1000 requests | 60 seconds | +| `/v1/generations/{id}/delete` | 100 requests | 60 seconds | +| `/v1/generations/{id}/share` | 100 requests | 60 seconds | +| `/v1/generations/{id}/download` | 100 requests | 60 seconds | +| `/v1/gallery` | 1000 requests | 60 seconds | +| `/v1/gallery/{id}` | 1000 requests | 60 seconds | +| `/v1/account` | 1000 requests | 60 seconds | +| `/v1/account/usage` | 1000 requests | 60 seconds | + +### Rate Limit Exceeded Response + +When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: + +```json +{ + "success": false, + "error": { + "code": "RATE_LIMIT_EXCEEDED", + "status": 429, + "message": "Rate limit exceeded for image generation." + }, + "metadata": { + "requestId": "req_1706123456789" + } +} +``` + +The response will also include rate limit headers: + +``` +x-ratelimit-limit-requests: 3 +x-ratelimit-remaining-requests: 0 +x-ratelimit-reset-requests: 1706123520 +``` + +### Best Practices + +1. **Monitor headers**: Check the `x-ratelimit-remaining-requests` header to know how many requests you have left +2. **Implement backoff**: When you receive a 429 response, wait until the `x-ratelimit-reset-requests` timestamp before retrying +3. **Batch operations**: Use `/v1/convert/batch` to process multiple files in a single request instead of making many individual requests +4. **Cache responses**: Cache generation details and gallery items to reduce redundant API calls + +--- + +## Changelog + +### v1.0.0 +- Initial release of v1 API +- Generate and Edit endpoints +- Storage control parameter +- Streaming support +- Base64 PNG and SVG text output options +- Rate limiting for all v1 API endpoints diff --git a/src/clients/BaseClient.ts b/src/clients/BaseClient.ts index 2689f34..4730756 100644 --- a/src/clients/BaseClient.ts +++ b/src/clients/BaseClient.ts @@ -1,7 +1,18 @@ import { SVGMakerClient } from '../core/SVGMakerClient'; import { HttpClient, RequestOptions } from '../utils/httpClient'; import { SVGMakerConfig } from '../types/config'; -import { ValidationError } from '../errors/CustomErrors'; +import { + ValidationError, + APIError, + AuthError, + RateLimitError, + InsufficientCreditsError, + ContentSafetyError, + FileSizeError, + FileFormatError, + EndpointDisabledError, +} from '../errors/CustomErrors'; +import { ResponseMetadata } from '../types/api'; import { Logger } from '../utils/logger'; import { z } from 'zod'; @@ -118,4 +129,104 @@ export abstract class BaseClient { target.config = this.config; target.logger = this.logger; } + + /** + * Map a v1 API error object to the appropriate SDK error class + */ + protected mapApiError( + errorObj: { + code?: string; + status?: number; + message?: string; + details?: Record; + }, + requestId?: string + ): Error { + const message = errorObj.message || 'Unknown API error'; + const code = errorObj.code || ''; + const status = errorObj.status || 500; + + switch (code) { + case 'INVALID_API_KEY': + return new AuthError(message); + case 'INSUFFICIENT_CREDITS': + return new InsufficientCreditsError( + message, + (errorObj.details?.creditsRequired as number) ?? undefined + ); + case 'RATE_LIMIT_EXCEEDED': + return new RateLimitError(message); + case 'CONTENT_POLICY': + return new ContentSafetyError(message); + case 'ENDPOINT_DISABLED': + return new EndpointDisabledError(message); + } + + // Fallback on HTTP status + switch (status) { + case 401: + return new AuthError(message); + case 402: + return new InsufficientCreditsError( + message, + (errorObj.details?.creditsRequired as number) ?? undefined + ); + case 413: + return new FileSizeError(message); + case 429: + return new RateLimitError(message); + } + + if (message.includes('file format') || message.includes('already in vector format')) { + return new FileFormatError(message); + } + + return new APIError(message, status, code, errorObj.details, requestId); + } + + /** + * Unwrap a v1 API response envelope. Throws on error envelope. + * @returns Object with data and metadata + */ + protected unwrapEnvelope(raw: any): { data: T; metadata: ResponseMetadata } { + if (raw.success === true && raw.data !== undefined) { + return { data: raw.data as T, metadata: raw.metadata }; + } + if (raw.success === false) { + throw this.mapApiError(raw.error || {}, raw.metadata?.requestId); + } + // Legacy fallback for non-envelope responses + return { + data: raw as T, + metadata: { requestId: '', creditsUsed: 0, creditsRemaining: 0 }, + }; + } + + /** + * Handle an error response from raw fetch calls by parsing the v1 error envelope + */ + protected async handleFetchErrorResponse(response: globalThis.Response): Promise { + let errorData: any = {}; + try { + const contentType = response.headers.get('Content-Type'); + if (contentType?.includes('application/json')) { + errorData = await response.json(); + } else { + errorData = { error: { message: await response.text() } }; + } + } catch { + errorData = { error: { message: `HTTP Error ${response.status}` } }; + } + + if (errorData.success === false && errorData.error) { + throw this.mapApiError(errorData.error, errorData.metadata?.requestId); + } + + // Fallback + const message = errorData.error?.message || errorData.error || `HTTP Error ${response.status}`; + throw new APIError( + typeof message === 'string' ? message : `HTTP Error ${response.status}`, + response.status + ); + } } diff --git a/src/clients/ConvertClient.ts b/src/clients/ConvertClient.ts index 17e3125..31e5759 100644 --- a/src/clients/ConvertClient.ts +++ b/src/clients/ConvertClient.ts @@ -1,293 +1,4 @@ -import { BaseClient } from './BaseClient'; -import { ConvertParams, ConvertResponse, ConvertStreamEvent } from '../types/api'; -import { SVGMakerClient } from '../core/SVGMakerClient'; -import { z } from 'zod'; -import { Readable } from 'stream'; -import * as fs from 'fs'; -import * as path from 'path'; -import { ValidationError } from '../errors/CustomErrors'; -import { decodeSvgContent } from '../utils/base64'; - /** - * Schema for validating convert parameters + * @deprecated Use AIVectorizeClient from './convert' instead */ -const convertParamsSchema = z.object({ - file: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]), - stream: z.boolean().optional(), - svgText: z.boolean().optional(), -}); - -/** - * Client for the Convert Image to SVG API - */ -export class ConvertClient extends BaseClient { - private params: Partial = {}; - - /** - * Create a new Convert client - * @param client Parent SVGMaker client - */ - constructor(client: SVGMakerClient) { - super(client); - } - - /** - * Execute the Convert request - * @returns Convert response - */ - public async execute(): Promise { - this.logger.debug('Starting image to SVG conversion', { - hasFile: !!this.params.file, - svgTextRequested: !!this.params.svgText, - }); - - // Validate parameters - this.validateRequest(this.params, convertParamsSchema); - - // Prepare form data - const formData = new FormData(); - - // Add file - await this.addFileToForm(formData, 'file', this.params.file!); - - // Add stream option if present - if (this.params.stream) { - formData.append('stream', String(this.params.stream)); - } - - if (this.params.svgText) { - formData.append('svgText', String(this.params.svgText)); - } - - // Execute request using native fetch - const response = await fetch(`${this.config.baseUrl}/convert`, { - method: 'POST', - headers: { - 'x-api-key': this.config.apiKey, - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - this.logger.error('Convert request failed', { status: response.status, error: errorText }); - throw new Error(`HTTP error ${response.status}: ${errorText}`); - } - - const result = await response.json(); - this.logger.debug('Image to SVG conversion completed', { - creditCost: result.creditCost, - hasSvgText: !!result.svgText, - }); - - // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) - if (result.svgText && typeof result.svgText === 'string') { - result.svgText = decodeSvgContent(result.svgText); - } - - return result as ConvertResponse; - } - - /** - * Configure the convert parameters - * @param config Configuration object with convert parameters - * @returns New client instance - */ - public configure(config: Partial): ConvertClient { - this.logger.debug('Configuring convert parameters', { config }); - - const client = this.clone(); - client.params = { ...client.params, ...config }; - return client; - } - - /** - * Stream the convert response - * @returns Readable stream of events - */ - public stream(): Readable { - // Create a clone with streaming enabled - const client = this.clone(); - client.params.stream = true; - - // Validate parameters - this.validateRequest(client.params, convertParamsSchema); - - // Create a readable stream for the events - const stream = new Readable({ - objectMode: true, - read() {}, - }); - - // Execute the request and handle streaming - (async () => { - try { - // Prepare form data - const formData = new FormData(); - - // Add file - await this.addFileToForm(formData, 'file', client.params.file!); - - // Add stream option - formData.append('stream', 'true'); - - if (client.params.svgText) { - formData.append('svgText', String(client.params.svgText)); - } - - // Make request to the streaming endpoint using native fetch - const response = await fetch(`${this.config.baseUrl}/convert`, { - method: 'POST', - headers: { - Accept: 'text/event-stream', - 'x-api-key': this.config.apiKey, - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error ${response.status}: ${errorText}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('Response body is not readable'); - } - - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - // Decode the chunk and add to buffer - buffer += decoder.decode(value, { stream: true }); - - // Split by newlines to get individual JSON objects - const lines = buffer.split('\n'); - - // Keep the last incomplete line in the buffer - buffer = lines.pop() || ''; - - // Process each complete line - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine === '') continue; - - try { - // Parse the JSON chunk directly (no "data:" prefix like SSE) - const event = JSON.parse(trimmedLine) as ConvertStreamEvent; - // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) - if (event.svgText && typeof event.svgText === 'string') { - event.svgText = decodeSvgContent(event.svgText); - } - stream.push(event); - - // End the stream when we get a complete or error status - if (event.status === 'complete' || event.status === 'error') { - stream.push(null); - return; - } - } catch (e) { - console.error('Error parsing streaming chunk:', e); - console.error('Problematic line:', trimmedLine); - } - } - } - - // Process any remaining data in buffer - if (buffer.trim()) { - try { - const event = JSON.parse(buffer.trim()) as ConvertStreamEvent; - stream.push(event); - } catch (e) { - console.error('Error parsing final chunk:', e); - } - } - - // End of stream - stream.push(null); - } catch (error) { - console.error('Streaming error:', error); - stream.emit('error', error); - stream.push(null); - } - })(); - - return stream; - } - - /** - * Add a file to the form data - * @param formData Form data - * @param fieldName Form field name - * @param file File path, buffer, or stream - */ - private async addFileToForm( - formData: FormData, - fieldName: string, - file: string | Buffer | Readable - ): Promise { - if (typeof file === 'string') { - // Check if the file exists - if (!fs.existsSync(file)) { - throw new ValidationError(`File not found: ${file}`); - } - - // Read file and create a Blob - const fileBuffer = fs.readFileSync(file); - const filename = path.basename(file); - const mimeType = this.getMimeType(filename); - - const blob = new Blob([fileBuffer], { type: mimeType }); - formData.append(fieldName, blob, filename); - } else if (Buffer.isBuffer(file)) { - // Convert buffer to Blob - const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' }); - formData.append(fieldName, blob, 'file'); - } else if (file instanceof Readable) { - // Convert stream to buffer then to Blob - const chunks: Buffer[] = []; - for await (const chunk of file) { - chunks.push(Buffer.from(chunk)); - } - const buffer = Buffer.concat(chunks); - const blob = new Blob([buffer], { type: 'application/octet-stream' }); - formData.append(fieldName, blob, 'file'); - } else { - throw new ValidationError(`Invalid file type: ${typeof file}`); - } - } - - /** - * Get MIME type from filename - * @param filename File name - * @returns MIME type - */ - private getMimeType(filename: string): string { - const ext = path.extname(filename).toLowerCase(); - const mimeTypes: Record = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.webp': 'image/webp', - '.bmp': 'image/bmp', - }; - return mimeTypes[ext] || 'application/octet-stream'; - } - - /** - * Create a clone of this client - * @returns New client instance - */ - protected clone(): ConvertClient { - const client = new ConvertClient(this.client); - this.copyTo(client); - client.params = { ...this.params }; - return client; - } -} +export { AIVectorizeClient as ConvertClient } from './convert/AIVectorizeClient'; diff --git a/src/clients/EditClient.ts b/src/clients/EditClient.ts index 891a048..ab8597d 100644 --- a/src/clients/EditClient.ts +++ b/src/clients/EditClient.ts @@ -11,35 +11,47 @@ import { decodeSvgContent, decodeBase64Png } from '../utils/base64'; /** * Schema for validating edit parameters */ -const editParamsSchema = z.object({ - image: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]), - prompt: z.string(), - styleParams: z - .object({ - style: z - .enum(['minimalist', 'cartoon', 'realistic', 'abstract', 'flat', 'isometric']) - .optional(), - color_mode: z.enum(['monochrome', '2-colors', '3-colors', 'full-color']).optional(), - image_complexity: z.enum(['simple', 'detailed']).optional(), - category: z.enum(['icon', 'illustration', 'pattern', 'logo', 'scene']).optional(), - composition: z.enum(['center-object', 'full-scene']).optional(), - advanced: z - .object({ - stroke_weight: z.enum(['thin', 'medium', 'thick']).optional(), - corner_style: z.enum(['none', 'rounded', 'sharp']).optional(), - shadow_effect: z.enum(['none', 'soft', 'hard']).optional(), - }) - .optional(), - }) - .optional(), - mask: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]).optional(), - quality: z.enum(['low', 'medium', 'high']).optional(), - aspectRatio: z.enum(['auto', 'portrait', 'landscape', 'square']).optional(), // Edit mode only supports these aspect ratios - background: z.enum(['auto', 'transparent', 'opaque']).optional(), - stream: z.boolean().optional(), - base64Png: z.boolean().optional(), - svgText: z.boolean().optional(), -}); +const editParamsSchema = z + .object({ + image: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]), + prompt: z.string().optional(), + styleParams: z + .object({ + style: z + .enum([ + 'flat', + 'line_art', + 'engraving', + 'linocut', + 'silhouette', + 'isometric', + 'cartoon', + 'ghibli', + ]) + .optional(), + color_mode: z.enum(['full_color', 'monochrome', 'few_colors']).optional(), + image_complexity: z.enum(['icon', 'illustration', 'scene']).optional(), + text: z.enum(['only_title', 'embedded_text']).optional(), + composition: z + .enum(['centered_object', 'repeating_pattern', 'full_scene', 'objects_in_grid']) + .optional(), + }) + .optional(), + quality: z.enum(['low', 'medium', 'high']).optional(), + aspectRatio: z.enum(['auto', 'portrait', 'landscape', 'square']).optional(), + background: z.enum(['auto', 'transparent', 'opaque']).optional(), + storage: z.boolean().optional(), + stream: z.boolean().optional(), + base64Png: z.boolean().optional(), + svgText: z.boolean().optional(), + model: z.string().optional(), + }) + .refine(data => data.prompt || data.styleParams, { + message: 'Either prompt or styleParams must be provided', + }) + .refine(data => !(data.model && data.quality), { + message: "Cannot specify both 'model' and 'quality'. Use one or the other.", + }); /** * Client for the Edit SVG/Image API @@ -64,7 +76,6 @@ export class EditClient extends BaseClient { prompt: this.params.prompt, quality: this.params.quality, hasImage: !!this.params.image, - hasMask: !!this.params.mask, }); // Apply defaults before validation @@ -79,19 +90,16 @@ export class EditClient extends BaseClient { // Add image file await this.addFileToForm(formData, 'image', this.params.image!); - // Add prompt - formData.append('prompt', this.params.prompt!); + // Add prompt if present + if (this.params.prompt) { + formData.append('prompt', this.params.prompt); + } // Add styleParams if present if (this.params.styleParams) { formData.append('styleParams', JSON.stringify(this.params.styleParams)); } - // Add mask if present - if (this.params.mask) { - await this.addFileToForm(formData, 'mask', this.params.mask); - } - // Add options if (this.params.quality) { formData.append('quality', this.params.quality); @@ -117,8 +125,16 @@ export class EditClient extends BaseClient { formData.append('svgText', String(this.params.svgText)); } + if (this.params.storage !== undefined) { + formData.append('storage', String(this.params.storage)); + } + + if (this.params.model) { + formData.append('model', this.params.model); + } + // Execute request using native fetch (for FormData compatibility) - const response = await fetch(`${this.config.baseUrl}/edit`, { + const response = await fetch(`${this.config.baseUrl}/v1/edit`, { method: 'POST', headers: { 'x-api-key': this.config.apiKey, @@ -127,31 +143,29 @@ export class EditClient extends BaseClient { }); if (!response.ok) { - const errorText = await response.text(); - this.logger.error('Edit request failed', { status: response.status, error: errorText }); - throw new Error(`HTTP error ${response.status}: ${errorText}`); + await this.handleFetchErrorResponse(response); } const rawResult = await response.json(); - // Only log the base64Png field for debugging PNG issues - console.log('base64Png from /edit API:', rawResult.base64Png); + const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); - // Manually decode base64Png to pngImageData (like the interceptor) let pngImageData: Buffer | undefined = undefined; - if (rawResult.base64Png && typeof rawResult.base64Png === 'string') { - pngImageData = decodeBase64Png(rawResult.base64Png); + if (data.base64Png && typeof data.base64Png === 'string') { + pngImageData = decodeBase64Png(data.base64Png); } - // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) let svgText: string | undefined = undefined; - if (rawResult.svgText && typeof rawResult.svgText === 'string') { - svgText = decodeSvgContent(rawResult.svgText); + if (data.svgText && typeof data.svgText === 'string') { + svgText = decodeSvgContent(data.svgText); } - // Compose the response to match EditResponse (only fields present in backend response) const result: EditResponse = { - svgUrl: rawResult.svgUrl, - creditCost: rawResult.creditCost, + svgUrl: data.svgUrl, + creditCost: data.creditCost, + message: data.message ?? '', + svgUrlExpiresIn: data.svgUrlExpiresIn, + generationId: data.generationId, + metadata: responseMetadata, pngImageData, svgText, }; @@ -187,8 +201,8 @@ export class EditClient extends BaseClient { * @private */ private applyDefaults(): void { - // Set default quality to medium if not specified - if (!this.params.quality) { + // Set default quality to medium if not specified (but not when model is used) + if (!this.params.quality && !this.params.model) { this.params.quality = 'medium'; } } @@ -223,19 +237,16 @@ export class EditClient extends BaseClient { // Add image file await this.addFileToForm(formData, 'image', client.params.image!); - // Add prompt - formData.append('prompt', client.params.prompt!); + // Add prompt if present + if (client.params.prompt) { + formData.append('prompt', client.params.prompt); + } // Add styleParams if present if (client.params.styleParams) { formData.append('styleParams', JSON.stringify(client.params.styleParams)); } - // Add mask if present - if (client.params.mask) { - await this.addFileToForm(formData, 'mask', client.params.mask); - } - // Add options if (client.params.quality) { formData.append('quality', client.params.quality); @@ -259,8 +270,16 @@ export class EditClient extends BaseClient { formData.append('svgText', String(client.params.svgText)); } + if (client.params.storage !== undefined) { + formData.append('storage', String(client.params.storage)); + } + + if (client.params.model) { + formData.append('model', client.params.model); + } + // Make request to the streaming endpoint using native fetch - const response = await fetch(`${this.config.baseUrl}/edit`, { + const response = await fetch(`${this.config.baseUrl}/v1/edit`, { method: 'POST', headers: { Accept: 'text/event-stream', @@ -270,8 +289,7 @@ export class EditClient extends BaseClient { }); if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error ${response.status}: ${errorText}`); + await this.handleFetchErrorResponse(response); } const reader = response.body?.getReader(); @@ -281,6 +299,7 @@ export class EditClient extends BaseClient { const decoder = new TextDecoder(); let buffer = ''; + const accumulated: Record = {}; while (true) { const { done, value } = await reader.read(); @@ -319,13 +338,22 @@ export class EditClient extends BaseClient { } // --- End: Normalize event fields --- - stream.push(event); + // Accumulate fields from all events + for (const [key, value] of Object.entries(event)) { + if (value !== undefined && key !== 'status' && key !== 'message') { + accumulated[key] = value; + } + } - // End the stream when we get a complete or error status + // When complete, merge accumulated fields into the event if (event.status === 'complete' || event.status === 'error') { + const mergedEvent = { ...accumulated, ...event }; + stream.push(mergedEvent); stream.push(null); return; } + + stream.push(event); } catch (e) { console.error('Error parsing streaming chunk:', e); console.error('Problematic line:', trimmedLine); diff --git a/src/clients/GenerateClient.ts b/src/clients/GenerateClient.ts index 4ee543c..38da1fd 100644 --- a/src/clients/GenerateClient.ts +++ b/src/clients/GenerateClient.ts @@ -12,56 +12,39 @@ const generateParamsSchema = z .object({ prompt: z.string().min(1, 'Prompt is required'), quality: z.enum(['low', 'medium', 'high']).optional(), - aspectRatio: z.enum(['auto', 'portrait', 'landscape', 'square', 'wide', 'tall']).optional(), + aspectRatio: z.enum(['auto', 'portrait', 'landscape', 'square']).optional(), background: z.enum(['auto', 'transparent', 'opaque']).optional(), + storage: z.boolean().optional(), stream: z.boolean().optional(), base64Png: z.boolean().optional(), svgText: z.boolean().optional(), + model: z.string().optional(), styleParams: z .object({ style: z - .enum(['minimalist', 'cartoon', 'realistic', 'abstract', 'flat', 'isometric']) + .enum([ + 'flat', + 'line_art', + 'engraving', + 'linocut', + 'silhouette', + 'isometric', + 'cartoon', + 'ghibli', + ]) .optional(), - color_mode: z.enum(['monochrome', '2-colors', '3-colors', 'full-color']).optional(), - image_complexity: z.enum(['simple', 'detailed']).optional(), - category: z.enum(['icon', 'illustration', 'pattern', 'logo', 'scene']).optional(), - composition: z.enum(['center-object', 'full-scene']).optional(), - advanced: z - .object({ - stroke_weight: z.enum(['thin', 'medium', 'thick']).optional(), - corner_style: z.enum(['none', 'rounded', 'sharp']).optional(), - shadow_effect: z.enum(['none', 'soft', 'hard']).optional(), - }) + color_mode: z.enum(['full_color', 'monochrome', 'few_colors']).optional(), + image_complexity: z.enum(['icon', 'illustration', 'scene']).optional(), + text: z.enum(['only_title', 'embedded_text']).optional(), + composition: z + .enum(['centered_object', 'repeating_pattern', 'full_scene', 'objects_in_grid']) .optional(), }) .optional(), }) - .refine( - data => { - // Validate quality and aspect ratio combinations - if (data.quality && data.aspectRatio) { - const quality = data.quality; - const aspectRatio = data.aspectRatio; - - if ( - (quality === 'low' || quality === 'medium') && - !['auto', 'portrait', 'landscape', 'square'].includes(aspectRatio) - ) { - return false; - } - - if (quality === 'high' && aspectRatio === 'auto') { - return false; - } - } - - return true; - }, - { - message: - 'Invalid quality and aspect ratio combination. Low/medium quality supports auto, portrait, landscape, square only. High quality supports all except auto.', - } - ); + .refine(data => !(data.model && data.quality), { + message: "Cannot specify both 'model' and 'quality'. Use one or the other.", + }); /** * Client for the Generate SVG API @@ -96,7 +79,7 @@ export class GenerateClient extends BaseClient { this.validateRequest(this.params, generateParamsSchema); // Execute request - const rawResult = await this.handleRequest('/generate', { + const rawResult = await this.handleRequest('/v1/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -115,11 +98,13 @@ export class GenerateClient extends BaseClient { const result: GenerateResponse = { svgUrl: rawResult.svgUrl, creditCost: rawResult.creditCost, - pngImageData: rawResult.pngImageData, // Provided by interceptor + message: rawResult.message ?? '', + svgUrlExpiresIn: rawResult.svgUrlExpiresIn, + generationId: rawResult.generationId, + metadata: rawResult.metadata, + pngImageData: rawResult.pngImageData, svgText, - prompt: rawResult.prompt ?? this.params.prompt, quality: rawResult.quality ?? this.params.quality ?? 'medium', - revisedPrompt: rawResult.revisedPrompt ?? '', }; this.logger.debug('SVG generation completed', { @@ -153,15 +138,7 @@ export class GenerateClient extends BaseClient { * @private */ private applyDefaults(): void { - // Set default aspect ratio based on quality if not already set - if (this.params.quality && !this.params.aspectRatio) { - if (this.params.quality === 'high') { - this.params.aspectRatio = 'square'; - } else { - // low or medium quality - this.params.aspectRatio = 'auto'; - } - } + // No quality-specific defaults in v1 API } /** @@ -189,7 +166,7 @@ export class GenerateClient extends BaseClient { (async () => { try { // Make request to the streaming endpoint using native fetch - const response = await fetch(`${this.config.baseUrl}/generate`, { + const response = await fetch(`${this.config.baseUrl}/v1/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -200,8 +177,7 @@ export class GenerateClient extends BaseClient { }); if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error ${response.status}: ${errorText}`); + await this.handleFetchErrorResponse(response); } const reader = response.body?.getReader(); @@ -211,6 +187,7 @@ export class GenerateClient extends BaseClient { const decoder = new TextDecoder(); let buffer = ''; + const accumulated: Record = {}; while (true) { const { done, value } = await reader.read(); @@ -252,13 +229,22 @@ export class GenerateClient extends BaseClient { } // --- End: Normalize event fields --- - stream.push(event); + // Accumulate fields from all events + for (const [key, value] of Object.entries(event)) { + if (value !== undefined && key !== 'status' && key !== 'message') { + accumulated[key] = value; + } + } - // End the stream when we get a complete or error status + // When complete, merge accumulated fields into the event if (event.status === 'complete' || event.status === 'error') { + const mergedEvent = { ...accumulated, ...event }; + stream.push(mergedEvent); stream.push(null); return; } + + stream.push(event); } catch (e) { console.error('Error parsing streaming chunk:', e); console.error('Problematic line:', trimmedLine); diff --git a/src/clients/convert/AIVectorizeClient.ts b/src/clients/convert/AIVectorizeClient.ts new file mode 100644 index 0000000..fe33dcf --- /dev/null +++ b/src/clients/convert/AIVectorizeClient.ts @@ -0,0 +1,322 @@ +import { BaseClient } from '../BaseClient'; +import { AiVectorizeParams, AiVectorizeResponse, AiVectorizeStreamEvent } from '../../types/api'; +import { SVGMakerClient } from '../../core/SVGMakerClient'; +import { z } from 'zod'; +import { Readable } from 'stream'; +import * as fs from 'fs'; +import * as path from 'path'; +import { ValidationError } from '../../errors/CustomErrors'; +import { decodeSvgContent } from '../../utils/base64'; + +/** + * Schema for validating AI vectorize parameters + */ +const aiVectorizeParamsSchema = z.object({ + file: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]), + storage: z.boolean().optional(), + stream: z.boolean().optional(), + svgText: z.boolean().optional(), +}); + +/** + * Client for the AI Vectorize (Convert Image to SVG) API + */ +export class AIVectorizeClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new AI Vectorize client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the AI Vectorize request + * @returns AI Vectorize response + */ + public async execute(): Promise { + this.logger.debug('Starting AI vectorize conversion', { + hasFile: !!this.params.file, + svgTextRequested: !!this.params.svgText, + }); + + // Validate parameters + this.validateRequest(this.params, aiVectorizeParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', this.params.file!); + + // Add storage option if present + if (this.params.storage !== undefined) { + formData.append('storage', String(this.params.storage)); + } + + // Add stream option if present + if (this.params.stream) { + formData.append('stream', String(this.params.stream)); + } + + if (this.params.svgText) { + formData.append('svgText', String(this.params.svgText)); + } + + // Execute request using native fetch + const response = await fetch(`${this.config.baseUrl}/v1/convert/ai-vectorize`, { + method: 'POST', + headers: { + 'x-api-key': this.config.apiKey, + }, + body: formData, + }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const rawResult = await response.json(); + const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); + + this.logger.debug('AI vectorize conversion completed', { + creditCost: data.creditCost, + hasSvgText: !!data.svgText, + }); + + // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) + let svgText: string | undefined = undefined; + if (data.svgText && typeof data.svgText === 'string') { + svgText = decodeSvgContent(data.svgText); + } + + return { + svgUrl: data.svgUrl, + creditCost: data.creditCost, + message: data.message ?? '', + svgUrlExpiresIn: data.svgUrlExpiresIn, + generationId: data.generationId, + metadata: responseMetadata, + svgText, + } as AiVectorizeResponse; + } + + /** + * Configure the AI vectorize parameters + * @param config Configuration object with AI vectorize parameters + * @returns New client instance + */ + public configure(config: Partial): AIVectorizeClient { + this.logger.debug('Configuring AI vectorize parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Stream the AI vectorize response + * @returns Readable stream of events + */ + public stream(): Readable { + // Create a clone with streaming enabled + const client = this.clone(); + client.params.stream = true; + + // Validate parameters + this.validateRequest(client.params, aiVectorizeParamsSchema); + + // Create a readable stream for the events + const stream = new Readable({ + objectMode: true, + read() {}, + }); + + // Execute the request and handle streaming + (async () => { + try { + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', client.params.file!); + + // Add storage option if present + if (client.params.storage !== undefined) { + formData.append('storage', String(client.params.storage)); + } + + // Add stream option + formData.append('stream', 'true'); + + if (client.params.svgText) { + formData.append('svgText', String(client.params.svgText)); + } + + // Make request to the streaming endpoint using native fetch + const response = await fetch(`${this.config.baseUrl}/v1/convert/ai-vectorize`, { + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'x-api-key': this.config.apiKey, + }, + body: formData, + }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('Response body is not readable'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + const accumulated: Record = {}; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Decode the chunk and add to buffer + buffer += decoder.decode(value, { stream: true }); + + // Split by newlines to get individual JSON objects + const lines = buffer.split('\n'); + + // Keep the last incomplete line in the buffer + buffer = lines.pop() || ''; + + // Process each complete line + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine === '') continue; + + try { + // Parse the JSON chunk directly (no "data:" prefix like SSE) + const event = JSON.parse(trimmedLine) as AiVectorizeStreamEvent; + // Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too) + if (event.svgText && typeof event.svgText === 'string') { + event.svgText = decodeSvgContent(event.svgText); + } + // Accumulate fields from all events + for (const [key, value] of Object.entries(event)) { + if (value !== undefined && key !== 'status' && key !== 'message') { + accumulated[key] = value; + } + } + + // When complete, merge accumulated fields into the event + if (event.status === 'complete' || event.status === 'error') { + const mergedEvent = { ...accumulated, ...event }; + stream.push(mergedEvent); + stream.push(null); + return; + } + + stream.push(event); + } catch (e) { + console.error('Error parsing streaming chunk:', e); + console.error('Problematic line:', trimmedLine); + } + } + } + + // Process any remaining data in buffer + if (buffer.trim()) { + try { + const event = JSON.parse(buffer.trim()) as AiVectorizeStreamEvent; + stream.push(event); + } catch (e) { + console.error('Error parsing final chunk:', e); + } + } + + // End of stream + stream.push(null); + } catch (error) { + console.error('Streaming error:', error); + stream.emit('error', error); + stream.push(null); + } + })(); + + return stream; + } + + /** + * Add a file to the form data + * @param formData Form data + * @param fieldName Form field name + * @param file File path, buffer, or stream + */ + private async addFileToForm( + formData: FormData, + fieldName: string, + file: string | Buffer | Readable + ): Promise { + if (typeof file === 'string') { + // Check if the file exists + if (!fs.existsSync(file)) { + throw new ValidationError(`File not found: ${file}`); + } + + // Read file and create a Blob + const fileBuffer = fs.readFileSync(file); + const filename = path.basename(file); + const mimeType = this.getMimeType(filename); + + const blob = new Blob([fileBuffer], { type: mimeType }); + formData.append(fieldName, blob, filename); + } else if (Buffer.isBuffer(file)) { + // Convert buffer to Blob + const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' }); + formData.append(fieldName, blob, 'file'); + } else if (file instanceof Readable) { + // Convert stream to buffer then to Blob + const chunks: Buffer[] = []; + for await (const chunk of file) { + chunks.push(Buffer.from(chunk)); + } + const buffer = Buffer.concat(chunks); + const blob = new Blob([buffer], { type: 'application/octet-stream' }); + formData.append(fieldName, blob, 'file'); + } else { + throw new ValidationError(`Invalid file type: ${typeof file}`); + } + } + + /** + * Get MIME type from filename + * @param filename File name + * @returns MIME type + */ + private getMimeType(filename: string): string { + const ext = path.extname(filename).toLowerCase(); + const mimeTypes: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + '.bmp': 'image/bmp', + }; + return mimeTypes[ext] || 'application/octet-stream'; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): AIVectorizeClient { + const client = new AIVectorizeClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/convert/index.ts b/src/clients/convert/index.ts new file mode 100644 index 0000000..530b57f --- /dev/null +++ b/src/clients/convert/index.ts @@ -0,0 +1 @@ +export { AIVectorizeClient } from './AIVectorizeClient'; diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index c692012..f60fac1 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -3,7 +3,7 @@ import { HttpClient, RequestOptions } from '../utils/httpClient'; import { ValidationError } from '../errors/CustomErrors'; import { GenerateClient } from '../clients/GenerateClient'; import { EditClient } from '../clients/EditClient'; -import { ConvertClient } from '../clients/ConvertClient'; +import { AIVectorizeClient } from '../clients/convert'; import { createRetryWrapper } from '../utils/retry'; import { createRateLimiter } from '../utils/rateLimit'; import { Logger, createLogger } from '../utils/logger'; @@ -58,9 +58,12 @@ export class SVGMakerClient { public readonly edit: EditClient; /** - * Convert Image to SVG client + * Convert namespace — contains AI vectorize and future conversion clients */ - public readonly convert: ConvertClient; + public readonly convert: { + /** AI-powered raster to SVG vectorization */ + aiVectorize: AIVectorizeClient; + }; /** * Create a new SVGMaker client @@ -97,7 +100,9 @@ export class SVGMakerClient { // Create API clients this.generate = new GenerateClient(this); this.edit = new EditClient(this); - this.convert = new ConvertClient(this); + this.convert = { + aiVectorize: new AIVectorizeClient(this), + }; this.logger.info('SVGMaker SDK initialized'); } diff --git a/src/errors/CustomErrors.ts b/src/errors/CustomErrors.ts index 40d5078..5388ba8 100644 --- a/src/errors/CustomErrors.ts +++ b/src/errors/CustomErrors.ts @@ -43,20 +43,33 @@ export class APIError extends SVGMakerError { /** * Error details returned by the API */ - public details?: string; + public details?: unknown; + + /** + * Request ID from the API response metadata + */ + public requestId?: string; /** * @param message Error message * @param statusCode HTTP status code * @param code Error code * @param details Error details - */ - constructor(message: string, statusCode?: number, code?: string, details?: string) { + * @param requestId Request ID from the API response metadata + */ + constructor( + message: string, + statusCode?: number, + code?: string, + details?: unknown, + requestId?: string + ) { super(message); this.name = 'APIError'; this.statusCode = statusCode; this.code = code; this.details = details; + this.requestId = requestId; Object.setPrototypeOf(this, APIError.prototype); } } @@ -167,12 +180,26 @@ export class ContentSafetyError extends APIError { * @param message Error message */ constructor(message: string) { - super(message, 400, 'content_safety'); + super(message, 422, 'CONTENT_POLICY'); this.name = 'ContentSafetyError'; Object.setPrototypeOf(this, ContentSafetyError.prototype); } } +/** + * Error for disabled endpoint + */ +export class EndpointDisabledError extends APIError { + /** + * @param message Error message + */ + constructor(message: string) { + super(message, 503, 'ENDPOINT_DISABLED'); + this.name = 'EndpointDisabledError'; + Object.setPrototypeOf(this, EndpointDisabledError.prototype); + } +} + /** * Error for file size issues */ diff --git a/src/index.ts b/src/index.ts index ee65154..0c8363c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { HttpClient } from './utils/httpClient'; // Export client classes import { GenerateClient } from './clients/GenerateClient'; import { EditClient } from './clients/EditClient'; +import { AIVectorizeClient } from './clients/convert'; import { ConvertClient } from './clients/ConvertClient'; // Export error classes @@ -23,7 +24,8 @@ export { SVGMakerClient, GenerateClient, EditClient, - ConvertClient, + AIVectorizeClient, + ConvertClient, // backward compat // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index c9e9805..7abb20c 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,28 +1,42 @@ /** - * Common types for the SVGMaker API + * Common types for the SVGMaker v1 API */ import { Readable } from 'stream'; export type Quality = 'low' | 'medium' | 'high'; -export type AspectRatio = 'auto' | 'portrait' | 'landscape' | 'square' | 'wide' | 'tall'; +export type AspectRatio = 'auto' | 'portrait' | 'landscape' | 'square'; export type Background = 'auto' | 'transparent' | 'opaque'; -export type Style = 'minimalist' | 'cartoon' | 'realistic' | 'abstract' | 'flat' | 'isometric'; -export type ColorMode = 'monochrome' | '2-colors' | '3-colors' | 'full-color'; -export type ImageComplexity = 'simple' | 'detailed'; -export type Category = 'icon' | 'illustration' | 'pattern' | 'logo' | 'scene'; -export type Composition = 'center-object' | 'full-scene'; -export type StrokeWeight = 'thin' | 'medium' | 'thick'; -export type CornerStyle = 'none' | 'rounded' | 'sharp'; -export type ShadowEffect = 'none' | 'soft' | 'hard'; - -/** - * Advanced style parameters - */ -export interface AdvancedStyleParams { - stroke_weight?: StrokeWeight; - corner_style?: CornerStyle; - shadow_effect?: ShadowEffect; +export type Style = + | 'flat' + | 'line_art' + | 'engraving' + | 'linocut' + | 'silhouette' + | 'isometric' + | 'cartoon' + | 'ghibli'; +export type ColorMode = 'full_color' | 'monochrome' | 'few_colors'; +export type ImageComplexity = 'icon' | 'illustration' | 'scene'; +export type Composition = + | 'centered_object' + | 'repeating_pattern' + | 'full_scene' + | 'objects_in_grid'; +export type TextStyle = 'only_title' | 'embedded_text'; + +/** + * Response metadata included in all v1 API responses + */ +export interface ResponseMetadata { + /** Unique identifier for the request */ + requestId: string; + + /** Number of credits consumed by this request */ + creditsUsed: number; + + /** Number of credits remaining on the account */ + creditsRemaining: number; } /** @@ -38,14 +52,11 @@ export interface StyleParams { /** Optional: Complexity level */ image_complexity?: ImageComplexity; - /** Optional: Content category */ - category?: Category; - /** Optional: Layout composition */ composition?: Composition; - /** Optional: Advanced styling parameters */ - advanced?: AdvancedStyleParams; + /** Optional: Text style for the generated SVG */ + text?: TextStyle; } /** @@ -58,7 +69,7 @@ export interface GenerateParams { /** Optional: Generation quality: "low", "medium", "high" */ quality?: Quality; - /** Optional: Aspect ratio: "auto", "portrait", "landscape", "square", "wide", "tall" */ + /** Optional: Aspect ratio: "auto", "portrait", "landscape", "square" */ aspectRatio?: AspectRatio; /** Optional: Background type: "auto", "transparent", "opaque" */ @@ -73,8 +84,14 @@ export interface GenerateParams { /** Optional: Include SVG source code as text in response (default: false) */ svgText?: boolean; - /** Optional: Style parameters object containing style, color_mode, image_complexity, category, composition, and advanced options */ + /** Optional: Style parameters object containing style, color_mode, image_complexity, composition, and text options */ styleParams?: StyleParams; + + /** Optional: Store the generated SVG on SVGMaker servers (default: false) */ + storage?: boolean; + + /** Optional: Specific AI model ID to use. Cannot be combined with quality. Credits are charged based on the model. */ + model?: string; } /** @@ -84,19 +101,16 @@ export interface EditParams { /** Required: Image file to edit */ image: string | Buffer | Readable; - /** Required: Edit instructions as a simple text string */ - prompt: string; + /** Optional: Edit instructions as a simple text string */ + prompt?: string; /** Optional: Style parameters as JSON object */ styleParams?: StyleParams; - /** Optional: Mask file for targeted editing */ - mask?: string | Buffer | Readable; - /** Optional: Quality level: "low", "medium", "high" (default: "medium") */ quality?: Quality; - /** Optional: Aspect ratio: "auto", "portrait", "landscape", "square", "wide", "tall" (default: "auto") */ + /** Optional: Aspect ratio: "auto", "portrait", "landscape", "square" (default: "auto") */ aspectRatio?: AspectRatio; /** Optional: Background: "auto", "transparent", "opaque" (default: "auto") */ @@ -110,12 +124,18 @@ export interface EditParams { /** Optional: Include SVG source code as text in response (default: false) */ svgText?: boolean; + + /** Optional: Store the edited SVG on SVGMaker servers (default: false) */ + storage?: boolean; + + /** Optional: Specific AI model ID to use. Cannot be combined with quality. Credits are charged based on the model. */ + model?: string; } /** - * Convert Image to SVG request parameters + * AI Vectorize (Convert Image to SVG) request parameters */ -export interface ConvertParams { +export interface AiVectorizeParams { /** Required: File to convert to SVG */ file: string | Buffer | Readable; @@ -124,8 +144,17 @@ export interface ConvertParams { /** Optional: Include SVG source code as text in response (default: false) */ svgText?: boolean; + + /** Optional: Store the converted SVG on SVGMaker servers (default: false) */ + storage?: boolean; } +/** + * Convert Image to SVG request parameters + * @deprecated Use AiVectorizeParams instead + */ +export type ConvertParams = AiVectorizeParams; + /** * Base SVGMaker API response */ @@ -135,6 +164,18 @@ export interface BaseResponse { /** Number of credits consumed by the operation */ creditCost: number; + + /** Response metadata with requestId, creditsUsed, and creditsRemaining */ + metadata: ResponseMetadata; + + /** Human-readable message about the operation result */ + message: string; + + /** Expiration time for the SVG URL (e.g., "24h") */ + svgUrlExpiresIn?: string; + + /** Unique identifier for this generation */ + generationId?: string; } /** @@ -147,14 +188,8 @@ export interface GenerateResponse extends BaseResponse { /** SVG source code as text - only when svgText=true */ svgText?: string; - /** The prompt used for generation */ - prompt: string; - /** The quality level used */ quality: Quality; - - /** The revised prompt used by the AI */ - revisedPrompt: string; } /** @@ -169,19 +204,25 @@ export interface EditResponse extends BaseResponse { } /** - * Convert Image to SVG response + * AI Vectorize (Convert Image to SVG) response */ -export interface ConvertResponse extends BaseResponse { +export interface AiVectorizeResponse extends BaseResponse { /** SVG source code as text - only when svgText=true */ svgText?: string; } +/** + * Convert Image to SVG response + * @deprecated Use AiVectorizeResponse instead + */ +export type ConvertResponse = AiVectorizeResponse; + /** * Stream event base type */ export interface StreamEventBase { /** Status of the streaming response */ - status: 'processing' | 'complete' | 'error'; + status: 'processing' | 'generated' | 'storing' | 'complete' | 'error'; /** SVG source code as text (decoded, only when svgText=true and available) */ svgText?: string; /** Base64-encoded PNG preview (only when base64Png=true and available) */ @@ -194,9 +235,17 @@ export interface StreamEventBase { * Processing stream event */ export interface ProcessingStreamEvent extends StreamEventBase { - status: 'processing'; + status: 'processing' | 'generated' | 'storing'; /** Progress message */ message: string; + /** URL to the generated/edited/converted SVG (may appear in generated event) */ + svgUrl?: string; + /** Number of credits consumed by the operation (may appear in generated event) */ + creditCost?: number; + /** Expiration time for the SVG URL (may appear in generated event) */ + svgUrlExpiresIn?: string; + /** The quality level used (may appear in generated event) */ + quality?: string; } /** @@ -206,10 +255,18 @@ export interface CompleteStreamEvent extends StreamEventBase { status: 'complete'; /** URL to the generated/edited/converted SVG */ svgUrl: string; - /** Simulation mode flag */ - simulationMode: boolean; /** SVG source code as text - only when svgText=true */ svgText?: string; + /** Expiration time for the SVG URL */ + svgUrlExpiresIn?: string; + /** Unique identifier for this generation */ + generationId?: string; + /** Number of credits consumed by the operation */ + creditCost?: number; + /** Human-readable message about the operation result */ + message?: string; + /** Response metadata */ + metadata?: ResponseMetadata; } /** @@ -242,3 +299,8 @@ export type EditStreamEvent = StreamEvent; * Convert stream event */ export type ConvertStreamEvent = StreamEvent; + +/** + * AI Vectorize stream event + */ +export type AiVectorizeStreamEvent = StreamEvent; diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts index bd52655..0bc8e38 100644 --- a/src/utils/httpClient.ts +++ b/src/utils/httpClient.ts @@ -7,6 +7,7 @@ import { RateLimitError, InsufficientCreditsError, ContentSafetyError, + EndpointDisabledError, FileSizeError, FileFormatError, } from '../errors/CustomErrors'; @@ -102,12 +103,45 @@ export class HttpClient { if (response.headers.get('Content-Type')?.includes('application/json')) { const jsonResponse = await response.json(); - // Process base64Png field if present + // Handle v1 envelope response + if (jsonResponse.success === true && jsonResponse.data !== undefined) { + // Unwrap the v1 envelope + const unwrapped = { ...jsonResponse.data, metadata: jsonResponse.metadata }; + + // Process base64Png field if present + if (unwrapped.base64Png && typeof unwrapped.base64Png === 'string') { + const base64Data = unwrapped.base64Png.replace(/^data:image\/png;base64,/, ''); + unwrapped.pngImageData = Buffer.from(base64Data, 'base64'); + } + + return unwrapped as T; + } else if (jsonResponse.success === false) { + // v1 error envelope that came with a 2xx status (unlikely but handle it) + const errorCode = jsonResponse.error?.code; + const errorMessage = jsonResponse.error?.message || `API Error`; + const errorDetails = jsonResponse.error?.details; + const requestId = jsonResponse.metadata?.requestId; + + if (errorCode === 'INVALID_API_KEY') throw new AuthError(errorMessage); + if (errorCode === 'INSUFFICIENT_CREDITS') + throw new InsufficientCreditsError(errorMessage, errorDetails?.creditsRequired); + if (errorCode === 'RATE_LIMIT_EXCEEDED') throw new RateLimitError(errorMessage, 60); + if (errorCode === 'CONTENT_POLICY') throw new ContentSafetyError(errorMessage); + if (errorCode === 'ENDPOINT_DISABLED') throw new EndpointDisabledError(errorMessage); + + throw new APIError( + errorMessage, + jsonResponse.error?.status, + errorCode, + errorDetails, + requestId + ); + } + + // Legacy non-envelope response if (jsonResponse.base64Png && typeof jsonResponse.base64Png === 'string') { - // Convert base64 PNG to Buffer for consistent API const base64Data = jsonResponse.base64Png.replace(/^data:image\/png;base64,/, ''); jsonResponse.pngImageData = Buffer.from(base64Data, 'base64'); - // Keep the original base64 string as well for compatibility } return jsonResponse as T; @@ -274,57 +308,80 @@ export class HttpClient { * @returns Error object */ private async handleErrorResponse(response: globalThis.Response): Promise { - let errorData: any = {}; + let body: any = {}; try { if (response.headers.get('Content-Type')?.includes('application/json')) { - errorData = await response.json(); + body = await response.json(); } else { - errorData = { error: await response.text() }; + body = { error: await response.text() }; } } catch { - errorData = { error: `HTTP Error ${response.status}` }; + body = { error: `HTTP Error ${response.status}` }; } - const message = errorData.error || errorData.details || `HTTP Error ${response.status}`; + // Extract v1 error envelope fields + const errorCode = body.error?.code; + const errorStatus = body.error?.status || response.status; + const errorMessage = + body.error?.message || body.error || body.details || `HTTP Error ${response.status}`; + const errorDetails = body.error?.details; + const requestId = body.metadata?.requestId; + + // Map by v1 error code first + if (errorCode) { + switch (errorCode) { + case 'INVALID_API_KEY': + return new AuthError(errorMessage); + + case 'INSUFFICIENT_CREDITS': + return new InsufficientCreditsError(errorMessage, errorDetails?.creditsRequired); + + case 'RATE_LIMIT_EXCEEDED': { + const retryAfter = parseInt( + response.headers.get('x-ratelimit-reset-requests') || + response.headers.get('Retry-After') || + '60', + 10 + ); + return new RateLimitError(errorMessage, retryAfter); + } - // Handle specific error types + case 'CONTENT_POLICY': + return new ContentSafetyError(errorMessage); + + case 'ENDPOINT_DISABLED': + return new EndpointDisabledError(errorMessage); + } + } + + // Fallback to HTTP status code mapping switch (response.status) { case 401: - return new AuthError(message); + return new AuthError(errorMessage); case 402: - return new InsufficientCreditsError(message, errorData.creditsRequired); + return new InsufficientCreditsError(errorMessage, errorDetails?.creditsRequired); case 413: - return new FileSizeError(message); + return new FileSizeError(errorMessage); case 429: return new RateLimitError( - message, + errorMessage, parseInt(response.headers.get('Retry-After') || '60', 10) ); } - // Handle content safety errors - if (errorData.errorType === 'content_safety') { - return new ContentSafetyError(message); - } - - // Handle file format errors + // Handle file format errors by message content if ( - message.includes('file format') || - message.includes('SVG files are already in vector format') + errorMessage.includes('file format') || + errorMessage.includes('SVG files are already in vector format') ) { - return new FileFormatError(message); + return new FileFormatError(errorMessage); } - // Generic API error - return new APIError( - message, - response.status, - errorData.errorType || errorData.error_type, - errorData.details - ); + // Generic API error with all extracted fields + return new APIError(errorMessage, errorStatus, errorCode, errorDetails, requestId); } } diff --git a/tests/SVGMakerClient.test.ts b/tests/SVGMakerClient.test.ts index 9aa3bc4..cd9a6f1 100644 --- a/tests/SVGMakerClient.test.ts +++ b/tests/SVGMakerClient.test.ts @@ -68,6 +68,7 @@ describe('SVGMakerClient', () => { expect(client.generate).toBeDefined(); expect(client.edit).toBeDefined(); expect(client.convert).toBeDefined(); + expect(client.convert.aiVectorize).toBeDefined(); }); it('should accept new base64Png and svgText parameters for generate', () => { @@ -97,12 +98,12 @@ describe('SVGMakerClient', () => { }).not.toThrow(); }); - it('should accept new svgText parameter for convert', () => { + it('should accept new svgText parameter for ai-vectorize', () => { const client = new SVGMakerClient('test-api-key'); // Should not throw when configuring with new parameters expect(() => { - client.convert.configure({ + client.convert.aiVectorize.configure({ file: Buffer.from('test'), svgText: true, }); diff --git a/tests/manual-test.ts b/tests/manual-test.ts index 03e8210..ee04768 100644 --- a/tests/manual-test.ts +++ b/tests/manual-test.ts @@ -4,563 +4,544 @@ * Manual Testing Script for SVGMaker SDK (TypeScript) * * Usage: - * npx ts-node manual-test.ts [test-type] [api-key] + * npx ts-node tests/manual-test.ts [test-type] [api-key] * * Test types: - * - generate: Test SVG generation - * - edit: Test image editing (requires input image) - * - convert: Test image conversion (requires input image) - * - stream-generate: Test streaming SVG generation - * - stream-edit: Test streaming image editing (requires input image) - * - stream-convert: Test streaming image conversion (requires input image) - * - streaming: Run all streaming tests - * - all: Run all tests (including streaming) + * - generate: All generate tests (model, quality, both, storage, stream, styleParams) + * - edit: All edit tests (model, quality, both, storage, stream, styleParams) + * - convert: All convert tests (basic, stream, storage) + * - all: Run everything * * Examples: - * npx ts-node manual-test.ts generate your-api-key - * npx ts-node manual-test.ts streaming your-api-key - * SVGMAKER_API_KEY=your-key npx ts-node manual-test.ts all + * npx ts-node tests/manual-test.ts generate svgmaker-ioYOURKEY + * npx ts-node tests/manual-test.ts all svgmaker-ioYOURKEY + * SVGMAKER_API_KEY=svgmaker-ioYOURKEY SVGMAKER_BASE_URL=http://localhost:3000/api npx ts-node tests/manual-test.ts all */ import { SVGMakerClient } from '../src/index'; -import * as Types from '../src/types/api'; -import { writeFileSync, existsSync } from 'fs'; +import { existsSync } from 'fs'; +import { Readable } from 'stream'; // Get command line arguments -const testType = process.argv[2] || 'generate'; +const testType = process.argv[2] || 'all'; const apiKey = process.argv[3] || process.env.SVGMAKER_API_KEY; if (!apiKey || apiKey === 'your-api-key') { - console.error('❌ Error: Please provide a valid API key'); - console.error('Usage: npx ts-node manual-test.ts [test-type] [api-key]'); + console.error('Error: Please provide a valid API key'); + console.error('Usage: npx ts-node tests/manual-test.ts [test-type] [api-key]'); console.error('Or set SVGMAKER_API_KEY environment variable'); process.exit(1); } -console.log('🧪 SVGMaker SDK Manual Testing (TypeScript)'); -console.log('============================================'); +const baseUrl = process.env.SVGMAKER_BASE_URL || 'http://localhost:3000/api'; + +console.log('SVGMaker SDK Manual Testing'); +console.log('==========================='); console.log(`Test Type: ${testType}`); -console.log(`API Key: ${apiKey.substring(0, 10)}...`); +console.log(`Base URL: ${baseUrl}`); +console.log(`API Key: ${apiKey.substring(0, 15)}...`); console.log(''); -// Create client with custom configuration for testing const client = new SVGMakerClient(apiKey, { - baseUrl: process.env.SVGMAKER_BASE_URL || 'https://svgmaker.io/api', // Use local URL - timeout: process.env.SVGMAKER_TIMEOUT ? parseInt(process.env.SVGMAKER_TIMEOUT, 10) : 300000, // 5 minutes timeout for manual testing - maxRetries: 2, // Fewer retries for faster feedback - logging: true, // Enable logging to see what's happening + baseUrl, + timeout: 300000, + maxRetries: 0, + logging: true, }); -async function testGenerate(): Promise { - console.log('🎨 Testing SVG Generation...'); - - try { - const generateParams: Types.GenerateParams = { - prompt: 'generate lion image', - quality: 'low', - styleParams: { - style: 'minimalist', - color_mode: 'monochrome', - }, - aspectRatio: 'landscape', - background: 'transparent', - base64Png: true, // Request base64-encoded PNG preview - svgText: true, // Request SVG source code as text - }; - - const result = await client.generate.configure(generateParams).execute(); - - console.log('✅ Generation successful!'); - console.log(`📄 SVG URL: ${result.svgUrl}`); - console.log(`💰 Credits used: ${result.creditCost}`); - - // Check if we got the SVG source code - if (result.svgText) { - const svgFilename = `tests/test-images/test-generated-ts-${Date.now()}.svg`; - writeFileSync(svgFilename, result.svgText); - console.log(`📄 SVG source saved as: ${svgFilename}`); - console.log(`📏 SVG text preview: ${result.svgText.substring(0, 100)}...`); - } - - // Save PNG image data if available - if (result.pngImageData) { - const filename = `tests/test-images/test-generated-ts-${Date.now()}.png`; - writeFileSync(filename, result.pngImageData); - console.log(`💾 PNG saved as: ${filename}`); - } - - return true; - } catch (error: any) { - console.error('❌ Generation failed:', error.message); - if (error.statusCode) console.error(`Status: ${error.statusCode}`); - return false; +function logResult(label: string, result: any) { + console.log(` SVG URL: ${result.svgUrl}`); + console.log(` Credits: ${result.creditCost}`); + console.log(` Message: ${result.message}`); + if (result.metadata) { + console.log(` Request ID: ${result.metadata.requestId}`); + console.log(` Credits remaining: ${result.metadata.creditsRemaining}`); } + if (result.svgUrlExpiresIn) console.log(` URL expires in: ${result.svgUrlExpiresIn}`); + if (result.generationId) console.log(` Generation ID: ${result.generationId}`); } -async function testEdit(): Promise { - console.log('✏️ Testing Image Editing...'); +function consumeStream(label: string, stream: Readable): Promise { + return new Promise(resolve => { + const timeoutId = setTimeout(() => { + console.log(` TIMEOUT after 120s`); + stream.destroy(); + resolve(false); + }, 120000); + + let eventCount = 0; + let success = false; + + stream.on('data', (event: any) => { + eventCount++; + console.log(` Event ${eventCount}: status=${event.status} ${event.message || ''}`); + if (event.status === 'complete') { + console.log(` SVG URL: ${event.svgUrl}`); + if (event.creditCost) console.log(` Credits: ${event.creditCost}`); + if (event.generationId) console.log(` Generation ID: ${event.generationId}`); + if (event.metadata) console.log(` Request ID: ${event.metadata?.requestId}`); + success = true; + } else if (event.status === 'error') { + console.log(` Stream error: ${event.error}`); + success = false; + } + }); + + stream.on('end', () => { + clearTimeout(timeoutId); + console.log(` Stream ended (${eventCount} events)`); + resolve(success); + }); + + stream.on('error', (error: any) => { + clearTimeout(timeoutId); + console.error(` Stream error: ${error.message}`); + resolve(false); + }); + }); +} - // Check if we have a test image - const testImages = [ +function findTestImage(): string | null { + const paths = [ 'tests/test-images/test-image.png', 'tests/test-images/test-image.jpg', 'tests/test-images/input.png', 'tests/test-images/image.jpg', ]; - const availableImage = testImages.find(img => existsSync(img)); + return paths.find(p => existsSync(p)) || null; +} + +// ========== GENERATE TESTS ========== - if (!availableImage) { - console.log('⚠️ No test image found. Creating a simple test image first...'); - console.log('You can manually create any of these files to test editing:'); - testImages.forEach(img => console.log(` - ${img}`)); +async function generateWithModel(): Promise { + console.log('\n[Generate] With model (no quality)'); + try { + const result = await client.generate + .configure({ + prompt: 'A minimalist mountain landscape', + model: 'gpt-image-1-mini', + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); return false; } +} +async function generateWithQuality(): Promise { + console.log('\n[Generate] With quality (no model)'); try { - const editParams: Types.EditParams = { - image: availableImage, - prompt: 'Add a simple blue border around the image', - styleParams: { - style: 'cartoon', - color_mode: 'full-color', - }, - quality: 'medium', - base64Png: true, // Request base64-encoded PNG preview - svgText: true, // Request SVG source code as text - }; - - const result = await client.edit.configure(editParams).execute(); - - console.log('✅ Edit successful!'); - console.log(`📄 SVG URL: ${result.svgUrl}`); - console.log(`💰 Credits used: ${result.creditCost}`); - - // Check if we got the SVG source code - if (result.svgText) { - const svgFilename = `tests/test-images/test-edited-ts-${Date.now()}.svg`; - writeFileSync(svgFilename, result.svgText); - console.log(`📄 SVG source saved as: ${svgFilename}`); - console.log(`📏 SVG text preview: ${result.svgText.substring(0, 100)}...`); - } - - if (result.pngImageData) { - const filename = `tests/test-images/test-edited-ts-${Date.now()}.png`; - writeFileSync(filename, result.pngImageData); - console.log(`💾 PNG saved as: ${filename}`); - } - + const result = await client.generate + .configure({ + prompt: 'A red sports car', + quality: 'high', + }) + .execute(); + console.log(' PASS'); + logResult('', result); return true; } catch (error: any) { - console.error('❌ Edit failed:', error.message); - if (error.statusCode) console.error(`Status: ${error.statusCode}`); + console.log(` FAIL: ${error.message}`); return false; } } -async function testConvert(): Promise { - console.log('🔄 Testing Image Conversion...'); - - const testImages = [ - 'tests/test-images/test-image.png', - 'tests/test-images/test-image.jpg', - 'tests/test-images/input.png', - 'tests/test-images/image.jpg', - ]; - const availableImage = testImages.find(img => existsSync(img)); - - if (!availableImage) { - console.log('⚠️ No test image found for conversion.'); - console.log('Create any of these files to test conversion:'); - testImages.forEach(img => console.log(` - ${img}`)); +async function generateWithBothModelAndQuality(): Promise { + console.log('\n[Generate] With BOTH model and quality (should fail validation)'); + try { + await client.generate + .configure({ + prompt: 'A cat on a fence', + quality: 'high', + model: 'gpt-image-1-mini', + }) + .execute(); + console.log(' FAIL: Should have thrown validation error'); return false; + } catch (error: any) { + console.log(` PASS (caught expected error): ${error.message}`); + return true; } +} +async function generateWithStorageFalse(): Promise { + console.log('\n[Generate] With storage: false'); try { - const convertParams: Types.ConvertParams = { - file: availableImage, - svgText: true, // Request SVG source code as text - }; - - const result = await client.convert.configure(convertParams).execute(); - - console.log('✅ Conversion successful!'); - console.log(`📄 SVG URL: ${result.svgUrl}`); - console.log(`💰 Credits used: ${result.creditCost}`); - - // Check if we got the SVG source code - if (result.svgText) { - const svgFilename = `tests/test-images/test-converted-ts-${Date.now()}.svg`; - writeFileSync(svgFilename, result.svgText); - console.log(`📄 SVG source saved as: ${svgFilename}`); - console.log(`📏 SVG text preview: ${result.svgText.substring(0, 100)}...`); - } - + const result = await client.generate + .configure({ + prompt: 'A simple house icon', + quality: 'low', + storage: false, + }) + .execute(); + console.log(' PASS'); + logResult('', result); return true; } catch (error: any) { - console.error('❌ Conversion failed:', error.message); - if (error.statusCode) console.error(`Status: ${error.statusCode}`); + console.log(` FAIL: ${error.message}`); return false; } } -async function testStreamGenerate(): Promise { - console.log('🌊 Testing Streaming SVG Generation...'); +async function generateWithStorageTrue(): Promise { + console.log('\n[Generate] With storage: true'); + try { + const result = await client.generate + .configure({ + prompt: 'A golden trophy', + quality: 'medium', + storage: true, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} +async function generateWithStream(): Promise { + console.log('\n[Generate] With stream'); try { const stream = client.generate .configure({ - prompt: 'A simple icon of a tree with streaming updates', + prompt: 'A rocket launching into space', quality: 'low', - styleParams: { - style: 'minimalist', - color_mode: 'monochrome', - }, - aspectRatio: 'square', - base64Png: true, // Request base64-encoded PNG preview - svgText: true, // Request SVG source code as text }) .stream(); - - console.log('📡 Streaming started...'); - let eventCount = 0; - let completedSuccessfully = false; - - return new Promise(resolve => { - // Set a timeout to avoid hanging indefinitely - const timeoutId = setTimeout(() => { - console.log('⏰ Stream timeout after 60 seconds'); - stream.destroy(); - resolve(false); - }, 60000); - - stream.on('data', event => { - eventCount++; - console.log(`📨 Event ${eventCount}: ${event.status}`); - console.log('DEBUG EVENT:', event); // Log the full event object for debugging - - if (event.status === 'processing') { - console.log(`⏳ Processing: ${event.message}`); - } else if (event.status === 'generated' || event.status === 'complete') { - if (event.svgText) { - console.log('DEBUG: event.svgText type:', typeof event.svgText); - console.log('DEBUG: event.svgText value:', event.svgText); - const svgFilename = `tests/test-images/test-stream-generated-ts-${Date.now()}.svg`; - writeFileSync(svgFilename, event.svgText); - console.log(`📄 SVG source saved as: ${svgFilename}`); - console.log(`📏 SVG text preview: ${event.svgText.substring(0, 100)}...`); - } - - // Save PNG file if available - if (event.pngImageData) { - const pngFilename = `tests/test-images/test-stream-generated-ts-${Date.now()}.png`; - writeFileSync(pngFilename, event.pngImageData); - console.log(`💾 PNG saved as: ${pngFilename}`); - } - - completedSuccessfully = true; - } else if (event.status === 'error') { - console.log(`❌ Stream error: ${event.error} (${event.errorType})`); - completedSuccessfully = false; - } - }); - - stream.on('end', () => { - clearTimeout(timeoutId); - console.log(`🏁 Stream ended after ${eventCount} events`); - resolve(completedSuccessfully); - }); - - stream.on('error', error => { - clearTimeout(timeoutId); - console.error('❌ Stream error:', error.message); - resolve(false); - }); - }); + const ok = await consumeStream('', stream); + console.log(ok ? ' PASS' : ' FAIL'); + return ok; } catch (error: any) { - console.error('❌ Streaming generation failed:', error.message); - if (error.statusCode) console.error(`Status: ${error.statusCode}`); + console.log(` FAIL: ${error.message}`); return false; } } -async function testStreamEdit(): Promise { - console.log('🌊 Testing Streaming Image Editing...'); +async function generateWithStyleParams(): Promise { + console.log('\n[Generate] With styleParams + aspectRatio + background'); + try { + const result = await client.generate + .configure({ + prompt: 'A forest with tall trees and a river', + quality: 'low', + aspectRatio: 'landscape', + background: 'transparent', + styleParams: { + style: 'flat', + color_mode: 'few_colors', + image_complexity: 'scene', + composition: 'full_scene', + text: 'only_title', + }, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} - // Check if we have a test image - const testImages = [ - 'tests/test-images/test-image.png', - 'tests/test-images/test-image.jpg', - 'tests/test-images/input.png', - 'tests/test-images/image.jpg', - ]; - const availableImage = testImages.find(img => existsSync(img)); +// ========== EDIT TESTS ========== - if (!availableImage) { - console.log('⚠️ No test image found for streaming edit test.'); +async function editWithModel(): Promise { + console.log('\n[Edit] With model (no quality)'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); return false; } - try { - const stream = client.edit + const result = await client.edit .configure({ - image: availableImage, - prompt: 'Add colorful borders with streaming updates', - styleParams: { - style: 'cartoon', - color_mode: 'full-color', - }, - quality: 'medium', - base64Png: true, // Request base64-encoded PNG preview - svgText: true, // Request SVG source code as text + image: img, + prompt: 'Add a golden frame around this image', + model: 'gpt-image-1-mini', }) - .stream(); + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} - console.log('📡 Edit streaming started...'); - let eventCount = 0; - let completedSuccessfully = false; - - return new Promise(resolve => { - // Set a timeout to avoid hanging indefinitely - const timeoutId = setTimeout(() => { - console.log('⏰ Edit stream timeout after 60 seconds'); - stream.destroy(); - resolve(false); - }, 60000); - - stream.on('data', event => { - eventCount++; - console.log(`📨 Edit Event ${eventCount}: ${event.status}`); - // Save files on 'generated' event for edit streaming - if (event.status === 'generated') { - if (event.svgText) { - const svgFilename = `tests/test-images/test-stream-edited-ts-${Date.now()}.svg`; - writeFileSync(svgFilename, event.svgText); - console.log(`📄 SVG source saved as: ${svgFilename}`); - console.log(`📏 SVG text preview: ${event.svgText.substring(0, 100)}...`); - } - if (event.pngImageData) { - const pngFilename = `tests/test-images/test-stream-edited-ts-${Date.now()}.png`; - writeFileSync(pngFilename, event.pngImageData); - console.log(`💾 PNG saved as: ${pngFilename}`); - } - } else if (event.status === 'processing') { - console.log(`⏳ Processing edit: ${event.message}`); - } else if (event.status === 'complete') { - if (event.svgUrl) { - console.log(`✅ Edit stream completed! SVG URL: ${event.svgUrl}`); - } - if (event.creditCost) { - console.log(`💰 Credits used: ${event.creditCost}`); - } - completedSuccessfully = true; - } else if (event.status === 'error') { - console.log(`❌ Edit stream error: ${event.error} (${event.errorType})`); - completedSuccessfully = false; - } - }); - - stream.on('end', () => { - clearTimeout(timeoutId); - console.log(`🏁 Edit stream ended after ${eventCount} events`); - resolve(completedSuccessfully); - }); - - stream.on('error', error => { - clearTimeout(timeoutId); - console.error('❌ Edit stream error:', error.message); - resolve(false); - }); - }); +async function editWithQuality(): Promise { + console.log('\n[Edit] With quality (no model)'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + const result = await client.edit + .configure({ + image: img, + prompt: 'Make the background blue', + quality: 'low', + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; } catch (error: any) { - console.error('❌ Streaming edit failed:', error.message); - if (error.statusCode) console.error(`Status: ${error.statusCode}`); + console.log(` FAIL: ${error.message}`); return false; } } -async function testStreamConvert(): Promise { - console.log('🌊 Testing Streaming Image Conversion...'); +async function editWithBothModelAndQuality(): Promise { + console.log('\n[Edit] With BOTH model and quality (should fail validation)'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + await client.edit + .configure({ + image: img, + prompt: 'Add stars to the sky', + quality: 'high', + model: 'gpt-image-1-mini', + }) + .execute(); + console.log(' FAIL: Should have thrown validation error'); + return false; + } catch (error: any) { + console.log(` PASS (caught expected error): ${error.message}`); + return true; + } +} - // Check if we have a test image - const testImages = [ - 'tests/test-images/test-image.png', - 'tests/test-images/test-image.jpg', - 'tests/test-images/input.png', - 'tests/test-images/image.jpg', - ]; - const availableImage = testImages.find(img => existsSync(img)); +async function editWithStorageFalse(): Promise { + console.log('\n[Edit] With storage: false'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + const result = await client.edit + .configure({ + image: img, + prompt: 'Convert to silhouette style', + quality: 'low', + storage: false, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} - if (!availableImage) { - console.log('⚠️ No test image found for streaming convert test.'); +async function editWithStorageTrue(): Promise { + console.log('\n[Edit] With storage: true'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + const result = await client.edit + .configure({ + image: img, + prompt: 'Add a sunset gradient', + quality: 'low', + storage: true, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); return false; } +} +async function editWithStream(): Promise { + console.log('\n[Edit] With stream'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } try { - const stream = client.convert + const stream = client.edit .configure({ - file: availableImage, - svgText: true, + image: img, + prompt: 'Make it look like a cartoon', + quality: 'low', }) .stream(); + const ok = await consumeStream('', stream); + console.log(ok ? ' PASS' : ' FAIL'); + return ok; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} - console.log('📡 Convert streaming started...'); - let eventCount = 0; - let completedSuccessfully = false; - - return new Promise(resolve => { - // Set a timeout to avoid hanging indefinitely - const timeoutId = setTimeout(() => { - console.log('⏰ Convert stream timeout after 60 seconds'); - stream.destroy(); - resolve(false); - }, 60000); - - stream.on('data', event => { - eventCount++; - console.log(`📨 Convert Event ${eventCount}: ${event.status}`); - // Save files on 'generated' event for convert streaming - if (event.status === 'generated') { - if (event.svgText) { - const svgFilename = `tests/test-images/test-stream-converted-ts-${Date.now()}.svg`; - writeFileSync(svgFilename, event.svgText); - console.log(`📄 SVG source saved as: ${svgFilename}`); - console.log(`📏 SVG text preview: ${event.svgText.substring(0, 100)}...`); - } - } else if (event.status === 'processing') { - console.log(`⏳ Processing conversion: ${event.message}`); - } else if (event.status === 'complete') { - if (event.svgUrl) { - console.log(`✅ Convert stream completed! SVG URL: ${event.svgUrl}`); - } - if (event.creditCost) { - console.log(`💰 Credits used: ${event.creditCost}`); - } - if (event.simulationMode !== undefined) { - console.log(`🔧 Simulation mode: ${event.simulationMode}`); - } - completedSuccessfully = true; - } else if (event.status === 'error') { - console.log(`❌ Convert stream error: ${event.error} (${event.errorType})`); - completedSuccessfully = false; - } - }); - - stream.on('end', () => { - clearTimeout(timeoutId); - console.log(`🏁 Convert stream ended after ${eventCount} events`); - resolve(completedSuccessfully); - }); - - stream.on('error', error => { - clearTimeout(timeoutId); - console.error('❌ Convert stream error:', error.message); - resolve(false); - }); - }); +async function editWithStyleParams(): Promise { + console.log('\n[Edit] With styleParams + aspectRatio + background'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + const result = await client.edit + .configure({ + image: img, + prompt: 'Restyle this image', + quality: 'low', + aspectRatio: 'square', + background: 'opaque', + styleParams: { + style: 'isometric', + color_mode: 'monochrome', + composition: 'centered_object', + }, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; } catch (error: any) { - console.error('❌ Streaming conversion failed:', error.message); - if (error.statusCode) console.error(`Status: ${error.statusCode}`); + console.log(` FAIL: ${error.message}`); return false; } } -async function testErrorHandling(): Promise { - console.log('🚨 Testing Error Handling...'); +// ========== CONVERT TESTS ========== +async function convertBasic(): Promise { + console.log('\n[Convert] Without stream'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } try { - // Test with invalid API key - const badClient = new SVGMakerClient('invalid-key'); - await badClient.generate.configure({ prompt: 'test' }).execute(); + const result = await client.convert.aiVectorize + .configure({ + file: img, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; } catch (error: any) { - console.log('✅ Error handling works for invalid API key'); - console.log(`Error type: ${error.name || error.constructor.name}`); + console.log(` FAIL: ${error.message}`); + return false; } +} +async function convertWithStream(): Promise { + console.log('\n[Convert] With stream'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } try { - // Test with missing prompt - await client.generate.configure({ prompt: '' }).execute(); + const stream = client.convert.aiVectorize + .configure({ + file: img, + }) + .stream(); + const ok = await consumeStream('', stream); + console.log(ok ? ' PASS' : ' FAIL'); + return ok; } catch (error: any) { - console.log('✅ Validation works for empty prompt'); - console.log(`Error type: ${error.name || error.constructor.name}`); + console.log(` FAIL: ${error.message}`); + return false; } } -async function testConfiguration(): Promise { - console.log('🔧 Testing Configuration...'); - - // Test configuration updates - client.setConfig({ - timeout: 45000, - maxRetries: 1, - }); - - const config = client.getConfig(); - console.log(`⚙️ Updated timeout: ${config.timeout}ms`); - console.log(`🔄 Updated max retries: ${config.maxRetries}`); - console.log('✅ Configuration update successful'); +async function convertWithStorage(): Promise { + console.log('\n[Convert] With storage: true'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + const result = await client.convert.aiVectorize + .configure({ + file: img, + storage: true, + }) + .execute(); + console.log(' PASS'); + logResult('', result); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } } +// ========== RUNNER ========== + async function runTests(): Promise { const results: Record = {}; - console.log('Starting TypeScript manual tests...\n'); - if (testType === 'generate' || testType === 'all') { - results.generate = await testGenerate(); - console.log(''); + results['generate-model'] = await generateWithModel(); + results['generate-quality'] = await generateWithQuality(); + results['generate-both-error'] = await generateWithBothModelAndQuality(); + results['generate-storage-false'] = await generateWithStorageFalse(); + results['generate-storage-true'] = await generateWithStorageTrue(); + results['generate-stream'] = await generateWithStream(); + results['generate-styleparams'] = await generateWithStyleParams(); } if (testType === 'edit' || testType === 'all') { - results.edit = await testEdit(); - console.log(''); + results['edit-model'] = await editWithModel(); + results['edit-quality'] = await editWithQuality(); + results['edit-both-error'] = await editWithBothModelAndQuality(); + results['edit-storage-false'] = await editWithStorageFalse(); + results['edit-storage-true'] = await editWithStorageTrue(); + results['edit-stream'] = await editWithStream(); + results['edit-styleparams'] = await editWithStyleParams(); } if (testType === 'convert' || testType === 'all') { - results.convert = await testConvert(); - console.log(''); - } - - // Individual streaming tests - if (testType === 'stream-generate' || testType === 'streaming' || testType === 'all') { - results['stream-generate'] = await testStreamGenerate(); - console.log(''); - } - - if (testType === 'stream-edit' || testType === 'streaming' || testType === 'all') { - results['stream-edit'] = await testStreamEdit(); - console.log(''); - } - - if (testType === 'stream-convert' || testType === 'streaming' || testType === 'all') { - results['stream-convert'] = await testStreamConvert(); - console.log(''); - } - - if (testType === 'all') { - await testErrorHandling(); - console.log(''); - - await testConfiguration(); - console.log(''); + results['convert-basic'] = await convertBasic(); + results['convert-stream'] = await convertWithStream(); + results['convert-storage'] = await convertWithStorage(); } // Summary - console.log('📊 Test Summary'); - console.log('================'); + console.log('\n\nTest Summary'); + console.log('============'); Object.entries(results).forEach(([test, passed]) => { - console.log(`${passed ? '✅' : '❌'} ${test}: ${passed ? 'PASSED' : 'FAILED'}`); + console.log(` ${passed ? 'PASS' : 'FAIL'} ${test}`); }); - const allPassed = Object.values(results).every(result => result); - console.log( - `\n${allPassed ? '🎉' : '⚠️'} Overall: ${allPassed ? 'ALL TESTS PASSED' : 'SOME TESTS FAILED'}` - ); - - console.log('\n✨ TypeScript testing complete!'); + const passed = Object.values(results).filter(r => r).length; + const total = Object.values(results).length; + console.log(`\n${passed}/${total} passed`); } -// Run the tests runTests().catch((error: any) => { - console.error('💥 Unexpected error during testing:', error); + console.error('Unexpected error:', error); process.exit(1); }); diff --git a/tests/quality-aspectratio-constraints.test.ts b/tests/quality-aspectratio-constraints.test.ts index e83d89e..c79069a 100644 --- a/tests/quality-aspectratio-constraints.test.ts +++ b/tests/quality-aspectratio-constraints.test.ts @@ -1,6 +1,6 @@ import { SVGMakerClient } from '../src/core/SVGMakerClient'; -describe('Quality and AspectRatio Constraints', () => { +describe('V1 API Validation Constraints', () => { let client: SVGMakerClient; beforeEach(() => { @@ -10,65 +10,65 @@ describe('Quality and AspectRatio Constraints', () => { }); describe('GenerateClient', () => { - it('should apply default aspectRatio "auto" for low quality', () => { - const generateClient = client.generate.configure({ - prompt: 'test prompt', - quality: 'low', - }); - - // Access the private params through any to test defaults - expect((generateClient as any).params.aspectRatio).toBe('auto'); - }); - - it('should apply default aspectRatio "auto" for medium quality', () => { - const generateClient = client.generate.configure({ - prompt: 'test prompt', - quality: 'medium', - }); - - expect((generateClient as any).params.aspectRatio).toBe('auto'); + it('should accept all quality levels without aspectRatio constraints', () => { + // In v1 API, all quality levels support all aspect ratios + expect(() => { + client.generate.configure({ + prompt: 'test prompt', + quality: 'low', + aspectRatio: 'landscape', + }); + }).not.toThrow(); + + expect(() => { + client.generate.configure({ + prompt: 'test prompt', + quality: 'medium', + aspectRatio: 'portrait', + }); + }).not.toThrow(); + + expect(() => { + client.generate.configure({ + prompt: 'test prompt', + quality: 'high', + aspectRatio: 'auto', + }); + }).not.toThrow(); }); - it('should apply default aspectRatio "square" for high quality', () => { - const generateClient = client.generate.configure({ - prompt: 'test prompt', - quality: 'high', - }); - - expect((generateClient as any).params.aspectRatio).toBe('square'); - }); - - it('should reject invalid aspectRatio for low/medium quality', async () => { + it('should reject invalid aspectRatio values', async () => { const generateClient = client.generate.configure({ prompt: 'test prompt', quality: 'low', - aspectRatio: 'wide', // not allowed for low quality + aspectRatio: 'wide' as any, }); await expect(generateClient.execute()).rejects.toThrow(); }); - it('should reject "auto" aspectRatio for high quality', async () => { - const generateClient = client.generate.configure({ - prompt: 'test prompt', - quality: 'high', - aspectRatio: 'auto', // not allowed for high quality - }); - - await expect(generateClient.execute()).rejects.toThrow(); + it('should accept v1 style params', () => { + expect(() => { + client.generate.configure({ + prompt: 'test prompt', + styleParams: { + style: 'line_art', + color_mode: 'few_colors', + image_complexity: 'scene', + text: 'only_title', + composition: 'objects_in_grid', + }, + }); + }).not.toThrow(); }); - it('should allow valid aspectRatio combinations', async () => { - // This should not throw during validation - const generateClient = client.generate.configure({ - prompt: 'test prompt', - quality: 'high', - aspectRatio: 'wide', // allowed for high quality - }); - - // We expect this to fail at the request level (network), not validation - // Let's just test that it doesn't throw validation errors - expect(() => generateClient).not.toThrow(); + it('should accept storage parameter', () => { + expect(() => { + client.generate.configure({ + prompt: 'test prompt', + storage: true, + }); + }).not.toThrow(); }); }); @@ -82,34 +82,65 @@ describe('Quality and AspectRatio Constraints', () => { expect((editClient as any).params.quality).toBe('medium'); }); - it('should reject non-medium quality', async () => { + it('should accept all quality levels', () => { + expect(() => { + client.edit.configure({ + image: 'test.png', + prompt: 'test prompt', + quality: 'high', + }); + }).not.toThrow(); + + expect(() => { + client.edit.configure({ + image: 'test.png', + prompt: 'test prompt', + quality: 'low', + }); + }).not.toThrow(); + }); + + it('should reject invalid aspectRatio values', async () => { const editClient = client.edit.configure({ image: 'test.png', prompt: 'test prompt', - quality: 'high' as any, // TypeScript should prevent this, but testing runtime + aspectRatio: 'wide' as any, }); await expect(editClient.execute()).rejects.toThrow(); }); - it('should reject invalid aspectRatio', async () => { + it('should allow valid aspectRatio values', () => { const editClient = client.edit.configure({ image: 'test.png', prompt: 'test prompt', - aspectRatio: 'wide' as any, // not allowed for edit mode + aspectRatio: 'square', }); - await expect(editClient.execute()).rejects.toThrow(); + expect((editClient as any).params.aspectRatio).toBe('square'); }); - it('should allow valid aspectRatio values', () => { + it('should require either prompt or styleParams', async () => { const editClient = client.edit.configure({ image: 'test.png', - prompt: 'test prompt', - aspectRatio: 'square', // allowed for edit mode + // no prompt and no styleParams }); - expect((editClient as any).params.aspectRatio).toBe('square'); + await expect(editClient.execute()).rejects.toThrow( + 'Either prompt or styleParams must be provided' + ); + }); + + it('should accept styleParams without prompt', () => { + expect(() => { + client.edit.configure({ + image: 'test.png', + styleParams: { + style: 'flat', + color_mode: 'monochrome', + }, + }); + }).not.toThrow(); }); }); }); diff --git a/tests/sdk-test-commands.md b/tests/sdk-test-commands.md new file mode 100644 index 0000000..936b3a2 --- /dev/null +++ b/tests/sdk-test-commands.md @@ -0,0 +1,181 @@ +# SDK Test Commands + +Base URL: `http://localhost:3000/api` +API Key: `svgmaker-iofe8211899cae7ee6` + +> Run `npm run build` first before testing. +> Edit and Convert tests need a test image at `tests/test-images/test-image.png` + +--- + +## Generate + +### 1. With model (no quality) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 2. With quality (no model) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generate.configure({ prompt: 'A red sports car', quality: 'high' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 3. With BOTH model and quality (should fail validation) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generate.configure({ prompt: 'A cat on a fence', quality: 'high', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` + +### 4. With storage: false +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generate.configure({ prompt: 'A simple house icon', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 5. With storage: true +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 6. With stream +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const stream = client.generate.configure({ prompt: 'A rocket launching into space', quality: 'low' }).stream(); +stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); +stream.on('end', () => console.log('Stream ended')); +stream.on('error', (e) => console.error('ERROR:', e.message)); +" +``` + +### 7. With styleParams + aspectRatio + background +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generate.configure({ prompt: 'A forest with tall trees and a river', quality: 'low', aspectRatio: 'landscape', background: 'transparent', styleParams: { style: 'flat', color_mode: 'few_colors', image_complexity: 'scene', composition: 'full_scene' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +--- + +## Edit + +### 8. With model (no quality) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 9. With quality (no model) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 10. With BOTH model and quality (should fail validation) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'high', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` + +### 11. With storage: false +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Convert to silhouette style', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 12. With storage: true +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a sunset gradient', quality: 'low', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 13. With stream +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const stream = client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make it look like a cartoon', quality: 'low' }).stream(); +stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); +stream.on('end', () => console.log('Stream ended')); +stream.on('error', (e) => console.error('ERROR:', e.message)); +" +``` + +### 14. With styleParams + aspectRatio + background +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Restyle this image', quality: 'low', aspectRatio: 'square', background: 'opaque', styleParams: { style: 'isometric', color_mode: 'monochrome', composition: 'centered_object' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +--- + +## Convert (AI Vectorize) + +### 15. Without stream +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 16. With stream +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const stream = client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).stream(); +stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); +stream.on('end', () => console.log('Stream ended')); +stream.on('error', (e) => console.error('ERROR:', e.message)); +" +``` + +### 17. With storage: true +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` From d448a81b56a7aa628298c76c38b83b038f0a802d Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Fri, 6 Feb 2026 16:04:05 +0530 Subject: [PATCH 02/15] generation endpoint done --- src/clients/GenerationsClient.ts | 176 +++++++++++++++++++++++++++++++ src/core/SVGMakerClient.ts | 7 ++ src/index.ts | 2 + src/types/api.ts | 92 ++++++++++++++++ tests/sdk-test-commands.md | 133 +++++++++++++++++++---- 5 files changed, 392 insertions(+), 18 deletions(-) create mode 100644 src/clients/GenerationsClient.ts diff --git a/src/clients/GenerationsClient.ts b/src/clients/GenerationsClient.ts new file mode 100644 index 0000000..228e02d --- /dev/null +++ b/src/clients/GenerationsClient.ts @@ -0,0 +1,176 @@ +import { BaseClient } from './BaseClient'; +import { SVGMakerClient } from '../core/SVGMakerClient'; +import { z } from 'zod'; +import { + GenerationsListParams, + GenerationsListResponse, + GenerationResponse, + GenerationDeleteResponse, + GenerationShareResponse, + GenerationDownloadParams, + GenerationDownloadResponse, +} from '../types/api'; + +/** + * Schema for validating list parameters + */ +const listParamsSchema = z + .object({ + page: z.number().int().positive().optional(), + limit: z.number().int().min(1).max(100).optional(), + type: z.union([z.string(), z.array(z.string())]).optional(), + hashtags: z.union([z.string(), z.array(z.string())]).optional(), + categories: z.union([z.string(), z.array(z.string())]).optional(), + query: z.string().optional(), + }) + .strict() + .optional(); + +/** + * Schema for validating download parameters + */ +const downloadParamsSchema = z + .object({ + format: z.enum(['svg', 'webp', 'png', 'svg-optimized', 'svgz']).optional(), + optimize: z.boolean().optional(), + }) + .strict() + .optional(); + +/** + * Schema for validating generation ID + */ +const idSchema = z.string().min(1, 'Generation ID is required'); + +/** + * Client for the Generations Management API + * Provides methods to list, get, delete, share, and download generations + */ +export class GenerationsClient extends BaseClient { + /** + * Create a new Generations client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * List generations with pagination and optional filters + * @param params Optional filtering and pagination parameters + * @returns Paginated list of generation IDs + */ + async list(params?: GenerationsListParams): Promise { + this.logger.debug('Listing generations', { params }); + + // Validate parameters + if (params) { + this.validateRequest(params, listParamsSchema as z.ZodType); + } + + // Build URL with query params (handle arrays manually since buildUrl only supports scalars) + const searchParams = new URLSearchParams(); + if (params) { + if (params.page !== undefined) searchParams.append('page', String(params.page)); + if (params.limit !== undefined) searchParams.append('limit', String(params.limit)); + if (params.query !== undefined) searchParams.append('query', params.query); + + // Handle array params (type=generate&type=edit) + const appendArray = (key: string, value?: string | string[]) => { + if (!value) return; + const values = Array.isArray(value) ? value : [value]; + values.forEach(v => searchParams.append(key, v)); + }; + appendArray('type', params.type); + appendArray('hashtags', params.hashtags); + appendArray('categories', params.categories); + } + + const queryString = searchParams.toString(); + const url = `/v1/generations${queryString ? `?${queryString}` : ''}`; + + return this.handleRequest(url, { method: 'GET' }); + } + + /** + * Get details of a single generation by ID + * @param id Generation ID + * @returns Generation details + */ + async get(id: string): Promise { + this.logger.debug('Getting generation', { id }); + + this.validateRequest(id, idSchema); + + return this.handleRequest(`/v1/generations/${encodeURIComponent(id)}`, { + method: 'GET', + }); + } + + /** + * Delete a generation and its associated files (paid users only) + * @param id Generation ID + * @returns Deletion confirmation + */ + async delete(id: string): Promise { + this.logger.debug('Deleting generation', { id }); + + this.validateRequest(id, idSchema); + + return this.handleRequest( + `/v1/generations/${encodeURIComponent(id)}`, + { method: 'DELETE' } + ); + } + + /** + * Share a generation (make it public) + * @param id Generation ID + * @returns Share confirmation with public URL + */ + async share(id: string): Promise { + this.logger.debug('Sharing generation', { id }); + + this.validateRequest(id, idSchema); + + return this.handleRequest( + `/v1/generations/${encodeURIComponent(id)}/share`, + { method: 'POST' } + ); + } + + /** + * Download a generation in various formats (paid users only) + * @param id Generation ID + * @param params Optional download parameters (format, optimize) + * @returns Download URL and file metadata + */ + async download( + id: string, + params?: GenerationDownloadParams + ): Promise { + this.logger.debug('Downloading generation', { id, params }); + + this.validateRequest(id, idSchema); + if (params) { + this.validateRequest(params, downloadParamsSchema as z.ZodType); + } + + const queryParams: Record = {}; + if (params?.format) queryParams.format = params.format; + if (params?.optimize !== undefined) queryParams.optimize = String(params.optimize); + + return this.handleRequest( + `/v1/generations/${encodeURIComponent(id)}/download`, + { method: 'GET', params: queryParams as Record } + ); + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): GenerationsClient { + return new GenerationsClient(this.client); + } +} diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index f60fac1..bab65ae 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -4,6 +4,7 @@ import { ValidationError } from '../errors/CustomErrors'; import { GenerateClient } from '../clients/GenerateClient'; import { EditClient } from '../clients/EditClient'; import { AIVectorizeClient } from '../clients/convert'; +import { GenerationsClient } from '../clients/GenerationsClient'; import { createRetryWrapper } from '../utils/retry'; import { createRateLimiter } from '../utils/rateLimit'; import { Logger, createLogger } from '../utils/logger'; @@ -65,6 +66,11 @@ export class SVGMakerClient { aiVectorize: AIVectorizeClient; }; + /** + * Generations management client + */ + public readonly generations: GenerationsClient; + /** * Create a new SVGMaker client * @param apiKey API key for authentication @@ -103,6 +109,7 @@ export class SVGMakerClient { this.convert = { aiVectorize: new AIVectorizeClient(this), }; + this.generations = new GenerationsClient(this); this.logger.info('SVGMaker SDK initialized'); } diff --git a/src/index.ts b/src/index.ts index 0c8363c..dfa4be3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { GenerateClient } from './clients/GenerateClient'; import { EditClient } from './clients/EditClient'; import { AIVectorizeClient } from './clients/convert'; import { ConvertClient } from './clients/ConvertClient'; +import { GenerationsClient } from './clients/GenerationsClient'; // Export error classes import * as Errors from './errors/CustomErrors'; @@ -26,6 +27,7 @@ export { EditClient, AIVectorizeClient, ConvertClient, // backward compat + GenerationsClient, // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index 7abb20c..2c22373 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -304,3 +304,95 @@ export type ConvertStreamEvent = StreamEvent; * AI Vectorize stream event */ export type AiVectorizeStreamEvent = StreamEvent; + +// --- Generations Management Types --- + +export interface GenerationsListParams { + /** Page number (1-indexed) */ + page?: number; + /** Number of items per page (1-100) */ + limit?: number; + /** Filter by type. Can be a single type or array of types */ + type?: string | string[]; + /** Filter by hashtag. Can be a single hashtag or array */ + hashtags?: string | string[]; + /** Filter by category. Can be a single category or array */ + categories?: string | string[]; + /** Search query for prompt/description */ + query?: string; +} + +export interface Pagination { + page: number; + limit: number; + totalItems: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; +} + +export interface GenerationsListResponse { + /** Array of generation IDs */ + items: string[]; + /** Pagination information */ + pagination: Pagination; + /** Response metadata */ + metadata: ResponseMetadata; +} + +export interface GenerationResponse { + /** Generation ID */ + id: string; + /** The prompt used to generate the image */ + prompt: string; + /** Generation type: generate, edit, convert, or crafted */ + type: string; + /** Quality level used */ + quality: string; + /** Whether the generation is public */ + isPublic: boolean; + /** Response metadata */ + metadata: ResponseMetadata; +} + +export interface GenerationDeleteResponse { + /** Status message */ + message: string; + /** Response metadata */ + metadata: ResponseMetadata; +} + +export interface GenerationShareResponse { + /** Status message */ + message: string; + /** Whether the generation is now public */ + isPublic: boolean; + /** The public share URL */ + shareUrl: string; + /** Response metadata */ + metadata: ResponseMetadata; +} + +export type DownloadFormat = 'svg' | 'webp' | 'png' | 'svg-optimized' | 'svgz'; + +export interface GenerationDownloadParams { + /** Output format (default: webp) */ + format?: DownloadFormat; + /** Optimize before compressing (only for svgz format) */ + optimize?: boolean; +} + +export interface GenerationDownloadResponse { + /** Generation ID */ + id: string; + /** Download URL (expires after 12h) */ + url: string; + /** URL expiration time */ + urlExpiresIn: string; + /** File format */ + format: string; + /** Suggested filename */ + filename: string; + /** Response metadata */ + metadata: ResponseMetadata; +} diff --git a/tests/sdk-test-commands.md b/tests/sdk-test-commands.md index 936b3a2..7a2948d 100644 --- a/tests/sdk-test-commands.md +++ b/tests/sdk-test-commands.md @@ -1,7 +1,7 @@ # SDK Test Commands Base URL: `http://localhost:3000/api` -API Key: `svgmaker-iofe8211899cae7ee6` +API Key: `svgmaker-ioa7eda109690877e6` > Run `npm run build` first before testing. > Edit and Convert tests need a test image at `tests/test-images/test-image.png` @@ -14,7 +14,7 @@ API Key: `svgmaker-iofe8211899cae7ee6` ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -23,7 +23,7 @@ client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'g ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A red sports car', quality: 'high' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -32,7 +32,7 @@ client.generate.configure({ prompt: 'A red sports car', quality: 'high' }).execu ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A cat on a fence', quality: 'high', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` @@ -41,7 +41,7 @@ client.generate.configure({ prompt: 'A cat on a fence', quality: 'high', model: ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A simple house icon', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -50,7 +50,7 @@ client.generate.configure({ prompt: 'A simple house icon', quality: 'low', stora ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -59,7 +59,7 @@ client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storag ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); const stream = client.generate.configure({ prompt: 'A rocket launching into space', quality: 'low' }).stream(); stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); stream.on('end', () => console.log('Stream ended')); @@ -71,7 +71,7 @@ stream.on('error', (e) => console.error('ERROR:', e.message)); ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A forest with tall trees and a river', quality: 'low', aspectRatio: 'landscape', background: 'transparent', styleParams: { style: 'flat', color_mode: 'few_colors', image_complexity: 'scene', composition: 'full_scene' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -84,7 +84,7 @@ client.generate.configure({ prompt: 'A forest with tall trees and a river', qual ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -93,7 +93,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -102,7 +102,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'high', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` @@ -111,7 +111,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Convert to silhouette style', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -120,7 +120,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Conv ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a sunset gradient', quality: 'low', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -129,7 +129,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); const stream = client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make it look like a cartoon', quality: 'low' }).stream(); stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); stream.on('end', () => console.log('Stream ended')); @@ -141,7 +141,7 @@ stream.on('error', (e) => console.error('ERROR:', e.message)); ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Restyle this image', quality: 'low', aspectRatio: 'square', background: 'opaque', styleParams: { style: 'isometric', color_mode: 'monochrome', composition: 'centered_object' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -154,7 +154,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Rest ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -163,7 +163,7 @@ client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); const stream = client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).stream(); stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); stream.on('end', () => console.log('Stream ended')); @@ -175,7 +175,104 @@ stream.on('error', (e) => console.error('ERROR:', e.message)); ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-iofe8211899cae7ee6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` + +--- + +## Generations Management + +### 18. List generations (default) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 19. List generations (with filters) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 20. List generations (with query search) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 21. Get generation by ID +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.get('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 22. Share generation +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.share('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 23. Download generation (as SVG) +> Paid users only. +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.download('jhn1KSCWlR07Xkd4Sc0d', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 24. Download generation (default format) +> Paid users only. +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.download('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 25. Delete generation +> Paid users only. This is destructive! +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.delete('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 26. Get generation (invalid ID — should fail) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` + +### 27. List generations (invalid limit — should fail validation) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` From 20b53a4e637bda0d6f3e7a34af71e798c11c1818 Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Fri, 6 Feb 2026 16:11:42 +0530 Subject: [PATCH 03/15] gallery endpoint done --- src/clients/GalleryClient.ts | 146 +++++++++++++++++++++++++++ src/core/SVGMakerClient.ts | 7 ++ src/index.ts | 2 + src/types/api.ts | 33 +++++++ tests/sdk-test-commands.md | 185 ++++++++++++++++++++++++++++------- 5 files changed, 337 insertions(+), 36 deletions(-) create mode 100644 src/clients/GalleryClient.ts diff --git a/src/clients/GalleryClient.ts b/src/clients/GalleryClient.ts new file mode 100644 index 0000000..0be3d16 --- /dev/null +++ b/src/clients/GalleryClient.ts @@ -0,0 +1,146 @@ +import { BaseClient } from './BaseClient'; +import { SVGMakerClient } from '../core/SVGMakerClient'; +import { z } from 'zod'; +import { + GalleryListParams, + GalleryListResponse, + GalleryItemResponse, + GalleryDownloadParams, + GalleryDownloadResponse, +} from '../types/api'; + +/** + * Schema for validating gallery list parameters + */ +const listParamsSchema = z + .object({ + page: z.number().int().positive().optional(), + limit: z.number().int().min(1).max(100).optional(), + type: z.union([z.string(), z.array(z.string())]).optional(), + hashtags: z.union([z.string(), z.array(z.string())]).optional(), + categories: z.union([z.string(), z.array(z.string())]).optional(), + query: z.string().optional(), + pro: z.string().optional(), + gold: z.string().optional(), + }) + .strict() + .optional(); + +/** + * Schema for validating download parameters + */ +const downloadParamsSchema = z + .object({ + format: z.enum(['svg', 'webp', 'png', 'svg-optimized', 'svgz']).optional(), + optimize: z.boolean().optional(), + }) + .strict() + .optional(); + +/** + * Schema for validating gallery item ID + */ +const idSchema = z.string().min(1, 'Gallery item ID is required'); + +/** + * Client for the Gallery API + * Provides methods to browse, get details, and download public gallery items + */ +export class GalleryClient extends BaseClient { + /** + * Create a new Gallery client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Browse the public gallery with pagination and optional filters + * @param params Optional filtering and pagination parameters + * @returns Paginated list of gallery item IDs + */ + async list(params?: GalleryListParams): Promise { + this.logger.debug('Browsing gallery', { params }); + + // Validate parameters + if (params) { + this.validateRequest(params, listParamsSchema as z.ZodType); + } + + // Build URL with query params (handle arrays manually since buildUrl only supports scalars) + const searchParams = new URLSearchParams(); + if (params) { + if (params.page !== undefined) searchParams.append('page', String(params.page)); + if (params.limit !== undefined) searchParams.append('limit', String(params.limit)); + if (params.query !== undefined) searchParams.append('query', params.query); + if (params.pro !== undefined) searchParams.append('pro', params.pro); + if (params.gold !== undefined) searchParams.append('gold', params.gold); + + // Handle array params (type=generate&type=edit) + const appendArray = (key: string, value?: string | string[]) => { + if (!value) return; + const values = Array.isArray(value) ? value : [value]; + values.forEach(v => searchParams.append(key, v)); + }; + appendArray('type', params.type); + appendArray('hashtags', params.hashtags); + appendArray('categories', params.categories); + } + + const queryString = searchParams.toString(); + const url = `/v1/gallery${queryString ? `?${queryString}` : ''}`; + + return this.handleRequest(url, { method: 'GET' }); + } + + /** + * Get details of a single gallery item by ID + * @param id Gallery item ID + * @returns Gallery item details + */ + async get(id: string): Promise { + this.logger.debug('Getting gallery item', { id }); + + this.validateRequest(id, idSchema); + + return this.handleRequest(`/v1/gallery/${encodeURIComponent(id)}`, { + method: 'GET', + }); + } + + /** + * Download a gallery item in various formats + * @param id Gallery item ID + * @param params Optional download parameters (format, optimize) + * @returns Download URL and file metadata + */ + async download( + id: string, + params?: GalleryDownloadParams + ): Promise { + this.logger.debug('Downloading gallery item', { id, params }); + + this.validateRequest(id, idSchema); + if (params) { + this.validateRequest(params, downloadParamsSchema as z.ZodType); + } + + const queryParams: Record = {}; + if (params?.format) queryParams.format = params.format; + if (params?.optimize !== undefined) queryParams.optimize = String(params.optimize); + + return this.handleRequest( + `/v1/gallery/${encodeURIComponent(id)}/download`, + { method: 'GET', params: queryParams as Record } + ); + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): GalleryClient { + return new GalleryClient(this.client); + } +} diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index bab65ae..7b750a3 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -5,6 +5,7 @@ import { GenerateClient } from '../clients/GenerateClient'; import { EditClient } from '../clients/EditClient'; import { AIVectorizeClient } from '../clients/convert'; import { GenerationsClient } from '../clients/GenerationsClient'; +import { GalleryClient } from '../clients/GalleryClient'; import { createRetryWrapper } from '../utils/retry'; import { createRateLimiter } from '../utils/rateLimit'; import { Logger, createLogger } from '../utils/logger'; @@ -71,6 +72,11 @@ export class SVGMakerClient { */ public readonly generations: GenerationsClient; + /** + * Gallery client for browsing and downloading public gallery items + */ + public readonly gallery: GalleryClient; + /** * Create a new SVGMaker client * @param apiKey API key for authentication @@ -110,6 +116,7 @@ export class SVGMakerClient { aiVectorize: new AIVectorizeClient(this), }; this.generations = new GenerationsClient(this); + this.gallery = new GalleryClient(this); this.logger.info('SVGMaker SDK initialized'); } diff --git a/src/index.ts b/src/index.ts index dfa4be3..3c6f82c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { EditClient } from './clients/EditClient'; import { AIVectorizeClient } from './clients/convert'; import { ConvertClient } from './clients/ConvertClient'; import { GenerationsClient } from './clients/GenerationsClient'; +import { GalleryClient } from './clients/GalleryClient'; // Export error classes import * as Errors from './errors/CustomErrors'; @@ -28,6 +29,7 @@ export { AIVectorizeClient, ConvertClient, // backward compat GenerationsClient, + GalleryClient, // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index 2c22373..c6bf041 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -396,3 +396,36 @@ export interface GenerationDownloadResponse { /** Response metadata */ metadata: ResponseMetadata; } + +// --- Gallery Types --- + +export interface GalleryListParams { + /** Page number (1-indexed) */ + page?: number; + /** Number of items per page (1-100) */ + limit?: number; + /** Filter by type. Can be a single type or array of types */ + type?: string | string[]; + /** Filter by hashtag. Can be a single hashtag or array */ + hashtags?: string | string[]; + /** Filter by category. Can be a single category or array */ + categories?: string | string[]; + /** Search query for prompt/description */ + query?: string; + /** Filter for pro images. Pass "true" to filter. */ + pro?: string; + /** Filter for gold images. Pass "true" to filter. */ + gold?: string; +} + +/** Gallery list response - same structure as GenerationsListResponse */ +export type GalleryListResponse = GenerationsListResponse; + +/** Gallery item response - same structure as GenerationResponse */ +export type GalleryItemResponse = GenerationResponse; + +/** Gallery download params - same structure as GenerationDownloadParams */ +export type GalleryDownloadParams = GenerationDownloadParams; + +/** Gallery download response - same structure as GenerationDownloadResponse */ +export type GalleryDownloadResponse = GenerationDownloadResponse; diff --git a/tests/sdk-test-commands.md b/tests/sdk-test-commands.md index 7a2948d..8d364a5 100644 --- a/tests/sdk-test-commands.md +++ b/tests/sdk-test-commands.md @@ -1,7 +1,7 @@ # SDK Test Commands Base URL: `http://localhost:3000/api` -API Key: `svgmaker-ioa7eda109690877e6` +API Key: `svgmaker-io7791b35175f510df` > Run `npm run build` first before testing. > Edit and Convert tests need a test image at `tests/test-images/test-image.png` @@ -14,7 +14,7 @@ API Key: `svgmaker-ioa7eda109690877e6` ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -23,7 +23,7 @@ client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'g ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A red sports car', quality: 'high' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -32,7 +32,7 @@ client.generate.configure({ prompt: 'A red sports car', quality: 'high' }).execu ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A cat on a fence', quality: 'high', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` @@ -41,7 +41,7 @@ client.generate.configure({ prompt: 'A cat on a fence', quality: 'high', model: ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A simple house icon', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -50,7 +50,7 @@ client.generate.configure({ prompt: 'A simple house icon', quality: 'low', stora ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -59,7 +59,7 @@ client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storag ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); const stream = client.generate.configure({ prompt: 'A rocket launching into space', quality: 'low' }).stream(); stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); stream.on('end', () => console.log('Stream ended')); @@ -71,7 +71,7 @@ stream.on('error', (e) => console.error('ERROR:', e.message)); ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generate.configure({ prompt: 'A forest with tall trees and a river', quality: 'low', aspectRatio: 'landscape', background: 'transparent', styleParams: { style: 'flat', color_mode: 'few_colors', image_complexity: 'scene', composition: 'full_scene' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -84,7 +84,7 @@ client.generate.configure({ prompt: 'A forest with tall trees and a river', qual ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -93,7 +93,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -102,7 +102,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'high', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` @@ -111,7 +111,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Convert to silhouette style', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -120,7 +120,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Conv ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a sunset gradient', quality: 'low', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -129,7 +129,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); const stream = client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make it look like a cartoon', quality: 'low' }).stream(); stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); stream.on('end', () => console.log('Stream ended')); @@ -141,7 +141,7 @@ stream.on('error', (e) => console.error('ERROR:', e.message)); ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Restyle this image', quality: 'low', aspectRatio: 'square', background: 'opaque', styleParams: { style: 'isometric', color_mode: 'monochrome', composition: 'centered_object' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -154,7 +154,7 @@ client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Rest ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -163,7 +163,7 @@ client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); const stream = client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).stream(); stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); stream.on('end', () => console.log('Stream ended')); @@ -175,7 +175,7 @@ stream.on('error', (e) => console.error('ERROR:', e.message)); ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -188,7 +188,7 @@ client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generations.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -197,7 +197,7 @@ client.generations.list().then(r => { console.log('Items:', r.items); console.lo ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -206,7 +206,7 @@ client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then( ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -215,8 +215,8 @@ client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.lo ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); -client.generations.get('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.get('bIJsOodcweYs5PriL7Bl').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -224,8 +224,8 @@ client.generations.get('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('ID:', r. ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); -client.generations.share('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.share('bIJsOodcweYs5PriL7Bl').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -234,8 +234,8 @@ client.generations.share('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('Messag ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); -client.generations.download('jhn1KSCWlR07Xkd4Sc0d', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.download('bIJsOodcweYs5PriL7Bl', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` @@ -244,35 +244,148 @@ client.generations.download('jhn1KSCWlR07Xkd4Sc0d', { format: 'svg' }).then(r => ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); -client.generations.download('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.download('bIJsOodcweYs5PriL7Bl').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` -### 25. Delete generation +### 25. Download generation (as SVGZ) +> Paid users only. +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.download('bIJsOodcweYs5PriL7Bl', { format: 'svgz' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 26. Delete generation > Paid users only. This is destructive! ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); -client.generations.delete('jhn1KSCWlR07Xkd4Sc0d').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.generations.delete('bIJsOodcweYs5PriL7Bl').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " ``` -### 26. Get generation (invalid ID — should fail) +### 27. Get generation (invalid ID — should fail) ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generations.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` -### 27. List generations (invalid limit — should fail validation) +### 28. List generations (invalid limit — should fail validation) ```bash node -e " const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('svgmaker-ioa7eda109690877e6', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); client.generations.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` + +--- + +## Gallery + +### 29. Browse gallery (default) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 30. Browse gallery (with filters) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 31. Browse gallery (pro filter) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.list({ pro: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 32. Browse gallery (gold filter) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.list({ gold: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 33. Browse gallery (with query search) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 34. Get gallery item by ID +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.get('B5EMo3ORhHtpBEnraspx').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 35. Download gallery item (as SVG — costs 1 credit) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.download('B5EMo3ORhHtpBEnraspx', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 36. Download gallery item (as WebP — free) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.download('B5EMo3ORhHtpBEnraspx', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 37. Download gallery item (default format) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.download('B5EMo3ORhHtpBEnraspx').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 38. Get gallery item (invalid ID — should fail) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` + +### 39. Browse gallery (invalid limit — should fail validation) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.gallery.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` From 683351c99113111d3957a929ea26fb62bd4090a8 Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Fri, 6 Feb 2026 16:20:44 +0530 Subject: [PATCH 04/15] account endpoint done --- src/clients/AccountClient.ts | 89 ++++++++++++++++++++++++++++++++++++ src/core/SVGMakerClient.ts | 7 +++ src/index.ts | 2 + src/types/api.ts | 72 +++++++++++++++++++++++++++++ tests/sdk-test-commands.md | 49 ++++++++++++++++++++ 5 files changed, 219 insertions(+) create mode 100644 src/clients/AccountClient.ts diff --git a/src/clients/AccountClient.ts b/src/clients/AccountClient.ts new file mode 100644 index 0000000..7bdd6af --- /dev/null +++ b/src/clients/AccountClient.ts @@ -0,0 +1,89 @@ +import { BaseClient } from './BaseClient'; +import { SVGMakerClient } from '../core/SVGMakerClient'; +import { z } from 'zod'; +import { + AccountResponse, + AccountUsageParams, + AccountUsageResponse, +} from '../types/api'; + +/** + * Schema for validating usage parameters + */ +const usageParamsSchema = z + .object({ + days: z.number().int().positive().optional(), + start: z.string().optional(), + end: z.string().optional(), + }) + .strict() + .refine( + (data) => !(data.days !== undefined && (data.start !== undefined || data.end !== undefined)), + { message: 'Use either days or start/end, not both.' } + ) + .refine( + (data) => { + if (data.start !== undefined || data.end !== undefined) { + return data.start !== undefined && data.end !== undefined; + } + return true; + }, + { message: 'Both start and end dates are required when using date range.' } + ) + .optional(); + +/** + * Client for the Account API + * Provides methods to get account information and usage statistics + */ +export class AccountClient extends BaseClient { + /** + * Create a new Account client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Get account information including email, display name, account type, and credit balance + * @returns Account information + */ + async getInfo(): Promise { + this.logger.debug('Getting account information'); + + return this.handleRequest('/v1/account', { method: 'GET' }); + } + + /** + * Get API usage statistics for your account + * @param params Optional parameters for date filtering (days or start/end range) + * @returns Usage statistics + */ + async getUsage(params?: AccountUsageParams): Promise { + this.logger.debug('Getting usage statistics', { params }); + + // Validate parameters + if (params) { + this.validateRequest(params, usageParamsSchema as z.ZodType); + } + + const queryParams: Record = {}; + if (params?.days !== undefined) queryParams.days = String(params.days); + if (params?.start !== undefined) queryParams.start = params.start; + if (params?.end !== undefined) queryParams.end = params.end; + + return this.handleRequest('/v1/account/usage', { + method: 'GET', + params: Object.keys(queryParams).length > 0 ? queryParams : undefined, + }); + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): AccountClient { + return new AccountClient(this.client); + } +} diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index 7b750a3..03e5263 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -6,6 +6,7 @@ import { EditClient } from '../clients/EditClient'; import { AIVectorizeClient } from '../clients/convert'; import { GenerationsClient } from '../clients/GenerationsClient'; import { GalleryClient } from '../clients/GalleryClient'; +import { AccountClient } from '../clients/AccountClient'; import { createRetryWrapper } from '../utils/retry'; import { createRateLimiter } from '../utils/rateLimit'; import { Logger, createLogger } from '../utils/logger'; @@ -77,6 +78,11 @@ export class SVGMakerClient { */ public readonly gallery: GalleryClient; + /** + * Account client for retrieving account info and usage statistics + */ + public readonly account: AccountClient; + /** * Create a new SVGMaker client * @param apiKey API key for authentication @@ -117,6 +123,7 @@ export class SVGMakerClient { }; this.generations = new GenerationsClient(this); this.gallery = new GalleryClient(this); + this.account = new AccountClient(this); this.logger.info('SVGMaker SDK initialized'); } diff --git a/src/index.ts b/src/index.ts index 3c6f82c..d40eb29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { AIVectorizeClient } from './clients/convert'; import { ConvertClient } from './clients/ConvertClient'; import { GenerationsClient } from './clients/GenerationsClient'; import { GalleryClient } from './clients/GalleryClient'; +import { AccountClient } from './clients/AccountClient'; // Export error classes import * as Errors from './errors/CustomErrors'; @@ -30,6 +31,7 @@ export { ConvertClient, // backward compat GenerationsClient, GalleryClient, + AccountClient, // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index c6bf041..345da83 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -429,3 +429,75 @@ export type GalleryDownloadParams = GenerationDownloadParams; /** Gallery download response - same structure as GenerationDownloadResponse */ export type GalleryDownloadResponse = GenerationDownloadResponse; + +// --- Account Types --- + +export interface AccountResponse { + /** User's email address */ + email: string; + /** User's display name */ + displayName: string; + /** Account type: "free" or "premium" */ + accountType: string; + /** Current credit balance */ + credits: number; + /** Response metadata */ + metadata: ResponseMetadata; +} + +export interface AccountUsageParams { + /** Number of days to look back (must be positive integer). Cannot be used with start/end. */ + days?: number; + /** Start date in YYYY-MM-DD format. Must be used with end. Cannot be used with days. */ + start?: string; + /** End date in YYYY-MM-DD format. Must be used with start. Cannot be used with days. */ + end?: string; +} + +export interface UsagePeriod { + /** Period type: "all", "days", or "range" */ + type: string; + /** Number of days (only present when type is "days") */ + days?: number; + /** Start date in YYYY-MM-DD format */ + from?: string; + /** End date in YYYY-MM-DD format */ + to?: string; +} + +export interface UsageSummary { + /** Total number of API requests */ + requests: number; + /** Total credits used */ + creditsUsed: number; + /** Number of successful requests */ + successCount: number; + /** Number of failed requests */ + errorCount: number; + /** Success rate (0-1) */ + successRate: number; +} + +export interface UsageDailyEntry { + /** Date in YYYY-MM-DD format */ + date: string; + /** Number of requests on this date */ + requests: number; + /** Credits used on this date */ + credits: number; +} + +export interface AccountUsageResponse { + /** Period information */ + period: UsagePeriod; + /** Summary statistics */ + summary: UsageSummary; + /** Usage statistics grouped by category */ + byCategory: Record; + /** Daily breakdown (only present when date range is specified) */ + daily?: UsageDailyEntry[]; + /** All-time totals (only present when date range is specified) */ + allTime?: { requests: number; creditsUsed: number }; + /** Response metadata */ + metadata: ResponseMetadata; +} diff --git a/tests/sdk-test-commands.md b/tests/sdk-test-commands.md index 8d364a5..d3298d0 100644 --- a/tests/sdk-test-commands.md +++ b/tests/sdk-test-commands.md @@ -389,3 +389,52 @@ const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'htt client.gallery.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` + +--- + +## Account + +### 40. Get account information +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.account.getInfo().then(r => { console.log('Email:', r.email); console.log('Display Name:', r.displayName); console.log('Account Type:', r.accountType); console.log('Credits:', r.credits); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 41. Get all-time usage statistics +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.account.getUsage().then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('By Category:', JSON.stringify(r.byCategory, null, 2)); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 42. Get usage for last 7 days +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.account.getUsage({ days: 7 }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 43. Get usage for date range +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.account.getUsage({ start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 44. Get usage with both days and start/end (should fail validation) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" +``` From 3f70dded8db10219390583e545e77c24b3674e6c Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Fri, 6 Feb 2026 16:43:08 +0530 Subject: [PATCH 05/15] svg-optimise route done --- src/clients/BaseClient.ts | 58 +++++++++++++ src/clients/EditClient.ts | 61 -------------- src/clients/OptimizeSvgClient.ts | 101 +++++++++++++++++++++++ src/clients/convert/AIVectorizeClient.ts | 61 -------------- src/core/SVGMakerClient.ts | 7 ++ src/index.ts | 2 + src/types/api.ts | 26 ++++++ tests/sdk-test-commands.md | 22 +++++ 8 files changed, 216 insertions(+), 122 deletions(-) create mode 100644 src/clients/OptimizeSvgClient.ts diff --git a/src/clients/BaseClient.ts b/src/clients/BaseClient.ts index 4730756..106663c 100644 --- a/src/clients/BaseClient.ts +++ b/src/clients/BaseClient.ts @@ -15,6 +15,9 @@ import { import { ResponseMetadata } from '../types/api'; import { Logger } from '../utils/logger'; import { z } from 'zod'; +import { Readable } from 'stream'; +import * as fs from 'fs'; +import * as path from 'path'; /** * Base client for API endpoints @@ -112,6 +115,61 @@ export abstract class BaseClient { } } + /** + * Add a file to the form data + * @param formData Form data + * @param fieldName Form field name + * @param file File path, buffer, or stream + */ + protected async addFileToForm( + formData: FormData, + fieldName: string, + file: string | Buffer | Readable + ): Promise { + if (typeof file === 'string') { + if (!fs.existsSync(file)) { + throw new ValidationError(`File not found: ${file}`); + } + const fileBuffer = fs.readFileSync(file); + const filename = path.basename(file); + const mimeType = this.getMimeType(filename); + const blob = new Blob([fileBuffer], { type: mimeType }); + formData.append(fieldName, blob, filename); + } else if (Buffer.isBuffer(file)) { + const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' }); + formData.append(fieldName, blob, 'file'); + } else if (file instanceof Readable) { + const chunks: Buffer[] = []; + for await (const chunk of file) { + chunks.push(Buffer.from(chunk)); + } + const buffer = Buffer.concat(chunks); + const blob = new Blob([buffer], { type: 'application/octet-stream' }); + formData.append(fieldName, blob, 'file'); + } else { + throw new ValidationError(`Invalid file type: ${typeof file}`); + } + } + + /** + * Get MIME type from filename + * @param filename File name + * @returns MIME type + */ + protected getMimeType(filename: string): string { + const ext = path.extname(filename).toLowerCase(); + const mimeTypes: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + '.bmp': 'image/bmp', + }; + return mimeTypes[ext] || 'application/octet-stream'; + } + /** * Create a new instance of this client * This should be implemented by child classes to support method chaining diff --git a/src/clients/EditClient.ts b/src/clients/EditClient.ts index ab8597d..1c66ffc 100644 --- a/src/clients/EditClient.ts +++ b/src/clients/EditClient.ts @@ -383,67 +383,6 @@ export class EditClient extends BaseClient { return stream; } - /** - * Add a file to the form data - * @param formData Form data - * @param fieldName Form field name - * @param file File path, buffer, or stream - */ - private async addFileToForm( - formData: FormData, - fieldName: string, - file: string | Buffer | Readable - ): Promise { - if (typeof file === 'string') { - // Check if the file exists - if (!fs.existsSync(file)) { - throw new ValidationError(`File not found: ${file}`); - } - - // Read file and create a Blob - const fileBuffer = fs.readFileSync(file); - const filename = path.basename(file); - const mimeType = this.getMimeType(filename); - - const blob = new Blob([fileBuffer], { type: mimeType }); - formData.append(fieldName, blob, filename); - } else if (Buffer.isBuffer(file)) { - // Convert buffer to Blob - const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' }); - formData.append(fieldName, blob, 'file'); - } else if (file instanceof Readable) { - // Convert stream to buffer then to Blob - const chunks: Buffer[] = []; - for await (const chunk of file) { - chunks.push(Buffer.from(chunk)); - } - const buffer = Buffer.concat(chunks); - const blob = new Blob([buffer], { type: 'application/octet-stream' }); - formData.append(fieldName, blob, 'file'); - } else { - throw new ValidationError(`Invalid file type: ${typeof file}`); - } - } - - /** - * Get MIME type from filename - * @param filename File name - * @returns MIME type - */ - private getMimeType(filename: string): string { - const ext = path.extname(filename).toLowerCase(); - const mimeTypes: Record = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.webp': 'image/webp', - '.bmp': 'image/bmp', - }; - return mimeTypes[ext] || 'application/octet-stream'; - } - /** * Create a clone of this client * @returns New client instance diff --git a/src/clients/OptimizeSvgClient.ts b/src/clients/OptimizeSvgClient.ts new file mode 100644 index 0000000..3a12dea --- /dev/null +++ b/src/clients/OptimizeSvgClient.ts @@ -0,0 +1,101 @@ +import { BaseClient } from './BaseClient'; +import { OptimizeSvgParams, OptimizeSvgResponse } from '../types/api'; +import { SVGMakerClient } from '../core/SVGMakerClient'; +import { z } from 'zod'; + +/** + * Schema for validating optimize SVG parameters + */ +const optimizeSvgParamsSchema = z.object({ + file: z.string(), + compress: z.boolean().optional(), +}); + +/** + * Client for the Optimize SVG API + */ +export class OptimizeSvgClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new Optimize SVG client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the Optimize SVG request + * @returns Optimize SVG response + */ + public async execute(): Promise { + this.logger.debug('Starting SVG optimization'); + + // Validate parameters + this.validateRequest(this.params, optimizeSvgParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', this.params.file!); + + // Add compress option if present + if (this.params.compress !== undefined) { + formData.append('compress', String(this.params.compress)); + } + + // Execute request using native fetch + const response = await fetch(`${this.config.baseUrl}/v1/svg/optimize`, { + method: 'POST', + headers: { + 'x-api-key': this.config.apiKey, + }, + body: formData, + }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const rawResult = await response.json(); + const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); + + this.logger.debug('SVG optimization completed'); + + return { + svgUrl: data.svgUrl, + svgUrlExpiresIn: data.svgUrlExpiresIn, + svgzUrl: data.svgzUrl, + svgzUrlExpiresIn: data.svgzUrlExpiresIn, + filename: data.filename, + compressedSize: data.compressedSize, + metadata: responseMetadata, + }; + } + + /** + * Configure the optimize SVG parameters + * @param config Configuration object with optimize SVG parameters + * @returns New client instance + */ + public configure(config: Partial): OptimizeSvgClient { + this.logger.debug('Configuring optimize SVG parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): OptimizeSvgClient { + const client = new OptimizeSvgClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/convert/AIVectorizeClient.ts b/src/clients/convert/AIVectorizeClient.ts index fe33dcf..c36a7ec 100644 --- a/src/clients/convert/AIVectorizeClient.ts +++ b/src/clients/convert/AIVectorizeClient.ts @@ -248,67 +248,6 @@ export class AIVectorizeClient extends BaseClient { return stream; } - /** - * Add a file to the form data - * @param formData Form data - * @param fieldName Form field name - * @param file File path, buffer, or stream - */ - private async addFileToForm( - formData: FormData, - fieldName: string, - file: string | Buffer | Readable - ): Promise { - if (typeof file === 'string') { - // Check if the file exists - if (!fs.existsSync(file)) { - throw new ValidationError(`File not found: ${file}`); - } - - // Read file and create a Blob - const fileBuffer = fs.readFileSync(file); - const filename = path.basename(file); - const mimeType = this.getMimeType(filename); - - const blob = new Blob([fileBuffer], { type: mimeType }); - formData.append(fieldName, blob, filename); - } else if (Buffer.isBuffer(file)) { - // Convert buffer to Blob - const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' }); - formData.append(fieldName, blob, 'file'); - } else if (file instanceof Readable) { - // Convert stream to buffer then to Blob - const chunks: Buffer[] = []; - for await (const chunk of file) { - chunks.push(Buffer.from(chunk)); - } - const buffer = Buffer.concat(chunks); - const blob = new Blob([buffer], { type: 'application/octet-stream' }); - formData.append(fieldName, blob, 'file'); - } else { - throw new ValidationError(`Invalid file type: ${typeof file}`); - } - } - - /** - * Get MIME type from filename - * @param filename File name - * @returns MIME type - */ - private getMimeType(filename: string): string { - const ext = path.extname(filename).toLowerCase(); - const mimeTypes: Record = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.webp': 'image/webp', - '.bmp': 'image/bmp', - }; - return mimeTypes[ext] || 'application/octet-stream'; - } - /** * Create a clone of this client * @returns New client instance diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index 03e5263..decf2f4 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -7,6 +7,7 @@ import { AIVectorizeClient } from '../clients/convert'; import { GenerationsClient } from '../clients/GenerationsClient'; import { GalleryClient } from '../clients/GalleryClient'; import { AccountClient } from '../clients/AccountClient'; +import { OptimizeSvgClient } from '../clients/OptimizeSvgClient'; import { createRetryWrapper } from '../utils/retry'; import { createRateLimiter } from '../utils/rateLimit'; import { Logger, createLogger } from '../utils/logger'; @@ -83,6 +84,11 @@ export class SVGMakerClient { */ public readonly account: AccountClient; + /** + * Optimize SVG client for optimizing SVG files using SVGO + */ + public readonly optimizeSvg: OptimizeSvgClient; + /** * Create a new SVGMaker client * @param apiKey API key for authentication @@ -124,6 +130,7 @@ export class SVGMakerClient { this.generations = new GenerationsClient(this); this.gallery = new GalleryClient(this); this.account = new AccountClient(this); + this.optimizeSvg = new OptimizeSvgClient(this); this.logger.info('SVGMaker SDK initialized'); } diff --git a/src/index.ts b/src/index.ts index d40eb29..7de88e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { ConvertClient } from './clients/ConvertClient'; import { GenerationsClient } from './clients/GenerationsClient'; import { GalleryClient } from './clients/GalleryClient'; import { AccountClient } from './clients/AccountClient'; +import { OptimizeSvgClient } from './clients/OptimizeSvgClient'; // Export error classes import * as Errors from './errors/CustomErrors'; @@ -32,6 +33,7 @@ export { GenerationsClient, GalleryClient, AccountClient, + OptimizeSvgClient, // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index 345da83..3a3a777 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -217,6 +217,32 @@ export interface AiVectorizeResponse extends BaseResponse { */ export type ConvertResponse = AiVectorizeResponse; +// --- Optimize SVG Types --- + +export interface OptimizeSvgParams { + /** Required: SVG file path to optimize */ + file: string; + /** Optional: Compress to SVGZ format (default: false) */ + compress?: boolean; +} + +export interface OptimizeSvgResponse { + /** URL to download the optimized SVG (when compress=false) */ + svgUrl?: string; + /** Expiration time for svgUrl */ + svgUrlExpiresIn?: string; + /** URL to download the SVGZ file (when compress=true) */ + svgzUrl?: string; + /** Expiration time for svgzUrl */ + svgzUrlExpiresIn?: string; + /** Filename of the compressed file (when compress=true) */ + filename?: string; + /** Size of the compressed file in bytes (when compress=true) */ + compressedSize?: number; + /** Response metadata */ + metadata: ResponseMetadata; +} + /** * Stream event base type */ diff --git a/tests/sdk-test-commands.md b/tests/sdk-test-commands.md index d3298d0..ff3521e 100644 --- a/tests/sdk-test-commands.md +++ b/tests/sdk-test-commands.md @@ -438,3 +438,25 @@ const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'htt client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " ``` + +--- + +## Optimize SVG + +### 45. Optimize SVG (default, no compression) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.optimizeSvg.configure({ file: './tests/test-images/test-generated-ts-1765444182368.svg' }).execute().then(r => { console.log('Optimized SVG URL:', r.svgUrl); console.log('Expires in:', r.svgUrlExpiresIn); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 46. Optimize SVG with SVGZ compression +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.optimizeSvg.configure({ file: './tests/test-images/test-generated-ts-1765444182368.svg', compress: true }).execute().then(r => { console.log('SVGZ URL:', r.svgzUrl); console.log('Filename:', r.filename); console.log('Compressed size:', r.compressedSize); }).catch(e => console.error('ERROR:', e.message)); +" +``` From fbc5adbf5cf8092fff0675236e43d1f2eb77bfd0 Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Fri, 6 Feb 2026 17:09:46 +0530 Subject: [PATCH 06/15] convert/* and enhance api endpoints done --- src/clients/BaseClient.ts | 44 ++++++++ src/clients/EditClient.ts | 66 ++---------- src/clients/EnhancePromptClient.ts | 87 +++++++++++++++ src/clients/OptimizeSvgClient.ts | 28 ++--- src/clients/convert/AIVectorizeClient.ts | 38 ++----- src/clients/convert/BatchConvertClient.ts | 111 ++++++++++++++++++++ src/clients/convert/RasterToRasterClient.ts | 94 +++++++++++++++++ src/clients/convert/SvgToVectorClient.ts | 96 +++++++++++++++++ src/clients/convert/TraceClient.ts | 101 ++++++++++++++++++ src/clients/convert/index.ts | 4 + src/core/SVGMakerClient.ts | 23 ++++ src/index.ts | 10 ++ src/types/api.ts | 95 +++++++++++++++++ tests/sdk-test-commands.md | 92 ++++++++++++++++ 14 files changed, 782 insertions(+), 107 deletions(-) create mode 100644 src/clients/EnhancePromptClient.ts create mode 100644 src/clients/convert/BatchConvertClient.ts create mode 100644 src/clients/convert/RasterToRasterClient.ts create mode 100644 src/clients/convert/SvgToVectorClient.ts create mode 100644 src/clients/convert/TraceClient.ts diff --git a/src/clients/BaseClient.ts b/src/clients/BaseClient.ts index 106663c..923dec0 100644 --- a/src/clients/BaseClient.ts +++ b/src/clients/BaseClient.ts @@ -170,6 +170,50 @@ export abstract class BaseClient { return mimeTypes[ext] || 'application/octet-stream'; } + /** + * Execute a POST request with FormData body and unwrap the v1 API envelope + * @param endpoint API endpoint path (e.g., '/v1/convert/trace') + * @param formData FormData body + * @returns Unwrapped data and metadata + */ + protected async executeFormDataRequest( + endpoint: string, + formData: FormData + ): Promise<{ data: T; metadata: ResponseMetadata }> { + const response = await fetch(`${this.config.baseUrl}${endpoint}`, { + method: 'POST', + headers: { + 'x-api-key': this.config.apiKey, + }, + body: formData, + }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const rawResult = await response.json(); + return this.unwrapEnvelope(rawResult); + } + + /** + * Append optional parameters to FormData, converting values to strings + * @param formData FormData to append to + * @param params Parameters object + * @param fieldNames Array of field names to check and append + */ + protected appendOptionalParams( + formData: FormData, + params: Record, + fieldNames: string[] + ): void { + for (const field of fieldNames) { + if (params[field] !== undefined) { + formData.append(field, String(params[field])); + } + } + } + /** * Create a new instance of this client * This should be implemented by child classes to support method chaining diff --git a/src/clients/EditClient.ts b/src/clients/EditClient.ts index 1c66ffc..effcd30 100644 --- a/src/clients/EditClient.ts +++ b/src/clients/EditClient.ts @@ -3,9 +3,6 @@ import { EditParams, EditResponse, EditStreamEvent } from '../types/api'; import { SVGMakerClient } from '../core/SVGMakerClient'; import { z } from 'zod'; import { Readable } from 'stream'; -import * as fs from 'fs'; -import * as path from 'path'; -import { ValidationError } from '../errors/CustomErrors'; import { decodeSvgContent, decodeBase64Png } from '../utils/base64'; /** @@ -90,64 +87,21 @@ export class EditClient extends BaseClient { // Add image file await this.addFileToForm(formData, 'image', this.params.image!); - // Add prompt if present - if (this.params.prompt) { - formData.append('prompt', this.params.prompt); - } - - // Add styleParams if present + // Add styleParams if present (requires JSON.stringify) if (this.params.styleParams) { formData.append('styleParams', JSON.stringify(this.params.styleParams)); } - // Add options - if (this.params.quality) { - formData.append('quality', this.params.quality); - } - - if (this.params.aspectRatio) { - formData.append('aspectRatio', this.params.aspectRatio); - } - - if (this.params.background) { - formData.append('background', this.params.background); - } - - if (this.params.stream) { - formData.append('stream', String(this.params.stream)); - } - - if (this.params.base64Png) { - formData.append('base64Png', String(this.params.base64Png)); - } - - if (this.params.svgText) { - formData.append('svgText', String(this.params.svgText)); - } - - if (this.params.storage !== undefined) { - formData.append('storage', String(this.params.storage)); - } - - if (this.params.model) { - formData.append('model', this.params.model); - } - - // Execute request using native fetch (for FormData compatibility) - const response = await fetch(`${this.config.baseUrl}/v1/edit`, { - method: 'POST', - headers: { - 'x-api-key': this.config.apiKey, - }, - body: formData, - }); - - if (!response.ok) { - await this.handleFetchErrorResponse(response); - } + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, [ + 'prompt', 'quality', 'aspectRatio', 'background', 'storage', 'stream', 'base64Png', 'svgText', 'model', + ]); - const rawResult = await response.json(); - const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/edit', + formData + ); let pngImageData: Buffer | undefined = undefined; if (data.base64Png && typeof data.base64Png === 'string') { diff --git a/src/clients/EnhancePromptClient.ts b/src/clients/EnhancePromptClient.ts new file mode 100644 index 0000000..d9aab07 --- /dev/null +++ b/src/clients/EnhancePromptClient.ts @@ -0,0 +1,87 @@ +import { BaseClient } from './BaseClient'; +import { EnhancePromptParams, EnhancePromptResponse } from '../types/api'; +import { SVGMakerClient } from '../core/SVGMakerClient'; +import { z } from 'zod'; + +/** + * Schema for validating enhance prompt parameters + */ +const enhancePromptParamsSchema = z.object({ + prompt: z.string(), +}); + +/** + * Client for the Enhance Prompt API + */ +export class EnhancePromptClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new Enhance Prompt client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the enhance prompt request + * @returns Enhanced prompt response + */ + public async execute(): Promise { + this.logger.debug('Starting prompt enhancement', { + hasPrompt: !!this.params.prompt, + }); + + // Validate parameters + this.validateRequest(this.params, enhancePromptParamsSchema); + + // Execute request using native fetch with JSON body + const response = await fetch(`${this.config.baseUrl}/v1/enhance-prompt`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': this.config.apiKey, + }, + body: JSON.stringify({ prompt: this.params.prompt }), + }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const rawResult = await response.json(); + const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); + + this.logger.debug('Prompt enhancement completed'); + + return { + enhancedPrompt: data.enhancedPrompt, + metadata: responseMetadata, + }; + } + + /** + * Configure the enhance prompt parameters + * @param config Configuration object with enhance prompt parameters + * @returns New client instance + */ + public configure(config: Partial): EnhancePromptClient { + this.logger.debug('Configuring enhance prompt parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): EnhancePromptClient { + const client = new EnhancePromptClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/OptimizeSvgClient.ts b/src/clients/OptimizeSvgClient.ts index 3a12dea..d1a0473 100644 --- a/src/clients/OptimizeSvgClient.ts +++ b/src/clients/OptimizeSvgClient.ts @@ -41,26 +41,14 @@ export class OptimizeSvgClient extends BaseClient { // Add file await this.addFileToForm(formData, 'file', this.params.file!); - // Add compress option if present - if (this.params.compress !== undefined) { - formData.append('compress', String(this.params.compress)); - } - - // Execute request using native fetch - const response = await fetch(`${this.config.baseUrl}/v1/svg/optimize`, { - method: 'POST', - headers: { - 'x-api-key': this.config.apiKey, - }, - body: formData, - }); - - if (!response.ok) { - await this.handleFetchErrorResponse(response); - } - - const rawResult = await response.json(); - const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, ['compress']); + + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/svg/optimize', + formData + ); this.logger.debug('SVG optimization completed'); diff --git a/src/clients/convert/AIVectorizeClient.ts b/src/clients/convert/AIVectorizeClient.ts index c36a7ec..b0e5d16 100644 --- a/src/clients/convert/AIVectorizeClient.ts +++ b/src/clients/convert/AIVectorizeClient.ts @@ -3,9 +3,6 @@ import { AiVectorizeParams, AiVectorizeResponse, AiVectorizeStreamEvent } from ' import { SVGMakerClient } from '../../core/SVGMakerClient'; import { z } from 'zod'; import { Readable } from 'stream'; -import * as fs from 'fs'; -import * as path from 'path'; -import { ValidationError } from '../../errors/CustomErrors'; import { decodeSvgContent } from '../../utils/base64'; /** @@ -51,35 +48,14 @@ export class AIVectorizeClient extends BaseClient { // Add file await this.addFileToForm(formData, 'file', this.params.file!); - // Add storage option if present - if (this.params.storage !== undefined) { - formData.append('storage', String(this.params.storage)); - } - - // Add stream option if present - if (this.params.stream) { - formData.append('stream', String(this.params.stream)); - } - - if (this.params.svgText) { - formData.append('svgText', String(this.params.svgText)); - } - - // Execute request using native fetch - const response = await fetch(`${this.config.baseUrl}/v1/convert/ai-vectorize`, { - method: 'POST', - headers: { - 'x-api-key': this.config.apiKey, - }, - body: formData, - }); - - if (!response.ok) { - await this.handleFetchErrorResponse(response); - } + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, ['storage', 'stream', 'svgText']); - const rawResult = await response.json(); - const { data, metadata: responseMetadata } = this.unwrapEnvelope(rawResult); + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/convert/ai-vectorize', + formData + ); this.logger.debug('AI vectorize conversion completed', { creditCost: data.creditCost, diff --git a/src/clients/convert/BatchConvertClient.ts b/src/clients/convert/BatchConvertClient.ts new file mode 100644 index 0000000..7d289e8 --- /dev/null +++ b/src/clients/convert/BatchConvertClient.ts @@ -0,0 +1,111 @@ +import { BaseClient } from '../BaseClient'; +import { BatchConvertParams, ConvertResultsResponse } from '../../types/api'; +import { SVGMakerClient } from '../../core/SVGMakerClient'; +import { z } from 'zod'; + +/** + * Schema for validating batch conversion parameters + */ +const batchConvertParamsSchema = z.object({ + files: z.array(z.string()).min(1).max(10), + toFormat: z.string(), + preset: z.enum(['bw', 'poster', 'photo']).optional(), + mode: z.enum(['pixel', 'polygon', 'spline']).optional(), + hierarchical: z.enum(['stacked', 'cutout']).optional(), + detail: z.number().optional(), + smoothness: z.number().optional(), + corners: z.number().optional(), + textToPath: z.boolean().optional(), + dxfVersion: z.enum(['R12', 'R14']).optional(), + quality: z.number().optional(), + width: z.number().optional(), + height: z.number().optional(), +}); + +/** + * Client for the Batch Convert API + */ +export class BatchConvertClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new Batch Convert client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the batch conversion request + * @returns Conversion results response + */ + public async execute(): Promise { + this.logger.debug('Starting batch conversion', { + fileCount: this.params.files?.length, + toFormat: this.params.toFormat, + }); + + // Validate parameters + this.validateRequest(this.params, batchConvertParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add each file using the inherited addFileToForm method + for (const file of this.params.files!) { + await this.addFileToForm(formData, 'file', file); + } + + // Add toFormat + formData.append('toFormat', this.params.toFormat!); + + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, [ + 'preset', 'mode', 'hierarchical', 'detail', 'smoothness', 'corners', + 'textToPath', 'dxfVersion', 'quality', 'width', 'height', + ]); + + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/convert/batch', + formData + ); + + this.logger.debug('Batch conversion completed', { + total: data.summary?.total, + successful: data.summary?.successful, + failed: data.summary?.failed, + }); + + return { + results: data.results, + summary: data.summary, + metadata: responseMetadata, + }; + } + + /** + * Configure the batch conversion parameters + * @param config Configuration object with batch conversion parameters + * @returns New client instance + */ + public configure(config: Partial): BatchConvertClient { + this.logger.debug('Configuring batch conversion parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): BatchConvertClient { + const client = new BatchConvertClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/convert/RasterToRasterClient.ts b/src/clients/convert/RasterToRasterClient.ts new file mode 100644 index 0000000..ec08238 --- /dev/null +++ b/src/clients/convert/RasterToRasterClient.ts @@ -0,0 +1,94 @@ +import { BaseClient } from '../BaseClient'; +import { RasterToRasterParams, ConvertResultsResponse } from '../../types/api'; +import { SVGMakerClient } from '../../core/SVGMakerClient'; +import { z } from 'zod'; + +/** + * Schema for validating raster-to-raster conversion parameters + */ +const rasterToRasterParamsSchema = z.object({ + file: z.string(), + toFormat: z.enum(['PNG', 'JPG', 'WEBP', 'TIFF', 'GIF', 'AVIF']), + quality: z.number().min(1).max(100).optional(), + width: z.number().positive().optional(), + height: z.number().positive().optional(), +}); + +/** + * Client for the Raster-to-Raster conversion API + */ +export class RasterToRasterClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new Raster-to-Raster client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the raster-to-raster conversion request + * @returns Conversion results response + */ + public async execute(): Promise { + this.logger.debug('Starting raster-to-raster conversion', { + hasFile: !!this.params.file, + toFormat: this.params.toFormat, + }); + + // Validate parameters + this.validateRequest(this.params, rasterToRasterParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', this.params.file!); + + // Add toFormat + formData.append('toFormat', this.params.toFormat!); + + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, ['quality', 'width', 'height']); + + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/convert/raster-to-raster', + formData + ); + + this.logger.debug('Raster-to-raster conversion completed'); + + return { + results: data.results, + summary: data.summary, + metadata: responseMetadata, + }; + } + + /** + * Configure the raster-to-raster conversion parameters + * @param config Configuration object with raster-to-raster parameters + * @returns New client instance + */ + public configure(config: Partial): RasterToRasterClient { + this.logger.debug('Configuring raster-to-raster parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): RasterToRasterClient { + const client = new RasterToRasterClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/convert/SvgToVectorClient.ts b/src/clients/convert/SvgToVectorClient.ts new file mode 100644 index 0000000..4203c5f --- /dev/null +++ b/src/clients/convert/SvgToVectorClient.ts @@ -0,0 +1,96 @@ +import { BaseClient } from '../BaseClient'; +import { SvgToVectorParams, ConvertResultsResponse } from '../../types/api'; +import { SVGMakerClient } from '../../core/SVGMakerClient'; +import { z } from 'zod'; + +/** + * Schema for validating SVG to vector parameters + */ +const svgToVectorParamsSchema = z.object({ + file: z.string(), + toFormat: z.enum(['PDF', 'EPS', 'DXF', 'AI', 'PS']), + textToPath: z.boolean().optional(), + dxfVersion: z.enum(['R12', 'R14']).optional(), +}); + +/** + * Client for the SVG to Vector conversion API + */ +export class SvgToVectorClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new SVG to Vector client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the SVG to vector conversion request + * @returns Convert results response with converted vector file results + */ + public async execute(): Promise { + this.logger.debug('Starting SVG to vector conversion', { + hasFile: !!this.params.file, + toFormat: this.params.toFormat, + }); + + // Validate parameters + this.validateRequest(this.params, svgToVectorParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', this.params.file!); + + // Add required toFormat + formData.append('toFormat', this.params.toFormat!); + + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, ['textToPath', 'dxfVersion']); + + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/convert/svg-to-vector', + formData + ); + + this.logger.debug('SVG to vector conversion completed', { + total: data.summary?.total, + successful: data.summary?.successful, + }); + + return { + results: data.results, + summary: data.summary, + metadata: responseMetadata, + }; + } + + /** + * Configure the SVG to vector parameters + * @param config Configuration object with SVG to vector parameters + * @returns New client instance + */ + public configure(config: Partial): SvgToVectorClient { + this.logger.debug('Configuring SVG to vector parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): SvgToVectorClient { + const client = new SvgToVectorClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/convert/TraceClient.ts b/src/clients/convert/TraceClient.ts new file mode 100644 index 0000000..c219a01 --- /dev/null +++ b/src/clients/convert/TraceClient.ts @@ -0,0 +1,101 @@ +import { BaseClient } from '../BaseClient'; +import { TraceParams, ConvertResultsResponse } from '../../types/api'; +import { SVGMakerClient } from '../../core/SVGMakerClient'; +import { z } from 'zod'; + +/** + * Schema for validating trace parameters + */ +const traceParamsSchema = z.object({ + file: z.string(), + algorithm: z.enum(['vtracer']).optional(), + preset: z.enum(['bw', 'poster', 'photo']).optional(), + mode: z.enum(['pixel', 'polygon', 'spline']).optional(), + hierarchical: z.enum(['stacked', 'cutout']).optional(), + detail: z.number().min(0).max(100).optional(), + smoothness: z.number().min(0).max(100).optional(), + corners: z.number().min(0).max(100).optional(), + reduceNoise: z.number().optional(), +}); + +/** + * Client for the Trace (raster to SVG) API + */ +export class TraceClient extends BaseClient { + private params: Partial = {}; + + /** + * Create a new Trace client + * @param client Parent SVGMaker client + */ + constructor(client: SVGMakerClient) { + super(client); + } + + /** + * Execute the trace request + * @returns Convert results response with traced SVG results + */ + public async execute(): Promise { + this.logger.debug('Starting trace conversion', { + hasFile: !!this.params.file, + preset: this.params.preset, + mode: this.params.mode, + }); + + // Validate parameters + this.validateRequest(this.params, traceParamsSchema); + + // Prepare form data + const formData = new FormData(); + + // Add file + await this.addFileToForm(formData, 'file', this.params.file!); + + // Add optional parameters + this.appendOptionalParams(formData, this.params as Record, [ + 'algorithm', 'preset', 'mode', 'hierarchical', 'detail', 'smoothness', 'corners', 'reduceNoise', + ]); + + // Execute request + const { data, metadata: responseMetadata } = await this.executeFormDataRequest( + '/v1/convert/trace', + formData + ); + + this.logger.debug('Trace conversion completed', { + total: data.summary?.total, + successful: data.summary?.successful, + }); + + return { + results: data.results, + summary: data.summary, + metadata: responseMetadata, + }; + } + + /** + * Configure the trace parameters + * @param config Configuration object with trace parameters + * @returns New client instance + */ + public configure(config: Partial): TraceClient { + this.logger.debug('Configuring trace parameters', { config }); + + const client = this.clone(); + client.params = { ...client.params, ...config }; + return client; + } + + /** + * Create a clone of this client + * @returns New client instance + */ + protected clone(): TraceClient { + const client = new TraceClient(this.client); + this.copyTo(client); + client.params = { ...this.params }; + return client; + } +} diff --git a/src/clients/convert/index.ts b/src/clients/convert/index.ts index 530b57f..7c3b23f 100644 --- a/src/clients/convert/index.ts +++ b/src/clients/convert/index.ts @@ -1 +1,5 @@ export { AIVectorizeClient } from './AIVectorizeClient'; +export { TraceClient } from './TraceClient'; +export { SvgToVectorClient } from './SvgToVectorClient'; +export { RasterToRasterClient } from './RasterToRasterClient'; +export { BatchConvertClient } from './BatchConvertClient'; diff --git a/src/core/SVGMakerClient.ts b/src/core/SVGMakerClient.ts index decf2f4..af57c1b 100644 --- a/src/core/SVGMakerClient.ts +++ b/src/core/SVGMakerClient.ts @@ -4,6 +4,11 @@ import { ValidationError } from '../errors/CustomErrors'; import { GenerateClient } from '../clients/GenerateClient'; import { EditClient } from '../clients/EditClient'; import { AIVectorizeClient } from '../clients/convert'; +import { TraceClient } from '../clients/convert/TraceClient'; +import { SvgToVectorClient } from '../clients/convert/SvgToVectorClient'; +import { RasterToRasterClient } from '../clients/convert/RasterToRasterClient'; +import { BatchConvertClient } from '../clients/convert/BatchConvertClient'; +import { EnhancePromptClient } from '../clients/EnhancePromptClient'; import { GenerationsClient } from '../clients/GenerationsClient'; import { GalleryClient } from '../clients/GalleryClient'; import { AccountClient } from '../clients/AccountClient'; @@ -67,6 +72,14 @@ export class SVGMakerClient { public readonly convert: { /** AI-powered raster to SVG vectorization */ aiVectorize: AIVectorizeClient; + /** Trace raster images to SVG using VTracer */ + trace: TraceClient; + /** Convert SVG to vector formats (PDF, EPS, DXF, AI, PS) */ + svgToVector: SvgToVectorClient; + /** Convert between raster image formats */ + rasterToRaster: RasterToRasterClient; + /** Batch convert multiple files */ + batch: BatchConvertClient; }; /** @@ -89,6 +102,11 @@ export class SVGMakerClient { */ public readonly optimizeSvg: OptimizeSvgClient; + /** + * Enhance prompt client for improving text prompts using AI + */ + public readonly enhancePrompt: EnhancePromptClient; + /** * Create a new SVGMaker client * @param apiKey API key for authentication @@ -126,7 +144,12 @@ export class SVGMakerClient { this.edit = new EditClient(this); this.convert = { aiVectorize: new AIVectorizeClient(this), + trace: new TraceClient(this), + svgToVector: new SvgToVectorClient(this), + rasterToRaster: new RasterToRasterClient(this), + batch: new BatchConvertClient(this), }; + this.enhancePrompt = new EnhancePromptClient(this); this.generations = new GenerationsClient(this); this.gallery = new GalleryClient(this); this.account = new AccountClient(this); diff --git a/src/index.ts b/src/index.ts index 7de88e4..1b5f22c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,11 @@ import { GenerationsClient } from './clients/GenerationsClient'; import { GalleryClient } from './clients/GalleryClient'; import { AccountClient } from './clients/AccountClient'; import { OptimizeSvgClient } from './clients/OptimizeSvgClient'; +import { TraceClient } from './clients/convert/TraceClient'; +import { SvgToVectorClient } from './clients/convert/SvgToVectorClient'; +import { RasterToRasterClient } from './clients/convert/RasterToRasterClient'; +import { BatchConvertClient } from './clients/convert/BatchConvertClient'; +import { EnhancePromptClient } from './clients/EnhancePromptClient'; // Export error classes import * as Errors from './errors/CustomErrors'; @@ -34,6 +39,11 @@ export { GalleryClient, AccountClient, OptimizeSvgClient, + TraceClient, + SvgToVectorClient, + RasterToRasterClient, + BatchConvertClient, + EnhancePromptClient, // Utils HttpClient, diff --git a/src/types/api.ts b/src/types/api.ts index 3a3a777..1f5d800 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -243,6 +243,101 @@ export interface OptimizeSvgResponse { metadata: ResponseMetadata; } +// --- Convert Results Types (shared by trace, svg-to-vector, raster-to-raster, batch) --- + +export interface ConvertResultItem { + filename: string; + success: boolean; + url?: string; + urlExpiresIn?: string; + format?: string; + error?: string; +} + +export interface ConvertSummary { + total: number; + successful: number; + failed: number; +} + +export interface ConvertResultsResponse { + results: ConvertResultItem[]; + summary: ConvertSummary; + metadata: ResponseMetadata; +} + +// --- Trace Types --- + +export type TraceAlgorithm = 'vtracer'; +export type TracePreset = 'bw' | 'poster' | 'photo'; +export type TraceMode = 'pixel' | 'polygon' | 'spline'; +export type TraceHierarchical = 'stacked' | 'cutout'; + +export interface TraceParams { + file: string; + algorithm?: TraceAlgorithm; + preset?: TracePreset; + mode?: TraceMode; + hierarchical?: TraceHierarchical; + detail?: number; + smoothness?: number; + corners?: number; + reduceNoise?: number; +} + +// --- SVG to Vector Types --- + +export type VectorFormat = 'PDF' | 'EPS' | 'DXF' | 'AI' | 'PS'; +export type DxfVersion = 'R12' | 'R14'; + +export interface SvgToVectorParams { + file: string; + toFormat: VectorFormat; + textToPath?: boolean; + dxfVersion?: DxfVersion; +} + +// --- Raster to Raster Types --- + +export type RasterFormat = 'PNG' | 'JPG' | 'WEBP' | 'TIFF' | 'GIF' | 'AVIF'; + +export interface RasterToRasterParams { + file: string; + toFormat: RasterFormat; + quality?: number; + width?: number; + height?: number; +} + +// --- Batch Convert Types --- + +export interface BatchConvertParams { + files: string[]; + toFormat: string; + preset?: TracePreset; + mode?: TraceMode; + hierarchical?: TraceHierarchical; + detail?: number; + smoothness?: number; + corners?: number; + textToPath?: boolean; + dxfVersion?: DxfVersion; + quality?: number; + width?: number; + height?: number; +} + +// --- Enhance Prompt Types --- + +export interface EnhancePromptParams { + prompt: string; +} + +export interface EnhancePromptResponse { + enhancedPrompt: string; + metadata: ResponseMetadata; +} + /** * Stream event base type */ diff --git a/tests/sdk-test-commands.md b/tests/sdk-test-commands.md index ff3521e..d598f92 100644 --- a/tests/sdk-test-commands.md +++ b/tests/sdk-test-commands.md @@ -460,3 +460,95 @@ const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'htt client.optimizeSvg.configure({ file: './tests/test-images/test-generated-ts-1765444182368.svg', compress: true }).execute().then(r => { console.log('SVGZ URL:', r.svgzUrl); console.log('Filename:', r.filename); console.log('Compressed size:', r.compressedSize); }).catch(e => console.error('ERROR:', e.message)); " ``` + +--- + +## Convert (Trace to SVG) + +### 47. Trace with default settings +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.trace.configure({ file: './tests/test-images/test-image.png' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 48. Trace with custom params (preset, mode, detail, smoothness) +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.trace.configure({ file: './tests/test-images/test-image.png', preset: 'photo', mode: 'spline', detail: 60, smoothness: 70 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +--- + +## Convert (SVG to Vector) + +### 49. Convert SVG to PDF +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.svgToVector.configure({ file: './tests/test-images/test-generated-ts-1765444182368.svg', toFormat: 'PDF' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 50. Convert SVG to DXF with textToPath +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.svgToVector.configure({ file: './tests/test-images/test-generated-ts-1765444182368.svg', toFormat: 'DXF', textToPath: true }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +--- + +## Convert (Raster to Raster) + +### 51. Convert PNG to JPG with quality +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'JPG', quality: 85 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +### 52. Convert PNG to WEBP with width resize +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'WEBP', width: 512 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +--- + +## Convert (Batch) + +### 53. Batch convert multiple files to SVG +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.convert.batch.configure({ files: ['./tests/test-images/test-image.png', '/Users/vishalm.a/Desktop/cat.jpg'], toFormat: 'SVG' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` + +--- + +## Enhance Prompt + +### 54. Basic prompt enhancement +```bash +node -e " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('svgmaker-io7791b35175f510df', { baseUrl: 'http://localhost:3000/api', timeout: 300000 }); +client.enhancePrompt.configure({ prompt: 'a cat sitting on a fence' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" +``` From 3c393531d724ad1a31a2fc52a456ea5a82f75d62 Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Mon, 9 Feb 2026 14:22:09 +0530 Subject: [PATCH 07/15] unit test --- tests/clients/AccountClient.test.ts | 215 ++++++ tests/clients/EditClient.test.ts | 616 ++++++++++++++++++ tests/clients/EnhancePromptClient.test.ts | 148 +++++ tests/clients/GalleryClient.test.ts | 300 +++++++++ tests/clients/GenerateClient.test.ts | 588 +++++++++++++++++ tests/clients/GenerationsClient.test.ts | 403 ++++++++++++ tests/clients/OptimizeSvgClient.test.ts | 166 +++++ .../clients/convert/AIVectorizeClient.test.ts | 441 +++++++++++++ .../convert/BatchConvertClient.test.ts | 277 ++++++++ .../convert/RasterToRasterClient.test.ts | 278 ++++++++ .../clients/convert/SvgToVectorClient.test.ts | 262 ++++++++ tests/clients/convert/TraceClient.test.ts | 290 +++++++++ tests/setup/index.ts | 2 + tests/setup/mock-factories.ts | 288 ++++++++ tests/setup/test-setup.ts | 147 +++++ 15 files changed, 4421 insertions(+) create mode 100644 tests/clients/AccountClient.test.ts create mode 100644 tests/clients/EditClient.test.ts create mode 100644 tests/clients/EnhancePromptClient.test.ts create mode 100644 tests/clients/GalleryClient.test.ts create mode 100644 tests/clients/GenerateClient.test.ts create mode 100644 tests/clients/GenerationsClient.test.ts create mode 100644 tests/clients/OptimizeSvgClient.test.ts create mode 100644 tests/clients/convert/AIVectorizeClient.test.ts create mode 100644 tests/clients/convert/BatchConvertClient.test.ts create mode 100644 tests/clients/convert/RasterToRasterClient.test.ts create mode 100644 tests/clients/convert/SvgToVectorClient.test.ts create mode 100644 tests/clients/convert/TraceClient.test.ts create mode 100644 tests/setup/index.ts create mode 100644 tests/setup/mock-factories.ts create mode 100644 tests/setup/test-setup.ts diff --git a/tests/clients/AccountClient.test.ts b/tests/clients/AccountClient.test.ts new file mode 100644 index 0000000..b60afb7 --- /dev/null +++ b/tests/clients/AccountClient.test.ts @@ -0,0 +1,215 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockAccountInfoData, + createMockAccountUsageData, +} from '../setup'; +import { + ValidationError, + RateLimitError, + APIError, +} from '../../src/errors/CustomErrors'; + +describe('AccountClient', () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // ========================================================================== + // .getInfo() + // ========================================================================== + + describe('.getInfo()', () => { + it('sends GET to /v1/account', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAccountInfoData()); + + await client.account.getInfo(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/account'); + expect(options.method).toBe('GET'); + }); + + it('sends API key in x-api-key header', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAccountInfoData()); + + await client.account.getInfo(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('returns properly shaped AccountResponse', async () => { + const client = createTestClient(); + const mockData = createMockAccountInfoData(); + mockFetchJsonResponse(mockData); + + const result = await client.account.getInfo(); + + expect(result.email).toBe(mockData.email); + expect(result.displayName).toBe(mockData.displayName); + expect(result.accountType).toBe(mockData.accountType); + expect(result.credits).toBe(mockData.credits); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .getUsage() + // ========================================================================== + + describe('.getUsage()', () => { + it('sends GET to /v1/account/usage with no query params when called with no args', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAccountUsageData()); + + await client.account.getUsage(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/account/usage'); + expect(url).not.toContain('?'); + expect(options.method).toBe('GET'); + }); + + it('appends days as query param when provided', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAccountUsageData()); + + await client.account.getUsage({ days: 7 }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('days=7'); + }); + + it('appends start and end as query params when provided', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAccountUsageData()); + + await client.account.getUsage({ start: '2025-01-01', end: '2025-01-31' }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('start=2025-01-01'); + expect(url).toContain('end=2025-01-31'); + }); + + it('returns properly shaped AccountUsageResponse', async () => { + const client = createTestClient(); + const mockData = createMockAccountUsageData(); + mockFetchJsonResponse(mockData); + + const result = await client.account.getUsage(); + + expect(result.period).toEqual(mockData.period); + expect(result.summary).toEqual(mockData.summary); + expect(result.byCategory).toEqual(mockData.byCategory); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .getUsage() validation + // ========================================================================== + + describe('.getUsage() validation', () => { + it('throws ValidationError when days is used with start', async () => { + const client = createTestClient(); + + await expect( + client.account.getUsage({ days: 7, start: '2025-01-01' } as any), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when days is used with end', async () => { + const client = createTestClient(); + + await expect( + client.account.getUsage({ days: 7, end: '2025-01-31' } as any), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when only start is provided without end', async () => { + const client = createTestClient(); + + await expect( + client.account.getUsage({ start: '2025-01-01' } as any), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError on unknown property due to .strict()', async () => { + const client = createTestClient(); + + await expect( + client.account.getUsage({ foo: 'bar' } as any), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + // Note: AccountClient methods use handleRequest() which goes through + // the retry wrapper. Non-retryable errors (AuthError, InsufficientCreditsError, + // EndpointDisabledError) are wrapped in AbortError by the retry utility. + // Retryable status codes (429, 500) are re-thrown as-is after exhausting retries. + + it('throws on INVALID_API_KEY (401)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); + + await expect( + client.account.getInfo(), + ).rejects.toThrow('Invalid API key'); + }); + + it('throws on INSUFFICIENT_CREDITS (402)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); + + await expect( + client.account.getInfo(), + ).rejects.toThrow('Not enough credits'); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED (429)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect( + client.account.getInfo(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws on ENDPOINT_DISABLED (503)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect( + client.account.getInfo(), + ).rejects.toThrow('This endpoint is disabled'); + }); + + it('throws APIError on generic server error (500)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('SERVER_ERROR', 500, 'Internal server error'); + + await expect( + client.account.getInfo(), + ).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/EditClient.test.ts b/tests/clients/EditClient.test.ts new file mode 100644 index 0000000..706b086 --- /dev/null +++ b/tests/clients/EditClient.test.ts @@ -0,0 +1,616 @@ +import { Readable } from 'stream'; +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + mockFetchStreamResponse, + createMockEditResponse, + createMockStreamEvents, +} from '../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + ContentSafetyError, + APIError, + EndpointDisabledError, + FileSizeError, + FileFormatError, +} from '../../src/errors/CustomErrors'; + +describe('EditClient', () => { + let fetchMock: jest.Mock; + const testImageBuffer = Buffer.from('fake-png-image-data'); + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.edit; + const configured = original.configure({ + image: testImageBuffer, + prompt: 'Make it blue', + }); + + expect(configured).not.toBe(original); + }); + + it('merges params correctly across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + const configured = client.edit + .configure({ image: testImageBuffer, prompt: 'Make it blue' }) + .configure({ quality: 'high' }); + + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + // EditClient uses FormData, so we check the fetch was called + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/edit'); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/edit with FormData body', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await client.edit + .configure({ image: testImageBuffer, prompt: 'Make it red' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/edit'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await client.edit + .configure({ image: testImageBuffer, prompt: 'Make it red' }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends image as Blob to FormData (Buffer input)', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await client.edit + .configure({ image: testImageBuffer, prompt: 'Make it green' }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + const imageField = formData.get('image'); + expect(imageField).toBeTruthy(); + }); + + it('appends optional params to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await client.edit + .configure({ + image: testImageBuffer, + prompt: 'Make it colorful', + quality: 'high', + aspectRatio: 'landscape', + background: 'transparent', + base64Png: true, + svgText: true, + storage: true, + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('prompt')).toBe('Make it colorful'); + expect(formData.get('quality')).toBe('high'); + expect(formData.get('aspectRatio')).toBe('landscape'); + expect(formData.get('background')).toBe('transparent'); + expect(formData.get('base64Png')).toBe('true'); + expect(formData.get('svgText')).toBe('true'); + expect(formData.get('storage')).toBe('true'); + }); + + it('appends styleParams as JSON string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + const styleParams = { + style: 'flat' as const, + color_mode: 'full_color' as const, + image_complexity: 'icon' as const, + }; + + await client.edit + .configure({ + image: testImageBuffer, + prompt: 'Stylize it', + styleParams, + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + const styleParamsField = formData.get('styleParams'); + expect(styleParamsField).toBe(JSON.stringify(styleParams)); + }); + + it('returns properly shaped EditResponse', async () => { + const client = createTestClient(); + const mockData = createMockEditResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(); + + expect(result.svgUrl).toBe(mockData.svgUrl); + expect(result.creditCost).toBe(mockData.creditCost); + expect(result.message).toBe(mockData.message); + expect(result.generationId).toBe(mockData.generationId); + expect(result.metadata).toBeDefined(); + }); + + it('decodes base64Png to pngImageData Buffer', async () => { + const client = createTestClient(); + const base64Png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=='; + mockFetchJsonResponse(createMockEditResponse({ base64Png })); + + const result = await client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this', base64Png: true }) + .execute(); + + expect(result.pngImageData).toBeInstanceOf(Buffer); + }); + + it('decodes svgText from raw SVG', async () => { + const client = createTestClient(); + const rawSvg = + ''; + mockFetchJsonResponse(createMockEditResponse({ svgText: rawSvg })); + + const result = await client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this', svgText: true }) + .execute(); + + expect(result.svgText).toBe(rawSvg); + }); + + it('defaults quality to medium when not specified', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('quality')).toBe('medium'); + }); + + it('does not set default quality when model is specified', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await client.edit + .configure({ + image: testImageBuffer, + prompt: 'Edit this', + model: 'custom-model-v1', + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + // quality should not be set when model is specified + expect(formData.get('model')).toBe('custom-model-v1'); + }); + + it('accepts image as file path string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + // Mock fs module + const fs = require('fs'); + const originalExistsSync = fs.existsSync; + const originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-png-data')); + + try { + await client.edit + .configure({ image: '/path/to/image.png', prompt: 'Edit this' }) + .execute(); + + expect(fs.existsSync).toHaveBeenCalledWith('/path/to/image.png'); + expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/image.png'); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + const imageField = formData.get('image'); + expect(imageField).toBeTruthy(); + } finally { + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + } + }); + + it('accepts image as Readable stream', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + const readable = new Readable(); + readable.push(Buffer.from('fake-stream-data')); + readable.push(null); + + await client.edit + .configure({ image: readable, prompt: 'Edit this' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const formData = fetchMock.mock.calls[0][1].body as FormData; + const imageField = formData.get('image'); + expect(imageField).toBeTruthy(); + }); + + it('throws ValidationError when file path does not exist', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await expect( + client.edit + .configure({ image: '/nonexistent/path/image.png', prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when image is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await expect( + client.edit.configure({ prompt: 'Edit this' } as any).execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when neither prompt nor styleParams given', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await expect( + client.edit.configure({ image: testImageBuffer }).execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when both model and quality are set', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + await expect( + client.edit + .configure({ + image: testImageBuffer, + prompt: 'Edit this', + model: 'test-model', + quality: 'high', + }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('accepts styleParams without prompt', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEditResponse()); + + const result = await client.edit + .configure({ + image: testImageBuffer, + styleParams: { style: 'cartoon' }, + }) + .execute(); + + expect(result).toBeDefined(); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws ContentSafetyError on CONTENT_POLICY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('CONTENT_POLICY', 422); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(ContentSafetyError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(APIError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(EndpointDisabledError); + }); + + it('throws FileSizeError on 413 status', async () => { + const client = createTestClient(); + mockFetchErrorResponse('FILE_TOO_LARGE', 413, 'File exceeds size limit'); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(FileSizeError); + }); + + it('throws FileFormatError when message mentions file format', async () => { + const client = createTestClient(); + mockFetchErrorResponse('UNKNOWN', 400, 'Unsupported file format'); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(FileFormatError); + }); + + it('handles non-JSON error response', async () => { + const client = createTestClient(); + + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 502, + headers: new Headers({ 'Content-Type': 'text/plain' }), + text: async () => 'Bad Gateway', + json: async () => { throw new Error('not json'); }, + }); + + await expect( + client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .execute(), + ).rejects.toThrow(APIError); + }); + }); + + // ========================================================================== + // .stream() + // ========================================================================== + + describe('.stream()', () => { + it('returns a Readable stream', () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .stream(); + + expect(stream).toBeInstanceOf(Readable); + }); + + it('sends FormData with stream=true', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .stream(); + + await new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.body).toBeInstanceOf(FormData); + const formData = options.body as FormData; + expect(formData.get('stream')).toBe('true'); + }); + + it('emits processing and complete events', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + expect(events.length).toBeGreaterThanOrEqual(2); + expect(events.some((e) => e.status === 'processing')).toBe(true); + expect(events.some((e) => e.status === 'complete')).toBe(true); + }); + + it('emits error on non-ok response', async () => { + const client = createTestClient(); + + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 401, + headers: new Headers({ 'Content-Type': 'application/json' }), + json: async () => ({ + success: false, + error: { code: 'INVALID_API_KEY', status: 401, message: 'Invalid API key' }, + metadata: { requestId: 'test-req-id' }, + }), + text: async () => '{"success":false}', + }); + + const stream = client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .stream(); + + await expect( + new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }), + ).rejects.toBeDefined(); + }); + + it('decodes svgText in stream events', async () => { + const client = createTestClient(); + const rawSvg = ''; + const streamEvents = [ + { status: 'processing', message: 'Editing...' }, + { + status: 'generated', + message: 'Edit complete', + svgText: rawSvg, + svgUrl: 'https://storage.example.com/test-edit.svg', + creditCost: 2, + }, + { + status: 'complete', + message: 'Edit complete', + generationId: 'edit-stream-001', + metadata: { requestId: 'test-req-id', creditsUsed: 2, creditsRemaining: 98 }, + }, + ]; + mockFetchStreamResponse(streamEvents); + + const stream = client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const generatedEvent = events.find((e) => e.status === 'generated'); + expect(generatedEvent).toBeDefined(); + expect(generatedEvent.svgText).toBe(rawSvg); + }); + + it('decodes base64Png to pngImageData in stream events', async () => { + const client = createTestClient(); + const base64Png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=='; + const streamEvents = [ + { status: 'processing', message: 'Editing...' }, + { + status: 'generated', + message: 'Edit complete', + svgUrl: 'https://storage.example.com/test-edit.svg', + creditCost: 2, + base64Png, + }, + { + status: 'complete', + message: 'Edit complete', + generationId: 'edit-stream-001', + metadata: { requestId: 'test-req-id', creditsUsed: 2, creditsRemaining: 98 }, + }, + ]; + mockFetchStreamResponse(streamEvents); + + const stream = client.edit + .configure({ image: testImageBuffer, prompt: 'Edit this' }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const generatedEvent = events.find((e) => e.status === 'generated'); + expect(generatedEvent).toBeDefined(); + expect(generatedEvent.pngImageData).toBeInstanceOf(Buffer); + }); + }); +}); diff --git a/tests/clients/EnhancePromptClient.test.ts b/tests/clients/EnhancePromptClient.test.ts new file mode 100644 index 0000000..fc422a4 --- /dev/null +++ b/tests/clients/EnhancePromptClient.test.ts @@ -0,0 +1,148 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockEnhancePromptResponse, +} from '../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../src/errors/CustomErrors'; + +describe('EnhancePromptClient', () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // .configure() - 2 tests + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.enhancePrompt; + const configured = original.configure({ prompt: 'A cat' }); + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEnhancePromptResponse()); + + // First configure sets prompt, second overrides it + const configured = client.enhancePrompt + .configure({ prompt: 'A cat' }) + .configure({ prompt: 'A dog' }); + + await configured.execute(); + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/enhance-prompt'); + const body = JSON.parse(options.body); + expect(body.prompt).toBe('A dog'); + }); + }); + + // .execute() - 4 tests + describe('.execute()', () => { + it('sends POST to /v1/enhance-prompt with JSON body', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEnhancePromptResponse()); + await client.enhancePrompt.configure({ prompt: 'A sunset' }).execute(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/enhance-prompt'); + expect(options.method).toBe('POST'); + }); + + it('sends API key and Content-Type headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEnhancePromptResponse()); + await client.enhancePrompt.configure({ prompt: 'A sunset' }).execute(); + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + expect(options.headers['Content-Type']).toBe('application/json'); + }); + + it('sends prompt in request body', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEnhancePromptResponse()); + await client.enhancePrompt.configure({ prompt: 'A mountain scene' }).execute(); + const [, options] = fetchMock.mock.calls[0]; + const body = JSON.parse(options.body); + expect(body.prompt).toBe('A mountain scene'); + }); + + it('returns properly shaped EnhancePromptResponse', async () => { + const client = createTestClient(); + const mockData = createMockEnhancePromptResponse(); + mockFetchJsonResponse(mockData); + const result = await client.enhancePrompt + .configure({ prompt: 'A sunset' }) + .execute(); + expect(result.enhancedPrompt).toBe(mockData.enhancedPrompt); + expect(result.metadata).toBeDefined(); + }); + }); + + // validation - 1 test + describe('validation', () => { + it('throws ValidationError when prompt is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockEnhancePromptResponse()); + await expect(client.enhancePrompt.execute()).rejects.toThrow(ValidationError); + }); + }); + + // error handling - 5 tests + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401); + await expect( + client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + await expect( + client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + await expect( + client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503); + await expect( + client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INTERNAL_ERROR', 500); + await expect( + client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/GalleryClient.test.ts b/tests/clients/GalleryClient.test.ts new file mode 100644 index 0000000..3dde1e0 --- /dev/null +++ b/tests/clients/GalleryClient.test.ts @@ -0,0 +1,300 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockGenerationsListData, + createMockGenerationData, + createMockGenerationDownloadData, +} from '../setup'; +import { + ValidationError, + RateLimitError, + APIError, +} from '../../src/errors/CustomErrors'; + +describe('GalleryClient', () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // ========================================================================== + // .list() + // ========================================================================== + + describe('.list()', () => { + it('sends GET to /v1/gallery with no query params when called with no args', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.gallery.list(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/gallery'); + expect(url).not.toContain('?'); + expect(options.method).toBe('GET'); + }); + + it('sends API key in x-api-key header', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.gallery.list(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends page and limit as query params', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.gallery.list({ page: 2, limit: 25 }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('page=2'); + expect(url).toContain('limit=25'); + }); + + it('handles array params (type, hashtags, categories)', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.gallery.list({ + type: ['generate', 'edit'], + hashtags: ['nature', 'art'], + categories: ['landscape'], + }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('type=generate'); + expect(url).toContain('type=edit'); + expect(url).toContain('hashtags=nature'); + expect(url).toContain('hashtags=art'); + expect(url).toContain('categories=landscape'); + }); + + it('appends query, pro, and gold params', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.gallery.list({ query: 'mountain', pro: 'true', gold: 'true' }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('query=mountain'); + expect(url).toContain('pro=true'); + expect(url).toContain('gold=true'); + }); + + it('returns properly shaped GalleryListResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationsListData(); + mockFetchJsonResponse(mockData); + + const result = await client.gallery.list(); + + expect(result.items).toEqual(mockData.items); + expect(result.pagination).toEqual(mockData.pagination); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .list() validation + // ========================================================================== + + describe('.list() validation', () => { + it('throws ValidationError when page is 0', async () => { + const client = createTestClient(); + + await expect( + client.gallery.list({ page: 0 }), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when limit exceeds 100', async () => { + const client = createTestClient(); + + await expect( + client.gallery.list({ limit: 101 }), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError on unknown property due to .strict()', async () => { + const client = createTestClient(); + + await expect( + client.gallery.list({ foo: 'bar' } as any), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // .get(id) + // ========================================================================== + + describe('.get(id)', () => { + it('sends GET to /v1/gallery/{id}', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationData()); + + await client.gallery.get('gallery-001'); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/gallery/gallery-001'); + expect(options.method).toBe('GET'); + }); + + it('returns properly shaped GalleryItemResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationData(); + mockFetchJsonResponse(mockData); + + const result = await client.gallery.get('gallery-001'); + + expect(result.id).toBe(mockData.id); + expect(result.prompt).toBe(mockData.prompt); + expect(result.type).toBe(mockData.type); + expect(result.quality).toBe(mockData.quality); + expect(result.isPublic).toBe(mockData.isPublic); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .get(id) validation + // ========================================================================== + + describe('.get(id) validation', () => { + it('throws ValidationError when id is empty string', async () => { + const client = createTestClient(); + + await expect( + client.gallery.get(''), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // .download(id, params?) + // ========================================================================== + + describe('.download(id, params?)', () => { + it('sends GET to /v1/gallery/{id}/download with no query params by default', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDownloadData()); + + await client.gallery.download('gallery-001'); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/gallery/gallery-001/download'); + expect(options.method).toBe('GET'); + }); + + it('appends format and optimize as query params', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDownloadData()); + + await client.gallery.download('gallery-001', { format: 'png', optimize: true }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('format=png'); + expect(url).toContain('optimize=true'); + }); + + it('returns properly shaped GalleryDownloadResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationDownloadData(); + mockFetchJsonResponse(mockData); + + const result = await client.gallery.download('gallery-001'); + + expect(result.id).toBe(mockData.id); + expect(result.url).toBe(mockData.url); + expect(result.urlExpiresIn).toBe(mockData.urlExpiresIn); + expect(result.format).toBe(mockData.format); + expect(result.filename).toBe(mockData.filename); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .download() validation + // ========================================================================== + + describe('.download() validation', () => { + it('throws ValidationError when id is empty string', async () => { + const client = createTestClient(); + + await expect( + client.gallery.download(''), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + // Note: GalleryClient methods use handleRequest() which goes through + // the retry wrapper. Non-retryable errors (AuthError, InsufficientCreditsError, + // EndpointDisabledError) are wrapped in AbortError by the retry utility. + // Retryable status codes (429, 500) are re-thrown as-is after exhausting retries. + + it('throws on INVALID_API_KEY (401)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); + + await expect( + client.gallery.list(), + ).rejects.toThrow('Invalid API key'); + }); + + it('throws on INSUFFICIENT_CREDITS (402)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); + + await expect( + client.gallery.list(), + ).rejects.toThrow('Not enough credits'); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED (429)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect( + client.gallery.list(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws on ENDPOINT_DISABLED (503)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect( + client.gallery.list(), + ).rejects.toThrow('This endpoint is disabled'); + }); + + it('throws APIError on generic server error (500)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('SERVER_ERROR', 500, 'Internal server error'); + + await expect( + client.gallery.list(), + ).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/GenerateClient.test.ts b/tests/clients/GenerateClient.test.ts new file mode 100644 index 0000000..962ba60 --- /dev/null +++ b/tests/clients/GenerateClient.test.ts @@ -0,0 +1,588 @@ +import { Readable } from 'stream'; +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + mockFetchStreamResponse, + createMockGenerateResponse, + createMockStreamEvents, +} from '../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + ContentSafetyError, + APIError, + EndpointDisabledError, +} from '../../src/errors/CustomErrors'; + +describe('GenerateClient', () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.generate; + const configured = original.configure({ prompt: 'A cat' }); + + expect(configured).not.toBe(original); + }); + + it('merges params correctly across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + const configured = client.generate + .configure({ prompt: 'A mountain' }) + .configure({ quality: 'high' }); + + // Execute to verify both params are sent + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + const callArgs = fetchMock.mock.calls[0]; + const body = JSON.parse(callArgs[1].body); + expect(body.prompt).toBe('A mountain'); + expect(body.quality).toBe('high'); + }); + + it('supports chaining configure().execute()', async () => { + const client = createTestClient(); + const mockData = createMockGenerateResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.generate + .configure({ prompt: 'A sunset' }) + .execute(); + + expect(result.svgUrl).toBe(mockData.svgUrl); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/generate with correct body', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + await client.generate + .configure({ prompt: 'A tree', quality: 'high' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generate'); + expect(options.method).toBe('POST'); + + const body = JSON.parse(options.body); + expect(body.prompt).toBe('A tree'); + expect(body.quality).toBe('high'); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + await client.generate + .configure({ prompt: 'A tree' }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('returns properly shaped GenerateResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerateResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.generate + .configure({ prompt: 'A flower' }) + .execute(); + + expect(result.svgUrl).toBe(mockData.svgUrl); + expect(result.creditCost).toBe(mockData.creditCost); + expect(result.message).toBe(mockData.message); + expect(result.generationId).toBe(mockData.generationId); + expect(result.quality).toBe('medium'); + expect(result.metadata).toBeDefined(); + }); + + it('decodes svgText from raw SVG', async () => { + const client = createTestClient(); + const rawSvg = + ''; + mockFetchJsonResponse(createMockGenerateResponse({ svgText: rawSvg })); + + const result = await client.generate + .configure({ prompt: 'A box', svgText: true }) + .execute(); + + expect(result.svgText).toBe(rawSvg); + }); + + it('decodes base64Png to pngImageData Buffer', async () => { + const client = createTestClient(); + const base64Png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=='; + mockFetchJsonResponse( + createMockGenerateResponse({ base64Png }), + ); + + const result = await client.generate + .configure({ prompt: 'A dot', base64Png: true }) + .execute(); + + expect(result.pngImageData).toBeInstanceOf(Buffer); + }); + + it('uses provided quality value', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse({ quality: 'high' })); + + const result = await client.generate + .configure({ prompt: 'A star', quality: 'high' }) + .execute(); + + expect(result.quality).toBe('high'); + }); + + it('passes all parameters in request body', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + await client.generate + .configure({ + prompt: 'A landscape', + quality: 'high', + aspectRatio: 'landscape', + background: 'transparent', + svgText: true, + base64Png: true, + storage: true, + styleParams: { + style: 'flat', + color_mode: 'full_color', + image_complexity: 'illustration', + composition: 'full_scene', + text: 'only_title', + }, + }) + .execute(); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body.prompt).toBe('A landscape'); + expect(body.quality).toBe('high'); + expect(body.aspectRatio).toBe('landscape'); + expect(body.background).toBe('transparent'); + expect(body.svgText).toBe(true); + expect(body.base64Png).toBe(true); + expect(body.storage).toBe(true); + expect(body.styleParams).toEqual({ + style: 'flat', + color_mode: 'full_color', + image_complexity: 'illustration', + composition: 'full_scene', + text: 'only_title', + }); + }); + + it('sends model param without quality defaulting', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse({ quality: undefined })); + + await client.generate + .configure({ prompt: 'A tree', model: 'custom-model-v1' }) + .execute(); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body.model).toBe('custom-model-v1'); + expect(body.prompt).toBe('A tree'); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when prompt is empty string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + await expect( + client.generate.configure({ prompt: '' }).execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when prompt is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + await expect(client.generate.execute()).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when both model and quality are set', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + await expect( + client.generate + .configure({ prompt: 'A cat', model: 'test-model', quality: 'high' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('accepts valid quality values', async () => { + const client = createTestClient(); + + for (const quality of ['low', 'medium', 'high'] as const) { + mockFetchJsonResponse(createMockGenerateResponse({ quality })); + const result = await client.generate + .configure({ prompt: 'Test', quality }) + .execute(); + expect(result).toBeDefined(); + } + }); + + it('accepts valid aspectRatio values', async () => { + const client = createTestClient(); + + for (const aspectRatio of ['auto', 'portrait', 'landscape', 'square'] as const) { + mockFetchJsonResponse(createMockGenerateResponse()); + const result = await client.generate + .configure({ prompt: 'Test', aspectRatio }) + .execute(); + expect(result).toBeDefined(); + } + }); + + it('accepts valid styleParams', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerateResponse()); + + const result = await client.generate + .configure({ + prompt: 'Test', + styleParams: { + style: 'cartoon', + color_mode: 'monochrome', + image_complexity: 'icon', + composition: 'centered_object', + text: 'embedded_text', + }, + }) + .execute(); + + expect(result).toBeDefined(); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + // Note: GenerateClient.execute() uses handleRequest() which goes through + // the retry wrapper. Non-retryable errors (AuthError, InsufficientCreditsError, + // ContentSafetyError) are wrapped in AbortError by the retry utility. + // Retryable status codes (429, 500) are re-thrown as-is after exhausting retries. + + it('throws on INVALID_API_KEY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow('Invalid API key'); + }); + + it('throws on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow('Not enough credits'); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws on CONTENT_POLICY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('CONTENT_POLICY', 422, 'Content policy violation'); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow('Content policy violation'); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow(APIError); + }); + + it('throws on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow('This endpoint is disabled'); + }); + + it('throws on FILE_SIZE_EXCEEDED (413)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('FILE_SIZE_EXCEEDED', 413, 'File too large'); + + await expect( + client.generate.configure({ prompt: 'A cat' }).execute(), + ).rejects.toThrow('File too large'); + }); + }); + + // ========================================================================== + // .stream() + // ========================================================================== + + describe('.stream()', () => { + it('returns a Readable stream', () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.generate + .configure({ prompt: 'A cat' }) + .stream(); + + expect(stream).toBeInstanceOf(Readable); + }); + + it('emits processing events', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.generate + .configure({ prompt: 'A cat' }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const processingEvent = events.find((e) => e.status === 'processing'); + expect(processingEvent).toBeDefined(); + expect(processingEvent.message).toBe('Generating SVG...'); + }); + + it('emits complete event with merged accumulated data', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.generate + .configure({ prompt: 'A cat' }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const completeEvent = events.find((e) => e.status === 'complete'); + expect(completeEvent).toBeDefined(); + // svgText from 'generated' event should be accumulated into 'complete' + expect(completeEvent.svgText).toBeDefined(); + expect(completeEvent.svgUrl).toBeDefined(); + expect(completeEvent.creditCost).toBeDefined(); + }); + + it('sends correct headers for streaming', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockStreamEvents()); + + const stream = client.generate + .configure({ prompt: 'A cat' }) + .stream(); + + // Wait for stream to start + await new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['Accept']).toBe('text/event-stream'); + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('emits error on non-ok response', async () => { + const client = createTestClient(); + + // Mock a non-ok response for streaming + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 401, + headers: new Headers({ 'Content-Type': 'application/json' }), + json: async () => ({ + success: false, + error: { code: 'INVALID_API_KEY', status: 401, message: 'Invalid API key' }, + metadata: { requestId: 'test-req-id' }, + }), + text: async () => '{"success":false}', + }); + + const stream = client.generate + .configure({ prompt: 'A cat' }) + .stream(); + + await expect( + new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }), + ).rejects.toBeDefined(); + }); + + it('decodes svgText in stream events', async () => { + const client = createTestClient(); + const rawSvg = ''; + const streamEvents = [ + { status: 'processing', message: 'Generating SVG...' }, + { + status: 'generated', + message: 'SVG generated', + svgText: rawSvg, + svgUrl: 'https://storage.example.com/test.svg', + creditCost: 1, + }, + { + status: 'complete', + message: 'Generation complete', + generationId: 'gen-stream-001', + metadata: { requestId: 'test-req-id', creditsUsed: 1, creditsRemaining: 99 }, + }, + ]; + mockFetchStreamResponse(streamEvents); + + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const generatedEvent = events.find((e) => e.status === 'generated'); + expect(generatedEvent).toBeDefined(); + expect(generatedEvent.svgText).toBe(rawSvg); + }); + + it('decodes base64Png to pngImageData in stream events', async () => { + const client = createTestClient(); + const base64Png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=='; + const streamEvents = [ + { status: 'processing', message: 'Generating SVG...' }, + { + status: 'generated', + message: 'SVG generated', + svgUrl: 'https://storage.example.com/test.svg', + creditCost: 1, + base64Png, + }, + { + status: 'complete', + message: 'Generation complete', + generationId: 'gen-stream-001', + metadata: { requestId: 'test-req-id', creditsUsed: 1, creditsRemaining: 99 }, + }, + ]; + mockFetchStreamResponse(streamEvents); + + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const generatedEvent = events.find((e) => e.status === 'generated'); + expect(generatedEvent).toBeDefined(); + expect(generatedEvent.pngImageData).toBeInstanceOf(Buffer); + }); + + it('accumulates fields from intermediate events into complete event', async () => { + const client = createTestClient(); + const rawSvg = ''; + const streamEvents = [ + { status: 'processing', message: 'Generating SVG...' }, + { + status: 'generated', + message: 'SVG generated', + svgText: rawSvg, + svgUrl: 'https://storage.example.com/test.svg', + creditCost: 1, + }, + { + status: 'complete', + message: 'Generation complete', + generationId: 'gen-stream-001', + }, + ]; + mockFetchStreamResponse(streamEvents); + + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const completeEvent = events.find((e) => e.status === 'complete'); + expect(completeEvent).toBeDefined(); + // Fields from 'generated' event should be accumulated into 'complete' + expect(completeEvent.svgText).toBe(rawSvg); + expect(completeEvent.svgUrl).toBe('https://storage.example.com/test.svg'); + expect(completeEvent.creditCost).toBe(1); + expect(completeEvent.generationId).toBe('gen-stream-001'); + }); + }); +}); diff --git a/tests/clients/GenerationsClient.test.ts b/tests/clients/GenerationsClient.test.ts new file mode 100644 index 0000000..42a0805 --- /dev/null +++ b/tests/clients/GenerationsClient.test.ts @@ -0,0 +1,403 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockGenerationsListData, + createMockGenerationData, + createMockGenerationDeleteData, + createMockGenerationShareData, + createMockGenerationDownloadData, +} from '../setup'; +import { + ValidationError, + RateLimitError, + APIError, +} from '../../src/errors/CustomErrors'; + +describe('GenerationsClient', () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // ========================================================================== + // .list() + // ========================================================================== + + describe('.list()', () => { + it('sends GET to /v1/generations with no query params when called with no args', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.generations.list(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generations'); + expect(url).not.toContain('?'); + expect(options.method).toBe('GET'); + }); + + it('sends API key in x-api-key header', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.generations.list(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends page and limit as query params', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.generations.list({ page: 2, limit: 25 }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('page=2'); + expect(url).toContain('limit=25'); + }); + + it('handles array params (type, hashtags, categories)', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.generations.list({ + type: ['generate', 'edit'], + hashtags: ['nature', 'art'], + categories: ['landscape'], + }); + + const [url] = fetchMock.mock.calls[0]; + // Arrays should be repeated as separate params: type=generate&type=edit + expect(url).toContain('type=generate'); + expect(url).toContain('type=edit'); + expect(url).toContain('hashtags=nature'); + expect(url).toContain('hashtags=art'); + expect(url).toContain('categories=landscape'); + }); + + it('appends query search param', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await client.generations.list({ query: 'mountain' }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('query=mountain'); + }); + + it('returns properly shaped response (items, pagination, metadata)', async () => { + const client = createTestClient(); + const mockData = createMockGenerationsListData(); + mockFetchJsonResponse(mockData); + + const result = await client.generations.list(); + + expect(result.items).toEqual(mockData.items); + expect(result.pagination).toEqual(mockData.pagination); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .list() validation + // ========================================================================== + + describe('.list() validation', () => { + it('throws ValidationError when page is 0', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await expect( + client.generations.list({ page: 0 }), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when limit exceeds 100', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await expect( + client.generations.list({ limit: 101 }), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError on unknown property due to .strict()', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationsListData()); + + await expect( + client.generations.list({ foo: 'bar' } as any), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // .get(id) + // ========================================================================== + + describe('.get(id)', () => { + it('sends GET to /v1/generations/{id}', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationData()); + + await client.generations.get('gen-001'); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generations/gen-001'); + expect(options.method).toBe('GET'); + }); + + it('URL-encodes the id', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationData()); + + await client.generations.get('id/with/slashes'); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generations/id%2Fwith%2Fslashes'); + }); + + it('returns properly shaped GenerationResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationData(); + mockFetchJsonResponse(mockData); + + const result = await client.generations.get('gen-001'); + + expect(result.id).toBe(mockData.id); + expect(result.prompt).toBe(mockData.prompt); + expect(result.type).toBe(mockData.type); + expect(result.quality).toBe(mockData.quality); + expect(result.isPublic).toBe(mockData.isPublic); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .get(id) validation + // ========================================================================== + + describe('.get(id) validation', () => { + it('throws ValidationError when id is empty string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationData()); + + await expect( + client.generations.get(''), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // .delete(id) + // ========================================================================== + + describe('.delete(id)', () => { + it('sends DELETE to /v1/generations/{id}', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDeleteData()); + + await client.generations.delete('gen-001'); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generations/gen-001'); + expect(options.method).toBe('DELETE'); + }); + + it('returns properly shaped GenerationDeleteResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationDeleteData(); + mockFetchJsonResponse(mockData); + + const result = await client.generations.delete('gen-001'); + + expect(result.message).toBe(mockData.message); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .delete(id) validation + // ========================================================================== + + describe('.delete(id) validation', () => { + it('throws ValidationError when id is empty string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDeleteData()); + + await expect( + client.generations.delete(''), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // .share(id) + // ========================================================================== + + describe('.share(id)', () => { + it('sends POST to /v1/generations/{id}/share', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationShareData()); + + await client.generations.share('gen-001'); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generations/gen-001/share'); + expect(options.method).toBe('POST'); + }); + + it('returns properly shaped GenerationShareResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationShareData(); + mockFetchJsonResponse(mockData); + + const result = await client.generations.share('gen-001'); + + expect(result.message).toBe(mockData.message); + expect(result.isPublic).toBe(mockData.isPublic); + expect(result.shareUrl).toBe(mockData.shareUrl); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .share(id) validation + // ========================================================================== + + describe('.share(id) validation', () => { + it('throws ValidationError when id is empty string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationShareData()); + + await expect( + client.generations.share(''), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // .download(id, params?) + // ========================================================================== + + describe('.download(id, params?)', () => { + it('sends GET to /v1/generations/{id}/download with no query params by default', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDownloadData()); + + await client.generations.download('gen-001'); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/generations/gen-001/download'); + expect(url).not.toContain('format='); + expect(url).not.toContain('optimize='); + expect(options.method).toBe('GET'); + }); + + it('appends format and optimize as query params', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDownloadData()); + + await client.generations.download('gen-001', { format: 'png', optimize: true }); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('format=png'); + expect(url).toContain('optimize=true'); + }); + + it('returns properly shaped GenerationDownloadResponse', async () => { + const client = createTestClient(); + const mockData = createMockGenerationDownloadData(); + mockFetchJsonResponse(mockData); + + const result = await client.generations.download('gen-001'); + + expect(result.id).toBe(mockData.id); + expect(result.url).toBe(mockData.url); + expect(result.urlExpiresIn).toBe(mockData.urlExpiresIn); + expect(result.format).toBe(mockData.format); + expect(result.filename).toBe(mockData.filename); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // .download() validation + // ========================================================================== + + describe('.download() validation', () => { + it('throws ValidationError when id is empty string', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockGenerationDownloadData()); + + await expect( + client.generations.download(''), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // Error handling (using .list() as representative) + // ========================================================================== + + describe('error handling', () => { + it('throws "Invalid API key" on INVALID_API_KEY (401)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); + + await expect( + client.generations.list(), + ).rejects.toThrow('Invalid API key'); + }); + + it('throws "Not enough credits" on INSUFFICIENT_CREDITS (402)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); + + await expect( + client.generations.list(), + ).rejects.toThrow('Not enough credits'); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED (429)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect( + client.generations.list(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws "This endpoint is disabled" on ENDPOINT_DISABLED (503)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect( + client.generations.list(), + ).rejects.toThrow('This endpoint is disabled'); + }); + + it('throws APIError on generic server error (500)', async () => { + const client = createTestClient(); + mockFetchErrorResponse('SERVER_ERROR', 500, 'Internal server error'); + + await expect( + client.generations.list(), + ).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/OptimizeSvgClient.test.ts b/tests/clients/OptimizeSvgClient.test.ts new file mode 100644 index 0000000..168fe75 --- /dev/null +++ b/tests/clients/OptimizeSvgClient.test.ts @@ -0,0 +1,166 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockOptimizeSvgResponse, +} from '../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../src/errors/CustomErrors'; + +describe('OptimizeSvgClient', () => { + let fetchMock: jest.Mock; + let originalExistsSync: any; + let originalReadFileSync: any; + + beforeEach(() => { + fetchMock = setupFetchMock(); + const fs = require('fs'); + originalExistsSync = fs.existsSync; + originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-svg-data')); + }); + + afterEach(() => { + cleanupMocks(); + const fs = require('fs'); + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + }); + + // .configure() - 2 tests + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.optimizeSvg; + const configured = original.configure({ file: '/test.svg' }); + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockOptimizeSvgResponse()); + const configured = client.optimizeSvg + .configure({ file: '/test.svg' }) + .configure({ compress: true }); + await configured.execute(); + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/svg/optimize'); + }); + }); + + // .execute() - 4 tests + describe('.execute()', () => { + it('sends POST to /v1/svg/optimize with FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockOptimizeSvgResponse()); + await client.optimizeSvg.configure({ file: '/test.svg' }).execute(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/svg/optimize'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockOptimizeSvgResponse()); + await client.optimizeSvg.configure({ file: '/test.svg' }).execute(); + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends file and optional compress param to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockOptimizeSvgResponse()); + await client.optimizeSvg + .configure({ file: '/test.svg', compress: true }) + .execute(); + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + expect(formData.get('compress')).toBe('true'); + }); + + it('returns properly shaped OptimizeSvgResponse', async () => { + const client = createTestClient(); + const mockData = createMockOptimizeSvgResponse(); + mockFetchJsonResponse(mockData); + const result = await client.optimizeSvg + .configure({ file: '/test.svg' }) + .execute(); + expect(result.svgUrl).toBe(mockData.svgUrl); + expect(result.svgUrlExpiresIn).toBe(mockData.svgUrlExpiresIn); + expect(result.metadata).toBeDefined(); + }); + }); + + // validation - 2 tests + describe('validation', () => { + it('throws ValidationError when file is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockOptimizeSvgResponse()); + await expect(client.optimizeSvg.execute()).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when file path does not exist', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockOptimizeSvgResponse()); + const fs = require('fs'); + fs.existsSync = jest.fn().mockReturnValue(false); + await expect( + client.optimizeSvg.configure({ file: '/nonexistent.svg' }).execute(), + ).rejects.toThrow(ValidationError); + }); + }); + + // error handling - 5 tests + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401); + await expect( + client.optimizeSvg.configure({ file: '/test.svg' }).execute(), + ).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + await expect( + client.optimizeSvg.configure({ file: '/test.svg' }).execute(), + ).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + await expect( + client.optimizeSvg.configure({ file: '/test.svg' }).execute(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503); + await expect( + client.optimizeSvg.configure({ file: '/test.svg' }).execute(), + ).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INTERNAL_ERROR', 500); + await expect( + client.optimizeSvg.configure({ file: '/test.svg' }).execute(), + ).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/convert/AIVectorizeClient.test.ts b/tests/clients/convert/AIVectorizeClient.test.ts new file mode 100644 index 0000000..65af577 --- /dev/null +++ b/tests/clients/convert/AIVectorizeClient.test.ts @@ -0,0 +1,441 @@ +import { Readable } from 'stream'; +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + mockFetchStreamResponse, + createMockAiVectorizeResponse, + createMockAiVectorizeStreamEvents, +} from '../../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../../src/errors/CustomErrors'; + +describe('AIVectorizeClient', () => { + let fetchMock: jest.Mock; + const testFileBuffer = Buffer.from('fake-png-image-data'); + + beforeEach(() => { + fetchMock = setupFetchMock(); + }); + + afterEach(() => { + cleanupMocks(); + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.convert.aiVectorize; + const configured = original.configure({ file: testFileBuffer }); + + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + const configured = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .configure({ svgText: true }); + + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/ai-vectorize'); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/convert/ai-vectorize with FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + await client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(); + + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/ai-vectorize'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + await client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends file as Blob to FormData (Buffer input)', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + await client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + }); + + it('appends optional params (storage, svgText) to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + await client.convert.aiVectorize + .configure({ file: testFileBuffer, storage: true, svgText: true }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('storage')).toBe('true'); + expect(formData.get('svgText')).toBe('true'); + }); + + it('returns properly shaped AiVectorizeResponse', async () => { + const client = createTestClient(); + const mockData = createMockAiVectorizeResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(); + + expect(result.svgUrl).toBe(mockData.svgUrl); + expect(result.creditCost).toBe(mockData.creditCost); + expect(result.message).toBe(mockData.message); + expect(result.generationId).toBe(mockData.generationId); + expect(result.metadata).toBeDefined(); + }); + + it('decodes raw SVG svgText via decodeSvgContent()', async () => { + const client = createTestClient(); + const rawSvg = + ''; + mockFetchJsonResponse(createMockAiVectorizeResponse({ svgText: rawSvg })); + + const result = await client.convert.aiVectorize + .configure({ file: testFileBuffer, svgText: true }) + .execute(); + + expect(result.svgText).toBe(rawSvg); + }); + + it('accepts file as string path', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + const fs = require('fs'); + const originalExistsSync = fs.existsSync; + const originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-png-data')); + + try { + await client.convert.aiVectorize + .configure({ file: '/path/to/image.png' }) + .execute(); + + expect(fs.existsSync).toHaveBeenCalledWith('/path/to/image.png'); + expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/image.png'); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + } finally { + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + } + }); + + it('accepts file as Readable stream', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockAiVectorizeResponse()); + + const readable = new Readable(); + readable.push(Buffer.from('fake-stream-data')); + readable.push(null); + + await client.convert.aiVectorize + .configure({ file: readable }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when file is missing', async () => { + const client = createTestClient(); + + await expect( + client.convert.aiVectorize.execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when file path does not exist', async () => { + const client = createTestClient(); + + await expect( + client.convert.aiVectorize + .configure({ file: '/nonexistent/path/image.png' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INVALID_API_KEY', 401); + + await expect( + client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(), + ).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + + await expect( + client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(), + ).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect( + client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(), + ).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect( + client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(), + ).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect( + client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .execute(), + ).rejects.toThrow(APIError); + }); + }); + + // ========================================================================== + // .stream() + // ========================================================================== + + describe('.stream()', () => { + it('returns a Readable stream', () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + expect(stream).toBeInstanceOf(Readable); + }); + + it('emits processing events', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const processingEvent = events.find((e) => e.status === 'processing'); + expect(processingEvent).toBeDefined(); + expect(processingEvent.message).toBe('Vectorizing image...'); + }); + + it('emits complete event with merged accumulated data', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const completeEvent = events.find((e) => e.status === 'complete'); + expect(completeEvent).toBeDefined(); + expect(completeEvent.svgUrl).toBeDefined(); + expect(completeEvent.creditCost).toBeDefined(); + expect(completeEvent.svgText).toBeDefined(); + }); + + it('sends FormData with stream=true', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + await new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('stream')).toBe('true'); + }); + + it('sends correct headers (Accept: text/event-stream, x-api-key)', async () => { + const client = createTestClient(); + mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + await new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['Accept']).toBe('text/event-stream'); + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('decodes svgText in stream events', async () => { + const client = createTestClient(); + const rawSvg = + ''; + const streamEvents = [ + { status: 'processing', message: 'Vectorizing...' }, + { + status: 'generated', + message: 'Done', + svgText: rawSvg, + svgUrl: 'https://x.com/test.svg', + creditCost: 2, + }, + { + status: 'complete', + message: 'Complete', + generationId: 'vec-001', + metadata: { requestId: 'r1', creditsUsed: 2, creditsRemaining: 97 }, + }, + ]; + mockFetchStreamResponse(streamEvents); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + const events: any[] = []; + await new Promise((resolve, reject) => { + stream.on('data', (event: any) => events.push(event)); + stream.on('end', resolve); + stream.on('error', reject); + }); + + const generatedEvent = events.find((e) => e.status === 'generated'); + expect(generatedEvent).toBeDefined(); + expect(generatedEvent.svgText).toBe(rawSvg); + }); + + it('emits error on non-ok response', async () => { + const client = createTestClient(); + + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 401, + headers: new Headers({ 'Content-Type': 'application/json' }), + json: async () => ({ + success: false, + error: { + code: 'INVALID_API_KEY', + status: 401, + message: 'Invalid API key', + }, + metadata: { requestId: 'test-req-id' }, + }), + text: async () => '{"success":false}', + }); + + const stream = client.convert.aiVectorize + .configure({ file: testFileBuffer }) + .stream(); + + await expect( + new Promise((resolve, reject) => { + stream.on('data', () => {}); + stream.on('end', resolve); + stream.on('error', reject); + }), + ).rejects.toBeDefined(); + }); + }); +}); diff --git a/tests/clients/convert/BatchConvertClient.test.ts b/tests/clients/convert/BatchConvertClient.test.ts new file mode 100644 index 0000000..7601dd0 --- /dev/null +++ b/tests/clients/convert/BatchConvertClient.test.ts @@ -0,0 +1,277 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockConvertResultsResponse, +} from '../../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../../src/errors/CustomErrors'; + +describe('BatchConvertClient', () => { + let fetchMock: jest.Mock; + let originalExistsSync: any; + let originalReadFileSync: any; + + beforeEach(() => { + fetchMock = setupFetchMock(); + const fs = require('fs'); + originalExistsSync = fs.existsSync; + originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-file-data')); + }); + + afterEach(() => { + cleanupMocks(); + const fs = require('fs'); + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.convert.batch; + const configured = original.configure({ files: ['/img.png'] }); + + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + const configured = client.convert.batch + .configure({ files: ['/a.png', '/b.jpg'] }) + .configure({ toFormat: 'svg' }) + .configure({ preset: 'bw' }); + + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/batch'); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/convert/batch with FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/batch'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends multiple files as separate file fields', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.batch + .configure({ files: ['/a.png', '/b.jpg'], toFormat: 'svg' }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.getAll('file').length).toBe(2); + }); + + it('appends toFormat and optional params to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.batch + .configure({ + files: ['/img.png'], + toFormat: 'svg', + preset: 'poster', + mode: 'spline', + detail: 50, + textToPath: true, + quality: 90, + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('toFormat')).toBe('svg'); + expect(formData.get('preset')).toBe('poster'); + expect(formData.get('mode')).toBe('spline'); + expect(formData.get('detail')).toBe('50'); + expect(formData.get('textToPath')).toBe('true'); + expect(formData.get('quality')).toBe('90'); + }); + + it('returns properly shaped ConvertResultsResponse with multiple results', async () => { + const client = createTestClient(); + mockFetchJsonResponse( + createMockConvertResultsResponse({ + results: [ + { filename: 'a.png', success: true, url: 'https://x.com/a.svg', urlExpiresIn: '24h', format: 'svg' }, + { filename: 'b.jpg', success: true, url: 'https://x.com/b.svg', urlExpiresIn: '24h', format: 'svg' }, + { filename: 'c.png', success: false, error: 'Processing failed' }, + ], + summary: { total: 3, successful: 2, failed: 1 }, + }), + ); + + const result = await client.convert.batch + .configure({ files: ['/a.png', '/b.jpg', '/c.png'], toFormat: 'svg' }) + .execute(); + + expect(result.results.length).toBe(3); + expect(result.summary.total).toBe(3); + expect(result.summary.successful).toBe(2); + expect(result.summary.failed).toBe(1); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when files is missing', async () => { + const client = createTestClient(); + + await expect( + client.convert.batch + .configure({ toFormat: 'svg' } as any) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when files is empty array', async () => { + const client = createTestClient(); + + await expect( + client.convert.batch + .configure({ files: [], toFormat: 'svg' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when files exceeds 10', async () => { + const client = createTestClient(); + + await expect( + client.convert.batch + .configure({ files: Array(11).fill('/img.png'), toFormat: 'svg' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when toFormat is missing', async () => { + const client = createTestClient(); + + await expect( + client.convert.batch + .configure({ files: ['/img.png'] }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('accepts boundary counts (1 file and 10 files)', async () => { + const client = createTestClient(); + + // Test with 1 file + mockFetchJsonResponse(createMockConvertResultsResponse()); + const result1 = await client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }) + .execute(); + expect(result1).toBeDefined(); + + // Test with 10 files + mockFetchJsonResponse(createMockConvertResultsResponse()); + const result10 = await client.convert.batch + .configure({ files: Array(10).fill('/img.png'), toFormat: 'svg' }) + .execute(); + expect(result10).toBeDefined(); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + const configured = client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }); + mockFetchErrorResponse('INVALID_API_KEY', 401); + + await expect(configured.execute()).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + const configured = client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + + await expect(configured.execute()).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + const configured = client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect(configured.execute()).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + const configured = client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect(configured.execute()).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + const configured = client.convert.batch + .configure({ files: ['/img.png'], toFormat: 'svg' }); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect(configured.execute()).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/convert/RasterToRasterClient.test.ts b/tests/clients/convert/RasterToRasterClient.test.ts new file mode 100644 index 0000000..c3d10f8 --- /dev/null +++ b/tests/clients/convert/RasterToRasterClient.test.ts @@ -0,0 +1,278 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockConvertResultsResponse, +} from '../../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../../src/errors/CustomErrors'; + +describe('RasterToRasterClient', () => { + let fetchMock: jest.Mock; + let originalExistsSync: any; + let originalReadFileSync: any; + + beforeEach(() => { + fetchMock = setupFetchMock(); + const fs = require('fs'); + originalExistsSync = fs.existsSync; + originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-file-data')); + }); + + afterEach(() => { + cleanupMocks(); + const fs = require('fs'); + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.convert.rasterToRaster; + const configured = original.configure({ file: '/img.png' }); + + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + const configured = client.convert.rasterToRaster + .configure({ file: '/img.png' }) + .configure({ toFormat: 'WEBP' }) + .configure({ quality: 80 }); + + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/raster-to-raster'); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/convert/raster-to-raster with FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/raster-to-raster'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends file, toFormat, and optional params to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.rasterToRaster + .configure({ + file: '/img.png', + toFormat: 'WEBP', + quality: 85, + width: 1024, + height: 768, + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + expect(formData.get('toFormat')).toBe('WEBP'); + expect(formData.get('quality')).toBe('85'); + expect(formData.get('width')).toBe('1024'); + expect(formData.get('height')).toBe('768'); + }); + + it('returns properly shaped ConvertResultsResponse', async () => { + const client = createTestClient(); + const mockData = createMockConvertResultsResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }) + .execute(); + + expect(Array.isArray(result.results)).toBe(true); + expect(result.summary.total).toBe(1); + expect(result.summary.successful).toBe(1); + expect(result.summary.failed).toBe(0); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when file is missing', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ toFormat: 'PNG' } as any) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when toFormat is missing', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ file: '/img.png' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError for invalid toFormat value', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'SVG' as any }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when quality < 1', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG', quality: 0 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when quality > 100', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG', quality: 101 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when width is non-positive', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG', width: 0 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when height is negative', async () => { + const client = createTestClient(); + + await expect( + client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG', height: -10 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('accepts all valid toFormat values', async () => { + const client = createTestClient(); + + for (const toFormat of ['PNG', 'JPG', 'WEBP', 'TIFF', 'GIF', 'AVIF'] as const) { + mockFetchJsonResponse(createMockConvertResultsResponse()); + const result = await client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat }) + .execute(); + expect(result).toBeDefined(); + } + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + const configured = client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }); + mockFetchErrorResponse('INVALID_API_KEY', 401); + + await expect(configured.execute()).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + const configured = client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + + await expect(configured.execute()).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + const configured = client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect(configured.execute()).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + const configured = client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect(configured.execute()).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + const configured = client.convert.rasterToRaster + .configure({ file: '/img.png', toFormat: 'PNG' }); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect(configured.execute()).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/convert/SvgToVectorClient.test.ts b/tests/clients/convert/SvgToVectorClient.test.ts new file mode 100644 index 0000000..1f0b898 --- /dev/null +++ b/tests/clients/convert/SvgToVectorClient.test.ts @@ -0,0 +1,262 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockConvertResultsResponse, +} from '../../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../../src/errors/CustomErrors'; + +describe('SvgToVectorClient', () => { + let fetchMock: jest.Mock; + let originalExistsSync: any; + let originalReadFileSync: any; + + beforeEach(() => { + fetchMock = setupFetchMock(); + const fs = require('fs'); + originalExistsSync = fs.existsSync; + originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-file-data')); + }); + + afterEach(() => { + cleanupMocks(); + const fs = require('fs'); + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.convert.svgToVector; + const configured = original.configure({ file: '/img.svg', toFormat: 'PDF' }); + + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + const configured = client.convert.svgToVector + .configure({ file: '/img.svg' }) + .configure({ toFormat: 'PDF' }) + .configure({ textToPath: true }); + + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/svg-to-vector'); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/convert/svg-to-vector with FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.svgToVector + .configure({ file: '/img.svg', toFormat: 'PDF' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/svg-to-vector'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.svgToVector + .configure({ file: '/img.svg', toFormat: 'PDF' }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends file, toFormat, and optional params to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.svgToVector + .configure({ + file: '/img.svg', + toFormat: 'DXF', + textToPath: true, + dxfVersion: 'R14', + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + expect(formData.get('toFormat')).toBe('DXF'); + expect(formData.get('textToPath')).toBe('true'); + expect(formData.get('dxfVersion')).toBe('R14'); + }); + + it('returns properly shaped ConvertResultsResponse', async () => { + const client = createTestClient(); + const mockData = createMockConvertResultsResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.convert.svgToVector + .configure({ file: '/img.svg', toFormat: 'PDF' }) + .execute(); + + expect(Array.isArray(result.results)).toBe(true); + expect(result.results[0].filename).toBe('test-image.png'); + expect(result.summary.total).toBe(1); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when file is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.svgToVector + .configure({ toFormat: 'PDF' } as any) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when toFormat is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.svgToVector + .configure({ file: '/img.svg' }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError for invalid toFormat value', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.svgToVector + .configure({ file: '/img.svg', toFormat: 'BMP' as any }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError for invalid dxfVersion', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.svgToVector + .configure({ file: '/img.svg', toFormat: 'DXF', dxfVersion: 'R15' as any }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('accepts all valid toFormat values', async () => { + const validFormats = ['PDF', 'EPS', 'DXF', 'AI', 'PS'] as const; + + for (const toFormat of validFormats) { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + const result = await client.convert.svgToVector + .configure({ file: '/img.svg', toFormat }) + .execute(); + + expect(result).toBeDefined(); + } + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + const configured = client.convert.svgToVector.configure({ + file: '/img.svg', + toFormat: 'PDF', + }); + mockFetchErrorResponse('INVALID_API_KEY', 401); + + await expect(configured.execute()).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + const configured = client.convert.svgToVector.configure({ + file: '/img.svg', + toFormat: 'PDF', + }); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + + await expect(configured.execute()).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + const configured = client.convert.svgToVector.configure({ + file: '/img.svg', + toFormat: 'PDF', + }); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect(configured.execute()).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + const configured = client.convert.svgToVector.configure({ + file: '/img.svg', + toFormat: 'PDF', + }); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect(configured.execute()).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + const configured = client.convert.svgToVector.configure({ + file: '/img.svg', + toFormat: 'PDF', + }); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect(configured.execute()).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/clients/convert/TraceClient.test.ts b/tests/clients/convert/TraceClient.test.ts new file mode 100644 index 0000000..0f772ca --- /dev/null +++ b/tests/clients/convert/TraceClient.test.ts @@ -0,0 +1,290 @@ +import { + createTestClient, + setupFetchMock, + cleanupMocks, + mockFetchJsonResponse, + mockFetchErrorResponse, + createMockConvertResultsResponse, +} from '../../setup'; +import { + ValidationError, + AuthError, + InsufficientCreditsError, + RateLimitError, + EndpointDisabledError, + APIError, +} from '../../../src/errors/CustomErrors'; + +describe('TraceClient', () => { + let fetchMock: jest.Mock; + let originalExistsSync: any; + let originalReadFileSync: any; + + beforeEach(() => { + fetchMock = setupFetchMock(); + const fs = require('fs'); + originalExistsSync = fs.existsSync; + originalReadFileSync = fs.readFileSync; + fs.existsSync = jest.fn().mockReturnValue(true); + fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-file-data')); + }); + + afterEach(() => { + cleanupMocks(); + const fs = require('fs'); + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + }); + + // ========================================================================== + // .configure() + // ========================================================================== + + describe('.configure()', () => { + it('returns a new instance (immutability)', () => { + const client = createTestClient(); + const original = client.convert.trace; + const configured = original.configure({ file: '/img.png' }); + + expect(configured).not.toBe(original); + }); + + it('merges params across multiple configure calls', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + const configured = client.convert.trace + .configure({ file: '/img.png' }) + .configure({ preset: 'poster' }); + + await configured.execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/trace'); + }); + }); + + // ========================================================================== + // .execute() + // ========================================================================== + + describe('.execute()', () => { + it('sends POST to /v1/convert/trace with FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.trace + .configure({ file: '/test.png' }) + .execute(); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain('/v1/convert/trace'); + expect(options.method).toBe('POST'); + expect(options.body).toBeInstanceOf(FormData); + }); + + it('sends API key in headers', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.trace + .configure({ file: '/test.png' }) + .execute(); + + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers['x-api-key']).toBe('test-api-key-123'); + }); + + it('appends file and all optional params to FormData', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await client.convert.trace + .configure({ + file: '/test.png', + algorithm: 'vtracer', + preset: 'bw', + mode: 'spline', + hierarchical: 'stacked', + detail: 50, + smoothness: 75, + corners: 30, + reduceNoise: 5, + }) + .execute(); + + const formData = fetchMock.mock.calls[0][1].body as FormData; + expect(formData.get('file')).toBeTruthy(); + expect(formData.get('algorithm')).toBe('vtracer'); + expect(formData.get('preset')).toBe('bw'); + expect(formData.get('mode')).toBe('spline'); + expect(formData.get('hierarchical')).toBe('stacked'); + expect(formData.get('detail')).toBe('50'); + expect(formData.get('smoothness')).toBe('75'); + expect(formData.get('corners')).toBe('30'); + expect(formData.get('reduceNoise')).toBe('5'); + }); + + it('returns properly shaped ConvertResultsResponse', async () => { + const client = createTestClient(); + const mockData = createMockConvertResultsResponse(); + mockFetchJsonResponse(mockData); + + const result = await client.convert.trace + .configure({ file: '/test.png' }) + .execute(); + + expect(Array.isArray(result.results)).toBe(true); + expect(result.results[0].filename).toBe('test-image.png'); + expect(result.summary.total).toBe(1); + expect(result.metadata).toBeDefined(); + }); + }); + + // ========================================================================== + // Validation + // ========================================================================== + + describe('validation', () => { + it('throws ValidationError when file is missing', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace.execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError for invalid preset value', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace + .configure({ file: '/img.png', preset: 'invalid' as any }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError for invalid mode value', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace + .configure({ file: '/img.png', mode: 'invalid' as any }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when detail < 0', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace + .configure({ file: '/img.png', detail: -1 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when detail > 100', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace + .configure({ file: '/img.png', detail: 101 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when smoothness > 100', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace + .configure({ file: '/img.png', smoothness: 101 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('throws ValidationError when corners < 0', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + await expect( + client.convert.trace + .configure({ file: '/img.png', corners: -1 }) + .execute(), + ).rejects.toThrow(ValidationError); + }); + + it('accepts valid params', async () => { + const client = createTestClient(); + mockFetchJsonResponse(createMockConvertResultsResponse()); + + const result = await client.convert.trace + .configure({ + file: '/img.png', + preset: 'photo', + mode: 'polygon', + detail: 0, + smoothness: 100, + corners: 50, + }) + .execute(); + + expect(result).toBeDefined(); + }); + }); + + // ========================================================================== + // Error Handling + // ========================================================================== + + describe('error handling', () => { + it('throws AuthError on INVALID_API_KEY', async () => { + const client = createTestClient(); + const configured = client.convert.trace.configure({ file: '/img.png' }); + mockFetchErrorResponse('INVALID_API_KEY', 401); + + await expect(configured.execute()).rejects.toThrow(AuthError); + }); + + it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { + const client = createTestClient(); + const configured = client.convert.trace.configure({ file: '/img.png' }); + mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); + + await expect(configured.execute()).rejects.toThrow(InsufficientCreditsError); + }); + + it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { + const client = createTestClient(); + const configured = client.convert.trace.configure({ file: '/img.png' }); + mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); + + await expect(configured.execute()).rejects.toThrow(RateLimitError); + }); + + it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { + const client = createTestClient(); + const configured = client.convert.trace.configure({ file: '/img.png' }); + mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); + + await expect(configured.execute()).rejects.toThrow(EndpointDisabledError); + }); + + it('throws APIError on generic server error', async () => { + const client = createTestClient(); + const configured = client.convert.trace.configure({ file: '/img.png' }); + mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); + + await expect(configured.execute()).rejects.toThrow(APIError); + }); + }); +}); diff --git a/tests/setup/index.ts b/tests/setup/index.ts new file mode 100644 index 0000000..1b41d50 --- /dev/null +++ b/tests/setup/index.ts @@ -0,0 +1,2 @@ +export * from './test-setup'; +export * from './mock-factories'; diff --git a/tests/setup/mock-factories.ts b/tests/setup/mock-factories.ts new file mode 100644 index 0000000..d59df79 --- /dev/null +++ b/tests/setup/mock-factories.ts @@ -0,0 +1,288 @@ +import { ResponseMetadata } from '../../src/types/api'; + +// ============================================================================ +// Mock Data Factories +// ============================================================================ + +/** + * Create mock response metadata + */ +export function createMockMetadata( + overrides?: Partial, +): ResponseMetadata { + return { + requestId: 'test-req-id', + creditsUsed: 1, + creditsRemaining: 99, + ...overrides, + }; +} + +/** + * Create mock generate response data (v1 envelope data field) + */ +export function createMockGenerateResponse( + overrides?: Record, +): Record { + return { + svgUrl: 'https://storage.example.com/test.svg', + creditCost: 1, + message: 'SVG generated successfully', + quality: 'medium', + svgUrlExpiresIn: '24h', + generationId: 'gen-test-123', + ...overrides, + }; +} + +/** + * Create mock edit response data (v1 envelope data field) + */ +export function createMockEditResponse( + overrides?: Record, +): Record { + return { + svgUrl: 'https://storage.example.com/test-edit.svg', + creditCost: 2, + message: 'Image edited successfully', + svgUrlExpiresIn: '24h', + generationId: 'edit-test-123', + ...overrides, + }; +} + +/** + * Create mock stream events for generate/edit streaming + */ +export function createMockStreamEvents(): Record[] { + return [ + { status: 'processing', message: 'Generating SVG...' }, + { + status: 'generated', + message: 'SVG generated', + svgUrl: 'https://storage.example.com/test.svg', + creditCost: 1, + svgText: + '', + }, + { + status: 'complete', + message: 'Generation complete', + generationId: 'gen-stream-001', + metadata: { + requestId: 'test-req-id', + creditsUsed: 1, + creditsRemaining: 99, + }, + }, + ]; +} + +/** + * Create mock AI vectorize response data (v1 envelope data field) + */ +export function createMockAiVectorizeResponse( + overrides?: Record, +): Record { + return { + svgUrl: 'https://storage.example.com/vectorized.svg', + creditCost: 2, + message: 'Image vectorized successfully', + svgUrlExpiresIn: '24h', + generationId: 'vec-test-123', + ...overrides, + }; +} + +/** + * Create mock convert results response data (v1 envelope data field) + */ +export function createMockConvertResultsResponse( + overrides?: Record, +): Record { + return { + results: [ + { + filename: 'test-image.png', + success: true, + url: 'https://storage.example.com/converted/test-image.svg', + urlExpiresIn: '24h', + format: 'svg', + }, + ], + summary: { + total: 1, + successful: 1, + failed: 0, + }, + ...overrides, + }; +} + +/** + * Create mock AI vectorize stream events + */ +export function createMockAiVectorizeStreamEvents(): Record[] { + return [ + { status: 'processing', message: 'Vectorizing image...' }, + { + status: 'generated', + message: 'SVG generated', + svgUrl: 'https://storage.example.com/vectorized.svg', + creditCost: 2, + svgText: + '', + }, + { + status: 'complete', + message: 'Vectorization complete', + generationId: 'vec-stream-001', + metadata: { + requestId: 'test-req-id', + creditsUsed: 2, + creditsRemaining: 97, + }, + }, + ]; +} + +/** + * Create mock optimize SVG response data + */ +export function createMockOptimizeSvgResponse( + overrides?: Record, +): Record { + return { + svgUrl: 'https://storage.example.com/optimized.svg', + svgUrlExpiresIn: '24h', + ...overrides, + }; +} + +/** + * Create mock enhance prompt response data + */ +export function createMockEnhancePromptResponse( + overrides?: Record, +): Record { + return { + enhancedPrompt: 'A beautifully detailed mountain landscape with snow-capped peaks', + ...overrides, + }; +} + +/** + * Create mock generations list response data (v1 envelope data field) + */ +export function createMockGenerationsListData( + overrides?: Record, +): Record { + return { + items: ['gen-001', 'gen-002'], + pagination: { + page: 1, + limit: 10, + totalItems: 2, + totalPages: 1, + hasNextPage: false, + hasPrevPage: false, + }, + ...overrides, + }; +} + +/** + * Create mock single generation response data (v1 envelope data field) + */ +export function createMockGenerationData( + overrides?: Record, +): Record { + return { + id: 'gen-001', + prompt: 'A mountain landscape', + type: 'generate', + quality: 'medium', + isPublic: false, + ...overrides, + }; +} + +/** + * Create mock generation delete response data (v1 envelope data field) + */ +export function createMockGenerationDeleteData( + overrides?: Record, +): Record { + return { + message: 'Generation deleted successfully', + ...overrides, + }; +} + +/** + * Create mock generation share response data (v1 envelope data field) + */ +export function createMockGenerationShareData( + overrides?: Record, +): Record { + return { + message: 'Generation shared', + isPublic: true, + shareUrl: 'https://svgmaker.io/gallery/gen-001', + ...overrides, + }; +} + +/** + * Create mock generation download response data (v1 envelope data field) + */ +export function createMockGenerationDownloadData( + overrides?: Record, +): Record { + return { + id: 'gen-001', + url: 'https://storage.example.com/gen-001.svg', + urlExpiresIn: '12h', + format: 'svg', + filename: 'gen-001.svg', + ...overrides, + }; +} + +/** + * Create mock account info response data (v1 envelope data field) + */ +export function createMockAccountInfoData( + overrides?: Record, +): Record { + return { + email: 'user@example.com', + displayName: 'Test User', + accountType: 'premium', + credits: 500, + ...overrides, + }; +} + +/** + * Create mock account usage response data (v1 envelope data field) + */ +export function createMockAccountUsageData( + overrides?: Record, +): Record { + return { + period: { type: 'all' }, + summary: { + requests: 42, + creditsUsed: 35, + successCount: 40, + errorCount: 2, + successRate: 0.95, + }, + byCategory: { + generate: { requests: 30, creditsUsed: 25 }, + edit: { requests: 12, creditsUsed: 10 }, + }, + ...overrides, + }; +} diff --git a/tests/setup/test-setup.ts b/tests/setup/test-setup.ts new file mode 100644 index 0000000..ddcb91b --- /dev/null +++ b/tests/setup/test-setup.ts @@ -0,0 +1,147 @@ +import { SVGMakerConfig } from '../../src/types/config'; +import { SVGMakerClient } from '../../src/core/SVGMakerClient'; + +// ============================================================================ +// Test Constants +// ============================================================================ + +export const TEST_API_KEY = 'test-api-key-123'; +export const TEST_BASE_URL = 'https://svgmaker.io/api'; + +/** + * Default test configuration with retries disabled + */ +export const DEFAULT_TEST_CONFIG: Partial = { + baseUrl: TEST_BASE_URL, + timeout: 30000, + maxRetries: 0, + retryBackoffFactor: 300, + retryStatusCodes: [429, 500], + logging: false, + logLevel: 'info', + caching: false, + cacheTTL: 300000, + rateLimit: 9999, +}; + +// ============================================================================ +// Client Factory +// ============================================================================ + +/** + * Create a new SVGMakerClient instance configured for testing + */ +export function createTestClient(): SVGMakerClient { + return new SVGMakerClient(TEST_API_KEY, DEFAULT_TEST_CONFIG); +} + +// ============================================================================ +// Fetch Mock Helpers +// ============================================================================ + +let originalFetch: typeof globalThis.fetch; + +/** + * Replace global.fetch with a Jest mock function and return it + */ +export function setupFetchMock(): jest.Mock { + originalFetch = globalThis.fetch; + const mock = jest.fn() as jest.Mock; + globalThis.fetch = mock; + return mock; +} + +/** + * Restore the original global.fetch and clear all jest mocks + */ +export function cleanupMocks(): void { + globalThis.fetch = originalFetch; + jest.restoreAllMocks(); +} + +// ============================================================================ +// Mock Response Builders +// ============================================================================ + +/** + * Configure the global fetch mock to return a successful v1 API envelope response + */ +export function mockFetchJsonResponse( + data: Record, + metadata?: Record, +): void { + const responseBody = { + success: true, + data, + metadata: metadata || { + requestId: 'test-req-id', + creditsUsed: 1, + creditsRemaining: 99, + }, + }; + + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers({ 'Content-Type': 'application/json' }), + json: async () => responseBody, + text: async () => JSON.stringify(responseBody), + }); +} + +/** + * Configure the global fetch mock to return an error response + */ +export function mockFetchErrorResponse( + errorCode: string, + status: number, + message?: string, +): void { + const responseBody = { + success: false, + error: { + code: errorCode, + status, + message: message || 'Error', + }, + metadata: { + requestId: 'test-req-id', + }, + }; + + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status, + headers: new Headers({ 'Content-Type': 'application/json' }), + json: async () => responseBody, + text: async () => JSON.stringify(responseBody), + }); +} + +/** + * Configure the global fetch mock to return a streaming response + * with newline-delimited JSON events + */ +export function mockFetchStreamResponse(events: Record[]): void { + const lines = events.map(e => JSON.stringify(e)).join('\n') + '\n'; + const encoded = new TextEncoder().encode(lines); + + let consumed = false; + + const mockReader = { + read: jest.fn().mockImplementation(async () => { + if (!consumed) { + consumed = true; + return { done: false, value: encoded }; + } + return { done: true, value: undefined }; + }), + }; + + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers({ 'Content-Type': 'text/event-stream' }), + body: { getReader: () => mockReader }, + }); +} From e0aa0334456e87de64407b439a7a05486211dfa8 Mon Sep 17 00:00:00 2001 From: vishal-genwave Date: Wed, 11 Feb 2026 15:20:09 +0530 Subject: [PATCH 08/15] fix: sync SDK response types with v1 API - Add hashTags and categories to GenerationResponse/GalleryItemResponse - Add quality field to EditResponse - Add reduceNoise param to BatchConvertParams - Update tests and mock factories --- src/clients/EditClient.ts | 1 + src/clients/GalleryClient.ts | 25 ++++++++++++++++++- src/clients/GenerationsClient.ts | 25 ++++++++++++++++++- src/clients/convert/BatchConvertClient.ts | 3 ++- src/types/api.ts | 8 ++++++ tests/clients/EditClient.test.ts | 1 + tests/clients/GalleryClient.test.ts | 2 ++ tests/clients/GenerationsClient.test.ts | 2 ++ .../convert/BatchConvertClient.test.ts | 2 ++ tests/setup/mock-factories.ts | 5 ++++ 10 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/clients/EditClient.ts b/src/clients/EditClient.ts index effcd30..7ea6217 100644 --- a/src/clients/EditClient.ts +++ b/src/clients/EditClient.ts @@ -122,6 +122,7 @@ export class EditClient extends BaseClient { metadata: responseMetadata, pngImageData, svgText, + quality: data.quality, }; this.logger.debug('Image/SVG edit completed', { diff --git a/src/clients/GalleryClient.ts b/src/clients/GalleryClient.ts index 0be3d16..59b6370 100644 --- a/src/clients/GalleryClient.ts +++ b/src/clients/GalleryClient.ts @@ -104,9 +104,32 @@ export class GalleryClient extends BaseClient { this.validateRequest(id, idSchema); - return this.handleRequest(`/v1/gallery/${encodeURIComponent(id)}`, { + const url = `${this.config.baseUrl}/v1/gallery/${encodeURIComponent(id)}`; + const response = await fetch(url, { method: 'GET', + headers: { + Accept: 'application/json', + 'x-api-key': this.config.apiKey, + }, }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const rawResult = await response.json(); + const { data, metadata } = this.unwrapEnvelope(rawResult); + + return { + id: data.id, + prompt: data.prompt, + type: data.type, + quality: data.quality, + isPublic: data.isPublic, + hashTags: data.metadata?.hashTags ?? [], + categories: data.metadata?.category ?? [], + metadata, + }; } /** diff --git a/src/clients/GenerationsClient.ts b/src/clients/GenerationsClient.ts index 228e02d..55c97b0 100644 --- a/src/clients/GenerationsClient.ts +++ b/src/clients/GenerationsClient.ts @@ -102,9 +102,32 @@ export class GenerationsClient extends BaseClient { this.validateRequest(id, idSchema); - return this.handleRequest(`/v1/generations/${encodeURIComponent(id)}`, { + const url = `${this.config.baseUrl}/v1/generations/${encodeURIComponent(id)}`; + const response = await fetch(url, { method: 'GET', + headers: { + Accept: 'application/json', + 'x-api-key': this.config.apiKey, + }, }); + + if (!response.ok) { + await this.handleFetchErrorResponse(response); + } + + const rawResult = await response.json(); + const { data, metadata } = this.unwrapEnvelope(rawResult); + + return { + id: data.id, + prompt: data.prompt, + type: data.type, + quality: data.quality, + isPublic: data.isPublic, + hashTags: data.metadata?.hashTags ?? [], + categories: data.metadata?.category ?? [], + metadata, + }; } /** diff --git a/src/clients/convert/BatchConvertClient.ts b/src/clients/convert/BatchConvertClient.ts index 7d289e8..b48b2f8 100644 --- a/src/clients/convert/BatchConvertClient.ts +++ b/src/clients/convert/BatchConvertClient.ts @@ -15,6 +15,7 @@ const batchConvertParamsSchema = z.object({ detail: z.number().optional(), smoothness: z.number().optional(), corners: z.number().optional(), + reduceNoise: z.number().optional(), textToPath: z.boolean().optional(), dxfVersion: z.enum(['R12', 'R14']).optional(), quality: z.number().optional(), @@ -62,7 +63,7 @@ export class BatchConvertClient extends BaseClient { // Add optional parameters this.appendOptionalParams(formData, this.params as Record, [ - 'preset', 'mode', 'hierarchical', 'detail', 'smoothness', 'corners', + 'preset', 'mode', 'hierarchical', 'detail', 'smoothness', 'corners', 'reduceNoise', 'textToPath', 'dxfVersion', 'quality', 'width', 'height', ]); diff --git a/src/types/api.ts b/src/types/api.ts index 1f5d800..97f8aab 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -201,6 +201,9 @@ export interface EditResponse extends BaseResponse { /** SVG source code as text - only when svgText=true */ svgText?: string; + + /** The quality level used */ + quality?: Quality; } /** @@ -320,6 +323,7 @@ export interface BatchConvertParams { detail?: number; smoothness?: number; corners?: number; + reduceNoise?: number; textToPath?: boolean; dxfVersion?: DxfVersion; quality?: number; @@ -472,6 +476,10 @@ export interface GenerationResponse { quality: string; /** Whether the generation is public */ isPublic: boolean; + /** Array of hashtags for this generation */ + hashTags: string[]; + /** Array of categories for this generation */ + categories: string[]; /** Response metadata */ metadata: ResponseMetadata; } diff --git a/tests/clients/EditClient.test.ts b/tests/clients/EditClient.test.ts index 706b086..9c33546 100644 --- a/tests/clients/EditClient.test.ts +++ b/tests/clients/EditClient.test.ts @@ -173,6 +173,7 @@ describe('EditClient', () => { expect(result.svgUrl).toBe(mockData.svgUrl); expect(result.creditCost).toBe(mockData.creditCost); expect(result.message).toBe(mockData.message); + expect(result.quality).toBe(mockData.quality); expect(result.generationId).toBe(mockData.generationId); expect(result.metadata).toBeDefined(); }); diff --git a/tests/clients/GalleryClient.test.ts b/tests/clients/GalleryClient.test.ts index 3dde1e0..6793b1e 100644 --- a/tests/clients/GalleryClient.test.ts +++ b/tests/clients/GalleryClient.test.ts @@ -166,6 +166,8 @@ describe('GalleryClient', () => { expect(result.type).toBe(mockData.type); expect(result.quality).toBe(mockData.quality); expect(result.isPublic).toBe(mockData.isPublic); + expect(result.hashTags).toEqual(mockData.metadata.hashTags); + expect(result.categories).toEqual(mockData.metadata.category); expect(result.metadata).toBeDefined(); }); }); diff --git a/tests/clients/GenerationsClient.test.ts b/tests/clients/GenerationsClient.test.ts index 42a0805..168c62f 100644 --- a/tests/clients/GenerationsClient.test.ts +++ b/tests/clients/GenerationsClient.test.ts @@ -180,6 +180,8 @@ describe('GenerationsClient', () => { expect(result.type).toBe(mockData.type); expect(result.quality).toBe(mockData.quality); expect(result.isPublic).toBe(mockData.isPublic); + expect(result.hashTags).toEqual(mockData.metadata.hashTags); + expect(result.categories).toEqual(mockData.metadata.category); expect(result.metadata).toBeDefined(); }); }); diff --git a/tests/clients/convert/BatchConvertClient.test.ts b/tests/clients/convert/BatchConvertClient.test.ts index 7601dd0..0e8c0ff 100644 --- a/tests/clients/convert/BatchConvertClient.test.ts +++ b/tests/clients/convert/BatchConvertClient.test.ts @@ -121,6 +121,7 @@ describe('BatchConvertClient', () => { preset: 'poster', mode: 'spline', detail: 50, + reduceNoise: 4, textToPath: true, quality: 90, }) @@ -131,6 +132,7 @@ describe('BatchConvertClient', () => { expect(formData.get('preset')).toBe('poster'); expect(formData.get('mode')).toBe('spline'); expect(formData.get('detail')).toBe('50'); + expect(formData.get('reduceNoise')).toBe('4'); expect(formData.get('textToPath')).toBe('true'); expect(formData.get('quality')).toBe('90'); }); diff --git a/tests/setup/mock-factories.ts b/tests/setup/mock-factories.ts index d59df79..0cbe4dd 100644 --- a/tests/setup/mock-factories.ts +++ b/tests/setup/mock-factories.ts @@ -45,6 +45,7 @@ export function createMockEditResponse( svgUrl: 'https://storage.example.com/test-edit.svg', creditCost: 2, message: 'Image edited successfully', + quality: 'medium', svgUrlExpiresIn: '24h', generationId: 'edit-test-123', ...overrides, @@ -203,6 +204,10 @@ export function createMockGenerationData( type: 'generate', quality: 'medium', isPublic: false, + metadata: { + hashTags: ['svg', 'landscape', 'mountain'], + category: ['flat', 'illustration'], + }, ...overrides, }; } From bb9b77b3beb6b7bdb12155b9ef9da8b9d1b23c5d Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 13 Feb 2026 14:27:29 +0530 Subject: [PATCH 09/15] add integration test runner script (58 tests) --- tests/run-all-tests.sh | 617 +++++++++++++++++++++++++++++++++++++ tests/test-images/test.svg | 3 + 2 files changed, 620 insertions(+) create mode 100755 tests/run-all-tests.sh create mode 100644 tests/test-images/test.svg diff --git a/tests/run-all-tests.sh b/tests/run-all-tests.sh new file mode 100755 index 0000000..d8004b2 --- /dev/null +++ b/tests/run-all-tests.sh @@ -0,0 +1,617 @@ +#!/usr/bin/env bash +# ============================================================================= +# SVGMaker SDK — Full Integration Test Suite (58 tests) +# ============================================================================= + +set -euo pipefail + +# ─── Configuration ─────────────────────────────────────────────────────────── +API_KEY="svgmaker-ioxxxx" +BASE_URL="http://localhost:3000/api" +DELAY=3 # seconds between tests +TIMEOUT=300000 # ms for SDK timeout + +# ─── Colors ────────────────────────────────────────────────────────────────── +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# ─── Counters ──────────────────────────────────────────────────────────────── +PASSED=0 +FAILED=0 +SKIPPED=0 +TOTAL=0 + +# ─── Test Fixtures ─────────────────────────────────────────────────────────── +echo -e "${BOLD}Setting up test fixtures...${NC}" + +mkdir -p tests/test-images + +if [ ! -f tests/test-images/test-image.png ]; then + echo " Creating tests/test-images/test-image.png ..." + node -e " +const fs = require('fs'); +const data = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', 'base64'); +fs.writeFileSync('tests/test-images/test-image.png', data); +" +fi + +if [ ! -f tests/test-images/test.svg ]; then + echo " Creating tests/test-images/test.svg ..." + cat > tests/test-images/test.svg << 'SVGEOF' + + + +SVGEOF +fi + +echo -e "${GREEN} Fixtures ready.${NC}" + +# ─── Pre-flight build ─────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Running npm run build...${NC}" +if ! npm run build; then + echo -e "${RED}Build failed — aborting tests.${NC}" + exit 1 +fi +echo -e "${GREEN}Build succeeded.${NC}" + +# ─── Timeout command detection ─────────────────────────────────────────────── +TIMEOUT_CMD="" +if command -v gtimeout &>/dev/null; then + TIMEOUT_CMD="gtimeout 120" +elif command -v timeout &>/dev/null; then + TIMEOUT_CMD="timeout 120" +fi + +# ─── Helper Functions ──────────────────────────────────────────────────────── + +run_test() { + local num="$1" + local name="$2" + local code="$3" + + TOTAL=$((TOTAL + 1)) + echo -e "${CYAN}[Test ${num}]${NC} ${name}..." + + local output + output=$(node -e "$code" 2>&1) || true + local exit_code=$? + + if [ $exit_code -eq 0 ] && ! echo "$output" | grep -q "ERROR:"; then + echo -e " ${GREEN}PASS${NC}" + PASSED=$((PASSED + 1)) + else + echo -e " ${RED}FAIL${NC}" + echo "$output" | sed 's/^/ /' + FAILED=$((FAILED + 1)) + fi + + sleep "$DELAY" +} + +run_expected_fail() { + local num="$1" + local name="$2" + local code="$3" + + TOTAL=$((TOTAL + 1)) + echo -e "${CYAN}[Test ${num}]${NC} ${name} ${YELLOW}(expected fail)${NC}..." + + local output + output=$(node -e "$code" 2>&1) || true + + if echo "$output" | grep -q "EXPECTED ERROR"; then + echo -e " ${YELLOW}EXPECTED FAIL (PASS)${NC}" + PASSED=$((PASSED + 1)) + else + echo -e " ${RED}FAIL${NC}" + echo "$output" | sed 's/^/ /' + FAILED=$((FAILED + 1)) + fi + + sleep "$DELAY" +} + +run_stream_test() { + local num="$1" + local name="$2" + local code="$3" + + TOTAL=$((TOTAL + 1)) + echo -e "${CYAN}[Test ${num}]${NC} ${name} ${CYAN}(stream)${NC}..." + + local output + if [ -n "$TIMEOUT_CMD" ]; then + output=$($TIMEOUT_CMD node -e "$code" 2>&1) || true + else + # Fallback: rely on the in-code setTimeout to kill the process + output=$(node -e "$code" 2>&1) || true + fi + + if echo "$output" | grep -q "Stream ended" || echo "$output" | grep -q "complete"; then + echo -e " ${GREEN}PASS${NC}" + PASSED=$((PASSED + 1)) + else + echo -e " ${RED}FAIL${NC}" + echo "$output" | sed 's/^/ /' + FAILED=$((FAILED + 1)) + fi + + sleep "$DELAY" +} + +# ============================================================================= +# TESTS BEGIN +# ============================================================================= + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} GENERATE TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 1 "Generate: with model" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 2 "Generate: with quality" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generate.configure({ prompt: 'A red sports car', quality: 'medium' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +" + +run_expected_fail 3 "Generate: model+quality together" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generate.configure({ prompt: 'A cat on a fence', quality: 'medium', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +run_test 4 "Generate: storage false" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generate.configure({ prompt: 'A simple house icon', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 5 "Generate: storage true" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_stream_test 6 "Generate: stream" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +const stream = client.generate.configure({ prompt: 'A rocket launching into space', quality: 'low' }).stream(); +stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); +stream.on('end', () => console.log('Stream ended')); +stream.on('error', (e) => console.error('ERROR:', e.message)); +setTimeout(() => process.exit(0), 120000); +" + +run_test 7 "Generate: styleParams + aspectRatio + background" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generate.configure({ prompt: 'A forest with tall trees and a river', quality: 'low', aspectRatio: 'landscape', background: 'transparent', styleParams: { style: 'flat', color_mode: 'few_colors', image_complexity: 'scene', composition: 'full_scene' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} EDIT TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 8 "Edit: with model" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 9 "Edit: with quality" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_expected_fail 10 "Edit: model+quality together" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'medium', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +run_test 11 "Edit: storage false" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Convert to silhouette style', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 12 "Edit: storage true" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a sunset gradient', quality: 'low', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_stream_test 13 "Edit: stream" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +const stream = client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make it look like a cartoon', quality: 'low' }).stream(); +stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); +stream.on('end', () => console.log('Stream ended')); +stream.on('error', (e) => console.error('ERROR:', e.message)); +setTimeout(() => process.exit(0), 120000); +" + +run_test 14 "Edit: styleParams + aspectRatio + background" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Restyle this image', quality: 'low', aspectRatio: 'square', background: 'opaque', styleParams: { style: 'isometric', color_mode: 'monochrome', composition: 'centered_object' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} CONVERT — AI VECTORIZE TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 15 "AI Vectorize: without stream" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_stream_test 16 "AI Vectorize: with stream" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +const stream = client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).stream(); +stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); +stream.on('end', () => console.log('Stream ended')); +stream.on('error', (e) => console.error('ERROR:', e.message)); +setTimeout(() => process.exit(0), 120000); +" + +run_test 17 "AI Vectorize: storage true" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} GENERATIONS TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 18 "Generations: list (default)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 19 "Generations: list (with filters)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 20 "Generations: list (with query)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 21 "Generations: get by ID" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.get('seed-gen-025').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 22 "Generations: share" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.share('seed-gen-025').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 23 "Generations: download (SVG)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.download('seed-gen-025', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 24 "Generations: download (default format)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.download('seed-gen-025').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 25 "Generations: download (SVGZ)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.download('seed-gen-025', { format: 'svgz' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 26 "Generations: delete (destructive)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.delete('seed-gen-025').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_expected_fail 27 "Generations: get (invalid ID)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +run_expected_fail 28 "Generations: list (invalid limit)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +run_expected_fail 29 "Generations: get private item (should fail)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.generations.get('seed-gen-001').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} GALLERY TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 30 "Gallery: browse (default)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 31 "Gallery: browse (with filters)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 32 "Gallery: browse (pro filter)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.list({ pro: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 33 "Gallery: browse (gold filter)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.list({ gold: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 34 "Gallery: browse (with query)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 35 "Gallery: get by ID" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.get('seed-gen-018').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 36 "Gallery: download (SVG — costs 1 credit)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.download('seed-gen-018', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 37 "Gallery: download (WebP — free)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.download('seed-gen-018', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 38 "Gallery: download (default format)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.download('seed-gen-018').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_expected_fail 39 "Gallery: get (invalid ID)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +run_expected_fail 40 "Gallery: list (invalid limit)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +run_test 41 "Gallery: get pro item" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.get('seed-gen-016').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 42 "Gallery: download pro item (WebP — free)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.download('seed-gen-016', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_expected_fail 43 "Gallery: get private item (should fail)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.gallery.get('seed-gen-020').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} ACCOUNT TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 44 "Account: get info" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.account.getInfo().then(r => { console.log('Email:', r.email); console.log('Display Name:', r.displayName); console.log('Account Type:', r.accountType); console.log('Credits:', r.credits); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 45 "Account: all-time usage" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.account.getUsage().then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('By Category:', JSON.stringify(r.byCategory, null, 2)); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 46 "Account: usage last 7 days" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.account.getUsage({ days: 7 }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 47 "Account: usage for date range" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.account.getUsage({ start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_expected_fail 48 "Account: usage with both days and start/end" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} OPTIMIZE SVG TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 49 "Optimize SVG: default (no compression)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.optimizeSvg.configure({ file: './tests/test-images/test.svg' }).execute().then(r => { console.log('Optimized SVG URL:', r.svgUrl); console.log('Expires in:', r.svgUrlExpiresIn); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 50 "Optimize SVG: with SVGZ compression" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.optimizeSvg.configure({ file: './tests/test-images/test.svg', compress: true }).execute().then(r => { console.log('SVGZ URL:', r.svgzUrl); console.log('Filename:', r.filename); console.log('Compressed size:', r.compressedSize); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} CONVERT — TRACE TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 51 "Trace: default settings" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.trace.configure({ file: './tests/test-images/test-image.png' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 52 "Trace: custom params" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.trace.configure({ file: './tests/test-images/test-image.png', preset: 'photo', mode: 'spline', detail: 60, smoothness: 70 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} CONVERT — SVG TO VECTOR TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 53 "SVG to Vector: convert to PDF" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'PDF' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 54 "SVG to Vector: convert to DXF with textToPath" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'DXF', textToPath: true }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} CONVERT — RASTER TO RASTER TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 55 "Raster to Raster: PNG to JPG with quality" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'JPG', quality: 85 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +run_test 56 "Raster to Raster: PNG to WEBP with width resize" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'WEBP', width: 512 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} BATCH CONVERT TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 57 "Batch Convert: multiple files to SVG" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.convert.batch.configure({ files: ['./tests/test-images/test-image.png', './tests/test-images/test-image.png'], toFormat: 'SVG' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} ENHANCE PROMPT TESTS${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 58 "Enhance Prompt: basic" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.enhancePrompt.configure({ prompt: 'a cat sitting on a fence' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" + +# ============================================================================= +# SUMMARY +# ============================================================================= + +echo "" +echo -e "${BOLD}══════════════════════════════════════════════════${NC}" +echo -e "${BOLD} TEST RESULTS SUMMARY${NC}" +echo -e "${BOLD}══════════════════════════════════════════════════${NC}" +echo "" +echo -e " Total: ${BOLD}${TOTAL}${NC}" +echo -e " Passed: ${GREEN}${PASSED}${NC}" +echo -e " Failed: ${RED}${FAILED}${NC}" +echo -e " Skipped: ${YELLOW}${SKIPPED}${NC}" +echo "" +echo -e "${BOLD}══════════════════════════════════════════════════${NC}" + +if [ "$FAILED" -gt 0 ]; then + echo -e "${RED}Some tests failed.${NC}" + exit 1 +else + echo -e "${GREEN}All tests passed!${NC}" + exit 0 +fi diff --git a/tests/test-images/test.svg b/tests/test-images/test.svg new file mode 100644 index 0000000..adf2cbd --- /dev/null +++ b/tests/test-images/test.svg @@ -0,0 +1,3 @@ + + + From 6687430f6b27da7e3b26714dea525b8f757a2b0d Mon Sep 17 00:00:00 2001 From: vishal Date: Wed, 18 Feb 2026 12:36:05 +0530 Subject: [PATCH 10/15] migrate SDK base URL from svgmaker.io/api to api.svgmaker.io --- .env.example | 4 +- README.md | 2 +- docs/api-documentation.md | 14 +- docs/v1-api.md | 90 ++--- src/types/config.ts | 4 +- tests/SVGMakerClient.test.ts | 4 +- ...-all-tests.sh => run-integration-tests.sh} | 360 +++++++----------- tests/setup/test-setup.ts | 2 +- 8 files changed, 201 insertions(+), 279 deletions(-) rename tests/{run-all-tests.sh => run-integration-tests.sh} (68%) mode change 100755 => 100644 diff --git a/.env.example b/.env.example index 1e02864..c7999ce 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,8 @@ # Your SVGMaker API Key (required) SVGMAKER_API_KEY=your-api-key-here -# Optional: Custom API base URL (default: https://svgmaker.io/api) -# SVGMAKER_BASE_URL=https://svgmaker.io/api +# Optional: Custom API base URL (default: https://api.svgmaker.io) +# SVGMAKER_BASE_URL=https://api.svgmaker.io # Optional: Request timeout in milliseconds (default: 30000) # SVGMAKER_TIMEOUT=60000 diff --git a/README.md b/README.md index 2af3c1f..de662b4 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ for await (const event of stream) { ```typescript interface SVGMakerConfig { apiKey: string; // API authentication key - baseUrl: string; // API base URL (default: "https://svgmaker.io/api") + baseUrl: string; // API base URL (default: "https://api.svgmaker.io") timeout: number; // Request timeout ms (default: 30000) maxRetries: number; // Max retry attempts (default: 3) retryBackoffFactor: number; // Retry delay factor ms (default: 300) diff --git a/docs/api-documentation.md b/docs/api-documentation.md index 541b198..36073dd 100644 --- a/docs/api-documentation.md +++ b/docs/api-documentation.md @@ -13,7 +13,7 @@ x-api-key: svgmaker-io{your-api-key} ## Base URL ``` -https://svgmaker.io/api +https://api.svgmaker.io ``` ## Credit System @@ -122,7 +122,7 @@ Both free and paid users have access to all quality levels (`'low'`, `'medium'`, ### Example Request ```bash -curl -X POST https://svgmaker.io/api/generate \ +curl -X POST https://api.svgmaker.io/generate \ -H "Content-Type: application/json" \ -H "x-api-key: svgmaker-io{your-api-key}" \ -d '{ @@ -194,7 +194,7 @@ Both free and paid users have access to all quality levels (`'low'`, `'medium'`, ### Example Request ```bash -curl -X POST https://svgmaker.io/api/edit \ +curl -X POST https://api.svgmaker.io/edit \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "image=@input-image.png" \ -F "prompt=Add a golden frame around this image" \ @@ -208,7 +208,7 @@ curl -X POST https://svgmaker.io/api/edit \ ### Example Request with Style Parameters ```bash -curl -X POST https://svgmaker.io/api/edit \ +curl -X POST https://api.svgmaker.io/edit \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "image=@input-image.svg" \ -F "prompt=Make this more cartoonish" \ @@ -262,7 +262,7 @@ x-api-key: svgmaker-io{your-api-key} ### Example Request ```bash -curl -X POST https://svgmaker.io/api/convert \ +curl -X POST https://api.svgmaker.io/convert \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image-to-convert.png" \ -F "stream=false" \ @@ -450,7 +450,7 @@ const fs = require('fs'); class SVGMakerAPI { constructor(apiKey) { this.apiKey = apiKey; - this.baseUrl = 'https://svgmaker.io/api'; + this.baseUrl = 'https://api.svgmaker.io'; } async generate(params) { @@ -544,7 +544,7 @@ import json class SVGMakerAPI: def __init__(self, api_key): self.api_key = api_key - self.base_url = 'https://svgmaker.io/api' + self.base_url = 'https://api.svgmaker.io' def _headers(self): return {'x-api-key': self.api_key} diff --git a/docs/v1-api.md b/docs/v1-api.md index 67e19b2..3e0159a 100644 --- a/docs/v1-api.md +++ b/docs/v1-api.md @@ -4,7 +4,7 @@ The SVGMaker API v1 provides a RESTful interface for generating, editing, and converting images to SVG format using advanced AI technology. All endpoints require authentication via API key and use a consistent response format. -**Base URL:** `https://svgmaker.io/api` +**Base URL:** `https://api.svgmaker.io` **API Version:** v1 @@ -177,7 +177,7 @@ The `styleParams` object can include: #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/generate \ +curl -X POST https://api.svgmaker.io/v1/generate \ -H "Content-Type: application/json" \ -H "x-api-key: svgmaker-io{your-api-key}" \ -d '{ @@ -199,7 +199,7 @@ curl -X POST https://svgmaker.io/api/v1/generate \ #### Example Request (with model) ```bash -curl -X POST https://svgmaker.io/api/v1/generate \ +curl -X POST https://api.svgmaker.io/v1/generate \ -H "Content-Type: application/json" \ -H "x-api-key: svgmaker-io{your-api-key}" \ -d '{ @@ -308,7 +308,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/edit \ +curl -X POST https://api.svgmaker.io/v1/edit \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "image=@input-image.png" \ -F "prompt=Add a golden frame around this image" \ @@ -324,7 +324,7 @@ curl -X POST https://svgmaker.io/api/v1/edit \ #### Example Request (with model) ```bash -curl -X POST https://svgmaker.io/api/v1/edit \ +curl -X POST https://api.svgmaker.io/v1/edit \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "image=@input-image.png" \ -F "prompt=Add a golden frame around this image" \ @@ -410,7 +410,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/enhance-prompt \ +curl -X POST https://api.svgmaker.io/v1/enhance-prompt \ -H "Content-Type: application/json" \ -H "x-api-key: svgmaker-io{your-api-key}" \ -d '{ @@ -505,7 +505,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/convert/ai-vectorize \ +curl -X POST https://api.svgmaker.io/v1/convert/ai-vectorize \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image-to-convert.png" \ -F "storage=false" \ @@ -651,7 +651,7 @@ x-api-key: svgmaker-io{your-api-key} ```bash # Using vtracer (default) -curl -X POST https://svgmaker.io/api/v1/convert/trace \ +curl -X POST https://api.svgmaker.io/v1/convert/trace \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.png" \ -F "preset=poster" \ @@ -661,13 +661,13 @@ curl -X POST https://svgmaker.io/api/v1/convert/trace \ -F "corners=50" # Explicitly specifying algorithm as query parameter -curl -X POST "https://svgmaker.io/api/v1/convert/trace?algorithm=vtracer" \ +curl -X POST "https://api.svgmaker.io/v1/convert/trace?algorithm=vtracer" \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.png" \ -F "preset=poster" # Or as form data field -curl -X POST https://svgmaker.io/api/v1/convert/trace \ +curl -X POST https://api.svgmaker.io/v1/convert/trace \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.png" \ -F "algorithm=vtracer" \ @@ -784,7 +784,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/convert/svg-to-vector \ +curl -X POST https://api.svgmaker.io/v1/convert/svg-to-vector \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.svg" \ -F "toFormat=PDF" \ @@ -858,7 +858,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/convert/raster-to-raster \ +curl -X POST https://api.svgmaker.io/v1/convert/raster-to-raster \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.png" \ -F "toFormat=JPG" \ @@ -941,7 +941,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X POST https://svgmaker.io/api/v1/convert/batch \ +curl -X POST https://api.svgmaker.io/v1/convert/batch \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image1.png" \ -F "file=@image2.jpg" \ @@ -1081,12 +1081,12 @@ x-api-key: svgmaker-io{your-api-key} ```bash # Optimize only -curl -X POST https://svgmaker.io/api/v1/svg/optimize \ +curl -X POST https://api.svgmaker.io/v1/svg/optimize \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.svg" # Optimize and compress to SVGZ -curl -X POST https://svgmaker.io/api/v1/svg/optimize \ +curl -X POST https://api.svgmaker.io/v1/svg/optimize \ -H "x-api-key: svgmaker-io{your-api-key}" \ -F "file=@image.svg" \ -F "compress=true" @@ -1215,31 +1215,31 @@ x-api-key: svgmaker-io{your-api-key} ```bash # Basic pagination -curl -X GET "https://svgmaker.io/api/v1/generations?page=1&limit=20" \ +curl -X GET "https://api.svgmaker.io/v1/generations?page=1&limit=20" \ -H "x-api-key: svgmaker-io{your-api-key}" | jq # Filter by type -curl -X GET "https://svgmaker.io/api/v1/generations?type=generate&limit=10" \ +curl -X GET "https://api.svgmaker.io/v1/generations?type=generate&limit=10" \ -H "x-api-key: svgmaker-io{your-api-key}" # Multiple types (OR condition) -curl -X GET "https://svgmaker.io/api/v1/generations?type=generate&type=edit" \ +curl -X GET "https://api.svgmaker.io/v1/generations?type=generate&type=edit" \ -H "x-api-key: svgmaker-io{your-api-key}" # Search by query (searches prompt/description) -curl -X GET "https://svgmaker.io/api/v1/generations?query=mountain%20landscape" \ +curl -X GET "https://api.svgmaker.io/v1/generations?query=mountain%20landscape" \ -H "x-api-key: svgmaker-io{your-api-key}" # Filter by hashtags -curl -X GET "https://svgmaker.io/api/v1/generations?hashtags=vector&hashtags=logo" \ +curl -X GET "https://api.svgmaker.io/v1/generations?hashtags=vector&hashtags=logo" \ -H "x-api-key: svgmaker-io{your-api-key}" # Filter by categories (AND condition - all must match) -curl -X GET "https://svgmaker.io/api/v1/generations?categories=flat&categories=icon" \ +curl -X GET "https://api.svgmaker.io/v1/generations?categories=flat&categories=icon" \ -H "x-api-key: svgmaker-io{your-api-key}" # Combined search: query + type + hashtags -curl -X GET "https://svgmaker.io/api/v1/generations?query=minimalist&type=generate&hashtags=vector&page=1&limit=20" \ +curl -X GET "https://api.svgmaker.io/v1/generations?query=minimalist&type=generate&hashtags=vector&page=1&limit=20" \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1312,7 +1312,7 @@ x-api-key: svgmaker-io{your-api-key} Replace `{generation_id}` with your actual generation ID. ```bash -curl -X GET https://svgmaker.io/api/v1/generations/{generation_id} \ +curl -X GET https://api.svgmaker.io/v1/generations/{generation_id} \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1413,7 +1413,7 @@ x-api-key: svgmaker-io{your-api-key} Replace `{generation_id}` with your actual generation ID. ```bash -curl -X DELETE https://svgmaker.io/api/v1/generations/{generation_id} \ +curl -X DELETE https://api.svgmaker.io/v1/generations/{generation_id} \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1479,7 +1479,7 @@ x-api-key: svgmaker-io{your-api-key} Replace `{generation_id}` with your actual generation ID. ```bash -curl -X POST https://svgmaker.io/api/v1/generations/{generation_id}/share \ +curl -X POST https://api.svgmaker.io/v1/generations/{generation_id}/share \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1581,11 +1581,11 @@ Replace `{generation_id}` with your actual generation ID. ```bash # Download as SVG -curl -X GET "https://svgmaker.io/api/v1/generations/{generation_id}/download?format=svg" \ +curl -X GET "https://api.svgmaker.io/v1/generations/{generation_id}/download?format=svg" \ -H "x-api-key: svgmaker-io{your-api-key}" # Download as SVGZ with optimization -curl -X GET "https://svgmaker.io/api/v1/generations/{generation_id}/download?format=svgz&optimize=true" \ +curl -X GET "https://api.svgmaker.io/v1/generations/{generation_id}/download?format=svgz&optimize=true" \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1690,47 +1690,47 @@ x-api-key: svgmaker-io{your-api-key} ```bash # Basic pagination -curl -X GET "https://svgmaker.io/api/v1/gallery?page=1&limit=20" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?page=1&limit=20" \ -H "x-api-key: svgmaker-io{your-api-key}" | jq # Filter by type -curl -X GET "https://svgmaker.io/api/v1/gallery?type=generate&limit=10" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?type=generate&limit=10" \ -H "x-api-key: svgmaker-io{your-api-key}" # Multiple types (OR condition) -curl -X GET "https://svgmaker.io/api/v1/gallery?type=generate&type=edit" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?type=generate&type=edit" \ -H "x-api-key: svgmaker-io{your-api-key}" # Search by query (searches prompt/description) -curl -X GET "https://svgmaker.io/api/v1/gallery?query=mountain%20landscape" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?query=mountain%20landscape" \ -H "x-api-key: svgmaker-io{your-api-key}" # Filter by hashtags -curl -X GET "https://svgmaker.io/api/v1/gallery?hashtags=vector&hashtags=logo" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?hashtags=vector&hashtags=logo" \ -H "x-api-key: svgmaker-io{your-api-key}" # Filter by categories (AND condition - all must match) -curl -X GET "https://svgmaker.io/api/v1/gallery?categories=flat&categories=icon" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?categories=flat&categories=icon" \ -H "x-api-key: svgmaker-io{your-api-key}" # Filter for pro images only -curl -X GET "https://svgmaker.io/api/v1/gallery?pro=true&limit=20" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?pro=true&limit=20" \ -H "x-api-key: svgmaker-io{your-api-key}" # Filter for gold images only -curl -X GET "https://svgmaker.io/api/v1/gallery?gold=true&limit=20" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?gold=true&limit=20" \ -H "x-api-key: svgmaker-io{your-api-key}" # Combined search: query + type + pro filter -curl -X GET "https://svgmaker.io/api/v1/gallery?query=minimalist&type=generate&pro=true&page=1&limit=20" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?query=minimalist&type=generate&pro=true&page=1&limit=20" \ -H "x-api-key: svgmaker-io{your-api-key}" # Combined search: gold images with specific hashtags -curl -X GET "https://svgmaker.io/api/v1/gallery?gold=true&hashtags=vector&hashtags=premium&page=1" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?gold=true&hashtags=vector&hashtags=premium&page=1" \ -H "x-api-key: svgmaker-io{your-api-key}" # Complex search: pro images, specific type, query, and categories -curl -X GET "https://svgmaker.io/api/v1/gallery?pro=true&type=generate&query=landscape&categories=illustration&page=1&limit=30" \ +curl -X GET "https://api.svgmaker.io/v1/gallery?pro=true&type=generate&query=landscape&categories=illustration&page=1&limit=30" \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1767,7 +1767,7 @@ x-api-key: svgmaker-io{your-api-key} Replace `{generation_id}` with your actual gallery item ID. ```bash -curl -X GET https://svgmaker.io/api/v1/gallery/{generation_id} \ +curl -X GET https://api.svgmaker.io/v1/gallery/{generation_id} \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1812,11 +1812,11 @@ Replace `{generation_id}` with your actual gallery item ID. ```bash # Download as SVG -curl -X GET "https://svgmaker.io/api/v1/gallery/{generation_id}/download?format=svg" \ +curl -X GET "https://api.svgmaker.io/v1/gallery/{generation_id}/download?format=svg" \ -H "x-api-key: svgmaker-io{your-api-key}" # Download as SVGZ with optimization -curl -X GET "https://svgmaker.io/api/v1/gallery/{generation_id}/download?format=svgz&optimize=true" \ +curl -X GET "https://api.svgmaker.io/v1/gallery/{generation_id}/download?format=svgz&optimize=true" \ -H "x-api-key: svgmaker-io{your-api-key}" ``` @@ -1889,7 +1889,7 @@ x-api-key: svgmaker-io{your-api-key} #### Example Request ```bash -curl -X GET https://svgmaker.io/api/v1/account \ +curl -X GET https://api.svgmaker.io/v1/account \ -H "x-api-key: svgmaker-io{your-api-key}" | jq ``` @@ -1970,15 +1970,15 @@ x-api-key: svgmaker-io{your-api-key} ```bash # Get all-time usage statistics -curl -X GET https://svgmaker.io/api/v1/account/usage \ +curl -X GET https://api.svgmaker.io/v1/account/usage \ -H "x-api-key: svgmaker-io{your-api-key}" | jq # Get usage for last 7 days -curl -X GET "https://svgmaker.io/api/v1/account/usage?days=7" \ +curl -X GET "https://api.svgmaker.io/v1/account/usage?days=7" \ -H "x-api-key: svgmaker-io{your-api-key}" | jq # Get usage for a date range -curl -X GET "https://svgmaker.io/api/v1/account/usage?start=2024-01-01&end=2024-01-31" \ +curl -X GET "https://api.svgmaker.io/v1/account/usage?start=2024-01-01&end=2024-01-31" \ -H "x-api-key: svgmaker-io{your-api-key}" | jq ``` diff --git a/src/types/config.ts b/src/types/config.ts index 47c6c91..d5cc074 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -9,7 +9,7 @@ export interface SVGMakerConfig { /** * Base URL for the SVGMaker API - * @default "https://svgmaker.io/api" + * @default "https://api.svgmaker.io" */ baseUrl: string; @@ -73,7 +73,7 @@ export interface SVGMakerConfig { */ export const DEFAULT_CONFIG: SVGMakerConfig = { apiKey: '', - baseUrl: 'https://svgmaker.io/api', + baseUrl: 'https://api.svgmaker.io', timeout: 30000, maxRetries: 3, retryBackoffFactor: 300, diff --git a/tests/SVGMakerClient.test.ts b/tests/SVGMakerClient.test.ts index cd9a6f1..8135017 100644 --- a/tests/SVGMakerClient.test.ts +++ b/tests/SVGMakerClient.test.ts @@ -19,7 +19,7 @@ describe('SVGMakerClient', () => { const config = client.getConfig(); expect(config.apiKey).toBe('test-api-key'); - expect(config.baseUrl).toBe('https://svgmaker.io/api'); + expect(config.baseUrl).toBe('https://api.svgmaker.io'); expect(config.timeout).toBe(30000); }); @@ -50,7 +50,7 @@ describe('SVGMakerClient', () => { const config = client.getConfig(); expect(config.timeout).toBe(60000); expect(config.maxRetries).toBe(5); - expect(config.baseUrl).toBe('https://svgmaker.io/api'); + expect(config.baseUrl).toBe('https://api.svgmaker.io'); }); it('should return the client instance for chaining', () => { diff --git a/tests/run-all-tests.sh b/tests/run-integration-tests.sh old mode 100755 new mode 100644 similarity index 68% rename from tests/run-all-tests.sh rename to tests/run-integration-tests.sh index d8004b2..7794bcb --- a/tests/run-all-tests.sh +++ b/tests/run-integration-tests.sh @@ -1,16 +1,28 @@ #!/usr/bin/env bash # ============================================================================= -# SVGMaker SDK — Full Integration Test Suite (58 tests) +# SVGMaker SDK — Rate-Limited Integration Test Suite (Production) # ============================================================================= set -euo pipefail # ─── Configuration ─────────────────────────────────────────────────────────── -API_KEY="svgmaker-ioxxxx" -BASE_URL="http://localhost:3000/api" +API_KEY="svgmaker-ioxxx" +BASE_URL="https://api.svgmaker.io" DELAY=3 # seconds between tests TIMEOUT=300000 # ms for SDK timeout +# ─── IDs (fill in before running) ────────────────────────────────────── +GENERATION_ID="" # A generation you own (for get/share/download) +DELETE_GENERATION_ID="" # Throwaway generation (will be permanently deleted) +GALLERY_ID="" # A public gallery item +PRO_GALLERY_ID="" # A pro public gallery item + +# ─── Pre-flight ID check ─────────────────────────────────────────────────── +if [[ "$GENERATION_ID" == "REPLACE_ME" ]] || [[ "$DELETE_GENERATION_ID" == "REPLACE_ME" ]] || [[ "$GALLERY_ID" == "REPLACE_ME" ]] || [[ "$PRO_GALLERY_ID" == "REPLACE_ME" ]]; then + echo -e "${RED:-\033[0;31m}ERROR: Please fill in the ID placeholders at the top of this script before running.${NC:-\033[0m}" + exit 1 +fi + # ─── Colors ────────────────────────────────────────────────────────────────── GREEN='\033[0;32m' RED='\033[0;31m' @@ -151,20 +163,20 @@ run_stream_test() { # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} GENERATE TESTS${NC}" +echo -e "${BOLD} GENERATE TESTS (limit: 3/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 1 "Generate: with model" " +run_test 1 "Generate: with quality (low)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A minimalist mountain landscape', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +client.generate.configure({ prompt: 'A minimalist mountain landscape', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " -run_test 2 "Generate: with quality" " +run_test 2 "Generate: with model (gpt-image-1-mini)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A red sports car', quality: 'medium' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +client.generate.configure({ prompt: 'A red sports car', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " run_expected_fail 3 "Generate: model+quality together" " @@ -173,111 +185,45 @@ const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeou client.generate.configure({ prompt: 'A cat on a fence', quality: 'medium', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " -run_test 4 "Generate: storage false" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A simple house icon', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 5 "Generate: storage true" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A golden trophy', quality: 'medium', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_stream_test 6 "Generate: stream" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -const stream = client.generate.configure({ prompt: 'A rocket launching into space', quality: 'low' }).stream(); -stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); -stream.on('end', () => console.log('Stream ended')); -stream.on('error', (e) => console.error('ERROR:', e.message)); -setTimeout(() => process.exit(0), 120000); -" - -run_test 7 "Generate: styleParams + aspectRatio + background" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A forest with tall trees and a river', quality: 'low', aspectRatio: 'landscape', background: 'transparent', styleParams: { style: 'flat', color_mode: 'few_colors', image_complexity: 'scene', composition: 'full_scene' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} EDIT TESTS${NC}" +echo -e "${BOLD} EDIT TESTS (limit: 3/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 8 "Edit: with model" " +run_test 4 "Edit: with quality (low)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " -run_test 9 "Edit: with quality" " +run_test 5 "Edit: with model (gpt-image-1-mini)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); " -run_expected_fail 10 "Edit: model+quality together" " +run_expected_fail 6 "Edit: model+quality together" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'medium', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " -run_test 11 "Edit: storage false" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Convert to silhouette style', quality: 'low', storage: false }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 12 "Edit: storage true" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a sunset gradient', quality: 'low', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_stream_test 13 "Edit: stream" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -const stream = client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make it look like a cartoon', quality: 'low' }).stream(); -stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); -stream.on('end', () => console.log('Stream ended')); -stream.on('error', (e) => console.error('ERROR:', e.message)); -setTimeout(() => process.exit(0), 120000); -" - -run_test 14 "Edit: styleParams + aspectRatio + background" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Restyle this image', quality: 'low', aspectRatio: 'square', background: 'opaque', styleParams: { style: 'isometric', color_mode: 'monochrome', composition: 'centered_object' } }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — AI VECTORIZE TESTS${NC}" +echo -e "${BOLD} CONVERT — AI VECTORIZE TESTS (limit: 5/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 15 "AI Vectorize: without stream" " +run_test 7 "AI Vectorize: basic (no storage)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_stream_test 16 "AI Vectorize: with stream" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -const stream = client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).stream(); -stream.on('data', (e) => { console.log('Event:', e.status, e.message || ''); if (e.status === 'complete') { console.log('SVG URL:', e.svgUrl); console.log('Credits:', e.creditCost); console.log('Generation ID:', e.generationId); } }); -stream.on('end', () => console.log('Stream ended')); -stream.on('error', (e) => console.error('ERROR:', e.message)); -setTimeout(() => process.exit(0), 120000); -" - -run_test 17 "AI Vectorize: storage true" " +run_test 8 "AI Vectorize: with storage true" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); @@ -286,310 +232,286 @@ client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} GENERATIONS TESTS${NC}" +echo -e "${BOLD} CONVERT — TRACE TESTS (limit: 5/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 18 "Generations: list (default)" " +run_test 9 "Trace: default settings" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.convert.trace.configure({ file: './tests/test-images/test-image.png' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 19 "Generations: list (with filters)" " +run_test 10 "Trace: custom params (preset: photo, mode: spline, detail: 60, smoothness: 70)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.convert.trace.configure({ file: './tests/test-images/test-image.png', preset: 'photo', mode: 'spline', detail: 60, smoothness: 70 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 20 "Generations: list (with query)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} CONVERT — SVG TO VECTOR TESTS (limit: 5/60s)${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── -run_test 21 "Generations: get by ID" " +run_test 11 "SVG to Vector: convert to PDF" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.get('seed-gen-025').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'PDF' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 22 "Generations: share" " +run_test 12 "SVG to Vector: convert to DXF with textToPath" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.share('seed-gen-025').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'DXF', textToPath: true }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 23 "Generations: download (SVG)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.download('seed-gen-025', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} CONVERT — RASTER TO RASTER TESTS (limit: 5/60s)${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── -run_test 24 "Generations: download (default format)" " +run_test 13 "Raster to Raster: PNG to JPG with quality 85" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.download('seed-gen-025').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'JPG', quality: 85 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 25 "Generations: download (SVGZ)" " +run_test 14 "Raster to Raster: PNG to WEBP with width 512" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.download('seed-gen-025', { format: 'svgz' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'WEBP', width: 512 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 26 "Generations: delete (destructive)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.delete('seed-gen-025').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} BATCH CONVERT TESTS (limit: 5/60s)${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── -run_expected_fail 27 "Generations: get (invalid ID)" " +run_test 15 "Batch Convert: multiple files to SVG" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.convert.batch.configure({ files: ['./tests/test-images/test-image.png', './tests/test-images/test-image.png'], toFormat: 'SVG' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_expected_fail 28 "Generations: list (invalid limit)" " +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} ENHANCE PROMPT TESTS (limit: 10/60s)${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── + +run_test 16 "Enhance Prompt: basic ('a cat sitting on a fence')" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.enhancePrompt.configure({ prompt: 'a cat sitting on a fence' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_expected_fail 29 "Generations: get private item (should fail)" " +run_test 17 "Enhance Prompt: another ('a minimalist logo')" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.get('seed-gen-001').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.enhancePrompt.configure({ prompt: 'a minimalist logo' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} GALLERY TESTS${NC}" +echo -e "${BOLD} OPTIMIZE SVG TESTS (limit: 10/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 30 "Gallery: browse (default)" " +run_test 18 "Optimize SVG: default (no compression)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.optimizeSvg.configure({ file: './tests/test-images/test.svg' }).execute().then(r => { console.log('Optimized SVG URL:', r.svgUrl); console.log('Expires in:', r.svgUrlExpiresIn); }).catch(e => console.error('ERROR:', e.message)); " -run_test 31 "Gallery: browse (with filters)" " +run_test 19 "Optimize SVG: with SVGZ compression" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.optimizeSvg.configure({ file: './tests/test-images/test.svg', compress: true }).execute().then(r => { console.log('SVGZ URL:', r.svgzUrl); console.log('Filename:', r.filename); console.log('Compressed size:', r.compressedSize); }).catch(e => console.error('ERROR:', e.message)); " -run_test 32 "Gallery: browse (pro filter)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ pro: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════${NC}" +echo -e "${BOLD} GENERATIONS TESTS (limit: 1000/60s)${NC}" +echo -e "${BOLD}══════════════════════════════════════${NC}" +# ───────────────────────────────────────────────────────────────────────────── -run_test 33 "Gallery: browse (gold filter)" " +run_test 20 "Generations: list (default)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ gold: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 34 "Gallery: browse (with query)" " +run_test 21 "Generations: list (with filters)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 35 "Gallery: get by ID" " +run_test 22 "Generations: list (with query)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('seed-gen-018').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 36 "Gallery: download (SVG — costs 1 credit)" " +run_test 23 "Generations: get by ID" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('seed-gen-018', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.get('${GENERATION_ID}').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 37 "Gallery: download (WebP — free)" " +run_test 24 "Generations: share" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('seed-gen-018', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.share('${GENERATION_ID}').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 38 "Gallery: download (default format)" " +run_test 25 "Generations: download (SVG)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('seed-gen-018').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.download('${GENERATION_ID}', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_expected_fail 39 "Gallery: get (invalid ID)" " +run_test 26 "Generations: download (default format)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.generations.download('${GENERATION_ID}').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_expected_fail 40 "Gallery: list (invalid limit)" " +run_test 27 "Generations: download (SVGZ)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.generations.download('${GENERATION_ID}', { format: 'svgz' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 41 "Gallery: get pro item" " +run_expected_fail 28 "Generations: get (invalid ID)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('seed-gen-016').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " -run_test 42 "Gallery: download pro item (WebP — free)" " +run_expected_fail 29 "Generations: list (invalid limit)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('seed-gen-016', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.generations.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " -run_expected_fail 43 "Gallery: get private item (should fail)" " +run_test 30 "Generations: delete (destructive — uses DELETE_GENERATION_ID)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('seed-gen-020').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.generations.delete('${DELETE_GENERATION_ID}').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} ACCOUNT TESTS${NC}" +echo -e "${BOLD} GALLERY TESTS (list/get: 1000/60s, download: 5/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 44 "Account: get info" " +run_test 31 "Gallery: browse (default)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getInfo().then(r => { console.log('Email:', r.email); console.log('Display Name:', r.displayName); console.log('Account Type:', r.accountType); console.log('Credits:', r.credits); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 45 "Account: all-time usage" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage().then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('By Category:', JSON.stringify(r.byCategory, null, 2)); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 46 "Account: usage last 7 days" " +run_test 32 "Gallery: browse (with filters)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage({ days: 7 }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.list({ type: ['generate'], limit: 5 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 47 "Account: usage for date range" " +run_test 33 "Gallery: browse (pro filter)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage({ start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.list({ pro: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_expected_fail 48 "Account: usage with both days and start/end" " +run_test 34 "Gallery: browse (with query)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); +client.gallery.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} OPTIMIZE SVG TESTS${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 49 "Optimize SVG: default (no compression)" " +run_test 35 "Gallery: get by ID" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.optimizeSvg.configure({ file: './tests/test-images/test.svg' }).execute().then(r => { console.log('Optimized SVG URL:', r.svgUrl); console.log('Expires in:', r.svgUrlExpiresIn); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.get('${GALLERY_ID}').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 50 "Optimize SVG: with SVGZ compression" " +run_test 36 "Gallery: get pro item" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.optimizeSvg.configure({ file: './tests/test-images/test.svg', compress: true }).execute().then(r => { console.log('SVGZ URL:', r.svgzUrl); console.log('Filename:', r.filename); console.log('Compressed size:', r.compressedSize); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.get('${PRO_GALLERY_ID}').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — TRACE TESTS${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 51 "Trace: default settings" " +run_test 37 "Gallery: download (WebP — free)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.trace.configure({ file: './tests/test-images/test-image.png' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.download('${GALLERY_ID}', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 52 "Trace: custom params" " +run_test 38 "Gallery: download (SVG — 1 credit)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.trace.configure({ file: './tests/test-images/test-image.png', preset: 'photo', mode: 'spline', detail: 60, smoothness: 70 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.download('${GALLERY_ID}', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — SVG TO VECTOR TESTS${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 53 "SVG to Vector: convert to PDF" " +run_test 39 "Gallery: download pro item (WebP — free for premium)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'PDF' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.download('${PRO_GALLERY_ID}', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 54 "SVG to Vector: convert to DXF with textToPath" " +run_expected_fail 40 "Gallery: get (invalid ID)" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'DXF', textToPath: true }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.gallery.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — RASTER TO RASTER TESTS${NC}" +echo -e "${BOLD} ACCOUNT TESTS (limit: 1000/60s)${NC}" echo -e "${BOLD}══════════════════════════════════════${NC}" # ───────────────────────────────────────────────────────────────────────────── -run_test 55 "Raster to Raster: PNG to JPG with quality" " +run_test 41 "Account: get info" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'JPG', quality: 85 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.account.getInfo().then(r => { console.log('Email:', r.email); console.log('Display Name:', r.displayName); console.log('Account Type:', r.accountType); console.log('Credits:', r.credits); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -run_test 56 "Raster to Raster: PNG to WEBP with width resize" " +run_test 42 "Account: all-time usage" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'WEBP', width: 512 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.account.getUsage().then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('By Category:', JSON.stringify(r.byCategory, null, 2)); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} BATCH CONVERT TESTS${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 57 "Batch Convert: multiple files to SVG" " +run_test 43 "Account: usage last 7 days" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.batch.configure({ files: ['./tests/test-images/test-image.png', './tests/test-images/test-image.png'], toFormat: 'SVG' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.account.getUsage({ days: 7 }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); " -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} ENHANCE PROMPT TESTS${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── +run_test 44 "Account: usage date range (2025-01-01 to 2025-06-30)" " +const { SVGMakerClient } = require('./dist/cjs/index.js'); +const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); +client.account.getUsage({ start: '2025-01-01', end: '2025-06-30' }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +" -run_test 58 "Enhance Prompt: basic" " +run_expected_fail 45 "Account: usage with both days and start/end" " const { SVGMakerClient } = require('./dist/cjs/index.js'); const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.enhancePrompt.configure({ prompt: 'a cat sitting on a fence' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); +client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); " # ============================================================================= diff --git a/tests/setup/test-setup.ts b/tests/setup/test-setup.ts index ddcb91b..05e6f62 100644 --- a/tests/setup/test-setup.ts +++ b/tests/setup/test-setup.ts @@ -6,7 +6,7 @@ import { SVGMakerClient } from '../../src/core/SVGMakerClient'; // ============================================================================ export const TEST_API_KEY = 'test-api-key-123'; -export const TEST_BASE_URL = 'https://svgmaker.io/api'; +export const TEST_BASE_URL = 'https://api.svgmaker.io'; /** * Default test configuration with retries disabled From 8536fb9f834cb424909032e8aeee4ff0671aeac2 Mon Sep 17 00:00:00 2001 From: razorback16 Date: Thu, 19 Feb 2026 17:15:50 -0800 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20v1.0.0-rc.1=20=E2=80=94=20automat?= =?UTF-8?q?ed=20release=20flow=20with=20AI-generated=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite release workflow: trigger on version bump in main (not tag push) - Auto-detect version changes by comparing package.json between commits - Generate release notes via GitHub API + enhance with AI (GitHub Models) - Add .github/release.yml for PR categorization - Remove manual release scripts, keep version bump helpers - Update CONTRIBUTING.md and CHANGELOG.md for automated flow - Consolidate and update SDK documentation for v1 API - Fix lint errors: replace require() with imports, remove unused imports - Format all source and test files with prettier - Fix security audit to only check production dependencies --- .github/release.yml | 32 + .github/workflows/release.yml | 223 ++++-- .github/workflows/security.yml | 2 +- CHANGELOG.md | 47 +- CONTRIBUTING.md | 20 +- README.md | 397 ++-------- ...ntation.md => legacy-api-documentation.md} | 4 +- docs/sdk-documentation.md | 722 +++++++++++++++--- docs/{v1-api.md => v1-api-documentation.md} | 0 package-lock.json | 178 ++--- package.json | 8 +- src/clients/AccountClient.ts | 10 +- src/clients/EditClient.ts | 10 +- src/clients/GalleryClient.ts | 5 +- src/clients/convert/AIVectorizeClient.ts | 6 +- src/clients/convert/BatchConvertClient.ts | 14 +- src/clients/convert/RasterToRasterClient.ts | 6 +- src/clients/convert/SvgToVectorClient.ts | 5 +- src/clients/convert/TraceClient.ts | 9 +- tests/clients/AccountClient.test.ts | 44 +- tests/clients/EditClient.test.ts | 96 +-- tests/clients/EnhancePromptClient.test.ts | 34 +- tests/clients/GalleryClient.test.ts | 46 +- tests/clients/GenerateClient.test.ts | 134 ++-- tests/clients/GenerationsClient.test.ts | 54 +- tests/clients/OptimizeSvgClient.test.ts | 47 +- .../clients/convert/AIVectorizeClient.test.ts | 90 +-- .../convert/BatchConvertClient.test.ts | 60 +- .../convert/RasterToRasterClient.test.ts | 53 +- .../clients/convert/SvgToVectorClient.test.ts | 27 +- tests/clients/convert/TraceClient.test.ts | 45 +- tests/setup/mock-factories.ts | 40 +- tests/setup/test-setup.ts | 8 +- 33 files changed, 1268 insertions(+), 1208 deletions(-) create mode 100644 .github/release.yml rename docs/{api-documentation.md => legacy-api-documentation.md} (99%) rename docs/{v1-api.md => v1-api-documentation.md} (100%) diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..b303772 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,32 @@ +# Configuration for GitHub's automatically generated release notes +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes + +changelog: + exclude: + labels: + - skip-changelog + authors: + - dependabot + - github-actions + categories: + - title: Breaking Changes + labels: + - breaking + - semver-major + - title: New Features + labels: + - feature + - enhancement + - semver-minor + - title: Bug Fixes + labels: + - bug + - fix + - semver-patch + - title: Documentation + labels: + - documentation + - docs + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb873a2..8c9eb57 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,67 +2,62 @@ name: Release on: push: - tags: - - 'v*.*.*' + branches: + - main permissions: - id-token: write # Required for OIDC - contents: read + contents: write + id-token: write + models: read jobs: - validate-release: + detect-release: runs-on: ubuntu-latest outputs: - version: ${{ steps.validate.outputs.version }} - should-release: ${{ steps.validate.outputs.should-release }} - + version: ${{ steps.detect.outputs.version }} + should-release: ${{ steps.detect.outputs.should-release }} + steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Validate version and tag match - id: validate + with: + fetch-depth: 2 + + - name: Detect version change + id: detect run: | - # Extract version from git tag - TAG_VERSION=${GITHUB_REF#refs/tags/v} - echo "Tag version: $TAG_VERSION" - - # Get version from package.json - PACKAGE_VERSION=$(node -p "require('./package.json').version") - echo "Package version: $PACKAGE_VERSION" - - # Check if versions match - if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then - echo "❌ Version mismatch: tag ($TAG_VERSION) != package.json ($PACKAGE_VERSION)" - echo "should-release=false" >> $GITHUB_OUTPUT - exit 1 - fi - - echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT - - # Check if version already exists on npm - if npm view "@genwave/svgmaker-sdk@$PACKAGE_VERSION" version >/dev/null 2>&1; then - echo "should-release=false" >> $GITHUB_OUTPUT - echo "❌ Version $PACKAGE_VERSION already exists on npm - skipping release" + NEW_VERSION=$(node -p "require('./package.json').version") + # Get previous commit's package.json version + OLD_VERSION=$(git show HEAD~1:package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") + if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then + # Check npm + if npm view "@genwave/svgmaker-sdk@$NEW_VERSION" version >/dev/null 2>&1; then + echo "Version $NEW_VERSION already on npm, skipping" + echo "should-release=false" >> $GITHUB_OUTPUT + else + echo "Version changed: $OLD_VERSION -> $NEW_VERSION" + echo "should-release=true" >> $GITHUB_OUTPUT + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + fi else - echo "should-release=true" >> $GITHUB_OUTPUT - echo "✅ Version $PACKAGE_VERSION is valid and ready for release" + echo "No version change, skipping" + echo "should-release=false" >> $GITHUB_OUTPUT fi release: runs-on: ubuntu-latest - needs: validate-release - if: needs.validate-release.outputs.should-release == 'true' + needs: detect-release + if: needs.detect-release.outputs.should-release == 'true' permissions: contents: write id-token: write - + steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - + - name: Use Node.js 20.x uses: actions/setup-node@v6 with: @@ -70,65 +65,137 @@ jobs: cache: 'npm' registry-url: 'https://registry.npmjs.org' - - name: Update npm for OIDC support + - name: Update npm run: npm install -g npm@latest - name: Install dependencies run: npm ci - + - name: Run tests run: npx jest --passWithNoTests --no-coverage - + - name: Run linting run: npm run lint - + - name: Build package run: npm run build - - - name: Generate changelog + + - name: Generate release notes from GitHub + id: github_notes + uses: actions/github-script@v7 + with: + script: | + const version = '${{ needs.detect-release.outputs.version }}'; + const tagName = `v${version}`; + + // Find previous tag + let previousTag; + try { + const tags = await github.rest.repos.listTags({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 10, + }); + // First tag that isn't the current version + const prev = tags.data.find(t => t.name !== tagName); + if (prev) previousTag = prev.name; + } catch (e) { + core.info(`No previous tags found: ${e.message}`); + } + + // Generate release notes via GitHub API + const params = { + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tagName, + }; + if (previousTag) params.previous_tag_name = previousTag; + + const { data } = await github.rest.repos.generateReleaseNotes(params); + core.setOutput('notes', data.body); + core.setOutput('prev_tag', previousTag || 'none'); + + - name: Enhance release notes with AI id: changelog + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - VERSION="${{ needs.validate-release.outputs.version }}" - - # Generate changelog entry - if [ -f CHANGELOG.md ]; then - # Extract changelog entry for this version - CHANGELOG_ENTRY=$(sed -n "/## \[$VERSION\]/,/## \[/p" CHANGELOG.md | sed '$d') - if [ -z "$CHANGELOG_ENTRY" ]; then - CHANGELOG_ENTRY="Release $VERSION" - fi - else - CHANGELOG_ENTRY="Release $VERSION" + VERSION="${{ needs.detect-release.outputs.version }}" + GITHUB_NOTES="${{ steps.github_notes.outputs.notes }}" + + # Build the prompt — GitHub notes as input, AI rewrites + PROMPT="You are a release notes writer for @genwave/svgmaker-sdk, a Node.js SDK. + Rewrite the following GitHub-generated release notes into polished, user-facing release notes for v${VERSION}. + + Rules: + - Use sections: ## What's New, ## Bug Fixes, ## Other Changes (omit empty ones) + - Write concise descriptions a developer would want to read + - If any changes are breaking or require user action, add a ## Migration Guide section + - Remove contributor/new-contributor boilerplate + - Do not wrap in code fences + - Do not include a title/heading for the version itself + + GitHub-generated notes: + ${GITHUB_NOTES}" + + PROMPT_JSON=$(echo "$PROMPT" | jq -Rs .) + + RESPONSE=$(curl -sS -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -H "Content-Type: application/json" \ + https://models.github.ai/inference/chat/completions \ + -d "{ + \"model\": \"openai/gpt-4.1\", + \"messages\": [ + {\"role\": \"user\", \"content\": $PROMPT_JSON} + ], + \"temperature\": 0.3 + }") + + BODY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty') + + # Fallback to GitHub's raw notes if AI fails + if [ -z "$BODY" ]; then + echo "AI enhancement failed, using GitHub-generated notes" + echo "API response: $RESPONSE" + BODY="$GITHUB_NOTES" fi - - # Save changelog to output + { - echo 'CHANGELOG_CONTENT<> $GITHUB_OUTPUT - - - name: Publish to npm + + - name: Create and push git tag run: | - VERSION="${{ needs.validate-release.outputs.version }}" - echo "📦 Publishing version $VERSION to npm..." - npm publish --provenance - - - name: Create GitHub Release - uses: actions/create-release@v1 + VERSION="${{ needs.detect-release.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v${VERSION}" -m "Release v${VERSION}" + git push origin "v${VERSION}" + + - name: Publish to npm + run: npm publish --provenance env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.validate-release.outputs.version }} - release_name: Release ${{ needs.validate-release.outputs.version }} - body: ${{ steps.changelog.outputs.CHANGELOG_CONTENT }} - draft: false - prerelease: ${{ contains(needs.validate-release.outputs.version, '-') }} - + tag_name: v${{ needs.detect-release.outputs.version }} + name: v${{ needs.detect-release.outputs.version }} + body: ${{ steps.changelog.outputs.body }} + prerelease: ${{ contains(needs.detect-release.outputs.version, '-') }} + generate_release_notes: false + - name: Notify success if: success() run: | - VERSION="${{ needs.validate-release.outputs.version }}" - echo "✅ Successfully released version $VERSION" - echo "📦 Package published to npm: https://www.npmjs.com/package/@genwave/svgmaker-sdk" - echo "🚀 GitHub release created: ${{ github.server_url }}/${{ github.repository }}/releases/tag/v$VERSION" \ No newline at end of file + VERSION="${{ needs.detect-release.outputs.version }}" + echo "Successfully released version $VERSION" + echo "Package published to npm: https://www.npmjs.com/package/@genwave/svgmaker-sdk" + echo "GitHub release created: ${{ github.server_url }}/${{ github.repository }}/releases/tag/v$VERSION" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9d5262a..41f5aab 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -27,7 +27,7 @@ jobs: run: npm ci - name: Run npm audit - run: npm audit --audit-level moderate + run: npm audit --audit-level moderate --omit=dev - name: Run npm audit fix if: failure() diff --git a/CHANGELOG.md b/CHANGELOG.md index 21be4d1..f950664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,49 +70,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## Release Notes Template - -When creating a new release, copy the template below and fill in the details: - -```markdown -## [X.Y.Z] - YYYY-MM-DD - -### Added -- New features - -### Changed -- Changes in existing functionality - -### Deprecated -- Soon-to-be removed features - -### Removed -- Now removed features - -### Fixed -- Bug fixes - -### Security -- Security improvements -``` - -## Migration Guide - -### From 0.1.x to 0.2.x - -The 0.2.0 release adds several new features but maintains backward compatibility: - -- **New Features**: Retry logic, rate limiting, enhanced logging, and detailed error messages -- **No Breaking Changes**: All existing code will continue to work -- **Recommended Updates**: Consider using the new error handling features for better debugging - -### From 0.x.x to 1.0.0 (Future Major Release) - -When we release version 1.0.0, any breaking changes will be documented here with migration instructions. - -## Support - -- **Documentation**: [API Documentation](docs/api-documentation.md) -- **Examples**: [Basic Usage](examples/basic-usage.ts) -- **Issues**: [GitHub Issues](https://github.com/GenWaveLLC/svgmaker-sdk-node/issues) -- **Releases**: [GitHub Releases](https://github.com/GenWaveLLC/svgmaker-sdk-node/releases) \ No newline at end of file +> Changelog entries for new releases are auto-generated from conventional commit messages by the release workflow. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c93fc1..10ee942 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,13 +71,25 @@ Follow the [Conventional Commits](https://www.conventionalcommits.org/) specific ## Pull Request Process 1. Update the README.md with details of changes if applicable. -2. Update the CHANGELOG.md with details of changes. -3. The PR should work on Node.js 16.x and above. -4. The PR will be merged once it passes all checks and is approved by a maintainer. +2. The PR should work on Node.js 18.x and above. +3. The PR will be merged once it passes all checks and is approved by a maintainer. ## Releasing -Releases are handled by the maintainers. If you think a new release should be published, please open an issue. +Releases are automated. When a push to `main` includes a version bump in `package.json`, the release workflow will: + +1. Auto-generate a changelog from conventional commit messages +2. Create a git tag (`vX.Y.Z`) +3. Publish to npm +4. Create a GitHub Release + +**To release a new version:** + +1. Bump the version in `package.json` (use `npm run version:patch`, `version:minor`, or `version:major`) +2. Merge to `main` (via PR or direct push) +3. The release happens automatically + +Because the changelog is generated from commit messages, always use [Conventional Commits](https://www.conventionalcommits.org/) format. ## Questions? diff --git a/README.md b/README.md index de662b4..e6316c2 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,12 @@ Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a cl ## Features -- 🎨 **Complete API Coverage**: Generate, edit, and convert SVGs with AI -- 🧰 **TypeScript Native**: Full type safety with comprehensive type definitions -- ⚙️ **Configuration-Based**: Clean, object-based parameter configuration -- 🔄 **Automatic Retries**: Built-in retry logic with exponential backoff -- 🌊 **Streaming Support**: Real-time progress updates via Server-Sent Events -- ✅ **Input Validation**: Zod-based schema validation for all requests -- 📦 **Dual Package**: ESM and CommonJS support with proper exports -- 🔌 **Extensible**: Request/response interceptors for customization +- **Complete API Coverage**: Generate, edit, convert, optimize, and manage SVGs +- **TypeScript Native**: Full type safety with comprehensive type definitions +- **Automatic Retries**: Built-in retry logic with exponential backoff +- **Streaming Support**: Real-time progress updates via Server-Sent Events +- **Input Validation**: Zod-based schema validation for all requests +- **Dual Package**: ESM and CommonJS support ## Installation @@ -22,12 +20,13 @@ Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a cl npm install @genwave/svgmaker-sdk ``` +Requires Node.js 18.0.0 or higher. + ## Quick Start ```typescript import { SVGMakerClient } from '@genwave/svgmaker-sdk'; -// Initialize client const client = new SVGMakerClient('your-api-key'); // Generate an SVG @@ -35,276 +34,58 @@ const result = await client.generate .configure({ prompt: 'A minimalist mountain landscape', quality: 'high', - styleParams: { - style: 'minimalist', - }, - svgText: true, // Get SVG source code - }) - .execute(); - -console.log('SVG URL:', result.svgUrl); -console.log('Credits used:', result.creditCost); -``` - -## Core API - -### Client Initialization - -```typescript -// Basic client -const client = new SVGMakerClient('your-api-key'); - -// Client with custom configuration -const client = new SVGMakerClient('your-api-key', { - timeout: 60000, - maxRetries: 5, - logging: true, -}); -``` - -### Generate SVG - -Create SVGs from text descriptions using AI. - -```typescript -const result = await client.generate - .configure({ - prompt: 'A geometric mountain landscape with sun', - quality: 'high', - aspectRatio: 'landscape', - styleParams: { - style: 'minimalist', - color_mode: 'monochrome', - composition: 'center-object', - advanced: { - stroke_weight: 'thin', - corner_style: 'rounded', - shadow_effect: 'none' - } - }, - base64Png: true, // Include PNG preview - svgText: true, // Include SVG source + svgText: true, }) .execute(); -// Access results console.log('SVG URL:', result.svgUrl); console.log('Credits used:', result.creditCost); -console.log('Revised prompt:', result.revisedPrompt); - -if (result.pngImageData) { - // PNG preview as Buffer - console.log('PNG size:', result.pngImageData.length, 'bytes'); -} - -if (result.svgText) { - // SVG source code - console.log('SVG content:', result.svgText); -} -``` - -#### Generation Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `prompt` | `string` | - | **Required.** Description of the SVG to generate | -| `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Generation quality level | -| `aspectRatio` | `'auto' \| 'portrait' \| 'landscape' \| 'square' \| 'wide' \| 'tall'` | `'auto'` for low/medium, `'square'` for high | Output aspect ratio. **Note:** Low and medium quality only support `'auto'`, `'portrait'`, `'landscape'`, `'square'`. High quality supports all options except `'auto'` | -| `background` | `'auto' \| 'transparent' \| 'opaque'` | `'auto'` | Background type | -| `styleParams` | `StyleParams` | `{}` | Style parameters object (see StyleParams table below) | -| `base64Png` | `boolean` | `false` | Include PNG preview in response | -| `svgText` | `boolean` | `false` | Include SVG source code in response | -#### StyleParams Object - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `style` | `'minimalist' \| 'cartoon' \| 'realistic' \| 'abstract' \| 'flat' \| 'isometric'` | `none` | Art style preference | -| `color_mode` | `'monochrome' \| '2-colors' \| '3-colors' \| 'full-color'` | `none` | Color scheme preference | -| `image_complexity` | `'simple' \| 'detailed'` | `none` | Complexity level | -| `category` | `'icon' \| 'illustration' \| 'pattern' \| 'logo' \| 'scene'` | `none` | Content category | -| `composition` | `'center-object' \| 'full-scene'` | `none` | Layout composition | -| `advanced` | `AdvancedStyleParams` | `{}` | Advanced styling parameters (see AdvancedStyleParams table below) | - -#### AdvancedStyleParams Object - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `stroke_weight` | `'thin' \| 'medium' \| 'thick'` | `none` | Stroke weight for lines and shapes | -| `corner_style` | `'none' \| 'rounded' \| 'sharp'` | `none` | Corner style for shapes | -| `shadow_effect` | `'none' \| 'soft' \| 'hard'` | `none` | Shadow effect type | - -### Edit SVG/Image - -Modify existing images or SVGs using AI-powered editing. - -```typescript -// Basic editing -const result = await client.edit +// Edit an existing image +const edited = await client.edit .configure({ image: './input.png', - prompt: 'Add a red border and make it more vibrant', + prompt: 'Add a red border', quality: 'medium', - base64Png: true, - svgText: true, }) .execute(); -// Advanced editing with style parameters -const result = await client.edit - .configure({ - image: fs.readFileSync('./input.svg'), - prompt: 'Make this more cartoonish', - styleParams: { - style: 'cartoon', - color_mode: '3-colors', - advanced: { - stroke_weight: 'medium', - corner_style: 'rounded' - } - }, - mask: './mask.png', // Optional mask for targeted editing - }) +// AI-powered vectorization +const vectorized = await client.convert.aiVectorize + .configure({ file: './photo.jpg', svgText: true }) .execute(); -``` - -#### Edit Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `image` | `string \| Buffer \| Readable` | - | **Required.** Image file to edit (file path, Buffer, or Readable stream) | -| `prompt` | `string` | - | **Required.** Edit instructions as a simple text string | -| `styleParams` | `StyleParams` | `{}` | Style parameters object (see StyleParams table above) | -| `mask` | `string \| Buffer \| Readable` | `none` | Optional mask for targeted editing (file path, Buffer, or Readable stream) | -| `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Processing quality | -| `aspectRatio` | `'auto' \| 'portrait' \| 'landscape' \| 'square'` | `'auto'` | Output aspect ratio | -| `background` | `'auto' \| 'transparent' \| 'opaque'` | `'auto'` | Background handling | -| `base64Png` | `boolean` | `false` | Include PNG preview in response | -| `svgText` | `boolean` | `false` | Include SVG source code in response | - -### Convert Image to SVG - -Convert raster images to vector SVG format. -```typescript -const result = await client.convert - .configure({ - file: './photo.jpg', - svgText: true, - }) - .execute(); - -console.log('SVG:', result.svgUrl); -console.log('SVG source:', result.svgText); -``` - -## Streaming Responses - -All operations support real-time progress updates via streaming. - -```typescript +// Stream progress for long operations const stream = client.generate - .configure({ - prompt: 'A detailed cityscape illustration', - quality: 'high', - }) + .configure({ prompt: 'A detailed cityscape', quality: 'high' }) .stream(); for await (const event of stream) { - switch (event.status) { - case 'processing': - console.log(`Progress: ${event.message}`); - break; - case 'complete': - console.log(`Complete! SVG: ${event.svgUrl}`); - break; - case 'error': - console.error(`Error: ${event.error}`); - break; - } + if (event.status === 'complete') console.log('Done:', event.svgUrl); } ``` -## Configuration - -### Client Configuration - -```typescript -interface SVGMakerConfig { - apiKey: string; // API authentication key - baseUrl: string; // API base URL (default: "https://api.svgmaker.io") - timeout: number; // Request timeout ms (default: 30000) - maxRetries: number; // Max retry attempts (default: 3) - retryBackoffFactor: number; // Retry delay factor ms (default: 300) - retryStatusCodes: number[]; // Status codes to retry (default: [408, 429, 500, 502, 503, 504]) - logging: boolean; // Enable request logging (default: false) - logLevel: 'debug' | 'info' | 'warn' | 'error'; // Log level (default: "info") - caching: boolean; // Enable response caching (default: false) - cacheTTL: number; // Cache TTL ms (default: 300000) - rateLimit: number; // Requests per minute (default: 60) -} -``` +## Available Clients -### Example Configuration +| Client | Access | Description | +|--------|--------|-------------| +| `generate` | `client.generate` | Create SVGs from text prompts | +| `edit` | `client.edit` | Modify existing images/SVGs with AI | +| `convert.aiVectorize` | `client.convert.aiVectorize` | AI-powered raster to SVG conversion | +| `convert.trace` | `client.convert.trace` | Algorithmic raster to SVG tracing | +| `convert.svgToVector` | `client.convert.svgToVector` | SVG to vector formats (PDF, EPS, DXF, AI, PS) | +| `convert.rasterToRaster` | `client.convert.rasterToRaster` | Convert between raster formats | +| `convert.batch` | `client.convert.batch` | Batch convert up to 10 files | +| `enhancePrompt` | `client.enhancePrompt` | Improve text prompts using AI | +| `optimizeSvg` | `client.optimizeSvg` | Optimize SVG files using SVGO | +| `generations` | `client.generations` | Manage your generations (list, download, share, delete) | +| `gallery` | `client.gallery` | Browse and download public gallery items | +| `account` | `client.account` | Account info and usage statistics | -```typescript -const client = new SVGMakerClient('your-api-key', { - timeout: 60000, - maxRetries: 5, - logging: true, - logLevel: 'debug', - caching: true, - cacheTTL: 600000, // 10 minutes -}); -``` - -## Logging - -The SDK includes built-in logging functionality for debugging and monitoring API requests. - -### Enable Logging - -```typescript -// Basic logging -const client = new SVGMakerClient('your-api-key', { - logging: true, - logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' -}); - -// Development (verbose) -const client = new SVGMakerClient('your-api-key', { - logging: true, - logLevel: 'debug', -}); - -// Production (errors only) -const client = new SVGMakerClient('your-api-key', { - logging: true, - logLevel: 'error', -}); -``` - -### Log Output - -``` -[SVGMaker SDK][2024-01-15T10:30:45.123Z][INFO] SVGMaker SDK initialized -[SVGMaker SDK][2024-01-15T10:30:46.250Z][DEBUG] Making API request to /generate -``` - -### Runtime Configuration - -```typescript -// Change log level during runtime -client.setConfig({ - logging: true, - logLevel: 'warn', -}); -``` +Most clients use the `configure().execute()` pattern. `generations`, `gallery`, and `account` use direct methods (e.g., `client.generations.list()`, `client.account.getInfo()`). ## Error Handling -The SDK provides comprehensive error handling with custom error types. - ```typescript import { SVGMakerClient, Errors } from '@genwave/svgmaker-sdk'; @@ -313,114 +94,25 @@ try { .configure({ prompt: 'A landscape' }) .execute(); } catch (error) { - if (error instanceof Errors.ValidationError) { - console.error('Invalid parameters:', error.message); - } else if (error instanceof Errors.APIError) { - console.error('API error:', error.message, error.statusCode); - } else if (error instanceof Errors.RateLimitError) { + if (error instanceof Errors.RateLimitError) { console.error('Rate limited. Retry after:', error.retryAfter, 'seconds'); - } else if (error instanceof Errors.AuthError) { - console.error('Authentication failed:', error.message); - } else if (error instanceof Errors.TimeoutError) { - console.error('Request timed out after:', error.timeout, 'ms'); - } else if (error instanceof Errors.NetworkError) { - console.error('Network error:', error.message); } else if (error instanceof Errors.InsufficientCreditsError) { console.error('Insufficient credits. Required:', error.creditsRequired); - } else if (error instanceof Errors.ContentSafetyError) { - console.error('Content safety violation:', error.message); - } else if (error instanceof Errors.FileSizeError) { - console.error('File too large:', error.message); - } else if (error instanceof Errors.FileFormatError) { - console.error('Unsupported file format:', error.message); + } else if (error instanceof Errors.AuthError) { + console.error('Authentication failed:', error.message); + } else if (error instanceof Errors.APIError) { + console.error('API error:', error.message, error.statusCode); } } ``` -## Advanced Features - -### Request/Response Interceptors - -Customize requests and responses with interceptors. - -```typescript -// Add request interceptor -client.addRequestInterceptor(async (request) => { - console.log('Making request:', request.url); - return request; -}); - -// Add response interceptor -client.addResponseInterceptor(async (response) => { - console.log('Received response:', response); - return response; -}); -``` - -### Dynamic Configuration - -Update client configuration at runtime. - -```typescript -// Update configuration -client.setConfig({ - timeout: 120000, - maxRetries: 2, -}); - -// Get current configuration -const config = client.getConfig(); -console.log('Current timeout:', config.timeout); -``` - -## TypeScript Support - -Full TypeScript support with comprehensive type definitions. - -```typescript -import { SVGMakerClient, Types } from '@genwave/svgmaker-sdk'; - -// Typed parameters -const generateParams: Types.GenerateParams = { - prompt: 'A minimalist logo', - quality: 'high', - styleParams: { - style: 'minimalist', - color_mode: 'monochrome', - }, -}; - -// Typed responses -const result: Types.GenerateResponse = await client.generate - .configure(generateParams) - .execute(); - -// Type-safe access -console.log(result.svgUrl); // string -console.log(result.creditCost); // number -console.log(result.pngImageData); // Buffer | undefined -``` - -## Package Structure - -``` -@genwave/svgmaker-sdk/ -├── dist/ -│ ├── cjs/ # CommonJS build -│ ├── esm/ # ES Module build -│ └── types/ # TypeScript declarations -├── src/ -│ ├── core/ # Core client implementation -│ ├── clients/ # API-specific clients -│ ├── types/ # TypeScript definitions -│ ├── errors/ # Custom error classes -│ └── utils/ # Utility functions -└── examples/ # Usage examples -``` +See the [SDK Documentation](docs/sdk-documentation.md) for the full list of error types. -## Browser Compatibility +## Documentation -The SDK is designed for Node.js environments and requires Node.js 18.0.0 or higher. For browser usage, consider the potential limitations with file system operations and ensure proper bundling configuration. +- **[SDK Documentation](docs/sdk-documentation.md)** - Full SDK reference with detailed examples for every client +- **[API Documentation](docs/v1-api-documentation.md)** - REST API reference with endpoint details and credit costs +- **[Legacy API Documentation](docs/legacy-api-documentation.md)** - Pre-v1 API reference ## Contributing @@ -432,6 +124,5 @@ MIT License - see the [`LICENSE`](LICENSE) file for details. ## Support -- **Documentation**: [API Documentation](docs/api-documentation.md) - **Issues**: [GitHub Issues](https://github.com/GenWaveLLC/svgmaker-sdk-node/issues) - **Repository**: [GitHub Repository](https://github.com/GenWaveLLC/svgmaker-sdk-node) diff --git a/docs/api-documentation.md b/docs/legacy-api-documentation.md similarity index 99% rename from docs/api-documentation.md rename to docs/legacy-api-documentation.md index 36073dd..de64c87 100644 --- a/docs/api-documentation.md +++ b/docs/legacy-api-documentation.md @@ -1,4 +1,6 @@ -# SVGMaker API Documentation +# SVGMaker API Documentation (Legacy) + +> **This document covers the legacy (pre-v1) API.** For the current v1 API, see [v1-api-documentation.md](v1-api-documentation.md). This documentation covers the three main API endpoints for SVG generation, editing, and conversion using API keys. diff --git a/docs/sdk-documentation.md b/docs/sdk-documentation.md index df9f564..46838a2 100644 --- a/docs/sdk-documentation.md +++ b/docs/sdk-documentation.md @@ -1,8 +1,8 @@ # SVGMaker Node.js SDK Documentation -Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a clean, type-safe interface for generating, editing, and converting SVG graphics using AI. -[!IMPORTANT] -**Access Notice:** The SVGMaker Node.js SDK is available only for paid users. You must have an active paid SVGMaker account to use the SDK and obtain an API key. +Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a clean, type-safe interface for generating, editing, converting, and managing SVG graphics using AI. + +> **Access Notice:** The SVGMaker Node.js SDK is available only for paid users. You must have an active paid SVGMaker account to use the SDK and obtain an API key. [![npm version](https://img.shields.io/npm/v/@genwave/svgmaker-sdk.svg)](https://www.npmjs.com/package/@genwave/svgmaker-sdk) [![License](https://img.shields.io/npm/l/@genwave/svgmaker-sdk.svg)](https://github.com/GenWaveLLC/svgmaker-sdk-node/blob/main/LICENSE) @@ -14,6 +14,20 @@ Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a cl - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [API Reference](#api-reference) + - [Client Initialization](#client-initialization) + - [Generate SVG](#generate-svg) + - [Edit SVG/Image](#edit-svgimage) + - [Convert Namespace](#convert-namespace) + - [AI Vectorize](#ai-vectorize) + - [Trace](#trace) + - [SVG to Vector](#svg-to-vector) + - [Raster to Raster](#raster-to-raster) + - [Batch Convert](#batch-convert) + - [Enhance Prompt](#enhance-prompt) + - [Optimize SVG](#optimize-svg) + - [Generations](#generations) + - [Gallery](#gallery) + - [Account](#account) - [Advanced Features](#advanced-features) - [Error Handling](#error-handling) - [TypeScript Support](#typescript-support) @@ -24,14 +38,14 @@ Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a cl ## Features -- 🎨 **Complete API Coverage**: Generate, edit, and convert SVGs with AI -- 🧰 **TypeScript Native**: Full type safety with comprehensive type definitions -- ⚙️ **Configuration-Based**: Clean, object-based parameter configuration -- 🔄 **Automatic Retries**: Built-in retry logic with exponential backoff -- 🌊 **Streaming Support**: Real-time progress updates via Server-Sent Events -- ✅ **Input Validation**: Zod-based schema validation for all requests -- 📦 **Dual Package**: ESM and CommonJS support with proper exports -- 🔌 **Extensible**: Request/response interceptors for customization +- **Complete API Coverage**: Generate, edit, convert, optimize, and manage SVGs with AI +- **TypeScript Native**: Full type safety with comprehensive type definitions +- **Configuration-Based**: Clean, object-based parameter configuration +- **Automatic Retries**: Built-in retry logic with exponential backoff +- **Streaming Support**: Real-time progress updates via Server-Sent Events +- **Input Validation**: Zod-based schema validation for all requests +- **Dual Package**: ESM and CommonJS support with proper exports +- **Extensible**: Request/response interceptors for customization ## Installation @@ -74,11 +88,20 @@ console.log('Credits used:', result.creditCost); ### Client Architecture -The SDK is built around a main `SVGMakerClient` class that provides access to three specialized clients: - -- **`generate`**: Create SVGs from text descriptions -- **`edit`**: Modify existing images or SVGs -- **`convert`**: Convert raster images to SVG format +The SDK is built around a main `SVGMakerClient` class that provides access to specialized clients: + +- **`generate`**: Create SVGs from text descriptions (configure/execute/stream) +- **`edit`**: Modify existing images or SVGs with AI (configure/execute/stream) +- **`convert.aiVectorize`**: AI-powered raster-to-SVG vectorization (configure/execute/stream) +- **`convert.trace`**: Trace raster images to SVG using VTracer (configure/execute) +- **`convert.svgToVector`**: Convert SVG to vector formats such as PDF, EPS, DXF, AI, PS (configure/execute) +- **`convert.rasterToRaster`**: Convert between raster image formats (configure/execute) +- **`convert.batch`**: Batch convert multiple files at once (configure/execute) +- **`enhancePrompt`**: Improve text prompts using AI (configure/execute) +- **`optimizeSvg`**: Optimize and compress SVG files (configure/execute) +- **`generations`**: Manage your generation history (list, get, delete, share, download) +- **`gallery`**: Browse and download public gallery items (list, get, download) +- **`account`**: Retrieve account info and usage statistics (getInfo, getUsage) ### Configuration System @@ -125,6 +148,7 @@ const result = await client.generate styleParams: { style: 'flat', color_mode: 'monochrome', + image_complexity: 'illustration', composition: 'centered_object', }, base64Png: true, // Include PNG preview @@ -132,7 +156,6 @@ const result = await client.generate }) .execute(); -// Access results console.log('SVG URL:', result.svgUrl); console.log('Generation ID:', result.generationId); console.log('Credits used:', result.creditCost); @@ -143,7 +166,6 @@ if (result.pngImageData) { } if (result.svgText) { - // SVG source code console.log('SVG content:', result.svgText); } ``` @@ -153,13 +175,29 @@ if (result.svgText) { | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `prompt` | `string` | - | **Required.** Description of the SVG to generate | -| `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Generation quality level | +| `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Generation quality level. Mutually exclusive with `model` | +| `model` | `string` | - | Specific model to use. Mutually exclusive with `quality` | | `aspectRatio` | `'auto' \| 'portrait' \| 'landscape' \| 'square'` | `'auto'` | Output aspect ratio | | `background` | `'auto' \| 'transparent' \| 'opaque'` | `'auto'` | Background type | | `styleParams` | `StyleParams` | `{}` | Style parameters object | | `base64Png` | `boolean` | `false` | Include PNG preview in response | | `svgText` | `boolean` | `false` | Include SVG source code in response | +#### Generation Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `svgUrl` | `string` | URL of the generated SVG | +| `creditCost` | `number` | Credits consumed by the request | +| `quality` | `string` | Quality level used | +| `message` | `string` | API response message | +| `svgUrlExpiresIn` | `number` | Seconds until the SVG URL expires | +| `generationId` | `string` | Unique ID for the generation | +| `pngImageData` | `Buffer \| undefined` | Decoded PNG preview (when `base64Png` is true) | +| `svgText` | `string \| undefined` | SVG source code (when `svgText` is true) | +| `revisedPrompt` | `string \| undefined` | Prompt as revised by the model | +| `metadata` | `object` | Additional API metadata | + #### Style Parameters | Parameter | Type | Description | @@ -167,7 +205,7 @@ if (result.svgText) { | `style` | `'flat' \| 'line_art' \| 'engraving' \| 'linocut' \| 'silhouette' \| 'isometric' \| 'cartoon' \| 'ghibli'` | Art style preference | | `color_mode` | `'full_color' \| 'monochrome' \| 'few_colors'` | Color scheme preference | | `image_complexity` | `'icon' \| 'illustration' \| 'scene'` | Complexity level | -| `text` | `'no_text' \| 'only_title' \| 'embedded_text'` | Text options | +| `text` | `'only_title' \| 'embedded_text'` | Text inclusion option | | `composition` | `'centered_object' \| 'repeating_pattern' \| 'full_scene' \| 'objects_in_grid'` | Layout composition | ### Edit SVG/Image @@ -175,7 +213,9 @@ if (result.svgText) { Modify existing images or SVGs using AI-powered editing. ```typescript -// Basic editing +import fs from 'fs'; + +// Edit using a file path const result = await client.edit .configure({ image: './input.png', @@ -186,8 +226,8 @@ const result = await client.edit }) .execute(); -// Advanced editing with style parameters -const result = await client.edit +// Edit using a Buffer with style parameters +const result2 = await client.edit .configure({ image: fs.readFileSync('./input.svg'), prompt: 'Make this more cartoonish', @@ -203,43 +243,419 @@ const result = await client.edit | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `image` | `string \| Buffer \| Readable` | - | **Required.** Image file to edit | +| `image` | `string \| Buffer \| Readable` | - | **Required.** Image file path, Buffer, or stream | | `prompt` | `string` | - | **Required.** Edit instructions | +| `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Processing quality. Mutually exclusive with `model` | +| `model` | `string` | - | Specific model to use. Mutually exclusive with `quality` | | `styleParams` | `StyleParams` | `{}` | Style parameters object | -| `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Processing quality | +| `mask` | `string \| Buffer \| Readable` | - | Optional mask image for inpainting | | `aspectRatio` | `'auto' \| 'portrait' \| 'landscape' \| 'square'` | `'auto'` | Output aspect ratio | | `background` | `'auto' \| 'transparent' \| 'opaque'` | `'auto'` | Background handling | | `base64Png` | `boolean` | `false` | Include PNG preview in response | | `svgText` | `boolean` | `false` | Include SVG source code in response | -### Convert Image to SVG +The response shape is identical to the [Generation Response Fields](#generation-response-fields) table above. + +### Convert Namespace + +The `convert` namespace groups all image conversion operations. Each sub-client follows the `configure().execute()` pattern. -Convert raster images to vector SVG format. +#### AI Vectorize + +Convert raster images to SVG using AI-powered vectorization. Supports streaming. ```typescript -const result = await client.convert +const result = await client.convert.aiVectorize .configure({ file: './photo.jpg', svgText: true, }) .execute(); -console.log('SVG:', result.svgUrl); +console.log('SVG URL:', result.svgUrl); console.log('SVG source:', result.svgText); +console.log('Generation ID:', result.generationId); +``` + +**Streaming AI Vectorize:** + +```typescript +const stream = client.convert.aiVectorize + .configure({ file: './photo.jpg' }) + .stream(); + +for await (const event of stream) { + if (event.status === 'complete') { + console.log('SVG URL:', event.svgUrl); + } +} ``` -#### Convert Parameters +**AI Vectorize Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `file` | `string \| Buffer \| Readable` | - | **Required.** File to convert to SVG | +| `file` | `string \| Buffer \| Readable` | - | **Required.** Image file to vectorize | +| `storage` | `boolean` | - | Whether to store the result | | `svgText` | `boolean` | `false` | Include SVG source code in response | +**AI Vectorize Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `svgUrl` | `string` | URL of the generated SVG | +| `creditCost` | `number` | Credits consumed | +| `message` | `string` | API response message | +| `svgUrlExpiresIn` | `number` | Seconds until URL expires | +| `generationId` | `string` | Unique ID for the generation | +| `svgText` | `string \| undefined` | SVG source code (when requested) | +| `metadata` | `object` | Additional API metadata | + +#### Trace + +Trace raster images to SVG using VTracer with fine-grained control over the tracing algorithm. + +```typescript +const result = await client.convert.trace + .configure({ + file: './logo.png', + preset: 'bw', + mode: 'spline', + detail: 80, + smoothness: 60, + }) + .execute(); + +for (const file of result.results) { + if (file.success) { + console.log(`${file.filename}: ${file.url}`); + } else { + console.error(`${file.filename} failed: ${file.error}`); + } +} +console.log(`Converted ${result.summary.successful} of ${result.summary.total} files`); +``` + +**Trace Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file` | `string \| Buffer \| Readable` | - | **Required.** Image file to trace | +| `preset` | `'bw' \| 'poster' \| 'photo'` | - | Tracing preset | +| `mode` | `'pixel' \| 'polygon' \| 'spline'` | - | Path generation mode | +| `hierarchical` | `'stacked' \| 'cutout'` | - | Layer hierarchy mode | +| `detail` | `number` (0–100) | - | Level of detail | +| `smoothness` | `number` (0–100) | - | Path smoothness | +| `corners` | `number` (0–100) | - | Corner detection sensitivity | +| `reduceNoise` | `number` | - | Noise reduction level | + +**Trace / Conversion Response Fields (shared by Trace, SvgToVector, RasterToRaster, and Batch):** + +| Field | Type | Description | +|-------|------|-------------| +| `results` | `array` | Per-file results | +| `results[].filename` | `string` | Original filename | +| `results[].success` | `boolean` | Whether conversion succeeded | +| `results[].url` | `string \| undefined` | Download URL (on success) | +| `results[].urlExpiresIn` | `number \| undefined` | Seconds until URL expires | +| `results[].format` | `string \| undefined` | Output format | +| `results[].error` | `string \| undefined` | Error message (on failure) | +| `summary.total` | `number` | Total files processed | +| `summary.successful` | `number` | Number of successful conversions | +| `summary.failed` | `number` | Number of failed conversions | +| `metadata` | `object` | Additional API metadata | + +#### SVG to Vector + +Convert SVG files to other vector formats such as PDF, EPS, DXF, AI, or PS. + +```typescript +const result = await client.convert.svgToVector + .configure({ + file: './diagram.svg', + toFormat: 'PDF', + textToPath: true, + }) + .execute(); + +console.log('Download URL:', result.results[0].url); +``` + +**SVG to Vector Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file` | `string \| Buffer \| Readable` | - | **Required.** SVG file to convert | +| `toFormat` | `'PDF' \| 'EPS' \| 'DXF' \| 'AI' \| 'PS'` | - | **Required.** Target vector format | +| `textToPath` | `boolean` | - | Convert text elements to paths | +| `dxfVersion` | `'R12' \| 'R14'` | - | DXF format version (DXF output only) | + +#### Raster to Raster + +Convert between raster image formats with optional resizing and quality control. + +```typescript +const result = await client.convert.rasterToRaster + .configure({ + file: './image.png', + toFormat: 'WEBP', + quality: 85, + width: 1920, + }) + .execute(); + +console.log('Download URL:', result.results[0].url); +``` + +**Raster to Raster Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file` | `string \| Buffer \| Readable` | - | **Required.** Image file to convert | +| `toFormat` | `'PNG' \| 'JPG' \| 'WEBP' \| 'TIFF' \| 'GIF' \| 'AVIF'` | - | **Required.** Target raster format | +| `quality` | `number` (1–100) | - | Output quality | +| `width` | `number` | - | Output width in pixels | +| `height` | `number` | - | Output height in pixels | + +#### Batch Convert + +Convert multiple files (up to 10) in a single request. Accepts any combination of options from the individual convert clients. + +```typescript +const result = await client.convert.batch + .configure({ + files: ['./a.png', './b.png', './c.png'], + toFormat: 'WEBP', + quality: 80, + }) + .execute(); + +for (const file of result.results) { + console.log(`${file.filename}: ${file.success ? file.url : file.error}`); +} +``` + +**Batch Convert Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `files` | `string[]` (1–10) | - | **Required.** Array of file paths to convert | +| `toFormat` | `string` | - | **Required.** Target output format | +| ...other | - | - | All options from Trace, SvgToVector, and RasterToRaster are also accepted | + +### Enhance Prompt + +Use AI to improve and expand text prompts before using them for generation. + +```typescript +const result = await client.enhancePrompt + .configure({ + prompt: 'a cat', + }) + .execute(); + +console.log('Enhanced prompt:', result.enhancedPrompt); + +// Use the enhanced prompt with generate +const svg = await client.generate + .configure({ + prompt: result.enhancedPrompt, + quality: 'high', + }) + .execute(); +``` + +**Enhance Prompt Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `prompt` | `string` | **Required.** The prompt to enhance | + +**Enhance Prompt Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `enhancedPrompt` | `string` | The improved prompt | +| `metadata` | `object` | Additional API metadata | + +### Optimize SVG + +Optimize and optionally compress SVG files using SVGO. + +```typescript +const result = await client.optimizeSvg + .configure({ + file: './unoptimized.svg', + compress: true, // Also produce a .svgz file + }) + .execute(); + +console.log('Optimized SVG URL:', result.svgUrl); +console.log('Compressed SVG URL:', result.svgzUrl); +console.log('Compressed size:', result.compressedSize, 'bytes'); +``` + +**Optimize SVG Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file` | `string \| Buffer \| Readable` | - | **Required.** SVG file to optimize | +| `compress` | `boolean` | `false` | Also produce a gzip-compressed `.svgz` file | + +**Optimize SVG Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `svgUrl` | `string` | URL of the optimized SVG | +| `svgUrlExpiresIn` | `number` | Seconds until SVG URL expires | +| `svgzUrl` | `string \| undefined` | URL of the compressed `.svgz` file (when `compress` is true) | +| `svgzUrlExpiresIn` | `number \| undefined` | Seconds until svgz URL expires | +| `filename` | `string` | Output filename | +| `compressedSize` | `number` | File size after optimization in bytes | +| `metadata` | `object` | Additional API metadata | + +### Generations + +Manage your personal generation history. This client uses direct method calls rather than the `configure().execute()` pattern. + +```typescript +// List recent generations +const { items, pagination } = await client.generations.list({ + page: 1, + limit: 20, + type: 'generate', +}); + +for (const item of items) { + console.log(item.id, item.prompt); +} + +// Get a specific generation +const generation = await client.generations.get('generation-id'); +console.log('Prompt:', generation.prompt); +console.log('Public:', generation.isPublic); + +// Share a generation +const shared = await client.generations.share('generation-id'); +console.log('Share URL:', shared.shareUrl); + +// Download a generation in a specific format +const download = await client.generations.download('generation-id', { + format: 'png', + optimize: true, +}); +console.log('Download URL:', download.url); + +// Delete a generation (paid users only) +await client.generations.delete('generation-id'); +``` + +**generations.list() Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `page` | `number` | Page number | +| `limit` | `number` | Items per page | +| `type` | `string` | Filter by generation type | +| `hashtags` | `string[]` | Filter by hashtags | +| `categories` | `string[]` | Filter by categories | +| `query` | `string` | Search query | + +**generations.download() Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `format` | `'svg' \| 'webp' \| 'png' \| 'svg-optimized' \| 'svgz'` | Output format | +| `optimize` | `boolean` | Optimize SVG output | + +### Gallery + +Browse and download items from the public gallery. Uses the same direct method pattern as the Generations client. + +```typescript +// List gallery items +const { items } = await client.gallery.list({ + page: 1, + limit: 20, + query: 'mountain', +}); + +// Get a specific gallery item +const item = await client.gallery.get('item-id'); +console.log('Prompt:', item.prompt); + +// Download a gallery item +const download = await client.gallery.download('item-id', { + format: 'svg', +}); +console.log('Download URL:', download.url); +``` + +The `gallery.list()` method accepts the same parameters as `generations.list()` with additional `pro` and `gold` boolean filters for filtering by creator tier. + +### Account + +Retrieve account information and API usage statistics. + +```typescript +// Get account information +const info = await client.account.getInfo(); +console.log('Email:', info.email); +console.log('Account type:', info.accountType); +console.log('Credits remaining:', info.credits); + +// Get usage statistics for the last 7 days +const usage = await client.account.getUsage({ days: 7 }); +console.log('Period:', usage.period); +console.log('Total requests:', usage.summary.requests); +console.log('Credits used:', usage.summary.creditsUsed); +console.log('Success rate:', usage.summary.successRate); + +// Get usage statistics for a specific date range +const rangeUsage = await client.account.getUsage({ + start: '2025-01-01', + end: '2025-01-31', +}); +``` + +**account.getInfo() Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `email` | `string` | Account email address | +| `displayName` | `string` | Display name | +| `accountType` | `string` | Account tier | +| `credits` | `number` | Remaining credits | +| `metadata` | `object` | Additional API metadata | + +**account.getUsage() Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `days` | `number` | Number of recent days to include. Mutually exclusive with `start`/`end` | +| `start` | `string` | Start date (ISO format). Use with `end` | +| `end` | `string` | End date (ISO format). Use with `start` | + +**account.getUsage() Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `period` | `object` | Period covered by the report | +| `summary.requests` | `number` | Total API requests | +| `summary.creditsUsed` | `number` | Total credits consumed | +| `summary.successCount` | `number` | Successful requests | +| `summary.errorCount` | `number` | Failed requests | +| `summary.successRate` | `number` | Success rate (0–1) | +| `byCategory` | `object` | Usage broken down by endpoint category | +| `daily` | `array` | Day-by-day usage data | +| `allTime` | `object` | All-time usage totals | +| `metadata` | `object` | Additional API metadata | + ## Advanced Features ### Streaming Responses -All operations support real-time progress updates via streaming. +`generate`, `edit`, and `convert.aiVectorize` support real-time progress updates via streaming. ```typescript const stream = client.generate @@ -396,34 +812,68 @@ console.log(result.pngImageData); // Buffer | undefined ### Available Types -- `GenerateParams`, `EditParams`, `ConvertParams` -- `GenerateResponse`, `EditResponse`, `ConvertResponse` +**Parameters:** +- `GenerateParams`, `EditParams` +- `AIVectorizeParams`, `TraceParams`, `SvgToVectorParams`, `RasterToRasterParams`, `BatchConvertParams` +- `EnhancePromptParams`, `OptimizeSvgParams` - `StyleParams` + +**Responses:** +- `GenerateResponse`, `EditResponse` +- `AIVectorizeResponse`, `ConvertResponse` (shared by Trace, SvgToVector, RasterToRaster, Batch) +- `EnhancePromptResponse`, `OptimizeSvgResponse` +- `GenerationItem`, `GenerationDownloadResponse` +- `GalleryItem`, `GalleryDownloadResponse` +- `AccountInfo`, `AccountUsage` + +**Style and Enum Types:** - `Quality`, `AspectRatio`, `Background` - `Style`, `ColorMode`, `ImageComplexity`, `TextOption`, `Composition` -- `StreamEvent`, `GenerateStreamEvent`, `EditStreamEvent`, `ConvertStreamEvent` + +**Stream Types:** +- `StreamEvent`, `GenerateStreamEvent`, `EditStreamEvent`, `AIVectorizeStreamEvent` + +**Exports:** +- `SVGMakerClient`, `GenerateClient`, `EditClient`, `AIVectorizeClient` +- `TraceClient`, `SvgToVectorClient`, `RasterToRasterClient`, `BatchConvertClient` +- `EnhancePromptClient`, `OptimizeSvgClient` +- `GenerationsClient`, `GalleryClient`, `AccountClient` +- `HttpClient`, `Types`, `SVGMakerConfig`, `Errors` +- `ConvertClient` (retained for backward compatibility) ## Best Practices ### Performance Optimization -1. **Reuse Client Instances**: Create one client instance and reuse it -2. **Use Appropriate Quality Levels**: Choose quality based on your needs -3. **Enable Streaming for Long Operations**: Use streaming for better UX -4. **Handle Errors Gracefully**: Implement proper error handling +1. **Reuse Client Instances**: Create one client instance and reuse it across your application +2. **Use Appropriate Quality Levels**: Choose quality based on your needs to avoid unnecessary credit spend +3. **Enable Streaming for Long Operations**: Use streaming for better UX on high-quality generations +4. **Handle Errors Gracefully**: Implement proper error handling for all API calls ```typescript // Good: Reuse client instance -const client = new SVGMakerClient('your-api-key'); +const client = new SVGMakerClient(process.env.SVGMAKER_API_KEY); // Bad: Creating new client for each request const result = await new SVGMakerClient('your-api-key').generate... ``` +### Using model vs. quality + +The `generate` and `edit` clients accept either `quality` or `model`, but not both. Use `quality` for a simple high/medium/low selection, or `model` to target a specific model version directly. + +```typescript +// Using quality (recommended for most cases) +await client.generate.configure({ prompt: '...', quality: 'high' }).execute(); + +// Using model (for specific model targeting) +await client.generate.configure({ prompt: '...', model: 'specific-model-id' }).execute(); +``` + ### Error Handling 1. **Catch Specific Errors**: Handle different error types appropriately -2. **Implement Retry Logic**: Use built-in retry mechanisms +2. **Implement Retry Logic**: Use built-in retry mechanisms or handle `RateLimitError` manually 3. **Log Errors**: Enable logging for debugging ```typescript @@ -441,9 +891,9 @@ try { ### Security -1. **Protect API Keys**: Never expose API keys in client-side code +1. **Protect API Keys**: Never expose API keys in client-side code or version control 2. **Use Environment Variables**: Store API keys securely -3. **Validate Input**: Always validate user input before sending to API +3. **Validate Input**: Always validate user input before sending to the API ```typescript // Good: Use environment variables @@ -455,31 +905,16 @@ const client = new SVGMakerClient('hardcoded-api-key'); ## Examples -### Basic Usage +### Generate an SVG with Full Style Control ```typescript import { SVGMakerClient } from '@genwave/svgmaker-sdk'; -const client = new SVGMakerClient('your-api-key'); - -// Generate a simple SVG -const result = await client.generate - .configure({ - prompt: 'A simple geometric logo', - quality: 'medium', - svgText: true, - }) - .execute(); - -console.log('Generated SVG:', result.svgUrl); -``` - -### Advanced Styling +const client = new SVGMakerClient(process.env.SVGMAKER_API_KEY); -```typescript const result = await client.generate .configure({ - prompt: 'A modern app icon', + prompt: 'A modern app icon for a fitness tracker', quality: 'high', aspectRatio: 'square', styleParams: { @@ -487,41 +922,48 @@ const result = await client.generate color_mode: 'few_colors', image_complexity: 'icon', composition: 'centered_object', - text: 'no_text', }, base64Png: true, svgText: true, }) .execute(); + +console.log('SVG URL:', result.svgUrl); +console.log('Revised prompt:', result.revisedPrompt); ``` -### Batch Processing +### Enhance a Prompt then Generate ```typescript -const prompts = [ - 'A mountain landscape', - 'A city skyline', - 'A forest scene' -]; +const enhanced = await client.enhancePrompt + .configure({ prompt: 'a mountain' }) + .execute(); -const results = await Promise.all( - prompts.map(prompt => - client.generate - .configure({ prompt, quality: 'medium' }) - .execute() - ) -); +const result = await client.generate + .configure({ + prompt: enhanced.enhancedPrompt, + quality: 'high', + svgText: true, + }) + .execute(); -console.log('Generated SVGs:', results.map(r => r.svgUrl)); +console.log('Generated from enhanced prompt:', result.svgUrl); ``` -### Streaming with Progress +### Edit an Image with Streaming Progress ```typescript -const stream = client.generate +import fs from 'fs'; + +const stream = client.edit .configure({ - prompt: 'A complex illustration', - quality: 'high', + image: fs.createReadStream('./input.png'), + prompt: 'Add a blue background and make it more vibrant', + quality: 'medium', + styleParams: { + style: 'flat', + color_mode: 'full_color', + }, base64Png: true, }) .stream(); @@ -532,39 +974,85 @@ for await (const event of stream) { console.log(`Progress: ${event.message}`); break; case 'complete': - console.log('Generation complete!'); - console.log('SVG URL:', event.svgUrl); - if (event.pngImageData) { - console.log('PNG preview available'); - } + console.log('Edited SVG:', event.svgUrl); break; case 'error': - console.error('Generation failed:', event.error); + console.error('Edit failed:', event.error); break; } } ``` -### File Upload and Editing +### Convert Multiple Files with Batch ```typescript -import fs from 'fs'; +const result = await client.convert.batch + .configure({ + files: ['./a.png', './b.png', './c.png'], + toFormat: 'WEBP', + quality: 80, + width: 1280, + }) + .execute(); -// Edit an existing image -const result = await client.edit +console.log(`Batch complete: ${result.summary.successful}/${result.summary.total} succeeded`); +for (const file of result.results) { + if (file.success) { + console.log(` ${file.filename} -> ${file.url}`); + } +} +``` + +### Manage Generations History + +```typescript +// List, filter, and download recent generations +const { items } = await client.generations.list({ + limit: 10, + type: 'generate', +}); + +for (const item of items) { + console.log(`${item.id}: ${item.prompt}`); +} + +// Share a generation and get a public link +const shared = await client.generations.share(items[0].id); +console.log('Public URL:', shared.shareUrl); + +// Download as optimized PNG +const download = await client.generations.download(items[0].id, { + format: 'png', + optimize: true, +}); +console.log('PNG download URL:', download.url); +``` + +### Check Account Usage + +```typescript +const info = await client.account.getInfo(); +console.log(`Account: ${info.displayName} (${info.accountType})`); +console.log(`Credits remaining: ${info.credits}`); + +const usage = await client.account.getUsage({ days: 30 }); +console.log(`Last 30 days: ${usage.summary.requests} requests, ${usage.summary.creditsUsed} credits used`); +console.log(`Success rate: ${(usage.summary.successRate * 100).toFixed(1)}%`); +``` + +### Optimize an SVG File + +```typescript +const result = await client.optimizeSvg .configure({ - image: fs.createReadStream('./input.png'), - prompt: 'Add a blue background and make it more vibrant', - quality: 'medium', - styleParams: { - style: 'flat', - color_mode: 'full_color', - }, - base64Png: true, + file: './large-diagram.svg', + compress: true, }) .execute(); -console.log('Edited SVG:', result.svgUrl); +console.log('Optimized SVG:', result.svgUrl); +console.log('Compressed (.svgz):', result.svgzUrl); +console.log('Compressed size:', result.compressedSize, 'bytes'); ``` ## Troubleshooting @@ -575,28 +1063,28 @@ console.log('Edited SVG:', result.svgUrl); **Problem**: `AuthError: Invalid API key` -**Solution**: +**Solution**: - Verify your API key is correct -- Check that the API key has sufficient credits -- Ensure the API key is properly formatted +- Check that your account is active and has remaining credits +- Ensure the API key is properly formatted and not truncated #### Rate Limiting **Problem**: `RateLimitError: Rate limit exceeded` **Solution**: -- Implement exponential backoff -- Use the `retryAfter` property to wait -- Consider upgrading your plan for higher limits +- Implement exponential backoff using the `retryAfter` property +- The SDK has built-in retry logic; increase `maxRetries` in the client config if needed +- Consider upgrading your plan for higher rate limits #### File Upload Issues **Problem**: `FileSizeError: File too large` **Solution**: -- Check file size limits (typically 10MB) +- Check file size limits for the specific endpoint - Compress images before upload -- Use supported file formats +- Use a supported file format for the target operation #### Network Issues @@ -604,8 +1092,8 @@ console.log('Edited SVG:', result.svgUrl); **Solution**: - Check internet connectivity -- Increase timeout configuration -- Implement retry logic +- Increase the `timeout` value in the client configuration +- The SDK will automatically retry failed requests based on the `maxRetries` config ### Debugging @@ -620,9 +1108,9 @@ const client = new SVGMakerClient('your-api-key', { ### Performance Issues -1. **Slow Generation**: Use lower quality settings for faster results -2. **Memory Issues**: Process files in smaller batches -3. **Network Timeouts**: Increase timeout configuration +1. **Slow Generation**: Use lower quality settings or the `model` parameter to select a faster model +2. **Memory Issues**: When processing many files, use batch operations or process in smaller groups +3. **Network Timeouts**: Increase the `timeout` configuration, especially for high-quality generation requests ## Support @@ -630,7 +1118,7 @@ const client = new SVGMakerClient('your-api-key', { - **Repository**: [GitHub Repository](https://github.com/GenWaveLLC/svgmaker-sdk-node) - **NPM Package**: [@genwave/svgmaker-sdk](https://www.npmjs.com/package/@genwave/svgmaker-sdk) -- **API Documentation**: [SVGMaker API Docs](https://svgmaker.io/docs) +- **API Documentation**: [API Documentation](v1-api-documentation.md) ### Contributing @@ -642,7 +1130,7 @@ MIT License - see the [`LICENSE`](../LICENSE) file for details. --- -**Version**: 0.3.0 -**Node.js**: >=18.0.0 -**License**: MIT -**Repository**: [GitHub](https://github.com/GenWaveLLC/svgmaker-sdk-node) \ No newline at end of file +**Version**: v1.0.0 +**Node.js**: >=18.0.0 +**License**: MIT +**Repository**: [GitHub](https://github.com/GenWaveLLC/svgmaker-sdk-node) diff --git a/docs/v1-api.md b/docs/v1-api-documentation.md similarity index 100% rename from docs/v1-api.md rename to docs/v1-api-documentation.md diff --git a/package-lock.json b/package-lock.json index 7a76f14..cddee5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genwave/svgmaker-sdk", - "version": "0.3.1", + "version": "1.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genwave/svgmaker-sdk", - "version": "0.3.1", + "version": "1.0.0-rc.1", "license": "MIT", "dependencies": { "async-retry": "^1.3.3", @@ -574,9 +574,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1670,20 +1670,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", - "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/type-utils": "8.49.0", - "@typescript-eslint/utils": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1693,23 +1693,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.49.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", - "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1719,20 +1719,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", - "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.49.0", - "@typescript-eslint/types": "^8.49.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1746,14 +1746,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", - "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1764,9 +1764,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", - "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -1781,17 +1781,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", - "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/utils": "8.49.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1801,14 +1801,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", - "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -1820,21 +1820,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", - "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.49.0", - "@typescript-eslint/tsconfig-utils": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1848,16 +1848,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", - "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1867,19 +1867,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", - "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1890,13 +1890,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2852,9 +2852,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5048,9 +5048,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -6512,9 +6512,9 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 545ed9c..2b0060e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@genwave/svgmaker-sdk", - "version": "0.3.1", + "version": "1.0.0-rc.1", "description": "Official Node.js SDK for SVGMaker API", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -37,11 +37,7 @@ "version:patch": "npm version patch --no-git-tag-version", "version:minor": "npm version minor --no-git-tag-version", "version:major": "npm version major --no-git-tag-version", - "release:shared": "git add package.json && git commit --amend --no-edit && git tag -a v$(node -p \"require('./package.json').version\") -m \"Release v$(node -p \"require('./package.json').version\")\" && git push --follow-tags", - "release:patch": "npm run version:patch && npm run release:shared", - "release:minor": "npm run version:minor && npm run release:shared", - "release:major": "npm run version:major && npm run release:shared", - "changelog": "echo 'Please update CHANGELOG.md manually before release'", + "changelog": "echo 'Changelog is auto-generated during release. Use conventional commits (feat:, fix:, etc.).'", "audit:fix": "npm audit fix", "outdated": "npm outdated" }, diff --git a/src/clients/AccountClient.ts b/src/clients/AccountClient.ts index 7bdd6af..a210a81 100644 --- a/src/clients/AccountClient.ts +++ b/src/clients/AccountClient.ts @@ -1,11 +1,7 @@ import { BaseClient } from './BaseClient'; import { SVGMakerClient } from '../core/SVGMakerClient'; import { z } from 'zod'; -import { - AccountResponse, - AccountUsageParams, - AccountUsageResponse, -} from '../types/api'; +import { AccountResponse, AccountUsageParams, AccountUsageResponse } from '../types/api'; /** * Schema for validating usage parameters @@ -18,11 +14,11 @@ const usageParamsSchema = z }) .strict() .refine( - (data) => !(data.days !== undefined && (data.start !== undefined || data.end !== undefined)), + data => !(data.days !== undefined && (data.start !== undefined || data.end !== undefined)), { message: 'Use either days or start/end, not both.' } ) .refine( - (data) => { + data => { if (data.start !== undefined || data.end !== undefined) { return data.start !== undefined && data.end !== undefined; } diff --git a/src/clients/EditClient.ts b/src/clients/EditClient.ts index 7ea6217..a26d892 100644 --- a/src/clients/EditClient.ts +++ b/src/clients/EditClient.ts @@ -94,7 +94,15 @@ export class EditClient extends BaseClient { // Add optional parameters this.appendOptionalParams(formData, this.params as Record, [ - 'prompt', 'quality', 'aspectRatio', 'background', 'storage', 'stream', 'base64Png', 'svgText', 'model', + 'prompt', + 'quality', + 'aspectRatio', + 'background', + 'storage', + 'stream', + 'base64Png', + 'svgText', + 'model', ]); // Execute request diff --git a/src/clients/GalleryClient.ts b/src/clients/GalleryClient.ts index 59b6370..0f0bd2f 100644 --- a/src/clients/GalleryClient.ts +++ b/src/clients/GalleryClient.ts @@ -138,10 +138,7 @@ export class GalleryClient extends BaseClient { * @param params Optional download parameters (format, optimize) * @returns Download URL and file metadata */ - async download( - id: string, - params?: GalleryDownloadParams - ): Promise { + async download(id: string, params?: GalleryDownloadParams): Promise { this.logger.debug('Downloading gallery item', { id, params }); this.validateRequest(id, idSchema); diff --git a/src/clients/convert/AIVectorizeClient.ts b/src/clients/convert/AIVectorizeClient.ts index b0e5d16..c2beca6 100644 --- a/src/clients/convert/AIVectorizeClient.ts +++ b/src/clients/convert/AIVectorizeClient.ts @@ -49,7 +49,11 @@ export class AIVectorizeClient extends BaseClient { await this.addFileToForm(formData, 'file', this.params.file!); // Add optional parameters - this.appendOptionalParams(formData, this.params as Record, ['storage', 'stream', 'svgText']); + this.appendOptionalParams(formData, this.params as Record, [ + 'storage', + 'stream', + 'svgText', + ]); // Execute request const { data, metadata: responseMetadata } = await this.executeFormDataRequest( diff --git a/src/clients/convert/BatchConvertClient.ts b/src/clients/convert/BatchConvertClient.ts index b48b2f8..424ba3a 100644 --- a/src/clients/convert/BatchConvertClient.ts +++ b/src/clients/convert/BatchConvertClient.ts @@ -63,8 +63,18 @@ export class BatchConvertClient extends BaseClient { // Add optional parameters this.appendOptionalParams(formData, this.params as Record, [ - 'preset', 'mode', 'hierarchical', 'detail', 'smoothness', 'corners', 'reduceNoise', - 'textToPath', 'dxfVersion', 'quality', 'width', 'height', + 'preset', + 'mode', + 'hierarchical', + 'detail', + 'smoothness', + 'corners', + 'reduceNoise', + 'textToPath', + 'dxfVersion', + 'quality', + 'width', + 'height', ]); // Execute request diff --git a/src/clients/convert/RasterToRasterClient.ts b/src/clients/convert/RasterToRasterClient.ts index ec08238..1e9f471 100644 --- a/src/clients/convert/RasterToRasterClient.ts +++ b/src/clients/convert/RasterToRasterClient.ts @@ -51,7 +51,11 @@ export class RasterToRasterClient extends BaseClient { formData.append('toFormat', this.params.toFormat!); // Add optional parameters - this.appendOptionalParams(formData, this.params as Record, ['quality', 'width', 'height']); + this.appendOptionalParams(formData, this.params as Record, [ + 'quality', + 'width', + 'height', + ]); // Execute request const { data, metadata: responseMetadata } = await this.executeFormDataRequest( diff --git a/src/clients/convert/SvgToVectorClient.ts b/src/clients/convert/SvgToVectorClient.ts index 4203c5f..ceb69fb 100644 --- a/src/clients/convert/SvgToVectorClient.ts +++ b/src/clients/convert/SvgToVectorClient.ts @@ -50,7 +50,10 @@ export class SvgToVectorClient extends BaseClient { formData.append('toFormat', this.params.toFormat!); // Add optional parameters - this.appendOptionalParams(formData, this.params as Record, ['textToPath', 'dxfVersion']); + this.appendOptionalParams(formData, this.params as Record, [ + 'textToPath', + 'dxfVersion', + ]); // Execute request const { data, metadata: responseMetadata } = await this.executeFormDataRequest( diff --git a/src/clients/convert/TraceClient.ts b/src/clients/convert/TraceClient.ts index c219a01..405ad9e 100644 --- a/src/clients/convert/TraceClient.ts +++ b/src/clients/convert/TraceClient.ts @@ -54,7 +54,14 @@ export class TraceClient extends BaseClient { // Add optional parameters this.appendOptionalParams(formData, this.params as Record, [ - 'algorithm', 'preset', 'mode', 'hierarchical', 'detail', 'smoothness', 'corners', 'reduceNoise', + 'algorithm', + 'preset', + 'mode', + 'hierarchical', + 'detail', + 'smoothness', + 'corners', + 'reduceNoise', ]); // Execute request diff --git a/tests/clients/AccountClient.test.ts b/tests/clients/AccountClient.test.ts index b60afb7..66456c3 100644 --- a/tests/clients/AccountClient.test.ts +++ b/tests/clients/AccountClient.test.ts @@ -7,11 +7,7 @@ import { createMockAccountInfoData, createMockAccountUsageData, } from '../setup'; -import { - ValidationError, - RateLimitError, - APIError, -} from '../../src/errors/CustomErrors'; +import { ValidationError, RateLimitError, APIError } from '../../src/errors/CustomErrors'; describe('AccountClient', () => { let fetchMock: jest.Mock; @@ -128,32 +124,30 @@ describe('AccountClient', () => { const client = createTestClient(); await expect( - client.account.getUsage({ days: 7, start: '2025-01-01' } as any), + client.account.getUsage({ days: 7, start: '2025-01-01' } as any) ).rejects.toThrow(ValidationError); }); it('throws ValidationError when days is used with end', async () => { const client = createTestClient(); - await expect( - client.account.getUsage({ days: 7, end: '2025-01-31' } as any), - ).rejects.toThrow(ValidationError); + await expect(client.account.getUsage({ days: 7, end: '2025-01-31' } as any)).rejects.toThrow( + ValidationError + ); }); it('throws ValidationError when only start is provided without end', async () => { const client = createTestClient(); - await expect( - client.account.getUsage({ start: '2025-01-01' } as any), - ).rejects.toThrow(ValidationError); + await expect(client.account.getUsage({ start: '2025-01-01' } as any)).rejects.toThrow( + ValidationError + ); }); it('throws ValidationError on unknown property due to .strict()', async () => { const client = createTestClient(); - await expect( - client.account.getUsage({ foo: 'bar' } as any), - ).rejects.toThrow(ValidationError); + await expect(client.account.getUsage({ foo: 'bar' } as any)).rejects.toThrow(ValidationError); }); }); @@ -171,45 +165,35 @@ describe('AccountClient', () => { const client = createTestClient(); mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); - await expect( - client.account.getInfo(), - ).rejects.toThrow('Invalid API key'); + await expect(client.account.getInfo()).rejects.toThrow('Invalid API key'); }); it('throws on INSUFFICIENT_CREDITS (402)', async () => { const client = createTestClient(); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); - await expect( - client.account.getInfo(), - ).rejects.toThrow('Not enough credits'); + await expect(client.account.getInfo()).rejects.toThrow('Not enough credits'); }); it('throws RateLimitError on RATE_LIMIT_EXCEEDED (429)', async () => { const client = createTestClient(); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); - await expect( - client.account.getInfo(), - ).rejects.toThrow(RateLimitError); + await expect(client.account.getInfo()).rejects.toThrow(RateLimitError); }); it('throws on ENDPOINT_DISABLED (503)', async () => { const client = createTestClient(); mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); - await expect( - client.account.getInfo(), - ).rejects.toThrow('This endpoint is disabled'); + await expect(client.account.getInfo()).rejects.toThrow('This endpoint is disabled'); }); it('throws APIError on generic server error (500)', async () => { const client = createTestClient(); mockFetchErrorResponse('SERVER_ERROR', 500, 'Internal server error'); - await expect( - client.account.getInfo(), - ).rejects.toThrow(APIError); + await expect(client.account.getInfo()).rejects.toThrow(APIError); }); }); }); diff --git a/tests/clients/EditClient.test.ts b/tests/clients/EditClient.test.ts index 9c33546..5add784 100644 --- a/tests/clients/EditClient.test.ts +++ b/tests/clients/EditClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { Readable } from 'stream'; import { createTestClient, @@ -75,9 +76,7 @@ describe('EditClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockEditResponse()); - await client.edit - .configure({ image: testImageBuffer, prompt: 'Make it red' }) - .execute(); + await client.edit.configure({ image: testImageBuffer, prompt: 'Make it red' }).execute(); expect(fetchMock).toHaveBeenCalled(); const [url, options] = fetchMock.mock.calls[0]; @@ -90,9 +89,7 @@ describe('EditClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockEditResponse()); - await client.edit - .configure({ image: testImageBuffer, prompt: 'Make it red' }) - .execute(); + await client.edit.configure({ image: testImageBuffer, prompt: 'Make it red' }).execute(); const [, options] = fetchMock.mock.calls[0]; expect(options.headers['x-api-key']).toBe('test-api-key-123'); @@ -102,9 +99,7 @@ describe('EditClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockEditResponse()); - await client.edit - .configure({ image: testImageBuffer, prompt: 'Make it green' }) - .execute(); + await client.edit.configure({ image: testImageBuffer, prompt: 'Make it green' }).execute(); const formData = fetchMock.mock.calls[0][1].body as FormData; const imageField = formData.get('image'); @@ -207,9 +202,7 @@ describe('EditClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockEditResponse()); - await client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(); + await client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute(); const formData = fetchMock.mock.calls[0][1].body as FormData; expect(formData.get('quality')).toBe('medium'); @@ -237,16 +230,14 @@ describe('EditClient', () => { mockFetchJsonResponse(createMockEditResponse()); // Mock fs module - const fs = require('fs'); + const originalExistsSync = fs.existsSync; const originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-png-data')); try { - await client.edit - .configure({ image: '/path/to/image.png', prompt: 'Edit this' }) - .execute(); + await client.edit.configure({ image: '/path/to/image.png', prompt: 'Edit this' }).execute(); expect(fs.existsSync).toHaveBeenCalledWith('/path/to/image.png'); expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/image.png'); @@ -268,9 +259,7 @@ describe('EditClient', () => { readable.push(Buffer.from('fake-stream-data')); readable.push(null); - await client.edit - .configure({ image: readable, prompt: 'Edit this' }) - .execute(); + await client.edit.configure({ image: readable, prompt: 'Edit this' }).execute(); expect(fetchMock).toHaveBeenCalled(); const formData = fetchMock.mock.calls[0][1].body as FormData; @@ -285,7 +274,7 @@ describe('EditClient', () => { await expect( client.edit .configure({ image: '/nonexistent/path/image.png', prompt: 'Edit this' }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); }); @@ -299,18 +288,18 @@ describe('EditClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockEditResponse()); - await expect( - client.edit.configure({ prompt: 'Edit this' } as any).execute(), - ).rejects.toThrow(ValidationError); + await expect(client.edit.configure({ prompt: 'Edit this' } as any).execute()).rejects.toThrow( + ValidationError + ); }); it('throws ValidationError when neither prompt nor styleParams given', async () => { const client = createTestClient(); mockFetchJsonResponse(createMockEditResponse()); - await expect( - client.edit.configure({ image: testImageBuffer }).execute(), - ).rejects.toThrow(ValidationError); + await expect(client.edit.configure({ image: testImageBuffer }).execute()).rejects.toThrow( + ValidationError + ); }); it('throws ValidationError when both model and quality are set', async () => { @@ -325,7 +314,7 @@ describe('EditClient', () => { model: 'test-model', quality: 'high', }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -354,9 +343,7 @@ describe('EditClient', () => { mockFetchErrorResponse('INVALID_API_KEY', 401); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(AuthError); }); @@ -365,9 +352,7 @@ describe('EditClient', () => { mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(InsufficientCreditsError); }); @@ -376,9 +361,7 @@ describe('EditClient', () => { mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(RateLimitError); }); @@ -387,9 +370,7 @@ describe('EditClient', () => { mockFetchErrorResponse('CONTENT_POLICY', 422); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(ContentSafetyError); }); @@ -398,9 +379,7 @@ describe('EditClient', () => { mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(APIError); }); @@ -409,9 +388,7 @@ describe('EditClient', () => { mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(EndpointDisabledError); }); @@ -420,9 +397,7 @@ describe('EditClient', () => { mockFetchErrorResponse('FILE_TOO_LARGE', 413, 'File exceeds size limit'); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(FileSizeError); }); @@ -431,9 +406,7 @@ describe('EditClient', () => { mockFetchErrorResponse('UNKNOWN', 400, 'Unsupported file format'); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(FileFormatError); }); @@ -445,13 +418,13 @@ describe('EditClient', () => { status: 502, headers: new Headers({ 'Content-Type': 'text/plain' }), text: async () => 'Bad Gateway', - json: async () => { throw new Error('not json'); }, + json: async () => { + throw new Error('not json'); + }, }); await expect( - client.edit - .configure({ image: testImageBuffer, prompt: 'Edit this' }) - .execute(), + client.edit.configure({ image: testImageBuffer, prompt: 'Edit this' }).execute() ).rejects.toThrow(APIError); }); }); @@ -508,8 +481,8 @@ describe('EditClient', () => { }); expect(events.length).toBeGreaterThanOrEqual(2); - expect(events.some((e) => e.status === 'processing')).toBe(true); - expect(events.some((e) => e.status === 'complete')).toBe(true); + expect(events.some(e => e.status === 'processing')).toBe(true); + expect(events.some(e => e.status === 'complete')).toBe(true); }); it('emits error on non-ok response', async () => { @@ -536,13 +509,14 @@ describe('EditClient', () => { stream.on('data', () => {}); stream.on('end', resolve); stream.on('error', reject); - }), + }) ).rejects.toBeDefined(); }); it('decodes svgText in stream events', async () => { const client = createTestClient(); - const rawSvg = ''; + const rawSvg = + ''; const streamEvents = [ { status: 'processing', message: 'Editing...' }, { @@ -572,7 +546,7 @@ describe('EditClient', () => { stream.on('error', reject); }); - const generatedEvent = events.find((e) => e.status === 'generated'); + const generatedEvent = events.find(e => e.status === 'generated'); expect(generatedEvent).toBeDefined(); expect(generatedEvent.svgText).toBe(rawSvg); }); @@ -609,7 +583,7 @@ describe('EditClient', () => { stream.on('error', reject); }); - const generatedEvent = events.find((e) => e.status === 'generated'); + const generatedEvent = events.find(e => e.status === 'generated'); expect(generatedEvent).toBeDefined(); expect(generatedEvent.pngImageData).toBeInstanceOf(Buffer); }); diff --git a/tests/clients/EnhancePromptClient.test.ts b/tests/clients/EnhancePromptClient.test.ts index fc422a4..a4cc0b1 100644 --- a/tests/clients/EnhancePromptClient.test.ts +++ b/tests/clients/EnhancePromptClient.test.ts @@ -86,9 +86,7 @@ describe('EnhancePromptClient', () => { const client = createTestClient(); const mockData = createMockEnhancePromptResponse(); mockFetchJsonResponse(mockData); - const result = await client.enhancePrompt - .configure({ prompt: 'A sunset' }) - .execute(); + const result = await client.enhancePrompt.configure({ prompt: 'A sunset' }).execute(); expect(result.enhancedPrompt).toBe(mockData.enhancedPrompt); expect(result.metadata).toBeDefined(); }); @@ -108,41 +106,41 @@ describe('EnhancePromptClient', () => { it('throws AuthError on INVALID_API_KEY', async () => { const client = createTestClient(); mockFetchErrorResponse('INVALID_API_KEY', 401); - await expect( - client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(AuthError); + await expect(client.enhancePrompt.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + AuthError + ); }); it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { const client = createTestClient(); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); - await expect( - client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(InsufficientCreditsError); + await expect(client.enhancePrompt.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + InsufficientCreditsError + ); }); it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { const client = createTestClient(); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); - await expect( - client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(RateLimitError); + await expect(client.enhancePrompt.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + RateLimitError + ); }); it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { const client = createTestClient(); mockFetchErrorResponse('ENDPOINT_DISABLED', 503); - await expect( - client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(EndpointDisabledError); + await expect(client.enhancePrompt.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + EndpointDisabledError + ); }); it('throws APIError on generic server error', async () => { const client = createTestClient(); mockFetchErrorResponse('INTERNAL_ERROR', 500); - await expect( - client.enhancePrompt.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(APIError); + await expect(client.enhancePrompt.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + APIError + ); }); }); }); diff --git a/tests/clients/GalleryClient.test.ts b/tests/clients/GalleryClient.test.ts index 6793b1e..df40019 100644 --- a/tests/clients/GalleryClient.test.ts +++ b/tests/clients/GalleryClient.test.ts @@ -8,11 +8,7 @@ import { createMockGenerationData, createMockGenerationDownloadData, } from '../setup'; -import { - ValidationError, - RateLimitError, - APIError, -} from '../../src/errors/CustomErrors'; +import { ValidationError, RateLimitError, APIError } from '../../src/errors/CustomErrors'; describe('GalleryClient', () => { let fetchMock: jest.Mock; @@ -115,25 +111,19 @@ describe('GalleryClient', () => { it('throws ValidationError when page is 0', async () => { const client = createTestClient(); - await expect( - client.gallery.list({ page: 0 }), - ).rejects.toThrow(ValidationError); + await expect(client.gallery.list({ page: 0 })).rejects.toThrow(ValidationError); }); it('throws ValidationError when limit exceeds 100', async () => { const client = createTestClient(); - await expect( - client.gallery.list({ limit: 101 }), - ).rejects.toThrow(ValidationError); + await expect(client.gallery.list({ limit: 101 })).rejects.toThrow(ValidationError); }); it('throws ValidationError on unknown property due to .strict()', async () => { const client = createTestClient(); - await expect( - client.gallery.list({ foo: 'bar' } as any), - ).rejects.toThrow(ValidationError); + await expect(client.gallery.list({ foo: 'bar' } as any)).rejects.toThrow(ValidationError); }); }); @@ -180,9 +170,7 @@ describe('GalleryClient', () => { it('throws ValidationError when id is empty string', async () => { const client = createTestClient(); - await expect( - client.gallery.get(''), - ).rejects.toThrow(ValidationError); + await expect(client.gallery.get('')).rejects.toThrow(ValidationError); }); }); @@ -238,9 +226,7 @@ describe('GalleryClient', () => { it('throws ValidationError when id is empty string', async () => { const client = createTestClient(); - await expect( - client.gallery.download(''), - ).rejects.toThrow(ValidationError); + await expect(client.gallery.download('')).rejects.toThrow(ValidationError); }); }); @@ -258,45 +244,35 @@ describe('GalleryClient', () => { const client = createTestClient(); mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); - await expect( - client.gallery.list(), - ).rejects.toThrow('Invalid API key'); + await expect(client.gallery.list()).rejects.toThrow('Invalid API key'); }); it('throws on INSUFFICIENT_CREDITS (402)', async () => { const client = createTestClient(); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); - await expect( - client.gallery.list(), - ).rejects.toThrow('Not enough credits'); + await expect(client.gallery.list()).rejects.toThrow('Not enough credits'); }); it('throws RateLimitError on RATE_LIMIT_EXCEEDED (429)', async () => { const client = createTestClient(); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); - await expect( - client.gallery.list(), - ).rejects.toThrow(RateLimitError); + await expect(client.gallery.list()).rejects.toThrow(RateLimitError); }); it('throws on ENDPOINT_DISABLED (503)', async () => { const client = createTestClient(); mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); - await expect( - client.gallery.list(), - ).rejects.toThrow('This endpoint is disabled'); + await expect(client.gallery.list()).rejects.toThrow('This endpoint is disabled'); }); it('throws APIError on generic server error (500)', async () => { const client = createTestClient(); mockFetchErrorResponse('SERVER_ERROR', 500, 'Internal server error'); - await expect( - client.gallery.list(), - ).rejects.toThrow(APIError); + await expect(client.gallery.list()).rejects.toThrow(APIError); }); }); }); diff --git a/tests/clients/GenerateClient.test.ts b/tests/clients/GenerateClient.test.ts index 962ba60..f3388b4 100644 --- a/tests/clients/GenerateClient.test.ts +++ b/tests/clients/GenerateClient.test.ts @@ -9,15 +9,7 @@ import { createMockGenerateResponse, createMockStreamEvents, } from '../setup'; -import { - ValidationError, - AuthError, - InsufficientCreditsError, - RateLimitError, - ContentSafetyError, - APIError, - EndpointDisabledError, -} from '../../src/errors/CustomErrors'; +import { ValidationError, RateLimitError, APIError } from '../../src/errors/CustomErrors'; describe('GenerateClient', () => { let fetchMock: jest.Mock; @@ -66,9 +58,7 @@ describe('GenerateClient', () => { const mockData = createMockGenerateResponse(); mockFetchJsonResponse(mockData); - const result = await client.generate - .configure({ prompt: 'A sunset' }) - .execute(); + const result = await client.generate.configure({ prompt: 'A sunset' }).execute(); expect(result.svgUrl).toBe(mockData.svgUrl); }); @@ -83,9 +73,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerateResponse()); - await client.generate - .configure({ prompt: 'A tree', quality: 'high' }) - .execute(); + await client.generate.configure({ prompt: 'A tree', quality: 'high' }).execute(); expect(fetchMock).toHaveBeenCalled(); const [url, options] = fetchMock.mock.calls[0]; @@ -101,9 +89,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerateResponse()); - await client.generate - .configure({ prompt: 'A tree' }) - .execute(); + await client.generate.configure({ prompt: 'A tree' }).execute(); const [, options] = fetchMock.mock.calls[0]; expect(options.headers['x-api-key']).toBe('test-api-key-123'); @@ -114,9 +100,7 @@ describe('GenerateClient', () => { const mockData = createMockGenerateResponse(); mockFetchJsonResponse(mockData); - const result = await client.generate - .configure({ prompt: 'A flower' }) - .execute(); + const result = await client.generate.configure({ prompt: 'A flower' }).execute(); expect(result.svgUrl).toBe(mockData.svgUrl); expect(result.creditCost).toBe(mockData.creditCost); @@ -132,9 +116,7 @@ describe('GenerateClient', () => { ''; mockFetchJsonResponse(createMockGenerateResponse({ svgText: rawSvg })); - const result = await client.generate - .configure({ prompt: 'A box', svgText: true }) - .execute(); + const result = await client.generate.configure({ prompt: 'A box', svgText: true }).execute(); expect(result.svgText).toBe(rawSvg); }); @@ -142,9 +124,7 @@ describe('GenerateClient', () => { it('decodes base64Png to pngImageData Buffer', async () => { const client = createTestClient(); const base64Png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=='; - mockFetchJsonResponse( - createMockGenerateResponse({ base64Png }), - ); + mockFetchJsonResponse(createMockGenerateResponse({ base64Png })); const result = await client.generate .configure({ prompt: 'A dot', base64Png: true }) @@ -208,9 +188,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerateResponse({ quality: undefined })); - await client.generate - .configure({ prompt: 'A tree', model: 'custom-model-v1' }) - .execute(); + await client.generate.configure({ prompt: 'A tree', model: 'custom-model-v1' }).execute(); const body = JSON.parse(fetchMock.mock.calls[0][1].body); expect(body.model).toBe('custom-model-v1'); @@ -227,9 +205,9 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerateResponse()); - await expect( - client.generate.configure({ prompt: '' }).execute(), - ).rejects.toThrow(ValidationError); + await expect(client.generate.configure({ prompt: '' }).execute()).rejects.toThrow( + ValidationError + ); }); it('throws ValidationError when prompt is missing', async () => { @@ -246,7 +224,7 @@ describe('GenerateClient', () => { await expect( client.generate .configure({ prompt: 'A cat', model: 'test-model', quality: 'high' }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -255,9 +233,7 @@ describe('GenerateClient', () => { for (const quality of ['low', 'medium', 'high'] as const) { mockFetchJsonResponse(createMockGenerateResponse({ quality })); - const result = await client.generate - .configure({ prompt: 'Test', quality }) - .execute(); + const result = await client.generate.configure({ prompt: 'Test', quality }).execute(); expect(result).toBeDefined(); } }); @@ -267,9 +243,7 @@ describe('GenerateClient', () => { for (const aspectRatio of ['auto', 'portrait', 'landscape', 'square'] as const) { mockFetchJsonResponse(createMockGenerateResponse()); - const result = await client.generate - .configure({ prompt: 'Test', aspectRatio }) - .execute(); + const result = await client.generate.configure({ prompt: 'Test', aspectRatio }).execute(); expect(result).toBeDefined(); } }); @@ -309,63 +283,63 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow('Invalid API key'); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + 'Invalid API key' + ); }); it('throws on INSUFFICIENT_CREDITS', async () => { const client = createTestClient(); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow('Not enough credits'); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + 'Not enough credits' + ); }); it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { const client = createTestClient(); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(RateLimitError); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + RateLimitError + ); }); it('throws on CONTENT_POLICY', async () => { const client = createTestClient(); mockFetchErrorResponse('CONTENT_POLICY', 422, 'Content policy violation'); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow('Content policy violation'); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + 'Content policy violation' + ); }); it('throws APIError on generic server error', async () => { const client = createTestClient(); mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow(APIError); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + APIError + ); }); it('throws on ENDPOINT_DISABLED', async () => { const client = createTestClient(); mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow('This endpoint is disabled'); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + 'This endpoint is disabled' + ); }); it('throws on FILE_SIZE_EXCEEDED (413)', async () => { const client = createTestClient(); mockFetchErrorResponse('FILE_SIZE_EXCEEDED', 413, 'File too large'); - await expect( - client.generate.configure({ prompt: 'A cat' }).execute(), - ).rejects.toThrow('File too large'); + await expect(client.generate.configure({ prompt: 'A cat' }).execute()).rejects.toThrow( + 'File too large' + ); }); }); @@ -378,9 +352,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockStreamEvents()); - const stream = client.generate - .configure({ prompt: 'A cat' }) - .stream(); + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); expect(stream).toBeInstanceOf(Readable); }); @@ -389,9 +361,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockStreamEvents()); - const stream = client.generate - .configure({ prompt: 'A cat' }) - .stream(); + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); const events: any[] = []; await new Promise((resolve, reject) => { @@ -400,7 +370,7 @@ describe('GenerateClient', () => { stream.on('error', reject); }); - const processingEvent = events.find((e) => e.status === 'processing'); + const processingEvent = events.find(e => e.status === 'processing'); expect(processingEvent).toBeDefined(); expect(processingEvent.message).toBe('Generating SVG...'); }); @@ -409,9 +379,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockStreamEvents()); - const stream = client.generate - .configure({ prompt: 'A cat' }) - .stream(); + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); const events: any[] = []; await new Promise((resolve, reject) => { @@ -420,7 +388,7 @@ describe('GenerateClient', () => { stream.on('error', reject); }); - const completeEvent = events.find((e) => e.status === 'complete'); + const completeEvent = events.find(e => e.status === 'complete'); expect(completeEvent).toBeDefined(); // svgText from 'generated' event should be accumulated into 'complete' expect(completeEvent.svgText).toBeDefined(); @@ -432,9 +400,7 @@ describe('GenerateClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockStreamEvents()); - const stream = client.generate - .configure({ prompt: 'A cat' }) - .stream(); + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); // Wait for stream to start await new Promise((resolve, reject) => { @@ -464,22 +430,21 @@ describe('GenerateClient', () => { text: async () => '{"success":false}', }); - const stream = client.generate - .configure({ prompt: 'A cat' }) - .stream(); + const stream = client.generate.configure({ prompt: 'A cat' }).stream(); await expect( new Promise((resolve, reject) => { stream.on('data', () => {}); stream.on('end', resolve); stream.on('error', reject); - }), + }) ).rejects.toBeDefined(); }); it('decodes svgText in stream events', async () => { const client = createTestClient(); - const rawSvg = ''; + const rawSvg = + ''; const streamEvents = [ { status: 'processing', message: 'Generating SVG...' }, { @@ -507,7 +472,7 @@ describe('GenerateClient', () => { stream.on('error', reject); }); - const generatedEvent = events.find((e) => e.status === 'generated'); + const generatedEvent = events.find(e => e.status === 'generated'); expect(generatedEvent).toBeDefined(); expect(generatedEvent.svgText).toBe(rawSvg); }); @@ -542,14 +507,15 @@ describe('GenerateClient', () => { stream.on('error', reject); }); - const generatedEvent = events.find((e) => e.status === 'generated'); + const generatedEvent = events.find(e => e.status === 'generated'); expect(generatedEvent).toBeDefined(); expect(generatedEvent.pngImageData).toBeInstanceOf(Buffer); }); it('accumulates fields from intermediate events into complete event', async () => { const client = createTestClient(); - const rawSvg = ''; + const rawSvg = + ''; const streamEvents = [ { status: 'processing', message: 'Generating SVG...' }, { @@ -576,7 +542,7 @@ describe('GenerateClient', () => { stream.on('error', reject); }); - const completeEvent = events.find((e) => e.status === 'complete'); + const completeEvent = events.find(e => e.status === 'complete'); expect(completeEvent).toBeDefined(); // Fields from 'generated' event should be accumulated into 'complete' expect(completeEvent.svgText).toBe(rawSvg); diff --git a/tests/clients/GenerationsClient.test.ts b/tests/clients/GenerationsClient.test.ts index 168c62f..d570b3f 100644 --- a/tests/clients/GenerationsClient.test.ts +++ b/tests/clients/GenerationsClient.test.ts @@ -10,11 +10,7 @@ import { createMockGenerationShareData, createMockGenerationDownloadData, } from '../setup'; -import { - ValidationError, - RateLimitError, - APIError, -} from '../../src/errors/CustomErrors'; +import { ValidationError, RateLimitError, APIError } from '../../src/errors/CustomErrors'; describe('GenerationsClient', () => { let fetchMock: jest.Mock; @@ -117,27 +113,21 @@ describe('GenerationsClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationsListData()); - await expect( - client.generations.list({ page: 0 }), - ).rejects.toThrow(ValidationError); + await expect(client.generations.list({ page: 0 })).rejects.toThrow(ValidationError); }); it('throws ValidationError when limit exceeds 100', async () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationsListData()); - await expect( - client.generations.list({ limit: 101 }), - ).rejects.toThrow(ValidationError); + await expect(client.generations.list({ limit: 101 })).rejects.toThrow(ValidationError); }); it('throws ValidationError on unknown property due to .strict()', async () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationsListData()); - await expect( - client.generations.list({ foo: 'bar' } as any), - ).rejects.toThrow(ValidationError); + await expect(client.generations.list({ foo: 'bar' } as any)).rejects.toThrow(ValidationError); }); }); @@ -195,9 +185,7 @@ describe('GenerationsClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationData()); - await expect( - client.generations.get(''), - ).rejects.toThrow(ValidationError); + await expect(client.generations.get('')).rejects.toThrow(ValidationError); }); }); @@ -239,9 +227,7 @@ describe('GenerationsClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationDeleteData()); - await expect( - client.generations.delete(''), - ).rejects.toThrow(ValidationError); + await expect(client.generations.delete('')).rejects.toThrow(ValidationError); }); }); @@ -285,9 +271,7 @@ describe('GenerationsClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationShareData()); - await expect( - client.generations.share(''), - ).rejects.toThrow(ValidationError); + await expect(client.generations.share('')).rejects.toThrow(ValidationError); }); }); @@ -346,9 +330,7 @@ describe('GenerationsClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockGenerationDownloadData()); - await expect( - client.generations.download(''), - ).rejects.toThrow(ValidationError); + await expect(client.generations.download('')).rejects.toThrow(ValidationError); }); }); @@ -361,45 +343,35 @@ describe('GenerationsClient', () => { const client = createTestClient(); mockFetchErrorResponse('INVALID_API_KEY', 401, 'Invalid API key'); - await expect( - client.generations.list(), - ).rejects.toThrow('Invalid API key'); + await expect(client.generations.list()).rejects.toThrow('Invalid API key'); }); it('throws "Not enough credits" on INSUFFICIENT_CREDITS (402)', async () => { const client = createTestClient(); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402, 'Not enough credits'); - await expect( - client.generations.list(), - ).rejects.toThrow('Not enough credits'); + await expect(client.generations.list()).rejects.toThrow('Not enough credits'); }); it('throws RateLimitError on RATE_LIMIT_EXCEEDED (429)', async () => { const client = createTestClient(); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); - await expect( - client.generations.list(), - ).rejects.toThrow(RateLimitError); + await expect(client.generations.list()).rejects.toThrow(RateLimitError); }); it('throws "This endpoint is disabled" on ENDPOINT_DISABLED (503)', async () => { const client = createTestClient(); mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); - await expect( - client.generations.list(), - ).rejects.toThrow('This endpoint is disabled'); + await expect(client.generations.list()).rejects.toThrow('This endpoint is disabled'); }); it('throws APIError on generic server error (500)', async () => { const client = createTestClient(); mockFetchErrorResponse('SERVER_ERROR', 500, 'Internal server error'); - await expect( - client.generations.list(), - ).rejects.toThrow(APIError); + await expect(client.generations.list()).rejects.toThrow(APIError); }); }); }); diff --git a/tests/clients/OptimizeSvgClient.test.ts b/tests/clients/OptimizeSvgClient.test.ts index 168fe75..de46e4f 100644 --- a/tests/clients/OptimizeSvgClient.test.ts +++ b/tests/clients/OptimizeSvgClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { createTestClient, setupFetchMock, @@ -22,7 +23,7 @@ describe('OptimizeSvgClient', () => { beforeEach(() => { fetchMock = setupFetchMock(); - const fs = require('fs'); + originalExistsSync = fs.existsSync; originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); @@ -31,7 +32,7 @@ describe('OptimizeSvgClient', () => { afterEach(() => { cleanupMocks(); - const fs = require('fs'); + fs.existsSync = originalExistsSync; fs.readFileSync = originalReadFileSync; }); @@ -81,9 +82,7 @@ describe('OptimizeSvgClient', () => { it('appends file and optional compress param to FormData', async () => { const client = createTestClient(); mockFetchJsonResponse(createMockOptimizeSvgResponse()); - await client.optimizeSvg - .configure({ file: '/test.svg', compress: true }) - .execute(); + await client.optimizeSvg.configure({ file: '/test.svg', compress: true }).execute(); const formData = fetchMock.mock.calls[0][1].body as FormData; expect(formData.get('file')).toBeTruthy(); expect(formData.get('compress')).toBe('true'); @@ -93,9 +92,7 @@ describe('OptimizeSvgClient', () => { const client = createTestClient(); const mockData = createMockOptimizeSvgResponse(); mockFetchJsonResponse(mockData); - const result = await client.optimizeSvg - .configure({ file: '/test.svg' }) - .execute(); + const result = await client.optimizeSvg.configure({ file: '/test.svg' }).execute(); expect(result.svgUrl).toBe(mockData.svgUrl); expect(result.svgUrlExpiresIn).toBe(mockData.svgUrlExpiresIn); expect(result.metadata).toBeDefined(); @@ -113,10 +110,10 @@ describe('OptimizeSvgClient', () => { it('throws ValidationError when file path does not exist', async () => { const client = createTestClient(); mockFetchJsonResponse(createMockOptimizeSvgResponse()); - const fs = require('fs'); + fs.existsSync = jest.fn().mockReturnValue(false); await expect( - client.optimizeSvg.configure({ file: '/nonexistent.svg' }).execute(), + client.optimizeSvg.configure({ file: '/nonexistent.svg' }).execute() ).rejects.toThrow(ValidationError); }); }); @@ -126,41 +123,41 @@ describe('OptimizeSvgClient', () => { it('throws AuthError on INVALID_API_KEY', async () => { const client = createTestClient(); mockFetchErrorResponse('INVALID_API_KEY', 401); - await expect( - client.optimizeSvg.configure({ file: '/test.svg' }).execute(), - ).rejects.toThrow(AuthError); + await expect(client.optimizeSvg.configure({ file: '/test.svg' }).execute()).rejects.toThrow( + AuthError + ); }); it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { const client = createTestClient(); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); - await expect( - client.optimizeSvg.configure({ file: '/test.svg' }).execute(), - ).rejects.toThrow(InsufficientCreditsError); + await expect(client.optimizeSvg.configure({ file: '/test.svg' }).execute()).rejects.toThrow( + InsufficientCreditsError + ); }); it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { const client = createTestClient(); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); - await expect( - client.optimizeSvg.configure({ file: '/test.svg' }).execute(), - ).rejects.toThrow(RateLimitError); + await expect(client.optimizeSvg.configure({ file: '/test.svg' }).execute()).rejects.toThrow( + RateLimitError + ); }); it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { const client = createTestClient(); mockFetchErrorResponse('ENDPOINT_DISABLED', 503); - await expect( - client.optimizeSvg.configure({ file: '/test.svg' }).execute(), - ).rejects.toThrow(EndpointDisabledError); + await expect(client.optimizeSvg.configure({ file: '/test.svg' }).execute()).rejects.toThrow( + EndpointDisabledError + ); }); it('throws APIError on generic server error', async () => { const client = createTestClient(); mockFetchErrorResponse('INTERNAL_ERROR', 500); - await expect( - client.optimizeSvg.configure({ file: '/test.svg' }).execute(), - ).rejects.toThrow(APIError); + await expect(client.optimizeSvg.configure({ file: '/test.svg' }).execute()).rejects.toThrow( + APIError + ); }); }); }); diff --git a/tests/clients/convert/AIVectorizeClient.test.ts b/tests/clients/convert/AIVectorizeClient.test.ts index 65af577..351ceaf 100644 --- a/tests/clients/convert/AIVectorizeClient.test.ts +++ b/tests/clients/convert/AIVectorizeClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { Readable } from 'stream'; import { createTestClient, @@ -68,9 +69,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockAiVectorizeResponse()); - await client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(); + await client.convert.aiVectorize.configure({ file: testFileBuffer }).execute(); const [url, options] = fetchMock.mock.calls[0]; expect(url).toContain('/v1/convert/ai-vectorize'); @@ -82,9 +81,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockAiVectorizeResponse()); - await client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(); + await client.convert.aiVectorize.configure({ file: testFileBuffer }).execute(); const [, options] = fetchMock.mock.calls[0]; expect(options.headers['x-api-key']).toBe('test-api-key-123'); @@ -94,9 +91,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockAiVectorizeResponse()); - await client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(); + await client.convert.aiVectorize.configure({ file: testFileBuffer }).execute(); const formData = fetchMock.mock.calls[0][1].body as FormData; expect(formData.get('file')).toBeTruthy(); @@ -120,9 +115,7 @@ describe('AIVectorizeClient', () => { const mockData = createMockAiVectorizeResponse(); mockFetchJsonResponse(mockData); - const result = await client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(); + const result = await client.convert.aiVectorize.configure({ file: testFileBuffer }).execute(); expect(result.svgUrl).toBe(mockData.svgUrl); expect(result.creditCost).toBe(mockData.creditCost); @@ -148,16 +141,13 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockAiVectorizeResponse()); - const fs = require('fs'); const originalExistsSync = fs.existsSync; const originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); fs.readFileSync = jest.fn().mockReturnValue(Buffer.from('fake-png-data')); try { - await client.convert.aiVectorize - .configure({ file: '/path/to/image.png' }) - .execute(); + await client.convert.aiVectorize.configure({ file: '/path/to/image.png' }).execute(); expect(fs.existsSync).toHaveBeenCalledWith('/path/to/image.png'); expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/image.png'); @@ -178,9 +168,7 @@ describe('AIVectorizeClient', () => { readable.push(Buffer.from('fake-stream-data')); readable.push(null); - await client.convert.aiVectorize - .configure({ file: readable }) - .execute(); + await client.convert.aiVectorize.configure({ file: readable }).execute(); expect(fetchMock).toHaveBeenCalled(); const formData = fetchMock.mock.calls[0][1].body as FormData; @@ -196,18 +184,14 @@ describe('AIVectorizeClient', () => { it('throws ValidationError when file is missing', async () => { const client = createTestClient(); - await expect( - client.convert.aiVectorize.execute(), - ).rejects.toThrow(ValidationError); + await expect(client.convert.aiVectorize.execute()).rejects.toThrow(ValidationError); }); it('throws ValidationError when file path does not exist', async () => { const client = createTestClient(); await expect( - client.convert.aiVectorize - .configure({ file: '/nonexistent/path/image.png' }) - .execute(), + client.convert.aiVectorize.configure({ file: '/nonexistent/path/image.png' }).execute() ).rejects.toThrow(ValidationError); }); }); @@ -222,9 +206,7 @@ describe('AIVectorizeClient', () => { mockFetchErrorResponse('INVALID_API_KEY', 401); await expect( - client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(), + client.convert.aiVectorize.configure({ file: testFileBuffer }).execute() ).rejects.toThrow(AuthError); }); @@ -233,9 +215,7 @@ describe('AIVectorizeClient', () => { mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); await expect( - client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(), + client.convert.aiVectorize.configure({ file: testFileBuffer }).execute() ).rejects.toThrow(InsufficientCreditsError); }); @@ -244,9 +224,7 @@ describe('AIVectorizeClient', () => { mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); await expect( - client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(), + client.convert.aiVectorize.configure({ file: testFileBuffer }).execute() ).rejects.toThrow(RateLimitError); }); @@ -255,9 +233,7 @@ describe('AIVectorizeClient', () => { mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); await expect( - client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(), + client.convert.aiVectorize.configure({ file: testFileBuffer }).execute() ).rejects.toThrow(EndpointDisabledError); }); @@ -266,9 +242,7 @@ describe('AIVectorizeClient', () => { mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); await expect( - client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .execute(), + client.convert.aiVectorize.configure({ file: testFileBuffer }).execute() ).rejects.toThrow(APIError); }); }); @@ -282,9 +256,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); expect(stream).toBeInstanceOf(Readable); }); @@ -293,9 +265,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); const events: any[] = []; await new Promise((resolve, reject) => { @@ -304,7 +274,7 @@ describe('AIVectorizeClient', () => { stream.on('error', reject); }); - const processingEvent = events.find((e) => e.status === 'processing'); + const processingEvent = events.find(e => e.status === 'processing'); expect(processingEvent).toBeDefined(); expect(processingEvent.message).toBe('Vectorizing image...'); }); @@ -313,9 +283,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); const events: any[] = []; await new Promise((resolve, reject) => { @@ -324,7 +292,7 @@ describe('AIVectorizeClient', () => { stream.on('error', reject); }); - const completeEvent = events.find((e) => e.status === 'complete'); + const completeEvent = events.find(e => e.status === 'complete'); expect(completeEvent).toBeDefined(); expect(completeEvent.svgUrl).toBeDefined(); expect(completeEvent.creditCost).toBeDefined(); @@ -335,9 +303,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); await new Promise((resolve, reject) => { stream.on('data', () => {}); @@ -353,9 +319,7 @@ describe('AIVectorizeClient', () => { const client = createTestClient(); mockFetchStreamResponse(createMockAiVectorizeStreamEvents()); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); await new Promise((resolve, reject) => { stream.on('data', () => {}); @@ -390,9 +354,7 @@ describe('AIVectorizeClient', () => { ]; mockFetchStreamResponse(streamEvents); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); const events: any[] = []; await new Promise((resolve, reject) => { @@ -401,7 +363,7 @@ describe('AIVectorizeClient', () => { stream.on('error', reject); }); - const generatedEvent = events.find((e) => e.status === 'generated'); + const generatedEvent = events.find(e => e.status === 'generated'); expect(generatedEvent).toBeDefined(); expect(generatedEvent.svgText).toBe(rawSvg); }); @@ -425,16 +387,14 @@ describe('AIVectorizeClient', () => { text: async () => '{"success":false}', }); - const stream = client.convert.aiVectorize - .configure({ file: testFileBuffer }) - .stream(); + const stream = client.convert.aiVectorize.configure({ file: testFileBuffer }).stream(); await expect( new Promise((resolve, reject) => { stream.on('data', () => {}); stream.on('end', resolve); stream.on('error', reject); - }), + }) ).rejects.toBeDefined(); }); }); diff --git a/tests/clients/convert/BatchConvertClient.test.ts b/tests/clients/convert/BatchConvertClient.test.ts index 0e8c0ff..a582c65 100644 --- a/tests/clients/convert/BatchConvertClient.test.ts +++ b/tests/clients/convert/BatchConvertClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { createTestClient, setupFetchMock, @@ -22,7 +23,7 @@ describe('BatchConvertClient', () => { beforeEach(() => { fetchMock = setupFetchMock(); - const fs = require('fs'); + originalExistsSync = fs.existsSync; originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); @@ -31,7 +32,7 @@ describe('BatchConvertClient', () => { afterEach(() => { cleanupMocks(); - const fs = require('fs'); + fs.existsSync = originalExistsSync; fs.readFileSync = originalReadFileSync; }); @@ -75,9 +76,7 @@ describe('BatchConvertClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }) - .execute(); + await client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }).execute(); expect(fetchMock).toHaveBeenCalled(); const [url, options] = fetchMock.mock.calls[0]; @@ -90,9 +89,7 @@ describe('BatchConvertClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }) - .execute(); + await client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }).execute(); const [, options] = fetchMock.mock.calls[0]; expect(options.headers['x-api-key']).toBe('test-api-key-123'); @@ -142,12 +139,24 @@ describe('BatchConvertClient', () => { mockFetchJsonResponse( createMockConvertResultsResponse({ results: [ - { filename: 'a.png', success: true, url: 'https://x.com/a.svg', urlExpiresIn: '24h', format: 'svg' }, - { filename: 'b.jpg', success: true, url: 'https://x.com/b.svg', urlExpiresIn: '24h', format: 'svg' }, + { + filename: 'a.png', + success: true, + url: 'https://x.com/a.svg', + urlExpiresIn: '24h', + format: 'svg', + }, + { + filename: 'b.jpg', + success: true, + url: 'https://x.com/b.svg', + urlExpiresIn: '24h', + format: 'svg', + }, { filename: 'c.png', success: false, error: 'Processing failed' }, ], summary: { total: 3, successful: 2, failed: 1 }, - }), + }) ); const result = await client.convert.batch @@ -171,9 +180,7 @@ describe('BatchConvertClient', () => { const client = createTestClient(); await expect( - client.convert.batch - .configure({ toFormat: 'svg' } as any) - .execute(), + client.convert.batch.configure({ toFormat: 'svg' } as any).execute() ).rejects.toThrow(ValidationError); }); @@ -181,9 +188,7 @@ describe('BatchConvertClient', () => { const client = createTestClient(); await expect( - client.convert.batch - .configure({ files: [], toFormat: 'svg' }) - .execute(), + client.convert.batch.configure({ files: [], toFormat: 'svg' }).execute() ).rejects.toThrow(ValidationError); }); @@ -193,7 +198,7 @@ describe('BatchConvertClient', () => { await expect( client.convert.batch .configure({ files: Array(11).fill('/img.png'), toFormat: 'svg' }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -201,9 +206,7 @@ describe('BatchConvertClient', () => { const client = createTestClient(); await expect( - client.convert.batch - .configure({ files: ['/img.png'] }) - .execute(), + client.convert.batch.configure({ files: ['/img.png'] }).execute() ).rejects.toThrow(ValidationError); }); @@ -233,8 +236,7 @@ describe('BatchConvertClient', () => { describe('error handling', () => { it('throws AuthError on INVALID_API_KEY', async () => { const client = createTestClient(); - const configured = client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }); + const configured = client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }); mockFetchErrorResponse('INVALID_API_KEY', 401); await expect(configured.execute()).rejects.toThrow(AuthError); @@ -242,8 +244,7 @@ describe('BatchConvertClient', () => { it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { const client = createTestClient(); - const configured = client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }); + const configured = client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); await expect(configured.execute()).rejects.toThrow(InsufficientCreditsError); @@ -251,8 +252,7 @@ describe('BatchConvertClient', () => { it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { const client = createTestClient(); - const configured = client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }); + const configured = client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); await expect(configured.execute()).rejects.toThrow(RateLimitError); @@ -260,8 +260,7 @@ describe('BatchConvertClient', () => { it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { const client = createTestClient(); - const configured = client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }); + const configured = client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }); mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); await expect(configured.execute()).rejects.toThrow(EndpointDisabledError); @@ -269,8 +268,7 @@ describe('BatchConvertClient', () => { it('throws APIError on generic server error', async () => { const client = createTestClient(); - const configured = client.convert.batch - .configure({ files: ['/img.png'], toFormat: 'svg' }); + const configured = client.convert.batch.configure({ files: ['/img.png'], toFormat: 'svg' }); mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); await expect(configured.execute()).rejects.toThrow(APIError); diff --git a/tests/clients/convert/RasterToRasterClient.test.ts b/tests/clients/convert/RasterToRasterClient.test.ts index c3d10f8..41e3f6f 100644 --- a/tests/clients/convert/RasterToRasterClient.test.ts +++ b/tests/clients/convert/RasterToRasterClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { createTestClient, setupFetchMock, @@ -22,7 +23,7 @@ describe('RasterToRasterClient', () => { beforeEach(() => { fetchMock = setupFetchMock(); - const fs = require('fs'); + originalExistsSync = fs.existsSync; originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); @@ -31,7 +32,7 @@ describe('RasterToRasterClient', () => { afterEach(() => { cleanupMocks(); - const fs = require('fs'); + fs.existsSync = originalExistsSync; fs.readFileSync = originalReadFileSync; }); @@ -146,9 +147,7 @@ describe('RasterToRasterClient', () => { const client = createTestClient(); await expect( - client.convert.rasterToRaster - .configure({ toFormat: 'PNG' } as any) - .execute(), + client.convert.rasterToRaster.configure({ toFormat: 'PNG' } as any).execute() ).rejects.toThrow(ValidationError); }); @@ -156,9 +155,7 @@ describe('RasterToRasterClient', () => { const client = createTestClient(); await expect( - client.convert.rasterToRaster - .configure({ file: '/img.png' }) - .execute(), + client.convert.rasterToRaster.configure({ file: '/img.png' }).execute() ).rejects.toThrow(ValidationError); }); @@ -168,7 +165,7 @@ describe('RasterToRasterClient', () => { await expect( client.convert.rasterToRaster .configure({ file: '/img.png', toFormat: 'SVG' as any }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -178,7 +175,7 @@ describe('RasterToRasterClient', () => { await expect( client.convert.rasterToRaster .configure({ file: '/img.png', toFormat: 'PNG', quality: 0 }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -188,7 +185,7 @@ describe('RasterToRasterClient', () => { await expect( client.convert.rasterToRaster .configure({ file: '/img.png', toFormat: 'PNG', quality: 101 }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -198,7 +195,7 @@ describe('RasterToRasterClient', () => { await expect( client.convert.rasterToRaster .configure({ file: '/img.png', toFormat: 'PNG', width: 0 }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -208,7 +205,7 @@ describe('RasterToRasterClient', () => { await expect( client.convert.rasterToRaster .configure({ file: '/img.png', toFormat: 'PNG', height: -10 }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); @@ -232,8 +229,10 @@ describe('RasterToRasterClient', () => { describe('error handling', () => { it('throws AuthError on INVALID_API_KEY', async () => { const client = createTestClient(); - const configured = client.convert.rasterToRaster - .configure({ file: '/img.png', toFormat: 'PNG' }); + const configured = client.convert.rasterToRaster.configure({ + file: '/img.png', + toFormat: 'PNG', + }); mockFetchErrorResponse('INVALID_API_KEY', 401); await expect(configured.execute()).rejects.toThrow(AuthError); @@ -241,8 +240,10 @@ describe('RasterToRasterClient', () => { it('throws InsufficientCreditsError on INSUFFICIENT_CREDITS', async () => { const client = createTestClient(); - const configured = client.convert.rasterToRaster - .configure({ file: '/img.png', toFormat: 'PNG' }); + const configured = client.convert.rasterToRaster.configure({ + file: '/img.png', + toFormat: 'PNG', + }); mockFetchErrorResponse('INSUFFICIENT_CREDITS', 402); await expect(configured.execute()).rejects.toThrow(InsufficientCreditsError); @@ -250,8 +251,10 @@ describe('RasterToRasterClient', () => { it('throws RateLimitError on RATE_LIMIT_EXCEEDED', async () => { const client = createTestClient(); - const configured = client.convert.rasterToRaster - .configure({ file: '/img.png', toFormat: 'PNG' }); + const configured = client.convert.rasterToRaster.configure({ + file: '/img.png', + toFormat: 'PNG', + }); mockFetchErrorResponse('RATE_LIMIT_EXCEEDED', 429); await expect(configured.execute()).rejects.toThrow(RateLimitError); @@ -259,8 +262,10 @@ describe('RasterToRasterClient', () => { it('throws EndpointDisabledError on ENDPOINT_DISABLED', async () => { const client = createTestClient(); - const configured = client.convert.rasterToRaster - .configure({ file: '/img.png', toFormat: 'PNG' }); + const configured = client.convert.rasterToRaster.configure({ + file: '/img.png', + toFormat: 'PNG', + }); mockFetchErrorResponse('ENDPOINT_DISABLED', 503, 'This endpoint is disabled'); await expect(configured.execute()).rejects.toThrow(EndpointDisabledError); @@ -268,8 +273,10 @@ describe('RasterToRasterClient', () => { it('throws APIError on generic server error', async () => { const client = createTestClient(); - const configured = client.convert.rasterToRaster - .configure({ file: '/img.png', toFormat: 'PNG' }); + const configured = client.convert.rasterToRaster.configure({ + file: '/img.png', + toFormat: 'PNG', + }); mockFetchErrorResponse('INTERNAL_ERROR', 500, 'Something went wrong'); await expect(configured.execute()).rejects.toThrow(APIError); diff --git a/tests/clients/convert/SvgToVectorClient.test.ts b/tests/clients/convert/SvgToVectorClient.test.ts index 1f0b898..8b4f7dc 100644 --- a/tests/clients/convert/SvgToVectorClient.test.ts +++ b/tests/clients/convert/SvgToVectorClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { createTestClient, setupFetchMock, @@ -22,7 +23,7 @@ describe('SvgToVectorClient', () => { beforeEach(() => { fetchMock = setupFetchMock(); - const fs = require('fs'); + originalExistsSync = fs.existsSync; originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); @@ -31,7 +32,7 @@ describe('SvgToVectorClient', () => { afterEach(() => { cleanupMocks(); - const fs = require('fs'); + fs.existsSync = originalExistsSync; fs.readFileSync = originalReadFileSync; }); @@ -75,9 +76,7 @@ describe('SvgToVectorClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await client.convert.svgToVector - .configure({ file: '/img.svg', toFormat: 'PDF' }) - .execute(); + await client.convert.svgToVector.configure({ file: '/img.svg', toFormat: 'PDF' }).execute(); expect(fetchMock).toHaveBeenCalled(); const [url, options] = fetchMock.mock.calls[0]; @@ -90,9 +89,7 @@ describe('SvgToVectorClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await client.convert.svgToVector - .configure({ file: '/img.svg', toFormat: 'PDF' }) - .execute(); + await client.convert.svgToVector.configure({ file: '/img.svg', toFormat: 'PDF' }).execute(); const [, options] = fetchMock.mock.calls[0]; expect(options.headers['x-api-key']).toBe('test-api-key-123'); @@ -144,9 +141,7 @@ describe('SvgToVectorClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.svgToVector - .configure({ toFormat: 'PDF' } as any) - .execute(), + client.convert.svgToVector.configure({ toFormat: 'PDF' } as any).execute() ).rejects.toThrow(ValidationError); }); @@ -155,9 +150,7 @@ describe('SvgToVectorClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.svgToVector - .configure({ file: '/img.svg' }) - .execute(), + client.convert.svgToVector.configure({ file: '/img.svg' }).execute() ).rejects.toThrow(ValidationError); }); @@ -166,9 +159,7 @@ describe('SvgToVectorClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.svgToVector - .configure({ file: '/img.svg', toFormat: 'BMP' as any }) - .execute(), + client.convert.svgToVector.configure({ file: '/img.svg', toFormat: 'BMP' as any }).execute() ).rejects.toThrow(ValidationError); }); @@ -179,7 +170,7 @@ describe('SvgToVectorClient', () => { await expect( client.convert.svgToVector .configure({ file: '/img.svg', toFormat: 'DXF', dxfVersion: 'R15' as any }) - .execute(), + .execute() ).rejects.toThrow(ValidationError); }); diff --git a/tests/clients/convert/TraceClient.test.ts b/tests/clients/convert/TraceClient.test.ts index 0f772ca..b66d66b 100644 --- a/tests/clients/convert/TraceClient.test.ts +++ b/tests/clients/convert/TraceClient.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { createTestClient, setupFetchMock, @@ -22,7 +23,7 @@ describe('TraceClient', () => { beforeEach(() => { fetchMock = setupFetchMock(); - const fs = require('fs'); + originalExistsSync = fs.existsSync; originalReadFileSync = fs.readFileSync; fs.existsSync = jest.fn().mockReturnValue(true); @@ -31,7 +32,7 @@ describe('TraceClient', () => { afterEach(() => { cleanupMocks(); - const fs = require('fs'); + fs.existsSync = originalExistsSync; fs.readFileSync = originalReadFileSync; }); @@ -74,9 +75,7 @@ describe('TraceClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await client.convert.trace - .configure({ file: '/test.png' }) - .execute(); + await client.convert.trace.configure({ file: '/test.png' }).execute(); expect(fetchMock).toHaveBeenCalled(); const [url, options] = fetchMock.mock.calls[0]; @@ -89,9 +88,7 @@ describe('TraceClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await client.convert.trace - .configure({ file: '/test.png' }) - .execute(); + await client.convert.trace.configure({ file: '/test.png' }).execute(); const [, options] = fetchMock.mock.calls[0]; expect(options.headers['x-api-key']).toBe('test-api-key-123'); @@ -132,9 +129,7 @@ describe('TraceClient', () => { const mockData = createMockConvertResultsResponse(); mockFetchJsonResponse(mockData); - const result = await client.convert.trace - .configure({ file: '/test.png' }) - .execute(); + const result = await client.convert.trace.configure({ file: '/test.png' }).execute(); expect(Array.isArray(result.results)).toBe(true); expect(result.results[0].filename).toBe('test-image.png'); @@ -152,9 +147,7 @@ describe('TraceClient', () => { const client = createTestClient(); mockFetchJsonResponse(createMockConvertResultsResponse()); - await expect( - client.convert.trace.execute(), - ).rejects.toThrow(ValidationError); + await expect(client.convert.trace.execute()).rejects.toThrow(ValidationError); }); it('throws ValidationError for invalid preset value', async () => { @@ -162,9 +155,7 @@ describe('TraceClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.trace - .configure({ file: '/img.png', preset: 'invalid' as any }) - .execute(), + client.convert.trace.configure({ file: '/img.png', preset: 'invalid' as any }).execute() ).rejects.toThrow(ValidationError); }); @@ -173,9 +164,7 @@ describe('TraceClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.trace - .configure({ file: '/img.png', mode: 'invalid' as any }) - .execute(), + client.convert.trace.configure({ file: '/img.png', mode: 'invalid' as any }).execute() ).rejects.toThrow(ValidationError); }); @@ -184,9 +173,7 @@ describe('TraceClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.trace - .configure({ file: '/img.png', detail: -1 }) - .execute(), + client.convert.trace.configure({ file: '/img.png', detail: -1 }).execute() ).rejects.toThrow(ValidationError); }); @@ -195,9 +182,7 @@ describe('TraceClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.trace - .configure({ file: '/img.png', detail: 101 }) - .execute(), + client.convert.trace.configure({ file: '/img.png', detail: 101 }).execute() ).rejects.toThrow(ValidationError); }); @@ -206,9 +191,7 @@ describe('TraceClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.trace - .configure({ file: '/img.png', smoothness: 101 }) - .execute(), + client.convert.trace.configure({ file: '/img.png', smoothness: 101 }).execute() ).rejects.toThrow(ValidationError); }); @@ -217,9 +200,7 @@ describe('TraceClient', () => { mockFetchJsonResponse(createMockConvertResultsResponse()); await expect( - client.convert.trace - .configure({ file: '/img.png', corners: -1 }) - .execute(), + client.convert.trace.configure({ file: '/img.png', corners: -1 }).execute() ).rejects.toThrow(ValidationError); }); diff --git a/tests/setup/mock-factories.ts b/tests/setup/mock-factories.ts index 0cbe4dd..6b6231d 100644 --- a/tests/setup/mock-factories.ts +++ b/tests/setup/mock-factories.ts @@ -7,9 +7,7 @@ import { ResponseMetadata } from '../../src/types/api'; /** * Create mock response metadata */ -export function createMockMetadata( - overrides?: Partial, -): ResponseMetadata { +export function createMockMetadata(overrides?: Partial): ResponseMetadata { return { requestId: 'test-req-id', creditsUsed: 1, @@ -21,9 +19,7 @@ export function createMockMetadata( /** * Create mock generate response data (v1 envelope data field) */ -export function createMockGenerateResponse( - overrides?: Record, -): Record { +export function createMockGenerateResponse(overrides?: Record): Record { return { svgUrl: 'https://storage.example.com/test.svg', creditCost: 1, @@ -38,9 +34,7 @@ export function createMockGenerateResponse( /** * Create mock edit response data (v1 envelope data field) */ -export function createMockEditResponse( - overrides?: Record, -): Record { +export function createMockEditResponse(overrides?: Record): Record { return { svgUrl: 'https://storage.example.com/test-edit.svg', creditCost: 2, @@ -83,7 +77,7 @@ export function createMockStreamEvents(): Record[] { * Create mock AI vectorize response data (v1 envelope data field) */ export function createMockAiVectorizeResponse( - overrides?: Record, + overrides?: Record ): Record { return { svgUrl: 'https://storage.example.com/vectorized.svg', @@ -99,7 +93,7 @@ export function createMockAiVectorizeResponse( * Create mock convert results response data (v1 envelope data field) */ export function createMockConvertResultsResponse( - overrides?: Record, + overrides?: Record ): Record { return { results: [ @@ -151,7 +145,7 @@ export function createMockAiVectorizeStreamEvents(): Record[] { * Create mock optimize SVG response data */ export function createMockOptimizeSvgResponse( - overrides?: Record, + overrides?: Record ): Record { return { svgUrl: 'https://storage.example.com/optimized.svg', @@ -164,7 +158,7 @@ export function createMockOptimizeSvgResponse( * Create mock enhance prompt response data */ export function createMockEnhancePromptResponse( - overrides?: Record, + overrides?: Record ): Record { return { enhancedPrompt: 'A beautifully detailed mountain landscape with snow-capped peaks', @@ -176,7 +170,7 @@ export function createMockEnhancePromptResponse( * Create mock generations list response data (v1 envelope data field) */ export function createMockGenerationsListData( - overrides?: Record, + overrides?: Record ): Record { return { items: ['gen-001', 'gen-002'], @@ -195,9 +189,7 @@ export function createMockGenerationsListData( /** * Create mock single generation response data (v1 envelope data field) */ -export function createMockGenerationData( - overrides?: Record, -): Record { +export function createMockGenerationData(overrides?: Record): Record { return { id: 'gen-001', prompt: 'A mountain landscape', @@ -216,7 +208,7 @@ export function createMockGenerationData( * Create mock generation delete response data (v1 envelope data field) */ export function createMockGenerationDeleteData( - overrides?: Record, + overrides?: Record ): Record { return { message: 'Generation deleted successfully', @@ -228,7 +220,7 @@ export function createMockGenerationDeleteData( * Create mock generation share response data (v1 envelope data field) */ export function createMockGenerationShareData( - overrides?: Record, + overrides?: Record ): Record { return { message: 'Generation shared', @@ -242,7 +234,7 @@ export function createMockGenerationShareData( * Create mock generation download response data (v1 envelope data field) */ export function createMockGenerationDownloadData( - overrides?: Record, + overrides?: Record ): Record { return { id: 'gen-001', @@ -257,9 +249,7 @@ export function createMockGenerationDownloadData( /** * Create mock account info response data (v1 envelope data field) */ -export function createMockAccountInfoData( - overrides?: Record, -): Record { +export function createMockAccountInfoData(overrides?: Record): Record { return { email: 'user@example.com', displayName: 'Test User', @@ -272,9 +262,7 @@ export function createMockAccountInfoData( /** * Create mock account usage response data (v1 envelope data field) */ -export function createMockAccountUsageData( - overrides?: Record, -): Record { +export function createMockAccountUsageData(overrides?: Record): Record { return { period: { type: 'all' }, summary: { diff --git a/tests/setup/test-setup.ts b/tests/setup/test-setup.ts index 05e6f62..78b9bd4 100644 --- a/tests/setup/test-setup.ts +++ b/tests/setup/test-setup.ts @@ -68,7 +68,7 @@ export function cleanupMocks(): void { */ export function mockFetchJsonResponse( data: Record, - metadata?: Record, + metadata?: Record ): void { const responseBody = { success: true, @@ -92,11 +92,7 @@ export function mockFetchJsonResponse( /** * Configure the global fetch mock to return an error response */ -export function mockFetchErrorResponse( - errorCode: string, - status: number, - message?: string, -): void { +export function mockFetchErrorResponse(errorCode: string, status: number, message?: string): void { const responseBody = { success: false, error: { From 62cf6ef4844f6935bb67ff4d9aa0ff28087f5ffe Mon Sep 17 00:00:00 2001 From: razorback16 Date: Thu, 19 Feb 2026 18:48:28 -0800 Subject: [PATCH 12/15] refactor: migrate integration tests from bash to Node.js with streaming test suite - Rewrite integration tests as Node.js (run-integration-tests.js) with dynamic generation ID capture, removing need for manual ID configuration - Add streaming test suite (run-streaming-tests.js) covering generate, edit, and AI vectorize with real-time event logging and asset downloads - Extract shared config (API keys, IDs) into tests/test-config.js with example template (test-config.example.js) - Add tests/test-config.js and tests/assets/ to .gitignore - Remove old bash test runner and unused test.svg --- .gitignore | 6 + tests/run-all-tests.sh | 29 ++ tests/run-integration-tests.js | 730 +++++++++++++++++++++++++++++++++ tests/run-integration-tests.sh | 539 ------------------------ tests/run-streaming-tests.js | 317 ++++++++++++++ tests/test-config.example.js | 18 + tests/test-images/test.svg | 3 - 7 files changed, 1100 insertions(+), 542 deletions(-) create mode 100755 tests/run-all-tests.sh create mode 100644 tests/run-integration-tests.js delete mode 100644 tests/run-integration-tests.sh create mode 100644 tests/run-streaming-tests.js create mode 100644 tests/test-config.example.js delete mode 100644 tests/test-images/test.svg diff --git a/.gitignore b/.gitignore index c9e1dcc..cd5443a 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,12 @@ test-generated-*.svg test-*.png test-*.svg +# Test config (contains API keys) +tests/test-config.js + +# Test output assets +tests/assets/ + # Temporary files tmp/ temp/ \ No newline at end of file diff --git a/tests/run-all-tests.sh b/tests/run-all-tests.sh new file mode 100755 index 0000000..142cf57 --- /dev/null +++ b/tests/run-all-tests.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# ============================================================================= +# SVGMaker SDK — Integration Test Runner (thin wrapper) +# Builds the SDK then runs the Node.js integration test suite. +# ============================================================================= + +set -euo pipefail + +# Ensure we run from the project root (parent of this script's directory) +cd "$(dirname "$0")/.." + +BOLD='\033[1m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +# ─── Pre-flight build ─────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Running npm run build...${NC}" +if ! npm run build; then + echo -e "${RED}Build failed — aborting tests.${NC}" + exit 1 +fi +echo -e "${GREEN}Build succeeded.${NC}" +echo "" + +# ─── Run tests ────────────────────────────────────────────────── +node tests/run-integration-tests.js +node tests/run-streaming-tests.js diff --git a/tests/run-integration-tests.js b/tests/run-integration-tests.js new file mode 100644 index 0000000..c241a85 --- /dev/null +++ b/tests/run-integration-tests.js @@ -0,0 +1,730 @@ +// ============================================================================= +// SVGMaker SDK — Integration Test Suite (Node.js) +// ============================================================================= + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const http = require('http'); +const config = require('./test-config'); + +// ─── Configuration ─────────────────────────────────────────────────────────── +const { API_KEY, BASE_URL, DELAY, TIMEOUT, ASSETS_DIR } = config; +const { GALLERY_ID, PRO_GALLERY_ID } = config; + +// ─── Dynamic IDs (populated by generate/edit tests with storage: true) ────── +let GENERATION_ID = null; +let DELETE_GENERATION_ID = null; + +// ─── Colors ────────────────────────────────────────────────────────────────── +const GREEN = '\x1b[0;32m'; +const RED = '\x1b[0;31m'; +const CYAN = '\x1b[0;36m'; +const BOLD = '\x1b[1m'; +const NC = '\x1b[0m'; + +// ─── Counters ──────────────────────────────────────────────────────────────── +let PASSED = 0; +let FAILED = 0; +let TOTAL = 0; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function downloadFile(url, filepath) { + return new Promise((resolve, reject) => { + const get = url.startsWith('https') ? https.get : http.get; + get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + downloadFile(res.headers.location, filepath).then(resolve).catch(reject); + return; + } + const chunks = []; + res.on('data', c => chunks.push(c)); + res.on('end', () => { + fs.writeFileSync(filepath, Buffer.concat(chunks)); + resolve(); + }); + res.on('error', reject); + }).on('error', reject); + }); +} + +let _capturedLogs = null; + +function logInfo(label, value) { + const line = typeof value === 'object' && value !== null + ? ` ${label}: ${JSON.stringify(value)}` + : ` ${label}: ${value}`; + if (_capturedLogs) { + _capturedLogs.push(line); + } else { + console.log(line); + } +} + +function captureStart() { + const logs = []; + _capturedLogs = logs; + return logs; +} + +function captureStop() { + _capturedLogs = null; +} + +function replayLogs(logs) { + for (const line of logs) { + console.log(line); + } +} + +async function tryDownload(url, filepath) { + if (!url) return; + try { + await downloadFile(url, filepath); + logInfo('Downloaded', filepath); + } catch (e) { + logInfo('Download failed (non-fatal)', e.message); + } +} + +function sectionHeader(title) { + console.log(''); + console.log(`${BOLD}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${NC}`); + console.log(`${BOLD} ${title}${NC}`); + console.log(`${BOLD}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${NC}`); +} + +async function runTest(num, name, fn) { + TOTAL++; + const padNum = String(num).padStart(2, '0'); + console.log(`${CYAN}[Test ${padNum}]${NC} ${name}...`); + const logs = captureStart(); + try { + await fn(); + captureStop(); + console.log(` ${GREEN}PASS${NC}`); + PASSED++; + } catch (e) { + captureStop(); + console.log(` ${RED}FAIL${NC}`); + console.log(` Error: ${e.message}`); + FAILED++; + } + replayLogs(logs); + await sleep(DELAY); +} + +async function runExpectedFail(num, name, fn) { + TOTAL++; + const padNum = String(num).padStart(2, '0'); + console.log(`${CYAN}[Test ${padNum}]${NC} ${name}...`); + try { + await fn(); + console.log(` ${RED}FAIL${NC} (expected error but succeeded)`); + FAILED++; + } catch (e) { + console.log(` ${GREEN}PASS (expected error)${NC}`); + logInfo('Error', e.message); + PASSED++; + } + await sleep(DELAY); +} + +async function runStreamTest(num, name, fn) { + TOTAL++; + const padNum = String(num).padStart(2, '0'); + console.log(`${CYAN}[Test ${padNum}]${NC} ${name} ${CYAN}(stream)${NC}...`); + const logs = captureStart(); + try { + await fn(); + captureStop(); + console.log(` ${GREEN}PASS${NC}`); + PASSED++; + } catch (e) { + captureStop(); + console.log(` ${RED}FAIL${NC}`); + console.log(` Error: ${e.message}`); + FAILED++; + } + replayLogs(logs); + await sleep(DELAY); +} + +// ─── Create client ─────────────────────────────────────────────────────────── + +function createClient() { + const { SVGMakerClient } = require('../dist/cjs/index.js'); + return new SVGMakerClient(API_KEY, { baseUrl: BASE_URL, timeout: TIMEOUT }); +} + +// ============================================================================= +// MAIN +// ============================================================================= + +async function main() { + // ─── Pre-flight ID check ───────────────────────────────────────────────── + if (GALLERY_ID === 'REPLACE_ME' || PRO_GALLERY_ID === 'REPLACE_ME') { + console.log(`${RED}ERROR: Please fill in GALLERY_ID and PRO_GALLERY_ID in tests/test-config.js before running.${NC}`); + process.exit(1); + } + + // ─── Prepare assets directory ──────────────────────────────────────────── + console.log(`${BOLD}Preparing assets output directory...${NC}`); + if (fs.existsSync(ASSETS_DIR)) { + fs.rmSync(ASSETS_DIR, { recursive: true, force: true }); + } + fs.mkdirSync(ASSETS_DIR, { recursive: true }); + console.log(`${GREEN} Assets directory ready: ${ASSETS_DIR}${NC}`); + + // ========================================================================= + // GENERATE TESTS + // ========================================================================= + sectionHeader('GENERATE TESTS (limit: 3/60s)'); + + await runTest(1, 'Generate: with quality (low) + storage', async () => { + const client = createClient(); + const r = await client.generate.configure({ prompt: 'A minimalist mountain landscape', quality: 'low', storage: true }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('Credit Cost', r.creditCost); + logInfo('Message', r.message); + logInfo('Quality', r.quality); + logInfo('Generation ID', r.generationId); + logInfo('Metadata', r.metadata); + if (!r.generationId) throw new Error('Missing generationId — storage may not have worked'); + GENERATION_ID = r.generationId; + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-01-generate-low.svg')); + }); + + await runTest(2, 'Generate: with model (gpt-image-1-mini)', async () => { + const client = createClient(); + const r = await client.generate.configure({ prompt: 'A red sports car', model: 'gpt-image-1-mini' }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('Credit Cost', r.creditCost); + logInfo('Message', r.message); + logInfo('Quality', r.quality); + logInfo('Generation ID', r.generationId); + logInfo('Metadata', r.metadata); + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-02-generate-model.svg')); + }); + + await runExpectedFail(3, 'Generate: model+quality together', async () => { + const client = createClient(); + await client.generate.configure({ prompt: 'A cat on a fence', quality: 'medium', model: 'gpt-image-1-mini' }).execute(); + }); + + // ========================================================================= + // EDIT TESTS + // ========================================================================= + sectionHeader('EDIT TESTS (limit: 3/60s)'); + + await runTest(4, 'Edit: with quality (low) + storage', async () => { + const client = createClient(); + const r = await client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low', storage: true }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('Credit Cost', r.creditCost); + logInfo('Message', r.message); + logInfo('Quality', r.quality); + logInfo('Generation ID', r.generationId); + logInfo('Metadata', r.metadata); + if (!r.generationId) throw new Error('Missing generationId — storage may not have worked'); + DELETE_GENERATION_ID = r.generationId; + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-04-edit-low.svg')); + }); + + await runTest(5, 'Edit: with model (gpt-image-1-mini)', async () => { + const client = createClient(); + const r = await client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('Credit Cost', r.creditCost); + logInfo('Message', r.message); + logInfo('Quality', r.quality); + logInfo('Generation ID', r.generationId); + logInfo('Metadata', r.metadata); + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-05-edit-model.svg')); + }); + + await runExpectedFail(6, 'Edit: model+quality together', async () => { + const client = createClient(); + await client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'medium', model: 'gpt-image-1-mini' }).execute(); + }); + + // ========================================================================= + // CONVERT — AI VECTORIZE TESTS + // ========================================================================= + sectionHeader('CONVERT \u2014 AI VECTORIZE TESTS (limit: 5/60s)'); + + await runTest(7, 'AI Vectorize: basic (no storage)', async () => { + const client = createClient(); + const r = await client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('Credit Cost', r.creditCost); + logInfo('Message', r.message); + logInfo('Generation ID', r.generationId); + logInfo('SVG URL Expires In', r.svgUrlExpiresIn); + logInfo('Metadata', r.metadata); + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-07-ai-vectorize-basic.svg')); + }); + + await runTest(8, 'AI Vectorize: with storage true', async () => { + const client = createClient(); + const r = await client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('Credit Cost', r.creditCost); + logInfo('Message', r.message); + logInfo('Generation ID', r.generationId); + logInfo('SVG URL Expires In', r.svgUrlExpiresIn); + logInfo('Metadata', r.metadata); + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-08-ai-vectorize-storage.svg')); + }); + + // ========================================================================= + // CONVERT — TRACE TESTS + // ========================================================================= + sectionHeader('CONVERT \u2014 TRACE TESTS (limit: 5/60s)'); + + await runTest(9, 'Trace: default settings', async () => { + const client = createClient(); + const r = await client.convert.trace.configure({ file: './tests/test-images/test-image.png' }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-09-trace-default.svg')); + } + }); + + await runTest(10, 'Trace: custom params (preset: photo, mode: spline, detail: 60, smoothness: 70)', async () => { + const client = createClient(); + const r = await client.convert.trace.configure({ file: './tests/test-images/test-image.png', preset: 'photo', mode: 'spline', detail: 60, smoothness: 70 }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-10-trace-custom.svg')); + } + }); + + // ========================================================================= + // CONVERT — SVG TO VECTOR TESTS + // ========================================================================= + sectionHeader('CONVERT \u2014 SVG TO VECTOR TESTS (limit: 5/60s)'); + + await runTest(11, 'SVG to Vector: convert to PDF', async () => { + const client = createClient(); + const r = await client.convert.svgToVector.configure({ file: './tests/test-images/test-image.svg', toFormat: 'PDF' }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-11-svg-to-pdf.pdf')); + } + }); + + await runTest(12, 'SVG to Vector: convert to DXF with textToPath', async () => { + const client = createClient(); + const r = await client.convert.svgToVector.configure({ file: './tests/test-images/test-image.svg', toFormat: 'DXF', textToPath: true }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-12-svg-to-dxf.dxf')); + } + }); + + // ========================================================================= + // CONVERT — RASTER TO RASTER TESTS + // ========================================================================= + sectionHeader('CONVERT \u2014 RASTER TO RASTER TESTS (limit: 5/60s)'); + + await runTest(13, 'Raster to Raster: PNG to JPG with quality 85', async () => { + const client = createClient(); + const r = await client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'JPG', quality: 85 }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-13-raster-to-jpg.jpg')); + } + }); + + await runTest(14, 'Raster to Raster: PNG to WEBP with width 512', async () => { + const client = createClient(); + const r = await client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'WEBP', width: 512 }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-14-raster-to-webp.webp')); + } + }); + + // ========================================================================= + // BATCH CONVERT TESTS + // ========================================================================= + sectionHeader('BATCH CONVERT TESTS (limit: 5/60s)'); + + await runTest(15, 'Batch Convert: multiple files to SVG', async () => { + const client = createClient(); + const r = await client.convert.batch.configure({ files: ['./tests/test-images/test-image.png', './tests/test-images/test-image.png'], toFormat: 'SVG' }).execute(); + logInfo('Results', r.results); + logInfo('Summary', r.summary); + logInfo('Metadata', r.metadata); + if (r.results && r.results.length > 0 && r.results[0].url) { + await tryDownload(r.results[0].url, path.join(ASSETS_DIR, 'test-15-batch-convert.svg')); + } + }); + + // ========================================================================= + // ENHANCE PROMPT TESTS + // ========================================================================= + sectionHeader('ENHANCE PROMPT TESTS (limit: 10/60s)'); + + await runTest(16, "Enhance Prompt: basic ('a cat sitting on a fence')", async () => { + const client = createClient(); + const r = await client.enhancePrompt.configure({ prompt: 'a cat sitting on a fence' }).execute(); + logInfo('Enhanced Prompt', r.enhancedPrompt); + logInfo('Metadata', r.metadata); + }); + + await runTest(17, "Enhance Prompt: another ('a minimalist logo')", async () => { + const client = createClient(); + const r = await client.enhancePrompt.configure({ prompt: 'a minimalist logo' }).execute(); + logInfo('Enhanced Prompt', r.enhancedPrompt); + logInfo('Metadata', r.metadata); + }); + + // ========================================================================= + // OPTIMIZE SVG TESTS + // ========================================================================= + sectionHeader('OPTIMIZE SVG TESTS (limit: 10/60s)'); + + await runTest(18, 'Optimize SVG: default (no compression)', async () => { + const client = createClient(); + const r = await client.optimizeSvg.configure({ file: './tests/test-images/test-image.svg' }).execute(); + logInfo('SVG URL', r.svgUrl); + logInfo('SVG URL Expires In', r.svgUrlExpiresIn); + logInfo('Metadata', r.metadata); + await tryDownload(r.svgUrl, path.join(ASSETS_DIR, 'test-18-optimize-svg.svg')); + }); + + await runTest(19, 'Optimize SVG: with SVGZ compression', async () => { + const client = createClient(); + const r = await client.optimizeSvg.configure({ file: './tests/test-images/test-image.svg', compress: true }).execute(); + logInfo('SVGZ URL', r.svgzUrl); + logInfo('SVGZ URL Expires In', r.svgzUrlExpiresIn); + logInfo('Filename', r.filename); + logInfo('Compressed Size', r.compressedSize); + logInfo('Metadata', r.metadata); + if (r.svgzUrl) { + await tryDownload(r.svgzUrl, path.join(ASSETS_DIR, 'test-19-optimize-svgz.svgz')); + } + }); + + // ========================================================================= + // GENERATIONS TESTS + // ========================================================================= + sectionHeader('GENERATIONS TESTS (limit: 1000/60s)'); + + if (!GENERATION_ID || !DELETE_GENERATION_ID) { + console.log(`${RED} SKIPPING generation ID tests — generate/edit storage tests above must pass first.${NC}`); + } + + await runTest(20, 'Generations: list (default)', async () => { + const client = createClient(); + const r = await client.generations.list(); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(21, 'Generations: list (with filters)', async () => { + const client = createClient(); + const r = await client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(22, 'Generations: list (with query)', async () => { + const client = createClient(); + const r = await client.generations.list({ query: 'mountain', limit: 10 }); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(23, 'Generations: get by ID', async () => { + if (!GENERATION_ID) throw new Error('Skipped — no GENERATION_ID from test 01'); + const client = createClient(); + const r = await client.generations.get(GENERATION_ID); + logInfo('ID', r.id); + logInfo('Prompt', r.prompt); + logInfo('Type', r.type); + logInfo('Quality', r.quality); + logInfo('Is Public', r.isPublic); + logInfo('Hash Tags', r.hashTags); + logInfo('Categories', r.categories); + logInfo('Metadata', r.metadata); + }); + + await runTest(24, 'Generations: share', async () => { + if (!GENERATION_ID) throw new Error('Skipped — no GENERATION_ID from test 01'); + const client = createClient(); + const r = await client.generations.share(GENERATION_ID); + logInfo('Message', r.message); + logInfo('Is Public', r.isPublic); + logInfo('Share URL', r.shareUrl); + logInfo('Metadata', r.metadata); + }); + + await runTest(25, 'Generations: download (SVG)', async () => { + if (!GENERATION_ID) throw new Error('Skipped — no GENERATION_ID from test 01'); + const client = createClient(); + const r = await client.generations.download(GENERATION_ID, { format: 'svg' }); + logInfo('ID', r.id); + logInfo('URL', r.url); + logInfo('URL Expires In', r.urlExpiresIn); + logInfo('Format', r.format); + logInfo('Filename', r.filename); + logInfo('Metadata', r.metadata); + await tryDownload(r.url, path.join(ASSETS_DIR, 'test-25-gen-download-svg.svg')); + }); + + await runTest(26, 'Generations: download (default format)', async () => { + if (!GENERATION_ID) throw new Error('Skipped — no GENERATION_ID from test 01'); + const client = createClient(); + const r = await client.generations.download(GENERATION_ID); + logInfo('ID', r.id); + logInfo('URL', r.url); + logInfo('URL Expires In', r.urlExpiresIn); + logInfo('Format', r.format); + logInfo('Filename', r.filename); + logInfo('Metadata', r.metadata); + await tryDownload(r.url, path.join(ASSETS_DIR, `test-26-gen-download-default.${r.format || 'svg'}`)); + }); + + await runTest(27, 'Generations: download (SVGZ)', async () => { + if (!GENERATION_ID) throw new Error('Skipped — no GENERATION_ID from test 01'); + const client = createClient(); + const r = await client.generations.download(GENERATION_ID, { format: 'svgz' }); + logInfo('ID', r.id); + logInfo('URL', r.url); + logInfo('URL Expires In', r.urlExpiresIn); + logInfo('Format', r.format); + logInfo('Filename', r.filename); + logInfo('Metadata', r.metadata); + await tryDownload(r.url, path.join(ASSETS_DIR, 'test-27-gen-download-svgz.svgz')); + }); + + await runExpectedFail(28, 'Generations: get (invalid ID)', async () => { + const client = createClient(); + await client.generations.get('nonexistent-id-12345'); + }); + + await runExpectedFail(29, 'Generations: list (invalid limit)', async () => { + const client = createClient(); + await client.generations.list({ limit: 200 }); + }); + + await runTest(30, 'Generations: delete (destructive \u2014 uses edit generation)', async () => { + if (!DELETE_GENERATION_ID) throw new Error('Skipped \u2014 no DELETE_GENERATION_ID from test 04'); + const client = createClient(); + const r = await client.generations.delete(DELETE_GENERATION_ID); + logInfo('Message', r.message); + logInfo('Metadata', r.metadata); + }); + + // ========================================================================= + // GALLERY TESTS + // ========================================================================= + sectionHeader('GALLERY TESTS (list/get: 1000/60s, download: 5/60s)'); + + await runTest(31, 'Gallery: browse (default)', async () => { + const client = createClient(); + const r = await client.gallery.list(); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(32, 'Gallery: browse (with filters)', async () => { + const client = createClient(); + const r = await client.gallery.list({ type: ['generate'], limit: 5 }); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(33, 'Gallery: browse (pro filter)', async () => { + const client = createClient(); + const r = await client.gallery.list({ pro: 'true', limit: 10 }); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(34, 'Gallery: browse (with query)', async () => { + const client = createClient(); + const r = await client.gallery.list({ query: 'mountain', limit: 10 }); + const first3 = (r.items || []).slice(0, 3).map(i => ({ id: i.id, prompt: (i.prompt || '').substring(0, 50), type: i.type })); + logInfo('Items (first 3)', first3); + logInfo('Pagination', r.pagination); + logInfo('Metadata', r.metadata); + }); + + await runTest(35, 'Gallery: get by ID', async () => { + const client = createClient(); + const r = await client.gallery.get(GALLERY_ID); + logInfo('ID', r.id); + logInfo('Prompt', r.prompt); + logInfo('Type', r.type); + logInfo('Quality', r.quality); + logInfo('Is Public', r.isPublic); + logInfo('Metadata', r.metadata); + }); + + await runTest(36, 'Gallery: get pro item', async () => { + const client = createClient(); + const r = await client.gallery.get(PRO_GALLERY_ID); + logInfo('ID', r.id); + logInfo('Prompt', r.prompt); + logInfo('Type', r.type); + logInfo('Quality', r.quality); + logInfo('Is Public', r.isPublic); + logInfo('Metadata', r.metadata); + }); + + await runTest(37, 'Gallery: download (WebP \u2014 free)', async () => { + const client = createClient(); + const r = await client.gallery.download(GALLERY_ID, { format: 'webp' }); + logInfo('ID', r.id); + logInfo('URL', r.url); + logInfo('Format', r.format); + logInfo('Filename', r.filename); + logInfo('Metadata', r.metadata); + await tryDownload(r.url, path.join(ASSETS_DIR, 'test-37-gallery-download-webp.webp')); + }); + + await runTest(38, 'Gallery: download (SVG \u2014 1 credit)', async () => { + const client = createClient(); + const r = await client.gallery.download(GALLERY_ID, { format: 'svg' }); + logInfo('ID', r.id); + logInfo('URL', r.url); + logInfo('URL Expires In', r.urlExpiresIn); + logInfo('Format', r.format); + logInfo('Filename', r.filename); + logInfo('Metadata', r.metadata); + await tryDownload(r.url, path.join(ASSETS_DIR, 'test-38-gallery-download-svg.svg')); + }); + + await runTest(39, 'Gallery: download pro item (WebP \u2014 free for premium)', async () => { + const client = createClient(); + const r = await client.gallery.download(PRO_GALLERY_ID, { format: 'webp' }); + logInfo('ID', r.id); + logInfo('URL', r.url); + logInfo('Format', r.format); + logInfo('Filename', r.filename); + logInfo('Metadata', r.metadata); + await tryDownload(r.url, path.join(ASSETS_DIR, 'test-39-gallery-download-pro-webp.webp')); + }); + + await runExpectedFail(40, 'Gallery: get (invalid ID)', async () => { + const client = createClient(); + await client.gallery.get('nonexistent-id-12345'); + }); + + // ========================================================================= + // ACCOUNT TESTS + // ========================================================================= + sectionHeader('ACCOUNT TESTS (limit: 1000/60s)'); + + await runTest(41, 'Account: get info', async () => { + const client = createClient(); + const r = await client.account.getInfo(); + logInfo('Email', r.email); + logInfo('Display Name', r.displayName); + logInfo('Account Type', r.accountType); + logInfo('Credits', r.credits); + logInfo('Metadata', r.metadata); + }); + + await runTest(42, 'Account: all-time usage', async () => { + const client = createClient(); + const r = await client.account.getUsage(); + logInfo('Period', r.period); + logInfo('Summary', r.summary); + const catKeys = Object.keys(r.byCategory || {}).slice(0, 5); + const catPreview = {}; + catKeys.forEach(k => catPreview[k] = r.byCategory[k]); + logInfo('By Category (first few)', catPreview); + logInfo('Metadata', r.metadata); + }); + + await runTest(43, 'Account: usage last 7 days', async () => { + const client = createClient(); + const r = await client.account.getUsage({ days: 7 }); + logInfo('Period', r.period); + logInfo('Summary', r.summary); + const catKeys = Object.keys(r.byCategory || {}).slice(0, 5); + const catPreview = {}; + catKeys.forEach(k => catPreview[k] = r.byCategory[k]); + logInfo('By Category (first few)', catPreview); + logInfo('Metadata', r.metadata); + }); + + await runTest(44, 'Account: usage date range (2025-01-01 to 2025-06-30)', async () => { + const client = createClient(); + const r = await client.account.getUsage({ start: '2025-01-01', end: '2025-06-30' }); + logInfo('Period', r.period); + logInfo('Summary', r.summary); + const catKeys = Object.keys(r.byCategory || {}).slice(0, 5); + const catPreview = {}; + catKeys.forEach(k => catPreview[k] = r.byCategory[k]); + logInfo('By Category (first few)', catPreview); + logInfo('Metadata', r.metadata); + }); + + await runExpectedFail(45, 'Account: usage with both days and start/end', async () => { + const client = createClient(); + await client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }); + }); + + // ========================================================================= + // SUMMARY + // ========================================================================= + console.log(''); + console.log(`${BOLD}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${NC}`); + console.log(`${BOLD} TEST RESULTS SUMMARY${NC}`); + console.log(`${BOLD}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${NC}`); + console.log(''); + console.log(` Total: ${BOLD}${TOTAL}${NC}`); + console.log(` Passed: ${GREEN}${PASSED}${NC}`); + console.log(` Failed: ${RED}${FAILED}${NC}`); + console.log(''); + console.log(`${BOLD}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${NC}`); + + if (FAILED > 0) { + console.log(`${RED}Some tests failed.${NC}`); + process.exit(1); + } else { + console.log(`${GREEN}All tests passed!${NC}`); + process.exit(0); + } +} + +main().catch(e => { + console.error(`${RED}Fatal error: ${e.message}${NC}`); + console.error(e.stack); + process.exit(1); +}); diff --git a/tests/run-integration-tests.sh b/tests/run-integration-tests.sh deleted file mode 100644 index 7794bcb..0000000 --- a/tests/run-integration-tests.sh +++ /dev/null @@ -1,539 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# SVGMaker SDK — Rate-Limited Integration Test Suite (Production) -# ============================================================================= - -set -euo pipefail - -# ─── Configuration ─────────────────────────────────────────────────────────── -API_KEY="svgmaker-ioxxx" -BASE_URL="https://api.svgmaker.io" -DELAY=3 # seconds between tests -TIMEOUT=300000 # ms for SDK timeout - -# ─── IDs (fill in before running) ────────────────────────────────────── -GENERATION_ID="" # A generation you own (for get/share/download) -DELETE_GENERATION_ID="" # Throwaway generation (will be permanently deleted) -GALLERY_ID="" # A public gallery item -PRO_GALLERY_ID="" # A pro public gallery item - -# ─── Pre-flight ID check ─────────────────────────────────────────────────── -if [[ "$GENERATION_ID" == "REPLACE_ME" ]] || [[ "$DELETE_GENERATION_ID" == "REPLACE_ME" ]] || [[ "$GALLERY_ID" == "REPLACE_ME" ]] || [[ "$PRO_GALLERY_ID" == "REPLACE_ME" ]]; then - echo -e "${RED:-\033[0;31m}ERROR: Please fill in the ID placeholders at the top of this script before running.${NC:-\033[0m}" - exit 1 -fi - -# ─── Colors ────────────────────────────────────────────────────────────────── -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -# ─── Counters ──────────────────────────────────────────────────────────────── -PASSED=0 -FAILED=0 -SKIPPED=0 -TOTAL=0 - -# ─── Test Fixtures ─────────────────────────────────────────────────────────── -echo -e "${BOLD}Setting up test fixtures...${NC}" - -mkdir -p tests/test-images - -if [ ! -f tests/test-images/test-image.png ]; then - echo " Creating tests/test-images/test-image.png ..." - node -e " -const fs = require('fs'); -const data = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', 'base64'); -fs.writeFileSync('tests/test-images/test-image.png', data); -" -fi - -if [ ! -f tests/test-images/test.svg ]; then - echo " Creating tests/test-images/test.svg ..." - cat > tests/test-images/test.svg << 'SVGEOF' - - - -SVGEOF -fi - -echo -e "${GREEN} Fixtures ready.${NC}" - -# ─── Pre-flight build ─────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}Running npm run build...${NC}" -if ! npm run build; then - echo -e "${RED}Build failed — aborting tests.${NC}" - exit 1 -fi -echo -e "${GREEN}Build succeeded.${NC}" - -# ─── Timeout command detection ─────────────────────────────────────────────── -TIMEOUT_CMD="" -if command -v gtimeout &>/dev/null; then - TIMEOUT_CMD="gtimeout 120" -elif command -v timeout &>/dev/null; then - TIMEOUT_CMD="timeout 120" -fi - -# ─── Helper Functions ──────────────────────────────────────────────────────── - -run_test() { - local num="$1" - local name="$2" - local code="$3" - - TOTAL=$((TOTAL + 1)) - echo -e "${CYAN}[Test ${num}]${NC} ${name}..." - - local output - output=$(node -e "$code" 2>&1) || true - local exit_code=$? - - if [ $exit_code -eq 0 ] && ! echo "$output" | grep -q "ERROR:"; then - echo -e " ${GREEN}PASS${NC}" - PASSED=$((PASSED + 1)) - else - echo -e " ${RED}FAIL${NC}" - echo "$output" | sed 's/^/ /' - FAILED=$((FAILED + 1)) - fi - - sleep "$DELAY" -} - -run_expected_fail() { - local num="$1" - local name="$2" - local code="$3" - - TOTAL=$((TOTAL + 1)) - echo -e "${CYAN}[Test ${num}]${NC} ${name} ${YELLOW}(expected fail)${NC}..." - - local output - output=$(node -e "$code" 2>&1) || true - - if echo "$output" | grep -q "EXPECTED ERROR"; then - echo -e " ${YELLOW}EXPECTED FAIL (PASS)${NC}" - PASSED=$((PASSED + 1)) - else - echo -e " ${RED}FAIL${NC}" - echo "$output" | sed 's/^/ /' - FAILED=$((FAILED + 1)) - fi - - sleep "$DELAY" -} - -run_stream_test() { - local num="$1" - local name="$2" - local code="$3" - - TOTAL=$((TOTAL + 1)) - echo -e "${CYAN}[Test ${num}]${NC} ${name} ${CYAN}(stream)${NC}..." - - local output - if [ -n "$TIMEOUT_CMD" ]; then - output=$($TIMEOUT_CMD node -e "$code" 2>&1) || true - else - # Fallback: rely on the in-code setTimeout to kill the process - output=$(node -e "$code" 2>&1) || true - fi - - if echo "$output" | grep -q "Stream ended" || echo "$output" | grep -q "complete"; then - echo -e " ${GREEN}PASS${NC}" - PASSED=$((PASSED + 1)) - else - echo -e " ${RED}FAIL${NC}" - echo "$output" | sed 's/^/ /' - FAILED=$((FAILED + 1)) - fi - - sleep "$DELAY" -} - -# ============================================================================= -# TESTS BEGIN -# ============================================================================= - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} GENERATE TESTS (limit: 3/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 1 "Generate: with quality (low)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A minimalist mountain landscape', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 2 "Generate: with model (gpt-image-1-mini)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A red sports car', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); -" - -run_expected_fail 3 "Generate: model+quality together" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generate.configure({ prompt: 'A cat on a fence', quality: 'medium', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} EDIT TESTS (limit: 3/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 4 "Edit: with quality (low)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Make the background blue', quality: 'low' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 5 "Edit: with model (gpt-image-1-mini)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add a golden frame around this image', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Metadata:', r.metadata); console.log('Generation ID:', r.generationId); }).catch(e => console.error('ERROR:', e.message)); -" - -run_expected_fail 6 "Edit: model+quality together" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.edit.configure({ image: 'tests/test-images/test-image.png', prompt: 'Add stars', quality: 'medium', model: 'gpt-image-1-mini' }).execute().then(r => { console.log('UNEXPECTED SUCCESS - should have failed'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — AI VECTORIZE TESTS (limit: 5/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 7 "AI Vectorize: basic (no storage)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png' }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 8 "AI Vectorize: with storage true" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.aiVectorize.configure({ file: 'tests/test-images/test-image.png', storage: true }).execute().then(r => { console.log('SVG URL:', r.svgUrl); console.log('Credits:', r.creditCost); console.log('Message:', r.message); console.log('Generation ID:', r.generationId); console.log('URL expires in:', r.svgUrlExpiresIn); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — TRACE TESTS (limit: 5/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 9 "Trace: default settings" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.trace.configure({ file: './tests/test-images/test-image.png' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 10 "Trace: custom params (preset: photo, mode: spline, detail: 60, smoothness: 70)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.trace.configure({ file: './tests/test-images/test-image.png', preset: 'photo', mode: 'spline', detail: 60, smoothness: 70 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — SVG TO VECTOR TESTS (limit: 5/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 11 "SVG to Vector: convert to PDF" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'PDF' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 12 "SVG to Vector: convert to DXF with textToPath" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.svgToVector.configure({ file: './tests/test-images/test.svg', toFormat: 'DXF', textToPath: true }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} CONVERT — RASTER TO RASTER TESTS (limit: 5/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 13 "Raster to Raster: PNG to JPG with quality 85" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'JPG', quality: 85 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 14 "Raster to Raster: PNG to WEBP with width 512" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.rasterToRaster.configure({ file: './tests/test-images/test-image.png', toFormat: 'WEBP', width: 512 }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} BATCH CONVERT TESTS (limit: 5/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 15 "Batch Convert: multiple files to SVG" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.convert.batch.configure({ files: ['./tests/test-images/test-image.png', './tests/test-images/test-image.png'], toFormat: 'SVG' }).execute().then(r => { console.log('Results:', JSON.stringify(r.results, null, 2)); console.log('Summary:', r.summary); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} ENHANCE PROMPT TESTS (limit: 10/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 16 "Enhance Prompt: basic ('a cat sitting on a fence')" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.enhancePrompt.configure({ prompt: 'a cat sitting on a fence' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 17 "Enhance Prompt: another ('a minimalist logo')" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.enhancePrompt.configure({ prompt: 'a minimalist logo' }).execute().then(r => { console.log('Enhanced Prompt:', r.enhancedPrompt); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} OPTIMIZE SVG TESTS (limit: 10/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 18 "Optimize SVG: default (no compression)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.optimizeSvg.configure({ file: './tests/test-images/test.svg' }).execute().then(r => { console.log('Optimized SVG URL:', r.svgUrl); console.log('Expires in:', r.svgUrlExpiresIn); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 19 "Optimize SVG: with SVGZ compression" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.optimizeSvg.configure({ file: './tests/test-images/test.svg', compress: true }).execute().then(r => { console.log('SVGZ URL:', r.svgzUrl); console.log('Filename:', r.filename); console.log('Compressed size:', r.compressedSize); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} GENERATIONS TESTS (limit: 1000/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 20 "Generations: list (default)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 21 "Generations: list (with filters)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list({ page: 1, limit: 5, type: ['generate', 'edit'] }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 22 "Generations: list (with query)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 23 "Generations: get by ID" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.get('${GENERATION_ID}').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 24 "Generations: share" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.share('${GENERATION_ID}').then(r => { console.log('Message:', r.message); console.log('Is Public:', r.isPublic); console.log('Share URL:', r.shareUrl); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 25 "Generations: download (SVG)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.download('${GENERATION_ID}', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 26 "Generations: download (default format)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.download('${GENERATION_ID}').then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 27 "Generations: download (SVGZ)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.download('${GENERATION_ID}', { format: 'svgz' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_expected_fail 28 "Generations: get (invalid ID)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); -" - -run_expected_fail 29 "Generations: list (invalid limit)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.list({ limit: 200 }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); -" - -run_test 30 "Generations: delete (destructive — uses DELETE_GENERATION_ID)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.generations.delete('${DELETE_GENERATION_ID}').then(r => { console.log('Message:', r.message); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} GALLERY TESTS (list/get: 1000/60s, download: 5/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 31 "Gallery: browse (default)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list().then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 32 "Gallery: browse (with filters)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ type: ['generate'], limit: 5 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 33 "Gallery: browse (pro filter)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ pro: 'true', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 34 "Gallery: browse (with query)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.list({ query: 'mountain', limit: 10 }).then(r => { console.log('Items:', r.items); console.log('Pagination:', r.pagination); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 35 "Gallery: get by ID" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('${GALLERY_ID}').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 36 "Gallery: get pro item" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('${PRO_GALLERY_ID}').then(r => { console.log('ID:', r.id); console.log('Prompt:', r.prompt); console.log('Type:', r.type); console.log('Quality:', r.quality); console.log('Is Public:', r.isPublic); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 37 "Gallery: download (WebP — free)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('${GALLERY_ID}', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 38 "Gallery: download (SVG — 1 credit)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('${GALLERY_ID}', { format: 'svg' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('URL Expires In:', r.urlExpiresIn); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 39 "Gallery: download pro item (WebP — free for premium)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.download('${PRO_GALLERY_ID}', { format: 'webp' }).then(r => { console.log('ID:', r.id); console.log('URL:', r.url); console.log('Format:', r.format); console.log('Filename:', r.filename); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_expected_fail 40 "Gallery: get (invalid ID)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.gallery.get('nonexistent-id-12345').then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); -" - -# ───────────────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════${NC}" -echo -e "${BOLD} ACCOUNT TESTS (limit: 1000/60s)${NC}" -echo -e "${BOLD}══════════════════════════════════════${NC}" -# ───────────────────────────────────────────────────────────────────────────── - -run_test 41 "Account: get info" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getInfo().then(r => { console.log('Email:', r.email); console.log('Display Name:', r.displayName); console.log('Account Type:', r.accountType); console.log('Credits:', r.credits); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 42 "Account: all-time usage" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage().then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('By Category:', JSON.stringify(r.byCategory, null, 2)); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 43 "Account: usage last 7 days" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage({ days: 7 }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_test 44 "Account: usage date range (2025-01-01 to 2025-06-30)" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage({ start: '2025-01-01', end: '2025-06-30' }).then(r => { console.log('Period:', r.period); console.log('Summary:', r.summary); console.log('Daily:', r.daily); console.log('All Time:', r.allTime); console.log('Metadata:', r.metadata); }).catch(e => console.error('ERROR:', e.message)); -" - -run_expected_fail 45 "Account: usage with both days and start/end" " -const { SVGMakerClient } = require('./dist/cjs/index.js'); -const client = new SVGMakerClient('${API_KEY}', { baseUrl: '${BASE_URL}', timeout: ${TIMEOUT} }); -client.account.getUsage({ days: 7, start: '2025-01-01', end: '2025-01-31' }).then(r => { console.log('UNEXPECTED SUCCESS'); }).catch(e => console.log('EXPECTED ERROR:', e.message)); -" - -# ============================================================================= -# SUMMARY -# ============================================================================= - -echo "" -echo -e "${BOLD}══════════════════════════════════════════════════${NC}" -echo -e "${BOLD} TEST RESULTS SUMMARY${NC}" -echo -e "${BOLD}══════════════════════════════════════════════════${NC}" -echo "" -echo -e " Total: ${BOLD}${TOTAL}${NC}" -echo -e " Passed: ${GREEN}${PASSED}${NC}" -echo -e " Failed: ${RED}${FAILED}${NC}" -echo -e " Skipped: ${YELLOW}${SKIPPED}${NC}" -echo "" -echo -e "${BOLD}══════════════════════════════════════════════════${NC}" - -if [ "$FAILED" -gt 0 ]; then - echo -e "${RED}Some tests failed.${NC}" - exit 1 -else - echo -e "${GREEN}All tests passed!${NC}" - exit 0 -fi diff --git a/tests/run-streaming-tests.js b/tests/run-streaming-tests.js new file mode 100644 index 0000000..f1e278d --- /dev/null +++ b/tests/run-streaming-tests.js @@ -0,0 +1,317 @@ +// ============================================================================= +// SVGMaker SDK — Streaming Integration Tests (Node.js) +// ============================================================================= + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const http = require('http'); +const config = require('./test-config'); + +// ─── Configuration ─────────────────────────────────────────────────────────── +const { API_KEY, BASE_URL, DELAY, TIMEOUT, ASSETS_DIR } = config; + +// ─── Colors ────────────────────────────────────────────────────────────────── +const GREEN = '\x1b[0;32m'; +const RED = '\x1b[0;31m'; +const CYAN = '\x1b[0;36m'; +const YELLOW = '\x1b[0;33m'; +const DIM = '\x1b[2m'; +const BOLD = '\x1b[1m'; +const NC = '\x1b[0m'; + +// ─── Counters ──────────────────────────────────────────────────────────────── +let PASSED = 0; +let FAILED = 0; +let TOTAL = 0; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function downloadFile(url, filepath) { + return new Promise((resolve, reject) => { + const get = url.startsWith('https') ? https.get : http.get; + get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + downloadFile(res.headers.location, filepath).then(resolve).catch(reject); + return; + } + const chunks = []; + res.on('data', c => chunks.push(c)); + res.on('end', () => { + fs.writeFileSync(filepath, Buffer.concat(chunks)); + resolve(); + }); + res.on('error', reject); + }).on('error', reject); + }); +} + +async function saveAsset(label, completeEvent, filepath) { + if (completeEvent.svgText) { + fs.writeFileSync(filepath, completeEvent.svgText); + console.log(` ${DIM}Saved svgText -> ${filepath}${NC}`); + } else if (completeEvent.svgUrl) { + try { + await downloadFile(completeEvent.svgUrl, filepath); + console.log(` ${DIM}Downloaded -> ${filepath}${NC}`); + } catch (e) { + console.log(` ${DIM}Download failed (non-fatal): ${e.message}${NC}`); + } + } +} + +function sectionHeader(title) { + console.log(''); + console.log('\u2550'.repeat(50)); + console.log(` ${title}`); + console.log('\u2550'.repeat(50)); +} + +function logEvent(event) { + const status = event.status || 'unknown'; + let color = DIM; + if (status === 'complete') color = GREEN; + else if (status === 'error') color = RED; + else if (status === 'generated') color = CYAN; + else if (status === 'processing' || status === 'storing') color = YELLOW; + + const message = event.message || ''; + const extra = []; + if (event.svgUrl) extra.push(`svgUrl: ${event.svgUrl.substring(0, 60)}...`); + if (event.creditCost !== undefined) extra.push(`credits: ${event.creditCost}`); + if (event.generationId) extra.push(`id: ${event.generationId}`); + if (event.svgText) extra.push(`svgText: ${event.svgText.length} chars`); + if (event.quality) extra.push(`quality: ${event.quality}`); + + console.log(` ${color}[${status}]${NC} ${message}${extra.length ? ` ${DIM}(${extra.join(', ')})${NC}` : ''}`); +} + +/** + * Consume a Readable object-mode stream and return all events. + */ +function consumeStream(readable) { + return new Promise((resolve, reject) => { + const events = []; + readable.on('data', (event) => { + logEvent(event); + events.push(event); + }); + readable.on('error', (err) => reject(err)); + readable.on('end', () => resolve(events)); + }); +} + +async function runTest(num, label, fn) { + TOTAL++; + const tag = `[Test ${String(num).padStart(2, '0')}]`; + console.log(`${BOLD}${tag}${NC} ${label}...`); + try { + await fn(); + console.log(` ${GREEN}PASS${NC}`); + PASSED++; + } catch (e) { + console.log(` ${RED}FAIL${NC}`); + console.log(` Error: ${e.message}`); + FAILED++; + } + await sleep(DELAY); +} + +function createClient() { + const { SVGMakerClient } = require('../dist/cjs/index.js'); + return new SVGMakerClient(API_KEY, { baseUrl: BASE_URL, timeout: TIMEOUT }); +} + +// ============================================================================= +// MAIN +// ============================================================================= + +async function main() { + // ─── Prepare assets directory ──────────────────────────────────────────── + console.log(`${BOLD}Preparing assets output directory...${NC}`); + fs.mkdirSync(ASSETS_DIR, { recursive: true }); + console.log(`${GREEN} Assets directory ready: ${ASSETS_DIR}${NC}`); + + // ========================================================================= + // STREAMING GENERATE TESTS + // ========================================================================= + sectionHeader('STREAMING GENERATE TESTS'); + + await runTest(1, 'Stream Generate: basic prompt + storage', async () => { + const client = createClient(); + const stream = client.generate.configure({ + prompt: 'A minimalist mountain landscape', + quality: 'low', + storage: true, + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgUrl) throw new Error('Missing svgUrl in complete event'); + if (!last.generationId) throw new Error('Missing generationId — storage may not have worked'); + await saveAsset('stream-01', last, path.join(ASSETS_DIR, 'stream-01-generate-basic.svg')); + }); + + await runTest(2, 'Stream Generate: with svgText', async () => { + const client = createClient(); + const stream = client.generate.configure({ + prompt: 'A simple red circle', + quality: 'low', + svgText: true, + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgText) throw new Error('Missing svgText in complete event'); + if (!last.svgText.includes(' { + const client = createClient(); + const stream = client.generate.configure({ + prompt: 'A blue butterfly', + model: 'gpt-image-1-mini', + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgUrl) throw new Error('Missing svgUrl in complete event'); + await saveAsset('stream-03', last, path.join(ASSETS_DIR, 'stream-03-generate-model.svg')); + }); + + // ========================================================================= + // STREAMING EDIT TESTS + // ========================================================================= + sectionHeader('STREAMING EDIT TESTS'); + + await runTest(4, 'Stream Edit: basic edit + storage', async () => { + const client = createClient(); + const stream = client.edit.configure({ + image: 'tests/test-images/test-image.png', + prompt: 'Make the background blue', + quality: 'low', + storage: true, + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgUrl) throw new Error('Missing svgUrl in complete event'); + if (!last.generationId) throw new Error('Missing generationId — storage may not have worked'); + await saveAsset('stream-04', last, path.join(ASSETS_DIR, 'stream-04-edit-basic.svg')); + }); + + await runTest(5, 'Stream Edit: with svgText', async () => { + const client = createClient(); + const stream = client.edit.configure({ + image: 'tests/test-images/test-image.png', + prompt: 'Add a golden frame', + quality: 'low', + svgText: true, + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgText) throw new Error('Missing svgText in complete event'); + await saveAsset('stream-05', last, path.join(ASSETS_DIR, 'stream-05-edit-svgtext.svg')); + }); + + await runTest(6, 'Stream Edit: with model', async () => { + const client = createClient(); + const stream = client.edit.configure({ + image: 'tests/test-images/test-image.png', + prompt: 'Add stars in the sky', + model: 'gpt-image-1-mini', + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgUrl) throw new Error('Missing svgUrl in complete event'); + await saveAsset('stream-06', last, path.join(ASSETS_DIR, 'stream-06-edit-model.svg')); + }); + + // ========================================================================= + // STREAMING CONVERT (AI VECTORIZE) TESTS + // ========================================================================= + sectionHeader('STREAMING CONVERT (AI VECTORIZE) TESTS'); + + await runTest(7, 'Stream AI Vectorize: basic', async () => { + const client = createClient(); + const stream = client.convert.aiVectorize.configure({ + file: 'tests/test-images/test-image.png', + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgUrl) throw new Error('Missing svgUrl in complete event'); + await saveAsset('stream-07', last, path.join(ASSETS_DIR, 'stream-07-vectorize-basic.svg')); + }); + + await runTest(8, 'Stream AI Vectorize: with svgText', async () => { + const client = createClient(); + const stream = client.convert.aiVectorize.configure({ + file: 'tests/test-images/test-image.png', + svgText: true, + }).stream(); + + const events = await consumeStream(stream); + if (events.length === 0) throw new Error('No events received'); + + const last = events[events.length - 1]; + if (last.status !== 'complete') throw new Error(`Expected final status "complete", got "${last.status}"`); + if (!last.svgText) throw new Error('Missing svgText in complete event'); + if (!last.svgText.includes(' 0 ? RED : GREEN}${FAILED}${NC}`); + console.log(''); + console.log('\u2550'.repeat(50)); + + if (FAILED > 0) { + console.log(`${RED}Some tests failed!${NC}`); + process.exit(1); + } else { + console.log(`${GREEN}All tests passed!${NC}`); + } +} + +main().catch((err) => { + console.error(`${RED}Unhandled error:${NC}`, err); + process.exit(1); +}); diff --git a/tests/test-config.example.js b/tests/test-config.example.js new file mode 100644 index 0000000..aeebd9e --- /dev/null +++ b/tests/test-config.example.js @@ -0,0 +1,18 @@ +// ============================================================================= +// SVGMaker SDK — Shared Test Configuration (Example) +// ============================================================================= +// Copy this file to test-config.js and fill in your values: +// cp tests/test-config.example.js tests/test-config.js +// ============================================================================= + +module.exports = { + API_KEY: 'your-api-key-here', + BASE_URL: 'https://api.svgmaker.io', + TIMEOUT: 300000, + DELAY: 3000, + ASSETS_DIR: 'tests/assets', + + // IDs used by integration tests (GENERATION_ID and DELETE_GENERATION_ID are auto-populated) + GALLERY_ID: 'REPLACE_ME', + PRO_GALLERY_ID: 'REPLACE_ME', +}; diff --git a/tests/test-images/test.svg b/tests/test-images/test.svg deleted file mode 100644 index adf2cbd..0000000 --- a/tests/test-images/test.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - From 37ceb0664573d639879b838acdd61715c788646d Mon Sep 17 00:00:00 2001 From: razorback16 Date: Thu, 19 Feb 2026 18:54:50 -0800 Subject: [PATCH 13/15] add prepare-release slash command for automated version bump workflow --- .claude/commands/prepare-release.md | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .claude/commands/prepare-release.md diff --git a/.claude/commands/prepare-release.md b/.claude/commands/prepare-release.md new file mode 100644 index 0000000..8b90656 --- /dev/null +++ b/.claude/commands/prepare-release.md @@ -0,0 +1,50 @@ +# Prepare Release + +Run all pre-release checks and prepare a version bump for the automated release pipeline. + +## Step 1: Determine Version + +Ask the user what version to release. Show the current version from `package.json` and ask: + +- **What type of version bump?** Options: patch, minor, major, prerelease (e.g. `1.0.0-rc.1`), or custom version string. +- If prerelease, ask for the prerelease identifier (e.g. `rc.1`, `beta.1`). +- Show what the new version will be before proceeding. Confirm with the user. + +## Step 2: Check Working Tree + +Run `git status` to verify the working tree is clean (no uncommitted changes). If there are uncommitted changes, warn the user and ask whether to proceed or abort. + +## Step 3: Bump Version + +Update the version in `package.json` using the appropriate method: +- For patch/minor/major: run `npm version --no-git-tag-version` +- For prerelease or custom: edit `package.json` directly with the exact version string + +## Step 4: Run Quality Checks + +Run these checks sequentially. Stop and report on the first failure: + +1. **Lint**: `npm run lint` +2. **Format**: `npm run format:check` — if this fails, run `npm run format` to auto-fix, then re-check +3. **Type Check**: `npm run typecheck` +4. **Tests**: `npx jest --passWithNoTests` +5. **Build**: `npm run build` +6. **Security Audit**: `npm audit --audit-level moderate --omit=dev` + +If any check fails (except format, which auto-fixes), stop and help the user fix the issue before continuing. + +## Step 5: Commit and Push + +After all checks pass: + +1. Stage `package.json` (and any files changed by formatting) +2. Create a commit with message: `chore: bump version to ` +3. Push to the current branch + +## Step 6: Summary + +Print a summary: +- Previous version → New version +- All checks passed +- Branch and commit hash +- Remind the user: "Merge this branch to `main` to trigger the automated release pipeline." From 5378fe77924a8150419b4eaf0dcdc69489fc7921 Mon Sep 17 00:00:00 2001 From: razorback16 Date: Thu, 19 Feb 2026 18:56:39 -0800 Subject: [PATCH 14/15] chore: bump version to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b0060e..3aa02c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@genwave/svgmaker-sdk", - "version": "1.0.0-rc.1", + "version": "1.0.0", "description": "Official Node.js SDK for SVGMaker API", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From c9e8f4c64774edb6d64bc1a68a3e99c4d2357131 Mon Sep 17 00:00:00 2001 From: razorback16 Date: Thu, 19 Feb 2026 19:02:48 -0800 Subject: [PATCH 15/15] docs: add v1.0.0 migration guide and breaking changes to README --- README.md | 12 ++ docs/migration-guide-v1.md | 224 +++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 docs/migration-guide-v1.md diff --git a/README.md b/README.md index e6316c2..f8098a0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ Official Node.js SDK for the [SVGMaker](https://svgmaker.io) API, providing a cl [![npm version](https://img.shields.io/npm/v/@genwave/svgmaker-sdk.svg)](https://www.npmjs.com/package/@genwave/svgmaker-sdk) [![License](https://img.shields.io/npm/l/@genwave/svgmaker-sdk.svg)](https://github.com/GenWaveLLC/svgmaker-sdk-node/blob/main/LICENSE) +## Upgrading to v1.0.0 + +v1.0.0 includes breaking changes. If you're upgrading from v0.x, see the **[Migration Guide](docs/migration-guide-v1.md)** for full details. Top breaking changes: + +- **Base URL changed** — now targets `api.svgmaker.io` (automatic if using defaults) +- **`client.convert` restructured** — old `client.convert.configure(...)` is now `client.convert.aiVectorize.configure(...)`, plus new sub-clients for tracing, format conversion, and batch operations +- **`styleParams` redesigned** — new style values (`flat`, `line_art`, `engraving`, `ghibli`, ...), `advanced` block removed, new `text` field added +- **Aspect ratio `wide`/`tall` removed** — only `auto`, `portrait`, `landscape`, `square` remain +- **`model` parameter**: New option on generate and edit — mutually exclusive with `quality` +- **Cloud storage no longer default** — generate, edit, and AI vectorize no longer store results to cloud by default; pass `storage: true` to persist + ## Features - **Complete API Coverage**: Generate, edit, convert, optimize, and manage SVGs @@ -110,6 +121,7 @@ See the [SDK Documentation](docs/sdk-documentation.md) for the full list of erro ## Documentation +- **[Migration Guide](docs/migration-guide-v1.md)** - Upgrading from v0.x to v1.0.0 - **[SDK Documentation](docs/sdk-documentation.md)** - Full SDK reference with detailed examples for every client - **[API Documentation](docs/v1-api-documentation.md)** - REST API reference with endpoint details and credit costs - **[Legacy API Documentation](docs/legacy-api-documentation.md)** - Pre-v1 API reference diff --git a/docs/migration-guide-v1.md b/docs/migration-guide-v1.md new file mode 100644 index 0000000..3a931d2 --- /dev/null +++ b/docs/migration-guide-v1.md @@ -0,0 +1,224 @@ +# Migrating to v1.0.0 + +This guide covers all breaking changes when upgrading from `@genwave/svgmaker-sdk` v0.x to v1.0.0. + +## Base URL Changed + +The SDK now targets the dedicated API domain. + +```diff +- https://svgmaker.io/api ++ https://api.svgmaker.io +``` + +If you were using the default `baseUrl`, no code changes are needed — the new default is applied automatically. If you hardcoded the old URL, update it: + +```typescript +const client = new SVGMakerClient('key', { + baseUrl: 'https://api.svgmaker.io', // new default +}); +``` + +## Convert Client Restructured + +The old `client.convert` was a single flat client. It is now a **namespace** with specialized sub-clients. + +### Before (v0.x) + +```typescript +// Single convert client for AI vectorization only +const result = await client.convert + .configure({ file: './photo.jpg', svgText: true }) + .execute(); +``` + +### After (v1.0.0) + +```typescript +// AI vectorization +const result = await client.convert.aiVectorize + .configure({ file: './photo.jpg', svgText: true }) + .execute(); + +// New: algorithmic tracing +const traced = await client.convert.trace + .configure({ file: './logo.png' }) + .execute(); + +// New: SVG to vector formats (PDF, EPS, DXF, AI, PS) +const pdf = await client.convert.svgToVector + .configure({ file: './icon.svg', format: 'pdf' }) + .execute(); + +// New: raster-to-raster conversion +const webp = await client.convert.rasterToRaster + .configure({ file: './photo.jpg', format: 'webp' }) + .execute(); + +// New: batch conversion (up to 10 files) +const batch = await client.convert.batch + .configure({ files: ['./a.png', './b.png'], format: 'webp' }) + .execute(); +``` + +**Action required:** Replace `client.convert.configure(...)` with `client.convert.aiVectorize.configure(...)`. + +## Generate: Style Parameters Overhauled + +The `styleParams` options have been completely redesigned to match the v1 API. + +### Before (v0.x) + +```typescript +await client.generate.configure({ + prompt: 'A mountain icon', + styleParams: { + style: 'minimalist', // removed + color_mode: '2-colors', // values changed + image_complexity: 'simple', // removed + category: 'icon', // removed + composition: 'center-object', // values changed + advanced: { // removed entirely + stroke_weight: 'thin', + corner_style: 'rounded', + shadow_effect: 'soft', + }, + }, +}).execute(); +``` + +### After (v1.0.0) + +```typescript +await client.generate.configure({ + prompt: 'A mountain icon', + styleParams: { + style: 'flat', // new values: flat, line_art, engraving, linocut, silhouette, isometric, cartoon, ghibli + color_mode: 'few_colors', // new values: full_color, monochrome, few_colors + image_complexity: 'icon', // now: icon, illustration, scene + text: 'only_title', // new field + composition: 'centered_object', // new values: centered_object, repeating_pattern, full_scene, objects_in_grid + }, +}).execute(); +``` + +### Key differences + +| Field | v0.x | v1.0.0 | +|-------|------|--------| +| `style` | minimalist, cartoon, realistic, abstract, flat, isometric | flat, line_art, engraving, linocut, silhouette, isometric, cartoon, ghibli | +| `color_mode` | monochrome, 2-colors, 3-colors, full-color | full_color, monochrome, few_colors | +| `image_complexity` | simple, detailed | icon, illustration, scene | +| `composition` | center-object, full-scene | centered_object, repeating_pattern, full_scene, objects_in_grid | +| `category` | icon, illustration, pattern, logo, scene | **removed** (use `image_complexity` instead) | +| `advanced` | stroke_weight, corner_style, shadow_effect | **removed** | +| `text` | — | **new**: only_title, embedded_text | + +## New `model` Parameter (Generate & Edit) + +Both `generate` and `edit` now accept a `model` parameter to specify the AI model directly instead of using `quality`: + +```typescript +// Using quality (still works) +await client.generate.configure({ prompt: '...', quality: 'high' }).execute(); + +// Using model directly (new — works on both generate and edit) +await client.generate.configure({ prompt: '...', model: 'flux-fast' }).execute(); +await client.edit.configure({ image: './input.png', prompt: '...', model: 'flux-fast' }).execute(); +``` + +**Note:** `model` and `quality` are mutually exclusive — you cannot use both. + +## Generate: Aspect Ratio Values Changed + +High quality no longer requires explicit aspect ratio. The `wide` and `tall` aspect ratios have been removed. + +| v0.x | v1.0.0 | +|------|--------| +| auto, portrait, landscape, square, wide, tall | auto, portrait, landscape, square | + +## Cloud Storage Default Changed + +In v0.x, `generate`, `edit`, and `convert` (now `convert.aiVectorize`) **stored results to cloud by default**. In v1.0.0, **results are not stored by default**. You must explicitly opt in with `storage: true`. + +### Before (v0.x) + +```typescript +// Results were automatically stored to your account +const result = await client.generate + .configure({ prompt: 'A logo' }) + .execute(); +// result.svgUrl pointed to a persisted cloud URL +``` + +### After (v1.0.0) + +```typescript +// Results are NOT stored by default — URLs are temporary +const result = await client.generate + .configure({ prompt: 'A logo' }) + .execute(); + +// To persist results, explicitly pass storage: true +const stored = await client.generate + .configure({ prompt: 'A logo', storage: true }) + .execute(); +// stored.svgUrl is now a permanent cloud URL +``` + +This applies to `generate`, `edit`, and `convert.aiVectorize`. If your application depends on persisted URLs (e.g. saving them to a database), add `storage: true` to those calls. + +## New Error Types + +v1.0.0 adds error classes for more precise error handling: + +```typescript +import { Errors } from '@genwave/svgmaker-sdk'; + +try { + await client.generate.configure({ prompt: '...' }).execute(); +} catch (error) { + if (error instanceof Errors.ContentSafetyError) { + // prompt flagged by content moderation + } else if (error instanceof Errors.FileSizeError) { + // uploaded file exceeds size limit + } else if (error instanceof Errors.FileFormatError) { + // unsupported file format + } else if (error instanceof Errors.EndpointDisabledError) { + // endpoint temporarily unavailable + } +} +``` + +All new error types extend `APIError`, so existing `catch (error instanceof Errors.APIError)` blocks will still catch them. + +## New Clients + +v1.0.0 adds these clients that did not exist in v0.x: + +| Client | Access | Purpose | +|--------|--------|---------| +| `enhancePrompt` | `client.enhancePrompt` | Improve prompts with AI before generating | +| `optimizeSvg` | `client.optimizeSvg` | Optimize SVG files using SVGO | +| `generations` | `client.generations` | List, download, share, delete your generations | +| `gallery` | `client.gallery` | Browse and download public gallery items | +| `account` | `client.account` | Account info and credit usage | + +## Response Types Updated + +Response types now include a `metadata` field with request tracking: + +```typescript +const result = await client.generate.configure({ prompt: '...' }).execute(); +// result.metadata.requestId — unique request ID for support inquiries +``` + +## Summary Checklist + +- [ ] Replace `client.convert.configure(...)` with `client.convert.aiVectorize.configure(...)` +- [ ] Update `styleParams` to use new enum values (if used) +- [ ] Remove `advanced` style params (if used) +- [ ] Remove `wide`/`tall` aspect ratios (if used) +- [ ] Remove hardcoded `baseUrl` pointing to `svgmaker.io/api` (if set) +- [ ] Add `storage: true` to generate/edit/aiVectorize calls if you need persisted cloud URLs +- [ ] Update error handling to use new error types (optional, existing catches still work)