Back to SDKs

Node.js SDK Guide

A complete, runnable example for integrating Zywrap's V1 offline data (including dynamic schemas) into your Node.js (Express) application.

Option 1: Cloud Proxy (Quick Start)

Don't want to host the offline data yourself? Use our official NPM package to connect directly to the Zywrap Cloud Proxy in seconds.

bash
npm install zywrap
javascript
import Zywrap from 'zywrap';

// Initialize the client
const zywrap = new Zywrap('YOUR_API_KEY');

async function main() {
  try {
    const response = await zywrap.execute({
      wrapperCodes: ['mc_seo_meta_titles_descriptions_base'],
      variables: {
        pageTopic: "AI project management software",
        pageIntent: "Compare software options",
        primaryKeyword: "agency project management",
        brandVoice: "clear and trustworthy"
      },
      model: 'openai-gpt-5.4'
    });

    console.log(response.data);
  } catch (error) {
    console.error("Execution failed:", error.message);
  }
}

main();
OR

Option 2: Advanced Local Database Sync

Self-host the wrapper data to ensure zero-latency lookups, complete offline capability, and highly customized API integrations.

Step 1: Download Your Data Bundle

Download the V1 ZIP file containing the highly compressed Tabular JSON data. Unzip the zywrap-data.json file from the bundle to use in the import script.

You must be logged in to download the SDK data bundle.

Step 2: Database Setup

Run this SQL. It features the relational use_cases table containing the dynamic schema_data.

sql

-- Database Schema for Zywrap Offline SDK v1.0 (PostgreSQL)

CREATE TABLE "ai_models" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" INT
);

CREATE TABLE "categories" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" INT
);

CREATE TABLE "languages" (
  "code" VARCHAR(10) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" INT
);

CREATE TABLE "use_cases" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "description" TEXT,
  "category_code" VARCHAR(255) REFERENCES categories(code) ON DELETE SET NULL,
  "schema_data" JSONB,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" BIGINT
);

CREATE TABLE "wrappers" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "description" TEXT,
  "use_case_code" VARCHAR(255) REFERENCES use_cases(code) ON DELETE SET NULL,
  "featured" BOOLEAN DEFAULT FALSE,
  "base" BOOLEAN DEFAULT FALSE,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" BIGINT
);

CREATE TABLE "block_templates" (
  "type" VARCHAR(50) NOT NULL,
  "code" VARCHAR(255) NOT NULL,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  PRIMARY KEY ("type", "code")
);

CREATE TABLE "settings" (
  "setting_key" VARCHAR(255) PRIMARY KEY,
  "setting_value" TEXT
);

