Node.js SDK Guide
A complete, runnable example for integrating Zywrap Enterprise catalog metadata (including dynamic schemas) into your Node.js (Express) application.
Option 1: Cloud Proxy (Quick Start)
Don't want to host Enterprise catalog metadata yourself? Use our official NPM package to connect directly to the Zywrap Cloud Proxy in seconds.
npm install zywrap// Live V2 API call. Keep zw_live keys server-side.
const response = await fetch('https://api.zywrap.com/v2/runs', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.ZYWRAP_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify({
wrapperCodes: ['marketing_copywriting_cold_out_ema_b2b'],
model: 'openai-gpt-4o-mini',
input: {
text: 'Product: Zywrap
Audience: SaaS developers
Goal: Write a concise outbound email.'
},
output: { format: 'markdown', artifact: true }
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const data = await response.json();
console.log(data.run.outputText);Complete V2 file + artifact workflow
Use this when your app needs to upload a file, attach that file to a wrapper-code run, save the output as an artifact, and download the generated result.
// Complete V2 workflow: upload a file, run a wrapper, download the artifact.
// Requirements: npm install axios form-data
// Keep zw_live keys on your server, not in browser/mobile clients.
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const crypto = require('crypto');
const API = 'https://api.zywrap.com';
const API_KEY = process.env.ZYWRAP_API_KEY;
function authHeaders(extra = {}) {
return {
Authorization: 'Bearer ' + API_KEY,
...extra,
};
}
async function uploadFile(filePath) {
const form = new FormData();
form.append('file', fs.createReadStream(filePath));
form.append('displayName', 'Customer feedback CSV');
const response = await axios.post(API + '/v2/files', form, {
headers: authHeaders(form.getHeaders()),
timeout: 120000,
});
return response.data.file?.id || response.data.id;
}
async function runWorkflow(fileId) {
const response = await axios.post(
API + '/v2/runs',
{
wrapperCodes: ['customer_support_reply_summary'],
model: 'openai-gpt-4o-mini',
input: {
text: 'Summarize the attached customer feedback and list the top action items.',
files: [fileId],
},
output: {
format: 'markdown',
artifact: true,
},
},
{
headers: authHeaders({
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
}),
timeout: 180000,
}
);
return response.data.run;
}
async function downloadArtifact(artifactId, outputPath = './zywrap-output.md') {
const response = await axios.get(API + '/v2/artifacts/' + artifactId + '/download', {
headers: authHeaders(),
responseType: 'stream',
timeout: 120000,
});
await new Promise((resolve, reject) => {
const writer = fs.createWriteStream(outputPath);
response.data.pipe(writer);
writer.on('finish', resolve);
writer.on('error', reject);
});
return outputPath;
}
async function main() {
if (!API_KEY) throw new Error('Missing ZYWRAP_API_KEY');
const fileId = await uploadFile('./customer-feedback.csv');
const run = await runWorkflow(fileId);
console.log('Run status:', run.status);
console.log('Output:', run.outputText || run.outputSummary);
const artifactId = run.artifacts?.[0]?.id;
if (artifactId) {
const savedTo = await downloadArtifact(artifactId);
console.log('Artifact saved to:', savedTo);
}
}
main().catch((error) => {
console.error(error.response?.data || error.message);
process.exit(1);
});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 Enterprise Catalog SDK
Download the Enterprise-only ZIP file containing compressed catalog metadata. This includes categories, use cases, wrapper codes, input schemas, models, and relations ā not prompt logic. Unzip the zywrap-data.json file from the bundle to use in the import script.
Login to check Enterprise Catalog SDK access.
Step 2: Database Setup
Run this SQL. It features the relational use_cases table containing the dynamic schema_data.
-- Database Schema for Zywrap Enterprise Catalog SDK (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.
// 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 Enterprise catalog JSON and securely bulk-inserts it into your database.
// 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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zywrap Enterprise Catalog 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>
// FILE: server.js
// A simple Express server to replicate the 'api.php' Enterprise catalog 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.
// 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 Enterprise Catalog 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.
// 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 Enterprise catalog metadata 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();