Instruction file imported from schroeder71/json2json (
.github/instructions/project.instructions.md). Copyright stays with the author.
Generation Specification: JSON-to-JSON Visual Transformation Mapper (Revised)
1. Solution Architecture Blueprint: The Visual JSON Mapper
1.1. Executive Overview
This document provides the complete technical architecture and implementation specification for a Next.js-based visual mapping application. The application's primary function is to facilitate the creation of a JSON-based "mapping file." This mapping file serves as a declarative set of instructions for an external processing engine (specified as a Groovy script) to execute data transformations. The application itself does not perform the final, production transformation but rather provides a dual-pane, tree-view interface to define, test, and export the transformation rules.
1.2. Architectural Philosophy
The application will be architected as a Hybrid Single-Page Application (SPA), leveraging the Next.js framework. This model is selected to provide a rich, interactive client-side user experience characteristic of an SPA, while simultaneously utilizing the server-side capabilities of Next.js for secure configuration and data access.
This approach embodies the "Backend-for-Frontend" (BFF) pattern. The Next.js server component will handle all operations requiring access to the server environment, such as reading configuration files and proxying API calls. The client-side React application will consume this data, presenting an interactive mapping interface with no direct knowledge of the server's file system or sensitive credentials.
1.3. The Mapping File as the "Source of Truth"
A foundational principle of this architecture is that the "mapping file" is the application's true source of truth, not the UI state. The visual interface is merely an ephemeral, user-friendly editor for constructing this mapping file. This realization dictates several key architectural decisions:
-
State Re-hydration: The requirement to upload a mapping file to "reuse predefined configuration" confirms this model. The application must be able to parse an uploaded mapping file and perfectly reconstruct the entire visual state, including all mappings and static values in the tree views.
-
State Serialization: The "download" functionality is a process of serializing the current visual state (the defined mappings and static values) into the canonical mapping file schema defined in this specification.
-
Test Fidelity: The "Test" button's function must first serialize the current UI state (the mappings array and static values) into the mapping file format in-memory. It then executes the transformation using a client-side engine that simulates the external Groovy script's logic. This ensures the test is a 1:1, high-fidelity preview of the final production output.
This separation of concerns—where the UI is a visual representation of the mapping file, not the state itself—is critical for a robust and maintainable design.
1.4. System Component Diagram
The system comprises four distinct components:
-
Client (Browser): The Next.js SPA. Manages all UI state (e.g., selected elements, mappings), hosts the source and target tree views, and displays test results. It communicates exclusively with the BFF.
-
BFF (Next.js Server): The API routes (pages/api or app/api). This layer manages secure file system access, reads configuration from .env files, and acts as a secure proxy for external REST API calls.
-
Data/Config (Server): Server-side resources, including the .env.local file, the directory of input JSON files (or the external REST API), and the target JSON schema file. These are never exposed to the client.
-
Artifact (User): The downloadable mapping.json file. This is the primary output of the application and the "data bridge" to the external Groovy processing environment.
2. Core Technology Justification
2.1. Next.js as a Hybrid SPA / Backend-for-Frontend (BFF)
The project requirements present a technical paradox: the user requests a "single page browser app" but also "server-side file-system access" and configuration. A "strict SPA" runs entirely in the browser and is fundamentally incapable of securely accessing a server's file system or environment variables.
The Next.js hybrid model is the precise solution to this paradox.
-
Frontend (SPA): The core application will be built as a client-side React application, providing the fast, interactive "SPA" experience.
-
Backend (BFF): All server-side operations will be handled by Next.js API Routes. These are server-side-only bundles that are collocated with the frontend code but execute exclusively on the server. This layer is responsible for:
1. Secure Configuration: Securely reading process.env variables from a .env.local file.
2. File System Access: Performing all Node.js file system operations (e.g., reading the SOURCE_PATH directory).
3. Secure API Proxying: Acting as a secure proxy for the source REST API. The BFF will make the fetch call, inject the secret API key (e.g., SOURCE_API_KEY) from process.env, and then pass the data to the client, ensuring the secret key is never exposed.
The "no database" constraint forces the adoption of this BFF pattern. The API routes become the entire, lightweight backend, creating a clean security boundary.
2.2. Visual Mapping: Dual Tree-View with Drag-and-Drop
The requirement is to display two "tree views" and connect them. The optimal solution is to combine a tree-view renderer with a drag-and-drop library.
-
Technology: We will use dnd-kit for the drag-and-drop functionality, combined with a custom recursive React component to render the tree structures. dnd-kit is a modern, flexible, and high-performance library ideal for this task.
-
Implementation: The application will render two separate tree view components: one for the source JSON and one for the target schema.
- A recursive function will parse the source and target JSON into a tree data structure (e.g., { id, label, children }).
- The id of each node is critical: it will be the full JSONPath for source nodes (e.g., source::$.'user.name') and the key-array path for target nodes (e.g., target::user.profile.name).
-
Connections as State: The user's "drag&drop" mapping action will be handled by dnd-kit's .
- The onDragEnd handler will capture the id of the dragged source node and the id of the dropped-on target node.
- This action will not draw a visual line, but will instead update a mappings array in the Zustand store. For example: addMapping({ sourcePath: '$.'user.name'', targetPath: ['user', 'profile', 'name'] }).
-
Visual Feedback: The primary visual feedback for a successful mapping will be color-coding the target tree node (e.g., turning it green).
2.3. State Management: Zustand (The UI State Engine)
The application's UI is highly interactive. The state of both trees, the list of current mappings, the selected element, and the test output JSON must be globally synchronized.
-
Analysis:
- React Context: While built-in, Context can cause performance issues with frequent state updates (like drag operations).
- Zustand: A minimal, hook-based state management library. It is an ideal choice, avoiding Context "provider hell" and allowing for high-performance, selective state subscription (slicing).
-
Synchronization Pattern: This architecture enables a powerful two-way data-binding pattern between the tree views and the properties panel.
1. UI -> State: The JsonTreeNode component's onClick handler will call an action in the Zustand store (e.g., setSelectedNodeId(nodeId)).
2. State -> UI: The component will listen to that same state slice (useStore(s => s.selectedNodeId)). When the user types a static value into the panel, it calls a different action (setStaticValue(value)).
3. Store -> UI: This action updates the targetTreeData array in the store. The JsonTreeView component, which is subscribed to this array, will automatically re-render only the changed node.
2.4. Table: Core Technology Comparison
| Requirement | Evaluated Options | Selected Technology | Justification & Key Sources |
|---|---|---|---|
| App Structure | Strict SPA, MPA, Next.js Hybrid | Next.js Hybrid (SPA+BFF) | Fulfills "SPA" and server-side "file system access" requirements. The BFF pattern is the only way to securely manage .env and proxy APIs without a separate backend. |
| Visual Mapping UI | React Flow, react-beautiful-dnd |
dnd-kit + Custom Tree View |
Per specification. Uses simple tree views. dnd-kit is a modern, flexible D&D library. Mappings are stored in a state array (mappings) instead of as visual edges. |
| UI State Mgt. | React Context, Redux | Zustand | High-performance, avoids provider-hell, and simplifies two-way data-binding between the trees and the properties panel. |
| Data Pathing | lodash.get/set, Custom Reducers |
JSONPath-Plus & lodash.set |
The "keys with dots" requirement breaks simple lodash.get. JSONPath is required to unambiguously read from the source. lodash.set with array paths is required to write to the target. |
2.5. Development & Security Standards
-
Software Versioning: All selected software, libraries, packages, modules, and other code dependencies (e.g., Next.js, React, dnd-kit, zustand) must be the most recent stable version available at the start of development.
-
Vulnerability Management: No code may be used that contains known critical vulnerabilities (e.g., as defined by CVE). Regular dependency audits (e.g., using npm audit) must be planned to identify and remediate vulnerabilities.
3. The Backend-for-Frontend (BFF) API Layer (Next.js API Routes)
3.1. Purpose
The BFF layer serves to abstract and secure all server-side resources. The client-side SPA will only communicate with its own BFF API endpoints. It will have no direct access to the file system, environment variables, or external REST APIs.
3.2. Configuration (.env.local)
A .env.local file at the project root will store all configuration. Per Next.js convention, these variables are loaded into process.env on the server and are not exposed to the client (unless prefixed with NEXT_PUBLIC_).
Example .env.local:
Ini, TOML
# Defines data source: 'FILE' or 'REST'
SOURCE_TYPE=FILE
# --- FILE Config (if SOURCE_TYPE=FILE) ---
# Path to directory or single file
SOURCE_PATH=./data/input
# 'true' or 'false'
SOURCE_WALK_SUBDIRS=true
# --- REST Config (if SOURCE_TYPE=REST) ---
SOURCE_API_URL=https://api.example.com/data
SOURCE_API_KEY=xxxxx_secret_key_xxxxx # Kept on server
SOURCE_API_PAGING_ENABLED=true
SOURCE_API_PAGE_PARAM_NAME=page
SOURCE_API_OFFSET_PARAM_NAME=offset
SOURCE_API_PAGE_SIZE=50
# --- Target Schema Config ---
TARGET_SCHEMA_PATH=./data/target_schema.json
# --- Test Runner Config ---
TEST_DATA_COUNT=1
3.3. Table: Next.js API Endpoint Specification
| Method | Endpoint | Purpose & Logic |
|---|---|---|
GET |
/api/config |
Reads process.env and returns a client-safe subset of configuration, including paging info (e.g., SOURCE_API_PAGING_ENABLED). Crucially, it will never return SOURCE_API_KEY or SOURCE_PATH. |
GET |
/api/source-data?page=N&offset=N |
The primary data-loading endpoint. 1. Reads process.env.SOURCE_TYPE. 2. If 'FILE': Uses Node.js fs to read process.env.SOURCE_PATH... 3. If 'REST': Acts as a proxy. Reads req.query.page and req.query.offset. If process.env.SOURCE_API_PAGING_ENABLED === 'true', it constructs a URL with paging query parameters (using the names from SOURCE_API_PAGE_PARAM_NAME, etc.). Calls fetch on the constructed URL, adding the Authorization header. Returns the response. |
GET |
/api/target-schema |
Uses Node.js fs.promises.readFile() to read the file at process.env.TARGET_SCHEMA_PATH and return its JSON content. |
4. Defining the "Groovy-Ready" Mapping File: A Formal Schema
4.1. The Central Data Model
This JSON schema is the most critical non-code artifact of the architecture. It is the "source of truth" and the application's primary output. It is designed to be trivially parsed by a Groovy JsonSlurper into a Map or List of instructions.
4.2. Solving the "Keys with Dots" Problem
A critical requirement is that "keys and values of the JSON files can have dots... do not split these!".
This requirement immediately invalidates any transformation logic based on simple, .-delimited string paths. For example, a path user.name is ambiguous: does it refer to obj.user.name or obj['user.name']?
To resolve this ambiguity, the mapping schema must adopt unambiguous pathing formalisms:
-
For Sources (Reading): We will use JSONPath. JSONPath has a clear syntax for handling keys with special characters. The path $.'user.name'['address.line1'] is unambiguous and correctly targets keys containing dots.
-
For Targets (Writing): The Groovy script will be setting values. The simplest and most robust way to specify a target path, especially one that may contain dots, is as an array of keys. This is modeled by libraries like lodash.set. A path to obj['user.name']['address.line1'] would be represented as ['user.name', 'address.line1'].
Therefore, our mapping schema will use JSONPath strings for all sources and key arrays for all targets.
4.3. Table: Mapping File Schema Definition (mapping.json)
JSON
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "JSON Transformation Mapping File",
"type": "object",
"properties": {
"metadata": {
"type": "object",
"properties": {
"version": { "type": "string", "description": "Mapping file version." },
"description": { "type": "string", "description": "Human-readable description." },
"createdAt": { "type": "string", "format": "date-time" }
}
},
"config": {
"type": "object",
"properties": {
"inputRootNode": {
"type": "string",
"description": "Optional JSONPath to apply to the source data before processing.",
"examples": ["$.data.records"]
}
}
},
"mappings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"target": {
"type": "array",
"items": { "type": "string" },
"description": "Path to the target field, as an array of keys. e.g., ['user', 'profile', 'name'] or ['user.name', 'address']"
},
"transform": {
"type": "string",
"enum": ["DIRECT", "STATIC", "CONCAT", "ARRAY_ADD"]
},
"source": {
"type": "string",
"description": "JSONPath string for a single source. Used by DIRECT and ARRAY_ADD.",
"examples": ["$.person.fullName"]
},
"sources": {
"type": "array",
"items": { "type": "string" },
"description": "JSONPath strings for multiple sources. Used by CONCAT."
},
"value": {
"description": "The static value to be set. Used by STATIC."
},
"options": {
"type": "object",
"properties": {
"delimiter": { "type": "string", "description": "Delimiter for CONCAT." }
}
}
},
"required": ["target", "transform"]
}
}
},
"required": ["metadata", "mappings"]
}
4.4. Justification
This schema is explicit and declarative. It provides the external Groovy script with unambiguous instructions. For example, a rule like {"target": ["user", "tags"], "transform": "ARRAY_ADD", "source": "$.category"} is trivial to parse. It instructs the script: "Get the value of $.category from the source, find the tags array inside the user object of the target, and add this value to it." This structure directly fulfills the "preserve this array" requirement.
5. Designing the Core Mapping Interface with Tree Views
5.1. Application Layout
The application will feature a new layout:
-
Header Row (Full-width): Contains the "Upload," "Download," and "Test" buttons.
-
Main Content Area (2-column):
- Left Column (e.g., 40% width): Renders the Source JSON Tree View.
- Right Column (e.g., 60% width): This column is split into two rows:
- Top Panel: Renders the Target JSON Tree View.
- Bottom Panel: Renders the Properties Panel. This panel will be expandable and retractable (e.g., using a tag or a simple state toggle) to show/hide the properties of the selected tree node.
5.2. Parsing JSON into a Tree Data Structure
A recursive function, jsonToTreeData(obj, side, pathArray = []), will be created to traverse the sourceJson and targetSchemaJson.
For each key-value pair, this function will generate a tree node object:
{ id: '...', label: 'key', value: '...', children: [...] }
The id of each node is the critical link between the visual tree and the mapping schema. The ID will be its full path:
-
Source Node ID Example: source::$.'user.data'['full-name'] (A valid JSONPath string)
-
Target Node ID Example: target::user.profile.name (A .-delimited string, easily convertible to ['user', 'profile', 'name'])
5.3. Custom Tree Node Component (JsonTreeNode.tsx)
A custom React component, JsonTreeNode, will be created to render each item in the tree.
-
It will render the key: value pair.
-
It will be wrapped by dnd-kit's useDraggable (if it's a source node) or useDroppable (if it's a target node).
-
Color-Coding: The node's data prop, managed by Zustand, will contain { key, value, valueType: 'default' | 'static' | 'mapped' }. CSS classes will be conditionally applied based on this valueType to provide the required color-coding:
- default: (e.g., Gray) - Value from the targetSchemaJson.
- static: (e.g., Blue) - Value has been set by the user in the properties panel.
- mapped: (e.g., Green) - A mapping rule exists in the mappings array that targets this node.
5.4. Mapping Logic with Drag-and-Drop
The core mapping logic resides in the onDragEnd handler of the <DndContext> component (managed by Zustand).
-
A user drags a source node and drops it onto a target node.
-
The onDragEnd handler fires, providing event.active (source) and event.over (target).
-
The handler extracts the paths from the node IDs:
- sourcePath = event.active.id (e.g., source::$.'user.name')
- targetPath = event.over.id (e.g., target::user.profile.name)
-
The handler checks if the targetPath already has a mapping in the mappings array.
-
If NO: A new mapping is added: addMapping({ sourcePath, targetPath, transform: 'DIRECT' }).
S
-
If YES: This triggers the "chaining" requirement. The application will prompt the user (e.g., via a modal) "This target is already mapped. Do you want to [Concatenate] or [Add to Array]?".
-
Based on this choice, the store will find the existing mapping(s) for that target and modify their transform type (e.g., to CONCAT) and update the list of sources.
6. State Management and UI Synchronization with Zustand
6.1. The Single Source of UI Truth
The Zustand store will manage the entire application UI state, centralizing the logic and abstracting it from the React components.
6.2. Table: Zustand Global Store Schema (useStore.ts)
| Key | Type | Description |
|---|---|---|
sourceJson |
object |
The raw source JSON loaded from /api/source-data. |
targetSchemaJson |
object |
The raw target JSON schema loaded from /api/target-schema. |
sourceTreeData |
TreeItem[] |
The hierarchical data for the source tree view. |
targetTreeData |
TreeItem[] |
The hierarchical data for the target tree view. |
mappings |
MappingRule[] |
Core State. Array of all defined mappings. |
testOutputJson |
object | null |
The result of the "Test" run, to be shown in a JSON viewer. |
selectedNodeId |
string | null |
The ID of the clicked tree node. |
onDragEnd |
(event) => void |
Core Logic: Handles D&D drop, extracts source/target paths, and manages chaining (Concat/Array) logic. |
loadInitialData |
async (paging?: { page?: number, offset?: number }) => void |
Action that calls the BFF endpoints. It will pass paging params to /api/source-data if provided (e.g., /api/source-data?page=1). Then runs the jsonToTreeData parser to populate sourceTreeData, targetTreeData, etc. |
setStaticValue |
(nodeId, value) => void |
Properties Panel -> Store: Finds a target node by nodeId in targetTreeData (recursively), updates its data.value and data.valueType = 'static', causing a re-render. |
runTest |
() => void |
Test Button Action: 1. Calls generateMappingFile() (in-memory). 2. Executes the "Test Transformation Engine" (Section 7). 3. Sets testOutputJson with the result. |
generateMappingFile |
() => MappingSchema |
Serialize State: Iterates mappings array and targetTreeData (for STATIC values) to build the JSON object from Section 4.3. |
loadMappingFile |
(data) => void |
Deserialize State: Parses an uploaded mapping file, repopulates the mappings array, and updates targetTreeData with any STATIC values. |
6.3. Synchronization Pattern (Properties Panel Example)
The following sequence details the two-way data-binding enabled by Zustand:
-
A user clicks a target .
-
The node's onClick handler calls the store action: useStore.getState().setSelectedNodeId(props.id).
-
The component, which is subscribed to the store using const selectedId = useStore(s => s.selectedNodeId, shallow), re-renders to display an input field.
-
The user types "New Static Value" into the panel's input. The onChange handler calls useStore.getState().setStaticValue(selectedId, "New Static Value").
-
The setStaticValue action finds the corresponding node in the targetTreeData structure and updates its data.value and data.valueType.
-
The JsonTreeView component, which is subscribed to the targetTreeData array, detects this change and re-renders only the changed node, which now displays the new value and the "static" color.
7. The Client-Side "Test" Transformation Engine
7.1. Purpose
The "Test" button will trigger a client-side JavaScript function that provides a 1:1 simulation of the external Groovy script's behavior. It does this by executing the exact mapping rules (from Section 4.3) generated from the current UI state against the loaded source JSON.
7.2. Simulating the External Process
The Groovy script will require two key utilities to function: a JSONPath library to resolve source paths and a nested-set utility to write to the target object.
Therefore, our client-side test engine must use JavaScript equivalents to ensure a high-fidelity simulation.
-
To Read Sources: We will use jsonpath-plus, a robust JSONPath implementation.
-
To Write Targets: We will use lodash.set. This utility perfectly handles setting values deep in an object using an array of keys, which matches our target path schema.
7.3. Core Logic (runTest implementation)
The runTest action in the Zustand store will perform the following:
-
const rules = useStore.getState().generateMappingFile(); // Get serializable rules.
-
const sourceJson = useStore.getState().sourceJson;
-
const baseTarget = useStore.getState().targetSchemaJson;
-
let targetResult = JSON.parse(JSON.stringify(baseTarget)); // Deep-clone base target to preserve default values.
-
Import JSONPath from jsonpath-plus and set, get from lodash.
-
Iterate rules.mappings:
javascript // (Inside runTest function) for (const rule of rules.mappings) { // rule.target is an array of keys: ['user.name', 'address'] // This handles keys with dots correctly. const targetPath = rule.target; switch (rule.transform) { case "STATIC": set(targetResult, targetPath, rule.value); break; case "DIRECT": // JSONPath.value() returns the first match const value = JSONPath.value(sourceJson, rule.source); set(targetResult, targetPath, value); break; case "CONCAT": const values = rule.sources.map(s => JSONPath.value(sourceJson, s)); set(targetResult, targetPath, values.join(rule.options.delimiter || '')); break; case "ARRAY_ADD": // This fulfills the "preserve this array" requirement. const arrValue = JSONPath.value(sourceJson, rule.source); // Use lodash.get to safely retrieve the existing array, or default to [] const existingArr = get(targetResult, targetPath, []); if (Array.isArray(arrValue)) { // Handle source being an array itself set(targetResult, targetPath, [...existingArr, ...arrValue]); } else { set(targetResult, targetPath, [...existingArr, arrValue]); } break; } }
-
useStore.getState().setTestOutput(targetResult);
7.4. Displaying the Result
The testOutputJson state will be passed as the value prop to a read-only @uiw/react-json-view component. This component is chosen for its ability to correctly display JSON keys that contain dots.
8. File Handling: Uploading and Downloading Mappings
8.1. Downloading the Mapping File
A "Download Mapping" button will trigger this client-side function:
-
It calls const mappingData = useStore.getState().generateMappingFile(); to get the serializable object.
-
const jsonString = JSON.stringify(mappingData, null, 2);.
-
const blob = new Blob([jsonString], { type: "application/json" });.
-
const href = URL.createObjectURL(blob);.
-
A temporary element is created in memory, its href is set to the object URL, and download is set to "mapping.json".
-
a.click() is called to trigger the browser's download prompt.
-
URL.revokeObjectURL(href); is called to release the memory.
8.2. Uploading an Existing Mapping File
An <input type="file" accept=".json"> element will be provided.
Logic (Parsing):
-
The onChange event provides the file = e.target.files[0].
-
A FileReader instance is created.
-
The reader.onload event handler is set up.
-
reader.readAsText(file) is called.
-
Inside the onload handler:
- const content = e.target.result;.
- const mappingData = JSON.parse(content);.
- useStore.getState().loadMappingFile(mappingData);.
Logic (Re-hydrating State):
The loadMappingFile(data) action is the inverse of generateMappingFile.
-
It gets the targetTreeData from the store.
-
It iterates over data.mappings.
-
For STATIC rules: It finds the target node in targetTreeData (recursively) and updates its data.value and data.valueType = 'static'.
-
For all other rules (DIRECT, CONCAT, etc.): It adds the rule object directly to the mappings array in the store: set({ mappings: data.mappings }).
-
It updates the targetTreeData (again, recursively) to set the valueType = 'mapped' for any node that is a target in the data.mappings array, providing the visual color-coding.
9. Exhaustive Prompt for Code Generation (Revised)
To the AI Coding Agent:
You are tasked with generating a Next.js (using the Pages Router for simplicity) application for visual JSON-to-JSON data mapping. The primary output is a JSON-based "mapping file" for an external Groovy script.
Follow these specifications exactly:
1. Project Goal & Core Concepts
-
Application: A single-page browser app for visually mapping fields from a source JSON to a target JSON.
-
Core Output: A mapping.json file. The UI is just an editor for this file.
-
Backend: A "Backend-for-Frontend" (BFF) using Next.js API Routes to handle all file system and configuration access. The client must not access the file system directly.
-
Visuals: Use simple tree views. Do not use React Flow. Mappings will be created using drag-and-drop from the source tree to the target tree.
-
State: Use Zustand for all global UI state.
-
Key Requirement (Keys with Dots): JSON keys may contain dots, dashes, or underscores (e.g., "user.name"). Your pathing logic must handle this without splitting the keys.
2. Tech Stack & Standards
-
Framework: Next.js (Pages Router)
-
Visual Drag-and-Drop: dnd-kit (@dnd-kit/core, @dnd-kit/modifiers, @dnd-kit/utilities)
-
Global State: zustand
-
JSON Pathing (Read): jsonpath-plus
-
Object Pathing (Write): lodash.set, lodash.get
-
JSON Viewing: @uiw/react-json-view
-
Styling: (Your choice, e.g., Tailwind CSS or CSS Modules)
-
Versioning: Use the most recent stable version of all dependencies.
-
Security: Ensure no dependencies with known critical vulnerabilities are used.
3. Project Structure (Minimal)
/
├── data/
│ ├── input/
│ │ └── sample1.json
│ └── target_schema.json
├── pages/
│ ├── api/
│ │ ├── config.ts
│ │ ├── source-data.ts
│ │ └── target-schema.ts
│ ├── _app.tsx
│ └── index.tsx (Main application page)
├── components/
│ ├── JsonTreeView.tsx (Renders a tree recursively)
│ ├── JsonTreeNode.tsx (The custom draggable/droppable node)
│ ├── PropertiesPanel.tsx (Shows details for selectedNodeId)
│ ├── TestOutput.tsx (Hosts @uiw/react-json-view)
│ └── TopBar.tsx (Buttons for Load, Save, Test)
├── store/
│ └── useStore.ts (Zustand store definition)
├── lib/
│ ├── jsonToTreeData.ts (JSON-to-Tree-Data parser)
│ ├── transformationEngine.ts (The "Test" button logic)
│ └── fileHandlers.ts (Upload/Download logic)
└──.env.local (Configuration file)
4. Backend-for-Frontend (BFF) Specification
-
File: .env.local
- Create this file with the contents from Section 3.2 (which now includes SOURCE_API_PAGING_ENABLED etc.).
-
Files: pages/api/config.ts, pages/api/source-data.ts, pages/api/target-schema.ts
- Implement these three API routes exactly as specified in Section 3.3.
-
File: pages/api/source-data.ts (Paging Logic)
- If 'REST':
- Reads req.query.page and req.query.offset.
- Checks process.env.SOURCE_API_PAGING_ENABLED.
- If true, it dynamically builds the URL: e.g., const url = new URL(process.env.SOURCE_API_URL) and adds query params like url.searchParams.append(process.env.SOURCE_API_PAGE_PARAM_NAME, req.query.page).
- fetch(url.toString(), { headers: { 'Authorization': \Bearer ${process.env.SOURCE_API_KEY} } }).
- This acts as a secure proxy.
- Return the JSON response from the external API.
5. Core Mapping File Schema
This is the central data model and the primary output of the app. Your generateMappingFile function must produce a JSON object validating against the schema defined in Section 4.3.
6. Global State (Zustand)
-
File: store/useStore.ts
- Create a Zustand store that implements the exact schema from the table in Section 6.2.
- Include: sourceJson, targetSchemaJson, sourceTreeData, targetTreeData, mappings, testOutputJson, selectedNodeId.
- onDragEnd: Implement the logic from Section 5.4.
- Get event.active.id (source path) and event.over.id (target path).
- Check if targetPath exists in the mappings array.
- If not, add a new DIRECT mapping.
- If yes, use window.prompt to ask "Concatenate or Add to Array?" and update the existing mapping's transform type.
- After updating mappings, update targetTreeData to apply the 'mapped' color-coding.
- loadInitialData: async (paging?: { page?: number, offset?: number }) => void. fetch from your 3 BFF API endpoints. Pass the paging object as query parameters to /api/source-data. After data is loaded, call the jsonToTreeData parser (see below) to populate sourceTreeData and targetTreeData.
- setStaticValue: Implement the logic from Section 6.3. Find the node in targetTreeData (recursively) by selectedNodeId, update its data.value and data.valueType = 'static'.
- generateMappingFile: Implement the "serialization" logic.
m - Find all STATIC values by traversing targetTreeData.
- Combine these with the mappings array.
- Return a JSON object matching the schema in Section 4.3.
- loadMappingFile: Implement the "re-hydration" logic from Section 8.2. Populate mappings from the file and traverse targetTreeData to set STATIC values and mapped colors.
- runTest: Call runTestTransformation from lib/transformationEngine.ts and set its output to testOutputJson.
7. Core UI (Tree Views)
-
File: lib/jsonToTreeData.ts
- Create a recursive function jsonToTreeData(obj: any, side: 'source' | 'target', path: string[] = []): TreeItem[].
- This function traverses the JSON object.
- For each key-value pair, it creates a TreeItem object (e.g., { id: '...', label: key, value: value, children: [...] }).
- Node ID (Critical): The ID must be the full path.
m - If side === 'source': The ID is a JSONPath string, e.g., source::$.'user.name'. Use JSONPath syntax to correctly handle keys with dots.
- If side === 'target': The ID is a .-delimited string, e.g., target::user.profile.name.
- Return a flat array of all nodes.
-
File: components/JsonTreeNode.tsx
- Create a custom component that renders a single node (props.node).
- It should recursively call for props.node.children.
- Wrap the node's content with dnd-kit's useDraggable (if side === 'source') or useDroppable (if side === 'target').
I
- Pass the id (the full path) to useDraggable / useDroppable.
- Render the data.label (key) and data.value.
- Apply conditional CSS classes based on data.valueType to achieve the default, static, and mapped color-coding.
- Add an onClick handler that calls useStore.getState().setSelectedNodeId(props.node.id).
-
File: pages/index.tsx (Main Layout)
- Implement the layout from Section 5.1.
- Render .
- Render a <DndContext onDragEnd={useStore(s => s.onDragEnd)}>.
e
- Inside, render the 2-column layout.
- Left Column: . Also include simple "Next/Previous" paging controls (e.g., buttons) in this column, which call loadInitialData({ page: currentPage + 1 }). These controls should only be visible if the config (fetched from /api/config) indicates paging is enabled.
s
- Right Column:
- Top:
- Bottom:
- Render (e.g., in a modal or another panel).
8. Properties Panel & Test UI
-
File: components/PropertiesPanel.tsx
- Implement this component as expandable and retractable (e.g., using tag).
- Subscribes to useStore(s => s.selectedNodeId).
- If selectedNodeId is a target node, show its path and an input field.
- The input field's value should be the node's current data.value.
- The onChange handler for the input must call useStore.getState().setStaticValue(selectedNodeId, e.target.value).
- If the selected node is mapped, also display the sourcePath(s) from the mappings array.
-
File: components/TestOutput.tsx
- Subscribes to useStore(s => s.testOutputJson).
- Renders the component from @uiw/react-json-view.
9. Test Engine (Client-Side Simulation)
-
File: lib/transformationEngine.ts
- Export a function runTestTransformation(rules: MappingSchema, sourceJson: any, baseTarget: any): any.
- This function must implement the exact logic from Section 7.3.
- Use jsonpath-plus to resolve all source and sources paths.
m
- Use lodash.set and lodash.get to write to the targetResult object, using the rule.target array as the path.
- Implement the switch statement for DIRECT, STATIC, CONCAT, and ARRAY_ADD.
- Return the final targetResult object.
10. File Handlers
-
File: lib/fileHandlers.ts
- Implement downloadMappingFile(rules: MappingSchema): void as specified in Section 8.1.
- Implement uploadMappingFile(file: File): Promise as specified in Section 8.2.
-
Component Integration:
- A "Download" button in TopBar.tsx will call const rules = useStore.getState().generateMappingFile(); downloadMappingFile(rules);.
- An in TopBar.tsx will, on change, call const rules = await uploadMappingFile(e.target.files[0]); useStore.getState().loadMappingFile(rules);.