CREATE TABLE "usage_logs" (
  "id" BIGSERIAL PRIMARY KEY,
  "trace_id" VARCHAR(255),
  "wrapper_code" VARCHAR(255),
  "model_code" VARCHAR(255),
  "prompt_tokens" INT DEFAULT 0,
  "completion_tokens" INT DEFAULT 0,
  "total_tokens" INT DEFAULT 0,
  "credits_used" BIGINT DEFAULT 0,
  "latency_ms" INT DEFAULT 0,
  "status" VARCHAR(50) DEFAULT 'success',
  "error_message" TEXT,
  "created_at" TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_usage_wrapper ON usage_logs(wrapper_code);
CREATE INDEX idx_usage_model ON usage_logs(model_code);
CREATE INDEX idx_use_case_cat ON use_cases(category_code);
CREATE INDEX idx_wrapper_uc ON wrappers(use_case_code);

Step 3: Database Connection

Save this as db.js. We recommend using the pg package for PostgreSQL.

javascript

// FILE: db.js
// Uses the popular 'pg' library for PostgreSQL
// npm install pg

const { Pool } = require('pg');

// Replace with your actual database credentials
const pool = new Pool({
  user: 'postgres',
  host: 'localhost',
  database: 'zywrap_db',
  password: 'password',
  port: 5432,
});

pool.on('error', (err, client) => {
  console.error('Unexpected error on idle client', err);
  process.exit(-1);
});

module.exports = {
  query: (text, params) => pool.query(text, params),
  pool
};

Step 4: Import Tabular Data

This script parses the compressed V1 JSON and securely bulk-inserts it into your database.

javascript

// FILE: import.js
// USAGE: node import.js
// This script assumes you have 'zywrap-data.json' in the same directory.
// Tip: To prevent memory limits on massive JSON files, run with: node --max-old-space-size=4096 import.js

const fs = require('fs/promises');
const { pool } = require('./db');

// Helper to expand tabular JSON data into arrays of objects
function extractTabular(tabularData) {
    if (!tabularData || !tabularData.cols || !tabularData.data) return [];
    const cols = tabularData.cols;
    return tabularData.data.map(row => {
        const obj = {};
        cols.forEach((col, i) => obj[col] = row[i]);
        return obj;
    });
}

async function main() {
    console.log('Starting lightning-fast v1.0 data import...');

    let data;
    try {
        const jsonFile = await fs.readFile('zywrap-data.json', 'utf8');
        data = JSON.parse(jsonFile);
    } catch (e) {
        console.error('FATAL: zywrap-data.json not found or invalid.', e.message);
        process.exit(1);
    }

    const client = await pool.connect();
    try {
        await client.query('BEGIN');

        console.log('Clearing tables...');
        await client.query('TRUNCATE wrappers, use_cases, categories, languages, block_templates, ai_models, settings RESTART IDENTITY CASCADE');

        // 1. Import Categories
        if (data.categories) {
            for (const c of extractTabular(data.categories)) {
                await client.query(
                    'INSERT INTO categories (code, name, status, ordering) VALUES ($1, $2, TRUE, $3)', 
                    [c.code, c.name, c.ordering ?? 99999]
                );
            }
            console.log('Categories imported successfully.');
        }
        
        // 2. Import Use Cases
        if (data.useCases) {
            for (const uc of extractTabular(data.useCases)) {
                const schemaJson = uc.schema ? JSON.stringify(uc.schema) : null;
                await client.query(
                    'INSERT INTO use_cases (code, name, description, category_code, schema_data, status, ordering) VALUES ($1, $2, $3, $4, $5, TRUE, $6)', 
                    [uc.code, uc.name, uc.desc, uc.cat, schemaJson, uc.ordering ?? 999999999]
                );
            }
            console.log('Use Cases imported successfully.');
        }

        // 3. Import Wrappers
        if (data.wrappers) {
            for (const w of extractTabular(data.wrappers)) {
                await client.query(
                    'INSERT INTO wrappers (code, name, description, use_case_code, featured, base, status, ordering) VALUES ($1, $2, $3, $4, $5, $6, TRUE, $7)',
                    [w.code, w.name, w.desc, w.usecase, !!w.featured, !!w.base, w.ordering ?? 999999999]
                );
            }
            console.log('Wrappers imported successfully.');
        }

        // 4. Import Languages
        if (data.languages) {
            let ord = 1;
            for (const l of extractTabular(data.languages)) {
                await client.query(
                    'INSERT INTO languages (code, name, status, ordering) VALUES ($1, $2, TRUE, $3)', 
                    [l.code, l.name, ord++]
                );
            }
            console.log('Languages imported successfully.');
        }

        // 5. Import AI Models
        if (data.aiModels) {
            for (const m of extractTabular(data.aiModels)) {
                await client.query(
                    'INSERT INTO ai_models (code, name, status, ordering) VALUES ($1, $2, TRUE, $3)',
                    [m.code, m.name, m.ordering ?? 99999]
                );
            }
            console.log('AI Models imported successfully.');
        }

        // 6. Import Block Templates
        if (data.templates) {
            for (const [type, tabular] of Object.entries(data.templates)) {
                for (const tpl of extractTabular(tabular)) {
                    await client.query(
                        'INSERT INTO block_templates (type, code, name, status) VALUES ($1, $2, $3, TRUE)', 
                        [type, tpl.code, tpl.name]
                    );
                }
            }
            console.log('Block templates imported successfully.');
        }

        // 7. Store Version
        if (data.version) {
            await client.query(
                "INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', $1) ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value",
                [data.version]
            );
            console.log('Data version saved to settings table.');
        }

        await client.query('COMMIT');
        console.log('\n✅ v1.0 Import complete! Version: ' + (data.version || 'N/A'));

    } catch (e) {
        await client.query('ROLLBACK');
        console.error('FATAL: Database error during import. Transaction rolled back.', e.message);
        process.exit(1);
    } finally {
        client.release();
        pool.end();
    }
}

main();

Step 5: The Dynamic API & Playground

This Express server reads from your local DB, handles dynamic schemas, forwards the final request to the Zywrap API, and logs local usage automatically.

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Zywrap V1 Offline Playground</title>
    <style>
        :root {
            --primary: #2563eb; --primary-hover: #1d4ed8;
            --bg: #f8fafc; --card-bg: #ffffff;
            --text-main: #0f172a; --text-muted: #64748b;
            --border: #e2e8f0; --radius: 0.75rem;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text-main); line-height: 1.5; padding: 2rem; }
        .container { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1.8fr; gap: 2rem; }
        @media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
        
        .card { background: var(--card-bg); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 1.5rem; }
        .card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem; color: var(--text-main); }
        
        .form-group { margin-bottom: 1rem; }
        label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 0.35rem; color: #334155; }
        
        .input, select, textarea { 
            width: 100%; padding: 0.625rem 0.75rem; border: 1px solid var(--border); 
            border-radius: 0.5rem; font-size: 0.875rem; background: #fff; transition: border-color 0.2s; 
        }
        .input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
        textarea { resize: vertical; min-height: 80px; }
        
        .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
        
        .btn { 
            width: 100%; background: var(--primary); color: white; border: none; padding: 0.875rem; 
            border-radius: 0.5rem; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s; 
        }
        .btn:hover { background: var(--primary-hover); }
        .btn:disabled { opacity: 0.7; cursor: not-allowed; }
        
        .schema-section { background: #f8fafc; border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; }
        .schema-title { font-size: 0.875rem; font-weight: 700; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 0.75rem; color: #475569; }
        
        #output { background: #f1f5f9; padding: 1.25rem; border-radius: 0.5rem; font-family: ui-monospace, monospace; font-size: 0.875rem; white-space: pre-wrap; min-height: 300px; border: 1px solid var(--border); overflow-x: auto; }
    </style>
</head>
<body>
    <div class="container">
        <div>
            <div class="card">
                <h2 class="card-title">Configuration</h2>
                
                <div class="form-group">
                    <label>1. Category</label>
                    <select id="category"><option value="">Loading...</option></select>
                </div>
                
                <div class="form-group">
                    <label>2. AI Solution</label>
                    <select id="usecase" disabled><option value="">-- Select Category First --</option></select>
                </div>

                <div class="form-group">
                    <label>3. Configuration Style</label>
                    <select id="wrapper" disabled><option value="">-- Select Solution First --</option></select>
                </div>

                <div class="form-group">
                    <label>4. AI Model</label>
                    <select id="model"><option value="">Loading...</option></select>
                </div>

                <div class="form-group">
                    <label>5. Target Language</label>
                    <select id="language"><option value="">Loading...</option></select>
                </div>
            </div>

            <div class="card">
                <h2 class="card-title" style="font-size: 1rem;">Advanced Overrides</h2>
                <div class="grid-2 overrides">
                    <div><label>Tone</label><select id="toneCode"><option value="">-- Default --</option></select></div>
                    <div><label>Style</label><select id="styleCode"><option value="">-- Default --</option></select></div>
                    <div><label>Formatting</label><select id="formatCode"><option value="">-- Default --</option></select></div>
                    <div><label>Complexity</label><select id="complexityCode"><option value="">-- Default --</option></select></div>
                    <div><label>Length</label><select id="lengthCode"><option value="">-- Default --</option></select></div>
                    <div><label>Audience</label><select id="audienceCode"><option value="">-- Default --</option></select></div>
                    <div><label>Goal</label><select id="responseGoalCode"><option value="">-- Default --</option></select></div>
                    <div><label>Output Type</label><select id="outputCode"><option value="">-- Default --</option></select></div>
                </div>
            </div>
        </div>

        <div>
            <div class="card">
                <div id="dynamic-schema-container"></div>
                
                <div class="form-group">
                    <label id="prompt-label">Prompt / Additional Context</label>
                    <textarea id="prompt" class="input" placeholder="Type your request or additional instructions here..."></textarea>
                </div>
                
                <button id="run-button" class="btn">Generate Response</button>
            </div>

            <div class="card">
                <h2 class="card-title">AI Response</h2>
                <pre id="output">Output will appear here...</pre>
            </div>
        </div>
    </div>

    <script>
        const API_ENDPOINT = 'http://localhost:5000/api';
        
        const elements = {
            category: document.getElementById('category'),
            usecase: document.getElementById('usecase'),
            wrapper: document.getElementById('wrapper'),
            model: document.getElementById('model'),
            language: document.getElementById('language'),
            schemaContainer: document.getElementById('dynamic-schema-container'),
            prompt: document.getElementById('prompt'),
            promptLabel: document.getElementById('prompt-label'),
            runBtn: document.getElementById('run-button'),
            output: document.getElementById('output')
        };

        async function fetchAPI(action, params = '') {
            const res = await fetch(API_ENDPOINT + '?action=' + action + '&' + params);
            return await res.json();
        }

        function populateSelect(el, data, placeholder = '-- Select --') {
            el.innerHTML = '<option value="">' + placeholder + '</option>';
            data.forEach(item => {
                const opt = document.createElement('option');
                opt.value = item.code; 
                opt.textContent = item.name;
                el.appendChild(opt);
            });
            el.disabled = false;
        }

        async function init() {
            const [categories, models, langs, templates] = await Promise.all([
                fetchAPI('get_categories'),
                fetchAPI('get_ai_models'),
                fetchAPI('get_languages'),
                fetchAPI('get_block_templates')
            ]);
            
            populateSelect(elements.category, categories);
            populateSelect(elements.model, models, 'Default Model');
            populateSelect(elements.language, langs, 'English (Default)');

            const overrideMap = {
                tones: 'toneCode', styles: 'styleCode', formattings: 'formatCode',
                complexities: 'complexityCode', lengths: 'lengthCode', audienceLevels: 'audienceCode',
                responseGoals: 'responseGoalCode', outputTypes: 'outputCode'
            };
            for (const [type, elId] of Object.entries(overrideMap)) {
                if (templates[type]) populateSelect(document.getElementById(elId), templates[type], '-- Default --');
            }
        }

        // CASCADING LOGIC 1: Category -> Load Use Cases
        elements.category.addEventListener('change', async () => {
            if (!elements.category.value) {
                elements.usecase.innerHTML = '<option value="">-- Select Category First --</option>';
                elements.usecase.disabled = true;
                elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            elements.usecase.innerHTML = '<option value="">Loading...</option>';
            const useCases = await fetchAPI('get_use_cases', 'category=' + elements.category.value);
            populateSelect(elements.usecase, useCases, '-- Select a Solution --');
            
            elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
            elements.wrapper.disabled = true;
            elements.schemaContainer.innerHTML = '';
        });

        // CASCADING LOGIC 2: Use Case -> Load Wrappers
        elements.usecase.addEventListener('change', async () => {
            if (!elements.usecase.value) {
                elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            
            elements.wrapper.innerHTML = '<option value="">Loading...</option>';
            let wrappers = await fetchAPI('get_wrappers', 'usecase=' + elements.usecase.value);
            
            elements.wrapper.innerHTML = '<option value="">-- Select a Style --</option>';
            
            let autoSelectCode = null; 

            wrappers.forEach((w, index) => {
                const opt = document.createElement('option');
                opt.value = w.code;
                const parts = w.name.split('—');
                const displayName = w.base ? '✨ Base Template - ' + parts[0].trim() : '↳ Variation: ' + (parts.length > 1 ? parts[1].trim() : w.name);
                opt.textContent = displayName;
                elements.wrapper.appendChild(opt);

                if (w.base) {
                    autoSelectCode = w.code;
                } else if (index === 0 && !autoSelectCode) {
                    autoSelectCode = w.code;
                }
            });
            elements.wrapper.disabled = false;

            if (autoSelectCode) {
                elements.wrapper.value = autoSelectCode;
                elements.wrapper.dispatchEvent(new Event('change')); 
            }
        });

        // CASCADING LOGIC 3: Wrapper -> Load Schema
        elements.wrapper.addEventListener('change', async () => {
            elements.schemaContainer.innerHTML = '';
            elements.promptLabel.textContent = "Prompt / Additional Context";
            if (!elements.wrapper.value) return;

            const schema = await fetchAPI('get_schema', 'wrapper=' + elements.wrapper.value);
            if (!schema || (!schema.req && !schema.opt)) return;

            let html = '';
            elements.promptLabel.textContent = "Additional Free-form Instructions";

            const buildSection = (title, data) => {
                if (!data || Object.keys(data).length === 0) return '';
                let sectionHtml = '<div class="schema-section"><h3 class="schema-title">' + title + '</h3><div class="grid-2">';
                for (const [key, def] of Object.entries(data)) {
                    const isPlaceholder = def.p !== undefined ? def.p : false;
                    const defaultVal = def.d !== undefined ? def.d : '';
                    const placeholderAttr = isPlaceholder ? 'placeholder="' + defaultVal + '"' : '';
                    const valueAttr = (!isPlaceholder && defaultVal) ? 'value="' + defaultVal + '"' : '';
                    const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
                    
                    sectionHtml += '<div><label>' + label + '</label><input type="text" class="input schema-input" data-key="' + key + '" ' + placeholderAttr + ' ' + valueAttr + '></div>';
                }
                return sectionHtml + '</div></div>';
            };

            html += buildSection('Core Inputs', schema.req);
            html += buildSection('Additional Context', schema.opt);
            elements.schemaContainer.innerHTML = html;
        });

        // Execute
        elements.runBtn.addEventListener('click', async () => {
            if (!elements.wrapper.value) return alert("Please select a wrapper.");
            elements.output.textContent = 'Executing...';
            elements.runBtn.disabled = true;
            elements.runBtn.textContent = 'Generating...';
            
            // 1. Grab the exact user prompt
            let finalPrompt = elements.prompt.value.trim();
            
            // 2. Build the variables object natively
            const variables = {};
            document.querySelectorAll('.schema-input').forEach(input => {
                const val = input.value.trim();
                if (val !== '') {
                    variables[input.dataset.key] = val;
                }
            });

            // 3. Build overrides
            const overrides = {};
            document.querySelectorAll('.overrides select').forEach(sel => {
                if (sel.value) overrides[sel.id] = sel.value;
            });

            try {
                const response = await fetch(API_ENDPOINT + '?action=execute', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        model: elements.model.value,
                        wrapperCode: elements.wrapper.value,
                        language: elements.language.value,
                        prompt: finalPrompt, // Only the raw text area
                        variables: variables, // Only the object (backend formats it!)
                        overrides: overrides
                    })
                });
                
                const text = await response.text();
                if (!response.ok) {
                    let errMsg = text;
                    try { errMsg = JSON.parse(text).error || errMsg; } catch(e) {}
                    throw new Error(errMsg);
                }
                
                const data = JSON.parse(text);
                elements.output.textContent = data.output || JSON.stringify(data, null, 2);
            } catch (error) {
                elements.output.textContent = 'Error: ' + error.message;
            } finally {
                elements.runBtn.disabled = false;
                elements.runBtn.textContent = 'Generate Response';
            }
        });

        init();
    </script>
</body>
</html>
javascript

// FILE: server.js
// A simple Express server to replicate the 'api.php' V1 playground backend.
//
// REQUIREMENTS:
// npm install express cors pg
// (Requires Node 18+ for native fetch)
//
// USAGE:
// 1. Save this as 'server.js'
// 2. Run: node server.js
// 3. Open 'playground.html' in your browser.

const express = require('express');
const cors = require('cors');
const { pool } = require('./db.js');

const app = express();
app.use(cors());
app.use(express.json());

const ZYWRAP_API_KEY = "YOUR_ZYWRAP_API_KEY";
const ZYWRAP_PROXY_URL = 'https://api.zywrap.com/v1/proxy';

// --- Database Helper Functions ---

async function getCategories() {
    const res = await pool.query("SELECT code, name FROM categories WHERE status = TRUE ORDER BY ordering ASC");
    return res.rows;
}

// 🚀 NEW: Fetch Solutions (Use Cases) by Category
async function getUseCases(categoryCode) {
    const res = await pool.query(
        "SELECT code, name FROM use_cases WHERE category_code = $1 AND status = TRUE ORDER BY ordering ASC",
        [categoryCode]
    );
    return res.rows;
}

// 🚀 UPDATED: Fetch Wrappers (Styles) by Use Case
async function getWrappersByUseCase(useCaseCode) {
    const res = await pool.query(
        "SELECT code, name, featured, base FROM wrappers WHERE use_case_code = $1 AND status = TRUE ORDER BY ordering ASC",
        [useCaseCode]
    );
    return res.rows;
}

async function getSchemaByWrapper(wrapperCode) {
    const res = await pool.query(
        "SELECT uc.schema_data FROM use_cases uc JOIN wrappers w ON w.use_case_code = uc.code WHERE w.code = $1 AND w.status = TRUE AND uc.status = TRUE",
        [wrapperCode]
    );
    return res.rows[0] ? res.rows[0].schema_data : null;
}

async function getLanguages() {
    const res = await pool.query("SELECT code, name FROM languages WHERE status = TRUE ORDER BY ordering ASC");
    return res.rows;
}

async function getAiModels() {
    const res = await pool.query("SELECT code, name FROM ai_models WHERE status = TRUE ORDER BY ordering ASC");
    return res.rows;
}

async function getBlockTemplates() {
    const res = await pool.query("SELECT type, code, name FROM block_templates WHERE status = TRUE ORDER BY type, name ASC");
    const grouped = {};
    res.rows.forEach(row => {
        if (!grouped[row.type]) grouped[row.type] = [];
        grouped[row.type].push({ code: row.code, name: row.name });
    });
    return grouped;
}

// ✅ HYBRID PROXY EXECUTION
async function executeZywrapProxy(apiKey, model, wrapperCode, prompt, language, variables = {}, overrides = {}) {
    const payloadData = {
        model: model,
        wrapperCodes: [wrapperCode],
        prompt: prompt,
        variables: variables,
        source: 'node_sdk'
    };
    
    if (language) payloadData.language = language;
    if (Object.keys(overrides).length > 0) Object.assign(payloadData, overrides);
        
    const cleanKey = apiKey.trim();
    
    try {
        const response = await fetch(ZYWRAP_PROXY_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + cleanKey,
                'User-Agent': 'ZywrapNodeSDK/1.1'
            },
            body: JSON.stringify(payloadData)
        });

        if (!response.ok) {
            const errText = await response.text();
            try { return { status: response.status, data: JSON.parse(errText) }; }
            catch (e) { return { status: response.status, data: { error: errText } }; }
        }

        // Parse SSE Stream
        const text = await response.text();
        const lines = text.split('\n');
        let finalJson = null;
        
        for (const line of lines) {
            const trimmed = line.trim();
            if (trimmed.startsWith('data: ')) {
                const jsonStr = trimmed.substring(6);
                try {
                    const data = JSON.parse(jsonStr);
                    if (data && (data.output || data.error)) finalJson = data;
                } catch (e) { /* ignore parse errors on partial streams */ }
            }
        }

        if (finalJson) {
            return { status: finalJson.error ? 400 : 200, data: finalJson };
        } else {
            return { status: 500, data: { error: 'Stream parse failed' } };
        }

    } catch (e) {
        return { status: 500, data: { error: e.message } };
    }
}

