Skip to content

Instantly share code, notes, and snippets.

@dexit
Created April 1, 2025 10:09
Show Gist options
  • Save dexit/480da7dec2c47f64525324f15707e51a to your computer and use it in GitHub Desktop.
Save dexit/480da7dec2c47f64525324f15707e51a to your computer and use it in GitHub Desktop.
field data mapping etl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Data Mapper</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
.drop-zone {
border: 2px dashed #ccc;
transition: all 0.3s;
}
.drop-zone.active {
border-color: #3b82f6;
background-color: #f0f7ff;
}
.mapping-item {
transition: all 0.2s;
}
.mapping-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.transform-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
}
.saved-mapping {
cursor: pointer;
transition: all 0.2s;
}
.saved-mapping:hover {
background-color: #f8fafc;
}
.json-editor {
min-height: 200px;
font-family: "Courier New", Courier, monospace;
}
.tab-button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
transition: all 0.2s;
}
.tab-button.active {
background-color: #3b82f6;
color: white;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8 flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold text-gray-800">Advanced Data Mapper</h1>
<p class="text-gray-600 mt-2">Connect and transform data with powerful mappings</p>
</div>
<button id="open-saved-modal" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition flex items-center">
<i class="fas fa-folder-open mr-2"></i> Saved Mappings
</button>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Source Data Panel -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-700">
<i class="fas fa-database mr-2 text-blue-500"></i> Source Data
</h2>
<div class="flex space-x-2">
<button id="sample-source-btn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded text-sm hover:bg-blue-200 transition">
<i class="fas fa-magic mr-1"></i> Sample Data
</button>
<button id="mysql-source-btn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded text-sm hover:bg-blue-200 transition">
<i class="fas fa-database mr-1"></i> MySQL
</button>
<button id="clear-source-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm hover:bg-gray-200 transition">
<i class="fas fa-trash-alt mr-1"></i> Clear
</button>
<button id="update-source-btn" class="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition">
<i class="fas fa-sync-alt mr-1"></i> Update
</button>
</div>
</div>
<div class="flex mb-3 border-b border-gray-200">
<button class="tab-button active" data-tab="json-source">JSON Editor</button>
<button class="tab-button" data-tab="upload-source">File Upload</button>
<button class="tab-button" data-tab="mysql-source">MySQL Query</button>
</div>
<div id="source-drop-zone" class="drop-zone rounded-lg p-4 h-64 overflow-y-auto">
<!-- JSON Editor Tab -->
<div id="json-source" class="tab-content active">
<textarea id="source-data-json" class="json-editor w-full h-60 border border-gray-300 rounded p-2 text-sm font-mono" placeholder='{
"example": {
"id": 1,
"name": "Sample",
"details": {
"active": true,
"createdAt": "2023-01-01"
}
}
}'></textarea>
</div>
<!-- Upload Tab -->
<div id="upload-source" class="tab-content hidden text-center py-8">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-300 mb-3"></i>
<p class="text-gray-500">Drag & drop a JSON file here</p>
<p class="text-sm text-gray-400 mt-1">Or click to select a file</p>
<input type="file" id="source-file-input" class="hidden" accept=".json,application/json">
<button id="source-file-btn" class="mt-3 px-4 py-2 bg-blue-100 text-blue-600 rounded-md hover:bg-blue-200 transition">
<i class="fas fa-file-upload mr-2"></i> Select File
</button>
</div>
<!-- MySQL Tab -->
<div id="mysql-source" class="tab-content hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">MySQL Connection</label>
<div class="grid grid-cols-2 gap-3 mb-3">
<input type="text" id="mysql-host" class="border-gray-300 rounded-md shadow-sm" placeholder="Host" value="localhost">
<input type="text" id="mysql-username" class="border-gray-300 rounded-md shadow-sm" placeholder="Username" value="root">
<input type="password" id="mysql-password" class="border-gray-300 rounded-md shadow-sm" placeholder="Password">
<input type="text" id="mysql-database" class="border-gray-300 rounded-md shadow-sm" placeholder="Database">
</div>
<textarea id="mysql-query" class="w-full border-gray-300 rounded-md shadow-sm h-20" placeholder="SELECT * FROM table"></textarea>
<button id="execute-mysql" class="mt-2 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition">
<i class="fas fa-play mr-2"></i> Execute Query
</button>
</div>
</div>
</div>
<!-- Target Data Panel -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-700">
<i class="fas fa-bullseye mr-2 text-green-500"></i> Target Schema
</h2>
<div class="flex space-x-2">
<button id="sample-target-btn" class="px-3 py-1 bg-green-100 text-green-600 rounded text-sm hover:bg-green-200 transition">
<i class="fas fa-magic mr-1"></i> Sample Schema
</button>
<button id="mysql-target-btn" class="px-3 py-1 bg-green-100 text-green-600 rounded text-sm hover:bg-green-200 transition">
<i class="fas fa-database mr-1"></i> MySQL
</button>
<button id="clear-target-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm hover:bg-gray-200 transition">
<i class="fas fa-trash-alt mr-1"></i> Clear
</button>
<button id="update-target-btn" class="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700 transition">
<i class="fas fa-sync-alt mr-1"></i> Update
</button>
</div>
</div>
<div class="flex mb-3 border-b border-gray-200">
<button class="tab-button active" data-tab="json-target">JSON Editor</button>
<button class="tab-button" data-tab="upload-target">File Upload</button>
<button class="tab-button" data-tab="mysql-target">MySQL Table</button>
</div>
<div id="target-drop-zone" class="drop-zone rounded-lg p-4 h-64 overflow-y-auto">
<!-- JSON Editor Tab -->
<div id="json-target" class="tab-content active">
<textarea id="target-schema-json" class="json-editor w-full h-60 border border-gray-300 rounded p-2 text-sm font-mono" placeholder='{
"example": {
"id": null,
"name": null,
"details": {
"active": null,
"createdAt": null
}
}
}'></textarea>
</div>
<!-- Upload Tab -->
<div id="upload-target" class="tab-content hidden text-center py-8">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-300 mb-3"></i>
<p class="text-gray-500">Drag & drop a JSON schema file here</p>
<p class="text-sm text-gray-400 mt-1">Or click to select a file</p>
<input type="file" id="target-file-input" class="hidden" accept=".json,application/json">
<button id="target-file-btn" class="mt-3 px-4 py-2 bg-green-100 text-green-600 rounded-md hover:bg-green-200 transition">
<i class="fas fa-file-upload mr-2"></i> Select File
</button>
</div>
<!-- MySQL Tab -->
<div id="mysql-target" class="tab-content hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">MySQL Connection</label>
<div class="grid grid-cols-2 gap-3 mb-3">
<input type="text" id="mysql-target-host" class="border-gray-300 rounded-md shadow-sm" placeholder="Host" value="localhost">
<input type="text" id="mysql-target-username" class="border-gray-300 rounded-md shadow-sm" placeholder="Username" value="root">
<input type="password" id="mysql-target-password" class="border-gray-300 rounded-md shadow-sm" placeholder="Password">
<input type="text" id="mysql-target-database" class="border-gray-300 rounded-md shadow-sm" placeholder="Database">
</div>
<div class="relative">
<select id="mysql-target-table" class="w-full border-gray-300 rounded-md shadow-sm transform-select">
<option value="">Select a table</option>
<!-- Tables will be populated here -->
</select>
<button id="load-table-schema" class="mt-2 w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md transition">
<i class="fas fa-table mr-2"></i> Load Table Schema
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Mapping Section -->
<div class="bg-white rounded-lg shadow p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-700">
<i class="fas fa-project-diagram mr-2 text-purple-500"></i> Field Mappings
</h2>
<div class="flex space-x-2">
<button id="auto-map-btn" class="px-3 py-1 bg-purple-100 text-purple-600 rounded text-sm hover:bg-purple-200 transition">
<i class="fas fa-robot mr-1"></i> Auto-Match Fields
</button>
<button id="save-mapping-btn" class="px-3 py-1 bg-indigo-100 text-indigo-600 rounded text-sm hover:bg-indigo-200 transition">
<i class="fas fa-save mr-1"></i> Save Mapping
</button>
<button id="clear-mappings-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm hover:bg-gray-200 transition">
<i class="fas fa-trash-alt mr-1"></i> Clear All
</button>
</div>
</div>
<div id="mapping-container" class="space-y-3">
<div id="no-mappings-message" class="text-center py-8 text-gray-500">
<i class="fas fa-exchange-alt text-3xl mb-3"></i>
<p>No mappings created yet. Add mappings by selecting fields below.</p>
</div>
<!-- Mappings will be added here dynamically -->
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Source Field</label>
<select id="source-field-select" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 transform-select">
<option value="">Select a source field</option>
<!-- Source fields will be populated here -->
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Target Field</label>
<select id="target-field-select" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 transform-select">
<option value="">Select a target field</option>
<!-- Target fields will be populated here -->
</select>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Transformation</label>
<select id="transform-select" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 transform-select">
<option value="none">None (Direct mapping)</option>
<option value="uppercase">Uppercase</option>
<option value="lowercase">Lowercase</option>
<option value="trim">Trim whitespace</option>
<option value="parseInt">Convert to integer</option>
<option value="parseFloat">Convert to float</option>
<option value="toString">Convert to string</option>
<option value="dateFormat">Format date</option>
<option value="concat">Concatenate fields</option>
<option value="custom">Custom function</option>
</select>
</div>
<div id="transform-param-container" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Parameter</label>
<input type="text" id="transform-param" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500" placeholder="e.g. YYYY-MM-DD">
</div>
<div class="flex items-end">
<button id="add-mapping-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition flex items-center justify-center">
<i class="fas fa-plus mr-2"></i> Add Mapping
</button>
</div>
</div>
</div>
<!-- Results Section -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-700">
<i class="fas fa-code mr-2 text-orange-500"></i> Mapped Output
</h2>
<div class="flex space-x-2">
<button id="preview-sql-btn" class="px-3 py-1 bg-orange-100 text-orange-600 rounded text-sm hover:bg-orange-200 transition">
<i class="fas fa-eye mr-1"></i> Preview SQL
</button>
<button id="copy-output-btn" class="px-3 py-1 bg-orange-100 text-orange-600 rounded text-sm hover:bg-orange-200 transition">
<i class="fas fa-copy mr-1"></i> Copy
</button>
<button id="download-output-btn" class="px-3 py-1 bg-orange-100 text-orange-600 rounded text-sm hover:bg-orange-200 transition">
<i class="fas fa-download mr-1"></i> Download
</button>
<button id="save-to-db-btn" class="px-3 py-1 bg-green-100 text-green-600 rounded text-sm hover:bg-green-200 transition">
<i class="fas fa-save mr-1"></i> Save to DB
</button>
</div>
</div>
<div class="relative">
<div id="output-empty-state" class="text-center py-8 text-gray-500">
<i class="fas fa-code-branch text-3xl mb-3"></i>
<p>Mapped output will appear here after creating mappings.</p>
</div>
<pre id="output-result" class="hidden bg-gray-50 p-4 rounded text-sm text-gray-800 font-mono overflow-x-auto"></pre>
<div id="sql-preview-container" class="hidden bg-gray-50 p-4 rounded text-sm text-gray-800 font-mono overflow-x-auto"></div>
<button id="execute-mapping-btn" class="hidden mt-4 w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md transition flex items-center justify-center">
<i class="fas fa-play mr-2"></i> Execute Mapping
</button>
</div>
</div>
</div>
<!-- Saved Mappings Modal -->
<div id="saved-mappings-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
<div class="border-b border-gray-200 px-6 py-4 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-folder-open mr-2"></i> Saved Mappings
</h3>
<button id="close-saved-modal" class="text-gray-400 hover:text-gray-500">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6 overflow-y-auto">
<div id="saved-mappings-list" class="space-y-2">
<!-- Saved mappings will appear here -->
</div>
<div id="no-saved-mappings" class="text-center py-8 text-gray-500">
<i class="fas fa-inbox text-3xl mb-3"></i>
<p>No saved mappings found.</p>
</div>
</div>
<div class="border-t border-gray-200 px-6 py-4 flex justify-end">
<button id="cancel-saved-modal" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition mr-2">
Cancel
</button>
</div>
</div>
</div>
<!-- Save Mapping Modal -->
<div id="save-mapping-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="border-b border-gray-200 px-6 py-4">
<h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-save mr-2"></i> Save Mapping
</h3>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Mapping Name</label>
<input type="text" id="mapping-name" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500" placeholder="Enter a name for this mapping">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Description (Optional)</label>
<textarea id="mapping-description" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500" rows="3" placeholder="Describe this mapping"></textarea>
</div>
</div>
<div class="border-t border-gray-200 px-6 py-4 flex justify-end">
<button id="cancel-save-mapping" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition mr-2">
Cancel
</button>
<button id="confirm-save-mapping" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
Save
</button>
</div>
</div>
</div>
<!-- Database Config Modal -->
<div id="db-config-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="border-b border-gray-200 px-6 py-4">
<h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-database mr-2"></i> Database Configuration
</h3>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Host</label>
<input type="text" id="config-host" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500" value="localhost">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Database</label>
<input type="text" id="config-database" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500" value="data_mapper">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input type="text" id="config-username" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500" value="root">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" id="config-password" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div>
</div>
<div class="border-t border-gray-200 px-6 py-4 flex justify-end">
<button id="cancel-db-config" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition mr-2">
Cancel
</button>
<button id="test-db-connection" class="px-4 py-2 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200 transition mr-2">
Test Connection
</button>
<button id="save-db-config" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
Save
</button>
</div>
</div>
</div>
<script>
$(document).ready(function() {
// DOM elements
const $sourceDropZone = $('#source-drop-zone');
const $targetDropZone = $('#target-drop-zone');
const $sourceDataJson = $('#source-data-json');
const $targetSchemaJson = $('#target-schema-json');
const $sourceFileInput = $('#source-file-input');
const $targetFileInput = $('#target-file-input');
const $sourceFileBtn = $('#source-file-btn');
const $targetFileBtn = $('#target-file-btn');
const $sourceFieldSelect = $('#source-field-select');
const $targetFieldSelect = $('#target-field-select');
const $transformSelect = $('#transform-select');
const $transformParamContainer = $('#transform-param-container');
const $transformParam = $('#transform-param');
const $addMappingBtn = $('#add-mapping-btn');
const $mappingContainer = $('#mapping-container');
const $noMappingsMessage = $('#no-mappings-message');
const $outputResult = $('#output-result');
const $outputEmptyState = $('#output-empty-state');
const $executeMappingBtn = $('#execute-mapping-btn');
const $copyOutputBtn = $('#copy-output-btn');
const $downloadOutputBtn = $('#download-output-btn');
const $autoMapBtn = $('#auto-map-btn');
const $saveMappingBtn = $('#save-mapping-btn');
const $clearMappingsBtn = $('#clear-mappings-btn');
const $sampleSourceBtn = $('#sample-source-btn');
const $sampleTargetBtn = $('#sample-target-btn');
const $clearSourceBtn = $('#clear-source-btn');
const $clearTargetBtn = $('#clear-target-btn');
const $updateSourceBtn = $('#update-source-btn');
const $updateTargetBtn = $('#update-target-btn');
const $mysqlSourceBtn = $('#mysql-source-btn');
const $mysqlTargetBtn = $('#mysql-target-btn');
const $previewSqlBtn = $('#preview-sql-btn');
const $sqlPreviewContainer = $('#sql-preview-container');
const $saveToDbBtn = $('#save-to-db-btn');
const $savedMappingsModal = $('#saved-mappings-modal');
const $openSavedModal = $('#open-saved-modal');
const $closeSavedModal = $('#close-saved-modal');
const $cancelSavedModal = $('#cancel-saved-modal');
const $savedMappingsList = $('#saved-mappings-list');
const $noSavedMappings = $('#no-saved-mappings');
const $saveMappingModal = $('#save-mapping-modal');
const $mappingName = $('#mapping-name');
const $mappingDescription = $('#mapping-description');
const $confirmSaveMapping = $('#confirm-save-mapping');
const $cancelSaveMapping = $('#cancel-save-mapping');
const $dbConfigModal = $('#db-config-modal');
const $configHost = $('#config-host');
const $configDatabase = $('#config-database');
const $configUsername = $('#config-username');
const $configPassword = $('#config-password');
const $testDbConnection = $('#test-db-connection');
const $saveDbConfig = $('#save-db-config');
const $cancelDbConfig = $('#cancel-db-config');
// Tab buttons
const $sourceTabButtons = $('[data-tab^="json-source"], [data-tab^="upload-source"], [data-tab^="mysql-source"]');
const $targetTabButtons = $('[data-tab^="json-target"], [data-tab^="upload-target"], [data-tab^="mysql-target"]');
const $sourceTabContents = $('.tab-content', $sourceDropZone);
const $targetTabContents = $('.tab-content', $targetDropZone);
// State
let sourceData = null;
let targetSchema = null;
let mappings = [];
let mappedOutput = null;
let dbConfig = {
host: 'localhost',
database: 'data_mapper',
username: 'root',
password: ''
};
// Load saved DB config from localStorage if available
if (localStorage.getItem('dbConfig')) {
dbConfig = JSON.parse(localStorage.getItem('dbConfig'));
$configHost.val(dbConfig.host);
$configDatabase.val(dbConfig.database);
$configUsername.val(dbConfig.username);
$configPassword.val(dbConfig.password);
}
// Initialize tab switching
$sourceTabButtons.on('click', function() {
const tabId = $(this).data('tab');
$sourceTabButtons.removeClass('active');
$(this).addClass('active');
$sourceTabContents.addClass('hidden');
$(`#${tabId}`).removeClass('hidden');
});
$targetTabButtons.on('click', function() {
const tabId = $(this).data('tab');
$targetTabButtons.removeClass('active');
$(this).addClass('active');
$targetTabContents.addClass('hidden');
$(`#${tabId}`).removeClass('hidden');
});
// Initialize file inputs
$sourceFileBtn.on('click', function() {
$sourceFileInput.click();
});
$targetFileBtn.on('click', function() {
$targetFileInput.click();
});
$sourceFileInput.on('change', function() {
if (this.files.length > 0) {
handleFileUpload(this.files, 'source');
}
});
$targetFileInput.on('change', function() {
if (this.files.length > 0) {
handleFileUpload(this.files, 'target');
}
});
function handleFileUpload(files, type) {
const file = files[0];
const reader = new FileReader();
reader.onload = function(e) {
try {
const content = JSON.parse(e.target.result);
if (type === 'source') {
loadSourceData(content);
$sourceDataJson.val(JSON.stringify(content, null, 2));
} else {
loadTargetSchema(content);
$targetSchemaJson.val(JSON.stringify(content, null, 2));
}
showSuccess('File loaded successfully!');
} catch (error) {
showError(`Invalid JSON: ${error.message}`);
}
};
reader.onerror = function() {
showError('Error reading file');
};
reader.readAsText(file);
}
// Update source/target data from JSON input
$updateSourceBtn.on('click', function() {
try {
const json = JSON.parse($sourceDataJson.val() || 'null');
if (json) {
loadSourceData(json);
showSuccess('Source data updated!');
} else {
showError('Please enter valid JSON data');
}
} catch (error) {
showError(`Invalid JSON: ${error.message}`);
}
});
$updateTargetBtn.on('click', function() {
try {
const json = JSON.parse($targetSchemaJson.val() || 'null');
if (json) {
loadTargetSchema(json);
showSuccess('Target schema updated!');
} else {
showError('Please enter valid JSON schema');
}
} catch (error) {
showError(`Invalid JSON: ${error.message}`);
}
});
// Load data functions
function loadSourceData(data) {
sourceData = data;
populateSourceFields();
$executeMappingBtn.removeClass('hidden');
}
function loadTargetSchema(schema) {
targetSchema = schema;
populateTargetFields();
$executeMappingBtn.removeClass('hidden');
}
// MySQL Source Connection
$mysqlSourceBtn.on('click', function() {
$('[data-tab="mysql-source"]').click();
});
$executeMysql.on('click', function() {
const host = $('#mysql-host').val();
const username = $('#mysql-username').val();
const password = $('#mysql-password').val();
const database = $('#mysql-database').val();
const query = $('#mysql-query').val();
if (!host || !username || !database || !query) {
showError('Please fill all required fields');
return;
}
$.ajax({
url: 'api/mysql_query.php',
method: 'POST',
dataType: 'json',
data: {
host: host,
username: username,
password: password,
database: database,
query: query
},
success: function(data) {
if (data.error) {
showError(data.error);
} else {
loadSourceData(data);
$sourceDataJson.val(JSON.stringify(data, null, 2));
showSuccess('Query executed successfully!');
}
},
error: function(xhr, status, error) {
showError('Error executing query: ' + error);
}
});
});
// MySQL Target Connection
$mysqlTargetBtn.on('click', function() {
$('[data-tab="mysql-target"]').click();
});
$loadTableSchema.on('click', function() {
const table = $mysqlTargetTable.val();
if (!table) {
showError('Please select a table');
return;
}
const host = $('#mysql-target-host').val();
const username = $('#mysql-target-username').val();
const password = $('#mysql-target-password').val();
const database = $('#mysql-target-database').val();
$.ajax({
url: 'api/mysql_schema.php',
method: 'POST',
dataType: 'json',
data: {
host: host,
username: username,
password: password,
database: database,
table: table
},
success: function(data) {
if (data.error) {
showError(data.error);
} else {
// Create schema object with null values
const schema = {};
data.columns.forEach(column => {
schema[column.name] = null;
});
loadTargetSchema(schema);
$targetSchemaJson.val(JSON.stringify(schema, null, 2));
showSuccess('Table schema loaded successfully!');
}
},
error: function(xhr, status, error) {
showError('Error loading table schema: ' + error);
}
});
});
// Field population functions
function populateSourceFields() {
$sourceFieldSelect.empty().append('<option value="">Select a source field</option>');
if (sourceData && typeof sourceData === 'object') {
const fields = extractFields(sourceData);
fields.forEach(field => {
$sourceFieldSelect.append(`<option value="${field}">${field}</option>`);
});
}
}
function populateTargetFields() {
$targetFieldSelect.empty().append('<option value="">Select a target field</option>');
if (targetSchema && typeof targetSchema === 'object') {
const fields = extractFields(targetSchema);
fields.forEach(field => {
$targetFieldSelect.append(`<option value="${field}">${field}</option>`);
});
}
}
function extractFields(obj, prefix = '') {
let fields = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const fullPath = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
fields = fields.concat(extractFields(obj[key], fullPath));
} else {
fields.push(fullPath);
}
}
}
return fields;
}
// Transformation select change
$transformSelect.on('change', function() {
if (this.value === 'dateFormat' || this.value === 'custom' || this.value === 'concat') {
$transformParamContainer.removeClass('hidden');
$transformParam.attr('placeholder',
this.value === 'dateFormat' ? 'e.g. YYYY-MM-DD' :
this.value === 'concat' ? 'e.g. "prefix-"+field1+"-"+field2' :
'e.g. value => value.toUpperCase()');
} else {
$transformParamContainer.addClass('hidden');
}
});
// Add mapping
$addMappingBtn.on('click', function() {
const sourceField = $sourceFieldSelect.val();
const targetField = $targetFieldSelect.val();
const transform = $transformSelect.val();
const transformParamValue = $transformParam.val();
if (!sourceField || !targetField) {
showError('Please select both source and target fields');
return;
}
// Check if this mapping already exists
if (mappings.some(m => m.targetField === targetField)) {
showError(`Target field "${targetField}" is already mapped`);
return;
}
const mapping = {
sourceField,
targetField,
transform,
transformParam: transformParamValue
};
mappings.push(mapping);
renderMappings();
// Reset form
$sourceFieldSelect.val('');
$targetFieldSelect.val('');
$transformSelect.val('none');
$transformParamContainer.addClass('hidden');
$transformParam.val('');
// Show execute button if we have mappings
if (mappings.length > 0) {
$executeMappingBtn.removeClass('hidden');
}
});
// Render mappings
function renderMappings() {
if (mappings.length === 0) {
$noMappingsMessage.removeClass('hidden');
$mappingContainer.empty();
return;
}
$noMappingsMessage.addClass('hidden');
$mappingContainer.empty();
mappings.forEach((mapping, index) => {
const mappingEl = $(`
<div class="mapping-item bg-gray-50 p-3 rounded-lg border border-gray-200 flex justify-between items-center">
<div class="flex-1">
<div class="flex items-center text-sm">
<span class="font-medium text-blue-600">${mapping.sourceField}</span>
<i class="fas fa-arrow-right mx-2 text-gray-400"></i>
<span class="font-medium text-green-600">${mapping.targetField}</span>
</div>
<div class="text-xs text-gray-500 mt-1">
${mapping.transform !== 'none' ?
`Transformation: ${mapping.transform}${mapping.transformParam ? ` (${mapping.transformParam})` : ''}` :
'No transformation'}
</div>
</div>
<button class="text-red-500 hover:text-red-700 ml-2">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`);
mappingEl.find('button').on('click', () => {
mappings.splice(index, 1);
renderMappings();
if (mappings.length === 0) {
$executeMappingBtn.addClass('hidden');
$outputResult.addClass('hidden');
$outputEmptyState.removeClass('hidden');
}
});
$mappingContainer.append(mappingEl);
});
}
// Execute mapping
$executeMappingBtn.on('click', function() {
if (!sourceData || !targetSchema || mappings.length === 0) {
showError('Please provide source data, target schema, and at least one mapping');
return;
}
try {
// Create a deep copy of the target schema to modify
const output = JSON.parse(JSON.stringify(targetSchema));
// Apply each mapping
mappings.forEach(mapping => {
const sourceValue = getNestedValue(sourceData, mapping.sourceField);
let transformedValue = sourceValue;
// Apply transformation
switch (mapping.transform) {
case 'uppercase':
if (typeof transformedValue === 'string') {
transformedValue = transformedValue.toUpperCase();
}
break;
case 'lowercase':
if (typeof transformedValue === 'string') {
transformedValue = transformedValue.toLowerCase();
}
break;
case 'trim':
if (typeof transformedValue === 'string') {
transformedValue = transformedValue.trim();
}
break;
case 'parseInt':
transformedValue = parseInt(transformedValue);
break;
case 'parseFloat':
transformedValue = parseFloat(transformedValue);
break;
case 'toString':
transformedValue = String(transformedValue);
break;
case 'dateFormat':
if (transformedValue instanceof Date) {
transformedValue = formatDate(transformedValue, mapping.transformParam || 'YYYY-MM-DD');
}
break;
case 'concat':
if (mapping.transformParam) {
try {
// Replace field references with actual values
let concatPattern = mapping.transformParam;
const fieldRefs = concatPattern.match(/\b[a-zA-Z0-9_.]+\b/g) || [];
fieldRefs.forEach(field => {
if (field.includes('.')) {
const val = getNestedValue(sourceData, field);
concatPattern = concatPattern.replace(new RegExp(field, 'g'), val !== undefined ? val : '');
}
});
transformedValue = concatPattern;
} catch (e) {
console.error('Concatenation error:', e);
}
}
break;
case 'custom':
try {
// WARNING: Using eval is dangerous in production!
// This is just for demo purposes
const customFn = eval(`(${mapping.transformParam})`);
transformedValue = customFn(transformedValue);
} catch (e) {
console.error('Custom function error:', e);
}
break;
// 'none' does nothing
}
// Set the value in the output
setNestedValue(output, mapping.targetField, transformedValue);
});
mappedOutput = output;
$outputResult.text(JSON.stringify(output, null, 2)).removeClass('hidden');
$outputEmptyState.addClass('hidden');
// Hide SQL preview if showing
$sqlPreviewContainer.addClass('hidden');
$previewSqlBtn.removeClass('bg-orange-200').addClass('bg-orange-100');
showSuccess('Mapping executed successfully!');
} catch (error) {
showError(`Error executing mappings: ${error.message}`);
}
});
// Helper functions for nested object access
function getNestedValue(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current[key] === undefined) {
return undefined;
}
current = current[key];
}
return current;
}
function setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (current[key] === undefined) {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
// Simple date formatting helper
function formatDate(date, format) {
const pad = num => num.toString().padStart(2, '0');
const replacements = {
'YYYY': date.getFullYear(),
'MM': pad(date.getMonth() + 1),
'DD': pad(date.getDate()),
'HH': pad(date.getHours()),
'mm': pad(date.getMinutes()),
'ss': pad(date.getSeconds())
};
let result = format;
for (const [token, value] of Object.entries(replacements)) {
result = result.replace(token, value);
}
return result;
}
// Copy output
$copyOutputBtn.on('click', function() {
if (!mappedOutput) {
showError('No output to copy');
return;
}
navigator.clipboard.writeText(JSON.stringify(mappedOutput, null, 2))
.then(() => {
showSuccess('Output copied to clipboard!');
})
.catch(err => {
showError('Failed to copy: ' + err);
});
});
// Download output
$downloadOutputBtn.on('click', function() {
if (!mappedOutput) {
showError('No output to download');
return;
}
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(mappedOutput, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', 'mapped-output.json');
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
});
// Preview SQL
$previewSqlBtn.on('click', function() {
if (!mappedOutput || !targetSchema) {
showError('Please execute mappings first');
return;
}
// Check if the target schema matches a single table structure
let tableName = 'target_table'; // Default or could prompt user
let fields = extractFields(targetSchema);
// Simple INSERT statement generation
let sql = `INSERT INTO ${tableName} (`;
sql += fields.join(', ') + ') VALUES (';
// Add placeholders for values
sql += fields.map(f => '?').join(', ') + ');';
// Display the SQL
$sqlPreviewContainer.text(sql).toggleClass('hidden');
$outputResult.toggleClass('hidden');
// Toggle button state
$(this).toggleClass('bg-orange-100 bg-orange-200');
if (!$sqlPreviewContainer.hasClass('hidden')) {
showSuccess('SQL preview generated!');
}
});
// Save to database
$saveToDbBtn.on('click', function() {
if (!mappedOutput) {
showError('No output to save to database');
return;
}
// Get target table info (would normally prompt user)
const host = $('#mysql-target-host').val() || dbConfig.host;
const username = $('#mysql-target-username').val() || dbConfig.username;
const password = $('#mysql-target-password').val() || dbConfig.password;
const database = $('#mysql-target-database').val() || dbConfig.database;
const table = $mysqlTargetTable.val();
if (!host || !username || !database || !table) {
showError('Please configure target database information first');
setTimeout(() => {
$mysqlTargetBtn.click();
}, 1000);
return;
}
$.ajax({
url: 'api/mysql_save.php',
method: 'POST',
dataType: 'json',
data: {
host: host,
username: username,
password: password,
database: database,
table: table,
data: JSON.stringify(mappedOutput)
},
success: function(data) {
if (data.error) {
showError(data.error);
} else {
showSuccess(`Data saved successfully to ${table}!`);
}
},
error: function(xhr, status, error) {
showError('Error saving to database: ' + error);
}
});
});
// Auto-map fields
$autoMapBtn.on('click', function() {
if (!sourceData || !targetSchema) {
showError('Please provide both source data and target schema');
return;
}
const sourceFields = extractFields(sourceData);
const targetFields = extractFields(targetSchema);
// Clear existing mappings
mappings = [];
// Simple matching by field name (could be enhanced)
targetFields.forEach(targetField => {
const targetFieldName = targetField.split('.').pop().toLowerCase();
// Find a source field with a similar name
const matchingSourceField = sourceFields.find(sourceField => {
const sourceFieldName = sourceField.split('.'').pop().toLowerCase();
return sourceFieldName === targetFieldName;
});
if (matchingSourceField) {
mappings.push({
sourceField: matchingSourceField,
targetField: targetField,
transform: 'none',
transformParam: ''
});
}
});
renderMappings();
if (mappings.length > 0) {
$executeMappingBtn.removeClass('hidden');
showSuccess(`Auto-matched ${mappings.length} fields`);
} else {
showError('No fields could be automatically matched');
}
});
// Save mapping
$saveMappingBtn.on('click', function() {
if (mappings.length === 0) {
showError('No mappings to save');
return;
}
// Prompt for mapping name
$mappingName.val('');
$mappingDescription.val('');
$saveMappingModal.removeClass('hidden');
});
$confirmSaveMapping.on('click', function() {
const name = $mappingName.val();
const description = $mappingDescription.val();
if (!name) {
showError('Please enter a name for the mapping');
return;
}
$.ajax({
url: 'api/save_mapping.php',
method: 'POST',
dataType: 'json',
data: {
name: name,
description: description,
sourceData: JSON.stringify(sourceData),
targetSchema: JSON.stringify(targetSchema),
mappings: JSON.stringify(mappings)
},
success: function(data) {
if (data.error) {
showError(data.error);
} else {
$saveMappingModal.addClass('hidden');
showSuccess('Mapping saved successfully!');
}
},
error: function(xhr, status, error) {
showError('Error saving mapping: ' + error);
}
});
});
$cancelSaveMapping.on('click', function() {
$saveMappingModal.addClass('hidden');
});
// Load saved mappings
$openSavedModal.on('click', function() {
$.ajax({
url: 'api/get_mappings.php',
method: 'GET',
dataType: 'json',
success: function(data) {
if (data.error) {
showError(data.error);
} else {
$savedMappingsList.empty();
if (data.mappings.length === 0) {
$noSavedMappings.removeClass('hidden');
} else {
$noSavedMappings.addClass('hidden');
data.mappings.forEach(mapping => {
const mappingEl = $(`
<div class="saved-mapping p-3 border border-gray-200 rounded-lg flex justify-between items-center">
<div>
<div class="font-medium">${mapping.name}</div>
<div class="text-sm text-gray-500">${mapping.description || 'No description'}</div>
<div class="text-xs text-gray-400">${new Date(mapping.created_at).toLocaleString()}</div>
</div>
<button class="text-blue-500 hover:text-blue-700" data-id="${mapping.id}">
<i class="fas fa-arrow-right"></i>
</button>
</div>
`);
mappingEl.find('button').on('click', function() {
loadSavedMapping($(this).data('id'));
});
$savedMappingsList.append(mappingEl);
});
}
$savedMappingsModal.removeClass('hidden');
}
},
error: function(xhr, status, error) {
showError('Error loading saved mappings: ' + error);
}
});
});
function loadSavedMapping(id) {
$.ajax({
url: 'api/get_mapping.php',
method: 'GET',
dataType: 'json',
data: { id: id },
success: function(data) {
if (data.error) {
showError(data.error);
} else {
try {
sourceData = JSON.parse(data.source_data);
targetSchema = JSON.parse(data.target_schema);
mappings = JSON.parse(data.mappings);
// Update JSON editors
$sourceDataJson.val(JSON.stringify(sourceData, null, 2));
$targetSchemaJson.val(JSON.stringify(targetSchema, null, 2));
populateSourceFields();
populateTargetFields();
renderMappings();
if (mappings.length > 0) {
$executeMappingBtn.removeClass('hidden');
}
$savedMappingsModal.addClass('hidden');
showSuccess(`Mapping "${data.name}" loaded successfully!`);
} catch (e) {
showError('Error parsing saved mapping: ' + e.message);
}
}
},
error: function(xhr, status, error) {
showError('Error loading mapping: ' + error);
}
});
}
$closeSavedModal.add($cancelSavedModal).on('click', function() {
$savedMappingsModal.addClass('hidden');
});
// Database configuration
$(document).on('keydown', function(e) {
// Ctrl+Alt+D to open DB config
if (e.ctrlKey && e.altKey && e.key === 'd') {
e.preventDefault();
$dbConfigModal.removeClass('hidden');
}
});
$testDbConnection.on('click', function() {
const host = $configHost.val();
const username = $configUsername.val();
const password = $configPassword.val();
const database = $configDatabase.val();
if (!host || !username || !database) {
showError('Please fill all required fields');
return;
}
$.ajax({
url: 'api/test_db_connection.php',
method: 'POST',
dataType: 'json',
data: {
host: host,
username: username,
password: password,
database: database
},
success: function(data) {
if (data.error) {
showError(data.error);
} else {
showSuccess('Connection successful!');
}
},
error: function(xhr, status, error) {
showError('Connection failed: ' + error);
}
});
});
$saveDbConfig.on('click', function() {
dbConfig = {
host: $configHost.val(),
database: $configDatabase.val(),
username: $configUsername.val(),
password: $configPassword.val()
};
localStorage.setItem('dbConfig', JSON.stringify(dbConfig));
$dbConfigModal.addClass('hidden');
showSuccess('Database configuration saved!');
});
$cancelDbConfig.on('click', function() {
$dbConfigModal.addClass('hidden');
});
// Clear buttons
$clearSourceBtn.on('click', function() {
sourceData = null;
$sourceDataJson.val('');
$sourceFieldSelect.empty().append('<option value="">Select a source field</option>');
$('[data-tab="json-source"]').click();
});
$clearTargetBtn.on('click', function() {
targetSchema = null;
$targetSchemaJson.val('');
$targetFieldSelect.empty().append('<option value="">Select a target field</option>');
$('[data-tab="json-target"]').click();
});
$clearMappingsBtn.on('click', function() {
mappings = [];
renderMappings();
$executeMappingBtn.addClass('hidden');
$outputResult.addClass('hidden');
$outputEmptyState.removeClass('hidden');
$sqlPreviewContainer.addClass('hidden');
$previewSqlBtn.removeClass('bg-orange-200').addClass('bg-orange-100');
});
// Sample data buttons
$sampleSourceBtn.on('click', function() {
const sampleData = {
"user": {
"id": 12345,
"name": "John Doe",
"email": " [email protected] ",
"registrationDate": new Date(),
"address": {
"street": "123 Main St",
"city": "Anytown",
"zip": "12345"
},
"preferences": {
"newsletter": true,
"notifications": false
}
},
"order": {
"id": "ORD-67890",
"total": "99.99",
"items": [
{"productId": 1, "quantity": 2},
{"productId": 2, "quantity": 1}
]
}
};
loadSourceData(sampleData);
$sourceDataJson.val(JSON.stringify(sampleData, null, 2));
$('[data-tab="json-source"]').click();
showSuccess('Sample source data loaded!');
});
$sampleTargetBtn.on('click', function() {
const sampleSchema = {
"customer": {
"customerId": null,
"fullName": null,
"emailAddress": null,
"signupDate": null,
"contactInfo": {
"streetAddress": null,
"city": null,
"postalCode": null
},
"marketingPreferences": {
"emailSubscribed": null
}
},
"transaction": {
"orderId": null,
"amount": null,
"lineItems": []
}
};
loadTargetSchema(sampleSchema);
$targetSchemaJson.val(JSON.stringify(sampleSchema, null, 2));
$('[data-tab="json-target"]').click();
showSuccess('Sample target schema loaded!');
});
// Notification functions
function showError(message) {
const notification = $(`
<div class="fixed top-4 right-4 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-lg max-w-xs z-50">
<div class="flex items-center">
<i class="fas fa-exclamation-circle mr-2"></i>
<span>${message}</span>
</div>
</div>
`);
$('body').append(notification);
setTimeout(() => {
notification.addClass('opacity-0 transition-opacity duration-500');
setTimeout(() => notification.remove(), 500);
}, 3000);
}
function showSuccess(message) {
const notification = $(`
<div class="fixed top-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded shadow-lg max-w-xs z-50">
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span>${message}</span>
</div>
</div>
`);
$('body').append(notification);
setTimeout(() => {
notification.addClass('opacity-0 transition-opacity duration-500');
setTimeout(() => notification.remove(), 500);
}, 3000);
}
// Initialize with JSON editor tabs selected
$('[data-tab="json-source"]').click();
$('[data-tab="json-target"]').click();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment