@flavoai/fastfold
Version:
Flavo frontend package
673 lines (613 loc) • 27.4 kB
JavaScript
export class ApiExplorer {
docGenerator;
constructor(docGenerator) {
this.docGenerator = docGenerator;
}
/**
* Generate HTML for the API explorer
*/
generateHtml() {
const docs = this.docGenerator.generateDocumentation();
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${docs.info.title} - API Explorer!!!</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: #f8f9fa;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.header h1 { color: #2563eb; margin-bottom: 10px; }
.header p { color: #6b7280; font-size: 16px; }
.base-url {
background: #f3f4f6;
padding: 10px 15px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
margin-top: 15px;
font-size: 14px;
}
.tables-section, .endpoints-section {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.section-title {
font-size: 24px;
margin-bottom: 20px;
color: #1f2937;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 10px;
}
.table-card {
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-bottom: 20px;
overflow: hidden;
}
.table-header {
background: #f9fafb;
padding: 15px 20px;
border-bottom: 1px solid #e5e7eb;
}
.table-name { font-size: 18px; font-weight: 600; color: #1f2937; }
.table-security {
font-size: 12px;
color: #6b7280;
margin-top: 5px;
padding: 4px 8px;
background: #fef3c7;
border-radius: 4px;
display: inline-block;
}
.table-schema { padding: 20px; }
.schema-field {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f3f4f6;
}
.field-name { font-weight: 500; color: #374151; }
.field-type {
color: #7c3aed;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
background: #f3f4f6;
padding: 2px 6px;
border-radius: 3px;
}
.endpoint {
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-bottom: 15px;
overflow: hidden;
}
.endpoint-header {
background: #f9fafb;
padding: 15px 20px;
display: flex;
align-items: center;
gap: 15px;
}
.endpoint-header-content {
cursor: pointer;
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.endpoint-header-content:hover {
background: rgba(0,0,0,0.02);
border-radius: 4px;
margin: -4px;
padding: 4px;
}
.endpoint-header .try-button {
cursor: pointer;
margin-top: 0;
}
.endpoint-header:hover { background: #f3f4f6; }
.method {
font-weight: 600;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
min-width: 60px;
text-align: center;
}
.method.GET { background: #dcfce7; color: #166534; }
.method.POST { background: #fef3c7; color: #92400e; }
.method.PUT { background: #dbeafe; color: #1e40af; }
.method.DELETE { background: #fee2e2; color: #dc2626; }
.endpoint-path {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 14px;
flex: 1;
}
.endpoint-description { color: #6b7280; font-size: 14px; }
.endpoint-details {
padding: 20px;
display: none;
border-top: 1px solid #e5e7eb;
}
.endpoint-details.active { display: block; }
.detail-section { margin-bottom: 20px; }
.detail-title {
font-weight: 600;
margin-bottom: 10px;
color: #374151;
}
.parameter, .response {
background: #f9fafb;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
}
.param-name { font-weight: 500; color: #7c3aed; }
.param-type { color: #059669; font-size: 12px; }
.param-required { color: #dc2626; font-size: 12px; }
.code-block {
background: #1f2937;
color: #f9fafb;
padding: 15px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
overflow-x: auto;
margin-top: 10px;
}
.response-status {
font-weight: 600;
margin-right: 10px;
}
.status-200 { color: #059669; }
.status-201 { color: #059669; }
.status-400 { color: #d97706; }
.status-403 { color: #dc2626; }
.status-404 { color: #dc2626; }
.status-500 { color: #dc2626; }
.try-button {
background: #2563eb;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-top: 10px;
}
.try-button:hover { background: #1d4ed8; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${docs.info.title}</h1>
<p>${docs.info.description}</p>
<div class="base-url">Base URL: ${docs.info.baseUrl}</div>
</div>
<div class="tables-section">
<h2 class="section-title">📊 Data Tables</h2>
${this.generateTablesHtml(docs.tables)}
</div>
<div class="endpoints-section">
<h2 class="section-title">🚀 API Endpoints</h2>
${this.generateEndpointsHtml(docs.endpoints)}
</div>
</div>
<script>
// Toggle endpoint details
document.querySelectorAll('.endpoint-header-content').forEach(headerContent => {
headerContent.addEventListener('click', () => {
const header = headerContent.parentElement;
const details = header.nextElementSibling;
const isActive = details.classList.contains('active');
// Close all others
document.querySelectorAll('.endpoint-details').forEach(d => d.classList.remove('active'));
// Toggle current
if (!isActive) {
details.classList.add('active');
}
});
});
// Add event listeners for try buttons
document.querySelectorAll('.try-button').forEach(button => {
button.addEventListener('click', () => {
const method = button.getAttribute('data-method');
const path = button.getAttribute('data-path');
const hasBody = button.getAttribute('data-has-body') === 'true';
const endpointId = button.getAttribute('data-endpoint-id');
tryEndpoint(method, path, hasBody, endpointId);
});
});
// Table schemas for generating examples
const tableSchemas = ${JSON.stringify(docs.tables)};
// Helper function to generate example request body
function generateExampleRequestBody(schema) {
if (!schema || Object.keys(schema).length === 0) return { "key": "value" };
const example = {};
for (const [fieldName, fieldType] of Object.entries(schema)) {
// Skip auto-generated fields like id, created_at, updated_at
if (fieldName === 'id' || fieldName === 'created_at' || fieldName === 'updated_at') {
continue;
}
switch (fieldType.toLowerCase()) {
case 'string':
example[fieldName] = fieldName === 'email' ? 'user@example.com' :
fieldName === 'name' ? 'John Doe' :
fieldName === 'title' ? 'Sample Title' :
\`sample_\${fieldName}\`;
break;
case 'number':
case 'integer':
example[fieldName] = fieldName === 'age' ? 25 :
fieldName === 'price' ? 99.99 :
42;
break;
case 'boolean':
example[fieldName] = fieldName === 'active' || fieldName === 'published' ? true : false;
break;
case 'date':
case 'datetime':
example[fieldName] = '2024-01-01T00:00:00.000Z';
break;
case 'json':
example[fieldName] = { "key": "value" };
break;
case 'text':
example[fieldName] = fieldName === 'content' || fieldName === 'description' ?
'This is a sample content or description text.' :
\`sample_\${fieldName}_text\`;
break;
default:
example[fieldName] = \`sample_\${fieldName}\`;
}
}
return example;
}
// Helper function to get security description
function getSecurityDescription(security) {
if (!security) return 'No security configured';
if (security === 'Public access - no authentication required') return security;
if (security === 'Admin only - requires admin role') return security;
if (security === 'Owner-based access - users can only access their own records') return security;
if (security === 'Team-based access - users can access records from their team') return security;
if (security === 'Authenticated users only') return security;
return security || 'Custom security rule';
}
// Try endpoint functionality with live API calls
async function tryEndpoint(method, path, hasBody = false, endpointId) {
const baseUrl = '${docs.info.baseUrl}';
let fullUrl = baseUrl + path;
// Create modal for input
const modal = createTryModal(method, path, hasBody, endpointId);
document.body.appendChild(modal);
// Show modal
modal.style.display = 'flex';
}
const createTryModal = (method, path, hasBody, endpointId) => {
const modal = document.createElement('div');
modal.style.cssText = \`
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center;
z-index: 1000;
\`;
const modalContent = document.createElement('div');
modalContent.style.cssText = \`
background: white; padding: 30px; border-radius: 8px; width: 80%; max-width: 800px;
max-height: 80vh; overflow-y: auto; position: relative;
\`;
let pathInputs = '';
const pathParams = path.match(/{([^}]+)}/g);
if (pathParams) {
pathInputs = pathParams.map(param => {
const paramName = param.slice(1, -1);
return \`
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: 600; margin-bottom: 5px;">
\${paramName} (path parameter):
</label>
<input type="text" id="param-\${paramName}" placeholder="Enter \${paramName}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
</div>
\`;
}).join('');
}
let bodyInput = '';
if (hasBody) {
// Extract table name from path (e.g., /api/blog -> blog)
const tableName = path.split('/')[2];
const tableSchema = tableSchemas[tableName]?.schema || {};
const exampleBody = generateExampleRequestBody(tableSchema);
bodyInput = \`
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Request Body (JSON):</label>
<div style="margin-bottom: 8px; font-size: 12px; color: #6b7280;">
Fields: \${Object.entries(tableSchema).map(([field, type]) => \`<strong>\${field}</strong> (\${type})\`).join(', ')}
</div>
<textarea id="request-body" rows="8" placeholder='\${JSON.stringify(exampleBody, null, 2)}'
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace;">\${JSON.stringify(exampleBody, null, 2)}</textarea>
</div>
\`;
}
// Get security info for this endpoint
const tableName = path.split('/')[2];
const tableInfo = tableSchemas[tableName];
const securityInfo = tableInfo ? getSecurityDescription(tableInfo.security) : 'Unknown security';
modalContent.innerHTML = \`
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<div>
<h3>Try \${method} \${path}</h3>
<div style="font-size: 12px; color: #6b7280; margin-top: 4px;">
🔒 Security: \${securityInfo}
</div>
</div>
<button class="modal-close-btn"
style="background: none; border: none; font-size: 24px; cursor: pointer;">×</button>
</div>
<form id="try-form">
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Authorization (optional):</label>
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<input type="text" id="auth-token" placeholder="Bearer your-jwt-token-here"
style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace;">
<button type="button" id="generate-test-token"
style="background: #10b981; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">
Test Token
</button>
</div>
<div style="font-size: 11px; color: #6b7280;">
For secured endpoints, provide a JWT token. Click "Test Token" for a sample token.
</div>
</div>
\${pathInputs}
\${bodyInput}
<div style="margin-bottom: 20px;">
<button type="submit" style="background: #2563eb; color: white; border: none;
padding: 10px 20px; border-radius: 4px; cursor: pointer;">
Send Request
</button>
</div>
</form>
<div id="response-section" style="display: none;">
<h4>Response:</h4>
<div id="response-status" style="margin-bottom: 10px; font-weight: 600;"></div>
<pre id="response-body" style="background: #f5f5f5; padding: 15px; border-radius: 4px;
overflow-x: auto; white-space: pre-wrap; font-size: 13px;"></pre>
</div>
\`;
modal.appendChild(modalContent);
modal.className = 'modal-backdrop';
// Handle close button
const closeBtn = modalContent.querySelector('.modal-close-btn');
closeBtn.addEventListener('click', () => {
modal.remove();
});
// Handle test token generation
const testTokenBtn = modalContent.querySelector('#generate-test-token');
const authInput = modalContent.querySelector('#auth-token');
testTokenBtn.addEventListener('click', () => {
// Show instructions for getting a real token
alert(\`To test authenticated endpoints:
Option A: Flavo Auth (recommended for Flavo-hosted apps)
1. Set auth.providers = { flavo: { userTable: 'users' } } in your Fastfold config
2. Sign in via your app's login page (OAuth through Flavo)
3. The token from the OAuth callback is used automatically
Option B: Custom JWT (HS256 secret)
1. Set auth.providers = { custom: { secret: 'your-secret' } } in your config
2. Create a real JWT token with your auth system
3. Paste the Bearer token below
Or use the public 'blog' table for testing (no auth required)
For owner-based security like userProfiles:
- The userId/email in the request body must match the user ID in your JWT token
- With Flavo auth, req.user.id is set to the user's email address\`);
// For demonstration, show what a token would look like
authInput.value = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6InVzZXIxMjMiLCJyb2xlIjoidXNlciJ9.your-signature-here';
});
// Handle form submission
const form = modalContent.querySelector('#try-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
await executeRequest(method, path, hasBody, modalContent);
});
return modal;
}
async function executeRequest(method, path, hasBody, modalContent) {
const baseUrl = '${docs.info.baseUrl}';
let finalPath = path;
// Replace path parameters
const pathParams = path.match(/{([^}]+)}/g);
if (pathParams) {
pathParams.forEach(param => {
const paramName = param.slice(1, -1);
const input = modalContent.querySelector(\`#param-\${paramName}\`);
if (input && input.value) {
finalPath = finalPath.replace(param, input.value);
}
});
}
const fullUrl = baseUrl + finalPath;
const requestOptions = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
// Add authorization header if provided
const authInput = modalContent.querySelector('#auth-token');
if (authInput && authInput.value.trim()) {
let authValue = authInput.value.trim();
// Add 'Bearer ' prefix if not already present
if (!authValue.startsWith('Bearer ')) {
authValue = 'Bearer ' + authValue;
}
requestOptions.headers['Authorization'] = authValue;
}
if (hasBody) {
const bodyInput = modalContent.querySelector('#request-body');
if (bodyInput && bodyInput.value.trim()) {
try {
requestOptions.body = bodyInput.value;
} catch (e) {
alert('Invalid JSON in request body');
return;
}
}
}
const responseSection = modalContent.querySelector('#response-section');
const responseStatus = modalContent.querySelector('#response-status');
const responseBody = modalContent.querySelector('#response-body');
try {
responseSection.style.display = 'block';
responseStatus.textContent = 'Loading...';
responseBody.textContent = '';
const response = await fetch(fullUrl, requestOptions);
const responseText = await response.text();
let responseData;
try {
responseData = JSON.parse(responseText);
} catch (e) {
responseData = responseText;
}
responseStatus.innerHTML = \`
<span style="color: \${response.ok ? '#059669' : '#dc2626'};">
\${response.status} \${response.statusText}
</span>
\`;
responseBody.textContent = typeof responseData === 'object'
? JSON.stringify(responseData, null, 2)
: responseData;
} catch (error) {
responseSection.style.display = 'block';
responseStatus.innerHTML = '<span style="color: #dc2626;">Network Error</span>';
responseBody.textContent = error.message;
}
}
</script>
</body>
</html>`;
}
/**
* Generate HTML for tables section
*/
generateTablesHtml(tables) {
return Object.entries(tables)
.map(([tableName, tableInfo]) => `
<div class="table-card">
<div class="table-header">
<div class="table-name">${tableName}</div>
<div class="table-security">🔒 ${tableInfo.security}</div>
</div>
<div class="table-schema">
${Object.entries(tableInfo.schema)
.map(([fieldName, fieldType]) => `
<div class="schema-field">
<span class="field-name">${fieldName}</span>
<span class="field-type">${fieldType}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
}
/**
* Generate HTML for endpoints section
*/
generateEndpointsHtml(endpoints) {
return endpoints
.map(endpoint => `
<div class="endpoint">
<div class="endpoint-header">
<div class="endpoint-header-content">
<span class="method ${endpoint.method}">${endpoint.method}</span>
<span class="endpoint-path">${endpoint.path}</span>
<span class="endpoint-description">${endpoint.description}</span>
</div>
<div style="margin-left: auto;">
${this.generateTryButton(endpoint)}
</div>
</div>
<div class="endpoint-details">
${this.generateEndpointDetailsHtml(endpoint)}
</div>
</div>
`).join('');
}
/**
* Generate HTML for endpoint details
*/
generateEndpointDetailsHtml(endpoint) {
let html = '';
// Parameters
if (endpoint.parameters && endpoint.parameters.length > 0) {
html += `
<div class="detail-section">
<div class="detail-title">Parameters</div>
${endpoint.parameters.map((param) => `
<div class="parameter">
<span class="param-name">${param.name}</span>
<span class="param-type">(${param.type})</span>
${param.required ? '<span class="param-required">required</span>' : ''}
<div>${param.description}</div>
</div>
`).join('')}
</div>
`;
}
// Request Body
if (endpoint.requestBody) {
html += `
<div class="detail-section">
<div class="detail-title">Request Body</div>
<div>${endpoint.requestBody.description}</div>
<div class="code-block">${JSON.stringify(endpoint.requestBody.schema.example || {}, null, 2)}</div>
</div>
`;
}
// Responses
if (endpoint.responses && endpoint.responses.length > 0) {
html += `
<div class="detail-section">
<div class="detail-title">Responses</div>
${endpoint.responses.map((response) => `
<div class="response">
<span class="response-status status-${response.status}">${response.status}</span>
${response.description}
${response.schema ? `<div class="code-block">${JSON.stringify(response.schema.example || response.schema, null, 2)}</div>` : ''}
</div>
`).join('')}
</div>
`;
}
return html;
}
/**
* Generate Try button for an endpoint
*/
generateTryButton(endpoint) {
const hasBody = endpoint.method === 'POST' || endpoint.method === 'PUT';
const endpointId = `${endpoint.method.toLowerCase()}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '-')}`;
return `
<button class="try-button" data-method="${endpoint.method}" data-path="${endpoint.path}" data-has-body="${hasBody}" data-endpoint-id="${endpointId}">
Try it out
</button>
`;
}
}
//# sourceMappingURL=explorer.js.map