// --- API Router ---
app.all('/api', async (req, res) => {
    try {
        const action = req.query.action || (req.body && req.body.action);

        if (req.method === 'GET') {
            if (action === 'get_categories') return res.json(await getCategories());
            if (action === 'get_use_cases') return res.json(await getUseCases(req.query.category));
            if (action === 'get_wrappers') return res.json(await getWrappersByUseCase(req.query.usecase));
            if (action === 'get_languages') return res.json(await getLanguages());
            if (action === 'get_ai_models') return res.json(await getAiModels());
            if (action === 'get_block_templates') return res.json(await getBlockTemplates());
            if (action === 'get_schema') return res.json(await getSchemaByWrapper(req.query.wrapper));
        }

        if (req.method === 'POST') {
            if (action === 'execute') {
                const startTime = Date.now();
                const inputData = req.body;
                
                const executionResult = await executeZywrapProxy(
                    ZYWRAP_API_KEY,
                    inputData.model,
                    inputData.wrapperCode || '',
                    inputData.prompt || '',
                    inputData.language,
                    inputData.variables || {},
                    inputData.overrides || {}
                );
                
                const status = executionResult.status;
                const result = executionResult.data;
                const latencyMs = Date.now() - startTime;

                // Async Logging
                try {
                    const statusText = status === 200 ? 'success' : 'error';
                    const traceId = result.id || null;
                    
                    const usage = result.usage || {};
                    const pTokens = usage.prompt_tokens || 0;
                    const cTokens = usage.completion_tokens || 0;
                    const tTokens = usage.total_tokens || 0;
                    
                    const creditsUsed = (result.cost || {}).credits_used || 0;
                    let errorMessage = statusText === 'error' ? result.error : null;
                    
                    if (errorMessage) {
                        const errMsgStr = String(errorMessage);
                        errorMessage = errMsgStr.length > 255 ? errMsgStr.substring(0, 252) + '...' : errMsgStr;
                    }

                    await pool.query(
                        "INSERT INTO usage_logs (trace_id, wrapper_code, model_code, prompt_tokens, completion_tokens, total_tokens, credits_used, latency_ms, status, error_message) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
                        [traceId, inputData.wrapperCode, inputData.model || 'default', pTokens, cTokens, tTokens, creditsUsed, latencyMs, statusText, errorMessage]
                    );
                } catch (logErr) {
                    console.error("Failed to write to usage_logs:", logErr.message);
                }

                return res.status(status).json(result);
            }
        }
        
        return res.status(400).json({ error: 'Invalid action' });

    } catch (e) {
        return res.status(500).json({ error: e.message });
    }
});

const PORT = 5000;
app.listen(PORT, () => {
    console.log("Zywrap Node SDK Playground backend listening at http://localhost:" + PORT);
});

Step 6: Synchronize Data

Run this script on a cron job. It features automatic unzipping for FULL resets and ON CONFLICT optimized upserts for DELTA updates.

javascript

// FILE: zywrap-sync.js
// USAGE: node zywrap-sync.js
// DEPENDENCIES: npm install axios pg adm-zip

const axios = require('axios');
const fs = require('fs');
const AdmZip = require('adm-zip');
const { execSync } = require('child_process');
const { pool } = require('./db.js'); 

// --- CONFIGURATION ---
const API_KEY = 'YOUR_ZYWRAP_API_KEY_HERE'; 
const API_URL = 'https://api.zywrap.com/v1/sdk/v1/sync';

// --- HELPER FUNCTIONS ---

async function upsertBatch(client, tableName, rows, columns, pk = 'code') {
    if (!rows.length) return;
    
    const BATCH_SIZE = 1000; 
    const colNames = columns.map(c => '"' + c + '"').join(', ');
    const updateCols = columns
        .filter(c => c !== pk && c !== 'type')
        .map(c => '"' + c + '" = EXCLUDED."' + c + '"')
        .join(', ');
    const conflictTarget = pk === 'compound_template' ? '(type, code)' : '("' + pk + '")';

    for (let i = 0; i < rows.length; i += BATCH_SIZE) {
        const chunk = rows.slice(i, i + BATCH_SIZE);
        const values = [];
        const rowPlaceholders = [];
        let counter = 1;

        for (const row of chunk) {
            const rowPh = [];
            for (const cell of row) {
                rowPh.push('$' + counter++);
                values.push(cell);
            }
            rowPlaceholders.push('(' + rowPh.join(', ') + ')');
        }

        const query = "INSERT INTO " + tableName + " (" + colNames + ") VALUES " + rowPlaceholders.join(', ') + " ON CONFLICT " + conflictTarget + " DO UPDATE SET " + updateCols;

        try {
            await client.query(query, values);
        } catch (e) {
            console.error("\n   [!] Error upserting batch in " + tableName + ":", e.message);
            throw e; 
        }
    }
    console.log("   [+] Upserted " + rows.length + " records into " + tableName);
}

async function deleteBatch(client, tableName, ids, pk = 'code') {
    if (!ids.length) return;
    const BATCH_SIZE = 2000;
    for (let i = 0; i < ids.length; i += BATCH_SIZE) {
        const chunk = ids.slice(i, i + BATCH_SIZE);
        await client.query('DELETE FROM "' + tableName + '" WHERE "' + pk + '" = ANY($1)', [chunk]);
    }
    console.log("   [-] Deleted " + ids.length + " records from " + tableName);
}

// --- MAIN ---
(async () => {
    console.log('--- 🚀 Starting Zywrap V1 Sync ---');
    const client = await pool.connect();
    
    try {
        const verRes = await client.query("SELECT setting_value FROM settings WHERE setting_key = 'data_version'");
        const localVersion = verRes.rows[0]?.setting_value || '';
        console.log("🔹 Local Version: " + (localVersion || 'None'));

        const response = await axios.get(API_URL, {
            params: { fromVersion: localVersion },
            headers: { 'Authorization': 'Bearer ' + API_KEY }
        });
        
        const json = response.data;
        console.log("🔹 Sync Mode: " + json.mode);

        if (json.mode === 'FULL_RESET') {
            const zipPath = 'zywrap-data.zip';
            const downloadUrl = json.wrappers.downloadUrl;

            console.log("⬇️  Attempting automatic download from Zywrap...");
            
            try {
                const dl = await axios({
                    url: downloadUrl,
                    method: 'GET',
                    responseType: 'arraybuffer',
                    headers: { 'Authorization': 'Bearer ' + API_KEY }
                });

                fs.writeFileSync(zipPath, dl.data);
                const mbSize = (fs.statSync(zipPath).size / (1024 * 1024)).toFixed(2);
                console.log("✅ Data bundle downloaded successfully (" + mbSize + " MB).");
                
                try {
                    console.log('📦 Attempting auto-unzip...');
                    const zip = new AdmZip(zipPath);
                    zip.extractAllTo(__dirname, true);
                    console.log('✅ Auto-unzip successful. Running import script...');
                    
                    fs.unlinkSync(zipPath);
                    
                    // Run the import script automatically
                    execSync('node import.js', { stdio: 'inherit' });
                    
                } catch (zErr) {
                    console.log("⚠️ Failed to auto-unzip (Check directory permissions).");
                    console.log("\n👉 ACTION REQUIRED:");
                    console.log("   1. Please manually unzip '" + zipPath + "' in this folder.");
                    console.log("   2. Then run: node import.js");
                }

            } catch (dlErr) {
                if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
                console.log("❌ Automatic download failed. HTTP Status: " + (dlErr.response?.status || 'Unknown'));
            }

        } else if (json.mode === 'DELTA_UPDATE') {
            await client.query('BEGIN');
            const meta = json.metadata || {};

            // Categories
            if (meta.categories) {
                const rows = meta.categories.map(r => [r.code, r.name, r.status ?? true, r.position || r.displayOrder || r.ordering]);
                await upsertBatch(client, 'categories', rows, ['code', 'name', 'status', 'ordering']);
            }

            // Languages
            if (meta.languages) {
                const rows = meta.languages.map(r => [r.code, r.name, r.status ?? true, r.ordering]);
                await upsertBatch(client, 'languages', rows, ['code', 'name', 'status', 'ordering']);
            }

            // AI Models
            if (meta.aiModels) {
                const rows = meta.aiModels.map(r => [r.code, r.name, r.status ?? true, r.displayOrder || r.ordering]);
                await upsertBatch(client, 'ai_models', rows, ['code', 'name', 'status', 'ordering']);
            }

            // Templates
            if (meta.templates) {
                const rows = [];
                for (const [type, items] of Object.entries(meta.templates)) {
                    for (const i of items) rows.push([type, i.code, i.label || i.name, i.status ?? true]);
                }
                await upsertBatch(client, 'block_templates', rows, ['type', 'code', 'name', 'status'], 'compound_template');
            }

            // Use Cases
            if (json.useCases?.upserts?.length) {
                const rows = json.useCases.upserts.map(uc => [
                    uc.code, uc.name, uc.description, uc.categoryCode, 
                    uc.schema ? JSON.stringify(uc.schema) : null, 
                    uc.status ?? true, uc.displayOrder || uc.ordering
                ]);
                await upsertBatch(client, 'use_cases', rows, ['code', 'name', 'description', 'category_code', 'schema_data', 'status', 'ordering']);
            }

            // Wrappers
            if (json.wrappers?.upserts?.length) {
                const rows = json.wrappers.upserts.map(w => [
                    w.code, w.name, w.description, w.useCaseCode || w.categoryCode, 
                    !!(w.featured || w.isFeatured), !!(w.base || w.isBaseWrapper), 
                    w.status ?? true, w.displayOrder || w.ordering
                ]);
                await upsertBatch(client, 'wrappers', rows, ['code', 'name', 'description', 'use_case_code', 'featured', 'base', 'status', 'ordering']);
            }

            // Deletes
            if (json.wrappers?.deletes?.length) await deleteBatch(client, 'wrappers', json.wrappers.deletes);
            if (json.useCases?.deletes?.length) await deleteBatch(client, 'use_cases', json.useCases.deletes);
            
            if (json.newVersion) {
                await client.query("INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', $1) ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value", [json.newVersion]);
            }
            
            await client.query('COMMIT');
            console.log('✅ Delta Sync Complete.');
        } else {
            console.log('✅ No updates needed.');
        }

    } catch (e) {
        if (client) await client.query('ROLLBACK');
        console.error('\n❌ Sync Error:', e.message);
    } finally {
        if (client) client.release();
        await pool.end();
    }
})();

Programmatically Download

Stream the download directly to disk to prevent memory issues.

javascript

// FILE: download-bundle.js
// USAGE: node download-bundle.js
// REQUIREMENTS: npm install axios adm-zip

const axios = require('axios');
const fs = require('fs');
const AdmZip = require('adm-zip');

// --- CONFIGURATION ---
const ZYWRAP_API_KEY = 'YOUR_API_KEY_HERE';
const API_ENDPOINT = 'https://api.zywrap.com/v1/sdk/v1/download';
const OUTPUT_FILE = './zywrap-data.zip';
// ---------------------

async function downloadBundle() {
  console.log('Downloading latest V1 wrapper data from Zywrap...');
  
  if (!ZYWRAP_API_KEY || ZYWRAP_API_KEY === 'YOUR_API_KEY_HERE') {
      console.error("FATAL: Please replace 'YOUR_API_KEY_HERE' with your actual Zywrap API key.");
      process.exit(1);
  }

  try {
    const response = await axios.get(API_ENDPOINT, {
      headers: { 'Authorization': 'Bearer ' + ZYWRAP_API_KEY },
      responseType: 'stream'
    });

    const writer = fs.createWriteStream(OUTPUT_FILE);
    response.data.pipe(writer);

    return new Promise((resolve, reject) => {
      writer.on('finish', () => {
        console.log("✅ Data bundle downloaded successfully.");
        
        try {
            console.log('📦 Extracting files...');
            const zip = new AdmZip(OUTPUT_FILE);
            zip.extractAllTo(__dirname, true);
            console.log('✅ Extraction complete.');
            
            // Clean up the zip file
            fs.unlinkSync(OUTPUT_FILE);
            
            console.log("👉 Next Step: Run 'node import.js' to load the data into your database.");
            resolve();
        } catch (zipErr) {
            console.error("⚠️ Failed to extract the zip file automatically.");
            console.log("Please manually unzip '" + OUTPUT_FILE + "' and run 'node import.js'.");
            resolve();
        }
      });
      writer.on('error', reject);
      response.data.on('error', reject);
    });

  } catch (error) {
    if (fs.existsSync(OUTPUT_FILE)) fs.unlinkSync(OUTPUT_FILE);
    console.error("❌ Error downloading bundle: " + (error.response?.status || error.message));
  }
}

downloadBundle();
Ready to ship?

Start building with Zywrap today.

Stop wrestling with prompts. Integrate powerful, structured AI into your applications in minutes.