16 Commits

Author SHA1 Message Date
57371feeb0 Release v1.3.1: Added PowerShell support and fixed theme issues 2026-01-31 11:06:25 +01:00
d398c34aa5 Migrate to Vite 7, improve UI (Copy/Download), and enhance API security 2026-01-31 10:16:49 +01:00
452e6e74cb update: moved input control buttons JSON Data area. 2026-01-31 09:10:55 +01:00
b7df3e731f Fix: the expression input box was getting reset while switching pages. Formatted the code text. 2026-01-31 09:05:07 +01:00
72d1be0bdc Enhance build-image script to support multi-architecture builds and improve help output 2026-01-30 00:06:11 +01:00
929d2ee5d2 Add upload scripts for PowerShell and NodeJS. 2026-01-30 00:04:02 +01:00
d0961c68fa Version 1.2.5 2026-01-26 18:38:47 +01:00
b1fd6da218 Update upload script to allow API URL and key to be set via environment variables 2026-01-26 16:41:04 +01:00
2a498124fe Fix API URL variable reference in upload script 2026-01-26 16:39:41 +01:00
37c73ddd2b Refactor upload script to use environment variables for API URL and key, and update usage instructions 2026-01-26 16:34:52 +01:00
03cc889cd0 Update copilot instructions with version release procedures 2026-01-23 14:46:19 +01:00
0d3832137f Fix version display issues, container shutdown problems, and modernize build scripts 2026-01-23 14:39:33 +01:00
81b3b84f81 Remove deprecated upload script 2026-01-23 12:57:21 +01:00
cebae83ae1 Fix dark theme colors, reload button functionality, and code cleanup 2026-01-23 12:50:56 +01:00
fd537026d3 Updated JSON upload script. 2026-01-23 10:00:03 +01:00
f2ca5d5f84 Add support for API key in upload-jmespath script 2026-01-23 09:57:59 +01:00
32 changed files with 3340 additions and 16041 deletions

View File

@@ -93,3 +93,17 @@ The application exposes a REST API for remotly uploading sample data. The API en
## Containerization
The application should be prepared for deployment using containerization. It should extend minimal Node 24 LTS container image.
## Updates
Always use `scripts/new-version.js` script to make a new release.
Correct procedure to make a new release:
- Review the code changes and ensure everything is working.
- Run `npm run build` to build the React application.
- Run `npm test` to execute the test suite and ensure all tests pass.
- Prepare a commit message describing the changes made.
- Use `scripts/new-version.js` to create a new version and commit the changes. Use `--force` option if repository is not clean.
- Don't push the changes without approval.
- Don't build docker image without approval.

View File

@@ -1,84 +0,0 @@
# Development Guide
## Quick Start Commands
```bash
# Install dependencies
npm install
# Start development server (with hot reload)
npm start
# Build for production
npm run build
# Run tests
npm test
# Serve production build locally
npm run serve
```
## Docker Commands
```bash
# Build Docker container
docker build -t jmespath-playground .
# Run Docker container
docker run -p 3000:3000 jmespath-playground
# Development with Docker Compose
docker-compose --profile dev up jmespath-playground-dev
# Production with Docker Compose
docker-compose up jmespath-playground
```
## Project Structure Overview
```
src/
├── App.js # Main component with JMESPath logic
├── App.css # App-specific styles
├── App.test.js # Basic tests
├── index.js # React entry point
├── index.css # Global styles and Bootstrap overrides
└── setupTests.js # Test configuration
```
## Key Features Implemented
1. **Real-time JMESPath Evaluation**: Uses the `jmespath` library to evaluate expressions as user types
2. **JSON Validation**: Parses and validates JSON input with error reporting
3. **Bootstrap UI**: Responsive layout with cards, buttons, and form controls
4. **Error Handling**: Clear error messages for both JSON and JMESPath syntax issues
5. **Sample Data**: Pre-loaded examples with "Load Sample" button
6. **JSON Formatting**: "Format JSON" button to prettify JSON input
7. **Clear Function**: "Clear All" button to reset all inputs
## Component Architecture
The main `App.js` component manages:
- State for JMESPath expression, JSON data, results, and errors
- Auto-evaluation using `useEffect` when inputs change
- Error handling for both JSON parsing and JMESPath evaluation
- UI event handlers for buttons and input changes
## Styling
- Bootstrap 5.3.2 for responsive grid and components
- Custom CSS for enhanced UX (color coding, hover effects)
- Gradient header with professional appearance
- Color-coded input areas (yellow for JMESPath, green for JSON, blue for results)
## Browser Compatibility
Built with React 18 and targets:
- Modern evergreen browsers
- Node.js 24 LTS compatibility
- Mobile-responsive design
## License
MIT License - see LICENSE file for details.

View File

@@ -14,31 +14,16 @@ COPY package*.json ./
# Install dependencies (production + dev for build)
RUN npm ci
# Copy source code and build scripts
# Copy source code and build dependencies
COPY src/ ./src/
COPY public/ ./public/
COPY scripts/ ./scripts/
COPY server.js ./server.js
COPY vite.config.js ./vite.config.js
COPY index.html ./index.html
# Generate version.js if version info provided, otherwise run normal build
RUN if [ -n "$VERSION" ]; then \
echo "// Auto-generated version file - do not edit manually" > src/version.js && \
echo "// Generated at: $(date -Iseconds)" >> src/version.js && \
echo "" >> src/version.js && \
echo "export const VERSION = '$VERSION';" >> src/version.js && \
echo "export const IS_RELEASE = $IS_RELEASE;" >> src/version.js && \
echo "export const BUILD_TIME = '$(date -Iseconds)';" >> src/version.js && \
echo "📝 Generated version.js with VERSION=$VERSION, IS_RELEASE=$IS_RELEASE"; \
fi
# Build the application (skip prebuild if we already generated version.js)
RUN if [ -n "$VERSION" ]; then \
echo "🚀 Building with pre-generated version.js" && \
npx react-scripts build; \
else \
echo "🚀 Building with version-check.js" && \
npm run build; \
fi
# Build the application
RUN npm run build
# Production stage
FROM node:24-alpine AS production
@@ -56,6 +41,10 @@ RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/build ./build
COPY --from=builder /app/server.js ./server.js
# Copy entrypoint script
COPY entrypoint.sh ./entrypoint.sh
RUN chmod +x entrypoint.sh
# Expose port 3000
EXPOSE 3000
@@ -64,4 +53,4 @@ ENV LISTEN_ADDR=0.0.0.0
ENV LISTEN_PORT=3000
# Start the integrated server
CMD ["node", "server.js"]
ENTRYPOINT ["./entrypoint.sh"]

174
README.md
View File

@@ -8,62 +8,63 @@ A React-based web application for testing and validating JMESPath expressions ag
## Features
- 🎯 **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type
- 📝 **JSON Validation**: Built-in JSON syntax validation and error reporting
- 📁 **File Upload**: Load JSON data directly from local files (supports JSON Lines format for .log files)
- 🎨 **Bootstrap UI**: Clean, responsive interface with Bootstrap styling
- 🔄 **Sample Data**: Pre-loaded examples to get started quickly
- 📱 **Responsive Design**: Works on desktop, tablet, and mobile devices
- 🐳 **Docker Ready**: Containerized for easy deployment
-**Error Handling**: Clear error messages for both JSON and JMESPath syntax issues
## Application Layout
The application is divided into three main sections:
1. **Top Section**: Title and description of the tool
2. **Middle Section**: Input area for JMESPath expressions
3. **Bottom Sections**:
- **Left**: JSON data input area
- **Right**: Query results output area
- **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type
- **File Upload**: Load JSON data directly from local files (supports JSON Lines format for .log files)
- **Remote API**: Upload sample data remotely via REST API with encrypted sessions
- **Container Ready**: Containerized for easy deployment
## Quick Start
### Prerequisites
- Node.js 24 LTS or higher
- npm or yarn package manager
- npm package manager
### Local Development
1. **Clone the repository**:
```bash
git clone <repository-url>
cd jmespath-playground
```
2. **Install dependencies**:
```bash
npm install
```
3. **Start the development server**:
```bash
npm start
```
4. **Open your browser** and navigate to `http://localhost:3000`
### Container Deployment (Optional)
### Development
You can optionally run the application in a Docker container:
For development with hot reload on component changes:
```bash
# Build the Docker image
docker build -t jmespath-playground .
npm run dev
```
# Run the container
This runs both the React dev server (with hot reload) and the API server concurrently. The React app will proxy API requests to the backend server.
### Container Deployment
You can optionally run the application in a container:
```bash
# Build the container image
npm run build-image
# Run the container (Docker or Apple Container Tools)
docker run -p 3000:3000 jmespath-playground
# or
container run -p 3000:3000 jmespath-playground
```
## Usage
@@ -82,93 +83,56 @@ docker run -p 3000:3000 jmespath-playground
- Format JSON for better readability
- Clear all inputs
### Example JMESPath Expressions
## Remote API Usage
Try these examples with the sample data:
- `people[*].name` - Get all names
- `people[0]` - Get the first person
- `people[?age > 30]` - Filter people older than 30
- `people[*].skills[0]` - Get the first skill of each person
- `length(people)` - Count the number of people
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in development mode. The page will reload when you make edits.
### `npm test`
Launches the test runner in interactive watch mode.
### `npm run build`
Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance.
### `npm run serve`
Serves the production build locally on port 3000.
### Docker Scripts
### `npm run docker:build`
Builds a Docker container.
### `npm run docker:run`
Runs the Docker container.
## Project Structure
The application includes a REST API for uploading sample data remotely:
1. **Access API Key**: Click the key-lock button (🔒) to view your unique API key
2. **Upload Data**: Use curl or any HTTP client to upload JSON data:
```bash
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
--data @sample-data.json \
"http://your-domain.com/api/v1/upload"
```
jmespath-playground/
├── .github/
│ ├── workflows/
│ │ └── build-container.yml # CI/CD pipeline
│ └── copilot-instructions.md # AI agent instructions
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── favicon.ico
├── src/
│ ├── App.js # Main application component
│ ├── App.css # App-specific styles
│ ├── App.test.js # App tests
│ ├── index.js # React entry point
│ ├── index.css # Global styles
│ ├── setupTests.js # Test configuration
│ └── reportWebVitals.js
├── scripts/
│ ├── build.sh # Build script
│ └── dev.sh # Development script
├── Dockerfile # Docker container
├── Dockerfile.dev # Development container
├── docker-compose.yml # Container orchestration
├── package.json # Dependencies and scripts
├── README.md # Comprehensive documentation
├── DEVELOPMENT.md # Developer guide
└── demo.sh # Demo script
3. **Auto-reload**: The running app will detect new data and show a reload button
**API Endpoints:**
- `POST /api/v1/upload` - Upload sample data
- `GET /api/v1/sample` - Retrieve current sample data
- `GET /api/v1/state` - Get current state ID
- `GET /api/v1/health` - Simple health check (returns "OK")
- `GET /api/v1/status` - Detailed status information (JSON)
## Server Configuration
The server can be configured using environment variables:
**Network Settings:**
- `LISTEN_ADDR` - Server bind address (default: `127.0.0.1`)
- `LISTEN_PORT` - Server port (default: `3000`)
**Session Management:**
- `MAX_SESSIONS` - Maximum number of concurrent sessions (default: `100`)
- `MAX_SAMPLE_SIZE` - Maximum size of uploaded sample data in bytes (default: `1048576` - 1MB)
- `MAX_SESSION_TTL` - Session time-to-live in milliseconds (default: `3600000` - 1 hour)
Example usage:
```bash
MAX_SESSIONS=200 MAX_SAMPLE_SIZE=2097152 LISTEN_PORT=8080 node server.js
```
## Technology Stack
- **React 18.2.0**: Frontend framework
- **Bootstrap 5.3.2**: CSS framework for styling
- **JMESPath 0.16.0**: JMESPath expression evaluation
- **React 18.2.0**: Frontend framework with modern hooks and components
- **Bootstrap 5.3.2**: CSS framework with dark/light theme support
- **JMESPath 0.16.0**: JMESPath expression evaluation library
- **Express.js 4.19.2**: Backend API server with session management
- **Node.js 24 LTS**: Runtime environment
- **Docker**: Optional containerization
## Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/new-feature`
3. Make your changes and commit them: `git commit -m 'Add new feature'`
4. Push to the branch: `git push origin feature/new-feature`
5. Submit a pull request
- **UUID 9.0.0**: Cryptographically secure session IDs
- **Container**: Containerization for easy deployment
## License
@@ -177,7 +141,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## About JMESPath
JMESPath is a query language for JSON. It allows you to declaratively specify how to extract elements from a JSON document. For more information about JMESPath syntax and capabilities, visit the [official JMESPath website](https://jmespath.org/).
## Support
If you encounter any issues or have questions, please [open an issue](../../issues) on GitHub.

72
bin/Upload-JMESPath.ps1 Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param(
[Parameter(HelpMessage='Path to JSON file; default: read from stdin')]
[string]$JsonFile = '-',
[Parameter(HelpMessage='API base URL')]
[string]$ApiUrl,
[Parameter(HelpMessage='API key for authentication')]
[string]$ApiKey,
[Parameter(HelpMessage='Show help')]
[switch]$Help
)
function Show-Usage {
$usage = @"
Usage: Upload-JMESPath.ps1 [-ApiUrl <url>] [-ApiKey <key>] [-JsonFile <file>]
Parameters:
-ApiUrl <url> The base URL of the JMESPath Playground API (default: http://localhost:3000 or $env:JMESPATH_PLAYGROUND_API_URL)
-ApiKey <key> The API key for authentication (can also be set via JMESPATH_PLAYGROUND_API_KEY)
-JsonFile <file> The JSON file to upload (default: stdin if not specified)
-Help Show this help message and exit
"@
Write-Output $usage
}
if ($Help) { Show-Usage; exit 0 }
# Apply environment defaults when parameters are not provided
if (-not $ApiUrl) {
if ($env:JMESPATH_PLAYGROUND_API_URL) { $ApiUrl = $env:JMESPATH_PLAYGROUND_API_URL } else { $ApiUrl = 'http://localhost:3000' }
}
if (-not $ApiKey) {
$ApiKey = $env:JMESPATH_PLAYGROUND_API_KEY
}
# Read JSON body from file or stdin
try {
if ($JsonFile -eq '-' -or [string]::IsNullOrEmpty($JsonFile)) {
$Body = [Console]::In.ReadToEnd()
} else {
if (-not (Test-Path -Path $JsonFile)) {
Write-Error "JSON file not found: $JsonFile"
exit 2
}
$Body = Get-Content -Raw -Path $JsonFile
}
} catch {
Write-Error "Failed to read JSON input: $($_.Exception.Message)"
exit 2
}
# Prepare headers
$Headers = @{ 'Accept' = 'application/json' }
if (-not [string]::IsNullOrEmpty($ApiKey)) { $Headers['X-API-Key'] = $ApiKey }
# POST to API
$Uri = "$ApiUrl/api/v1/upload"
try {
$response = Invoke-RestMethod -Uri $Uri -Method Post -Headers $Headers -ContentType 'application/json' -Body $Body -ErrorAction Stop
if ($null -ne $response) {
$response | ConvertTo-Json -Depth 10
} else {
Write-Output "Upload completed."
}
} catch {
Write-Error "Upload failed: $($_.Exception.Message)"
exit 3
}

View File

@@ -2,18 +2,33 @@
set -euo pipefail
API_URL="https://jmespath-playground.koszewscy.waw.pl"
JMESPATH_PLAYGROUND_API_URL="${JMESPATH_PLAYGROUND_API_URL:-http://localhost:3000}" # May be set in bash profile
JMESPATH_PLAYGROUND_API_KEY="${JMESPATH_PLAYGROUND_API_KEY:-}" # Required if not localhost
JSON_FILE="-"
function usage() {
echo "Usage: $0 [--api-url <url>] [--json-file <file>]"
exit 1
echo
echo "Options:"
echo " --api-url <url> The base URL of the JMESPath Playground API (default: http://localhost:3000)"
echo " --api-key <key> The API key for authentication (required if not localhost)"
echo " --json-file <file> The JSON file to upload (default: stdin if not specified)"
echo " -h, --help Show this help message and exit"
echo
echo "Environment Variables:"
echo " JMESPATH_PLAYGROUND_API_URL Can be used to set the API URL"
echo " JMESPATH_PLAYGROUND_API_KEY Can be used to set the API key"
}
while [[ $# -gt 0 ]]; do
case $1 in
--api-url)
API_URL="$2"
JMESPATH_PLAYGROUND_API_URL="$2"
shift 2
;;
--api-key)
JMESPATH_PLAYGROUND_API_KEY="$2"
shift 2
;;
--json-file)
@@ -32,10 +47,12 @@ while [[ $# -gt 0 ]]; do
esac
done
ADD_HEADERS+=("-H" "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY")
# Send the POST request
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
"${ADD_HEADERS[@]}" \
--data @${JSON_FILE} \
"$API_URL/api/v1/upload"
"$JMESPATH_PLAYGROUND_API_URL/api/v1/upload"

94
bin/upload-jmespath.mjs Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
// Parse command-line arguments
import { parseArgs } from "util";
const args = parseArgs({
options: {
"api-url": {
type: "string",
short: "a",
default: process.env.JMESPATH_PLAYGROUND_API_URL || "http://localhost:3000",
},
"api-key": {
type: "string",
short: "k",
default: process.env.JMESPATH_PLAYGROUND_API_KEY || "",
},
"json-file": {
type: "string",
short: "j",
default: "",
},
help: { type: "boolean", short: "h" },
},
});
// Show help message
if (args.values.help) {
console.log(`
Usage: upload-jmespath.mjs [options]
Options:
-a, --api-url API base URL (default: http://localhost:3000)
-k, --api-key API key for authentication
-j, --json-file Path to the JSON file to upload
-h, --help Show this help message
`);
process.exit(0);
}
// Read the JSON from the specfied file or from stdin if no file is provided
async function readJson(filePath) {
if (filePath) {
const absolutePath = path.resolve(filePath);
const fileContent = fs.readFileSync(absolutePath, "utf-8");
return JSON.parse(fileContent);
} else {
return new Promise((resolve, reject) => {
let data = "";
process.stdin.on("data", (chunk) => {
data += chunk;
});
process.stdin.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
}
}
// Upload the JSON data to the API using built-in fetch
async function uploadJson(apiUrl, apiKey, jsonData) {
const response = await fetch(`${apiUrl}/api/v1/upload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(apiKey ? { "X-API-Key": `${apiKey}` } : {}),
},
body: JSON.stringify(jsonData),
});
if (!response.ok) {
throw new Error(`Failed to upload JSON: ${response.status} ${response.statusText}`);
}
}
// Main function
async function main() {
try {
const jsonData = await readJson(args.values["json-file"]);
await uploadJson(args.values["api-url"], args.values["api-key"], jsonData);
console.log("JSON uploaded successfully.");
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
}
main();

79
demo.sh
View File

@@ -1,79 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Demo Script
echo "🚀 JMESPath Testing Tool Demo"
echo "==============================="
echo ""
# Check if Node.js is installed
if command -v node &> /dev/null; then
echo "✅ Node.js version: $(node --version)"
else
echo "❌ Node.js not found. Please install Node.js 24 LTS or higher."
exit 1
fi
# Check if npm is installed
if command -v npm &> /dev/null; then
echo "✅ npm version: $(npm --version)"
else
echo "❌ npm not found. Please install npm."
exit 1
fi
# Check Docker
if command -v docker &> /dev/null; then
echo "✅ Docker available: $(docker --version | cut -d' ' -f3 | cut -d',' -f1)"
DOCKER_AVAILABLE=true
else
echo "⚠️ Docker not found"
DOCKER_AVAILABLE=false
fi
echo ""
echo "📦 Installing dependencies..."
npm install
echo ""
echo "🧪 Running tests..."
npm test -- --watchAll=false
echo ""
echo "🔨 Building React application..."
echo " (Version will be automatically tagged as -dev since not building from git tag)"
npm run build
echo ""
echo "🎉 Demo completed successfully!"
echo ""
echo "Available commands:"
echo "==================="
echo ""
echo "Development:"
echo " npm start - Start React development server (port 3000)"
echo " npm run server - Start Express API server only (port 3000)"
echo " npm test - Run test suite"
echo ""
echo "Production:"
echo " npm run build - Build React app for production"
echo " node server/server.js - Start integrated server with built app"
echo ""
if [ "$DOCKER_AVAILABLE" = true ]; then
echo "Docker:"
echo " docker build -t jmespath-playground ."
echo " docker run -p 3000:3000 jmespath-playground"
echo ""
echo "Docker Compose:"
echo " docker compose up --build"
echo " docker compose down"
else
echo "Docker (install Docker first):"
echo " docker build -t jmespath-playground ."
echo " docker run -p 3000:3000 jmespath-playground"
echo " docker compose up --build"
fi
echo ""
echo "🌐 The application will be available at:"
echo " http://localhost:3000"

3
entrypoint.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
set -e
exec node server.js

View File

@@ -2,12 +2,11 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="JMESPath Testing Tool - Validate and test JMESPath expressions against JSON data" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700&family=Noto+Sans+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
@@ -16,5 +15,6 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>

16276
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,17 @@
{
"name": "jmespath-playground",
"version": "1.2.0",
"version": "1.3.1",
"description": "A React-based web application for testing JMESPath expressions against JSON data",
"main": "index.js",
"scripts": {
"start": "react-scripts start",
"start": "vite",
"prebuild": "node scripts/version-check.js",
"build": "react-scripts build",
"test": "react-scripts test",
"server": "node server.js"
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"server": "node server.js --dev",
"dev": "concurrently \"npm start\" \"npm run server\"",
"build-image": "node scripts/build-image.js"
},
"engines": {
"node": ">=24.0.0"
@@ -22,21 +25,8 @@
"jmespath": "^0.16.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"uuid": "^9.0.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/index.js"
]
},
"browserslist": {
"production": [
">0.2%",
@@ -59,6 +49,12 @@
"author": "",
"license": "MIT",
"devDependencies": {
"supertest": "^7.2.2"
"@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.18",
"concurrently": "^8.2.2",
"jsdom": "^27.4.0",
"supertest": "^7.2.2",
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
}

164
scripts/build-image.js Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const { parseArgs } = require('util');
function execCommand(command, description) {
try {
console.log(`${description}...`);
execSync(command, { stdio: 'inherit' });
} catch (error) {
console.error(`Error: ${description} failed`);
process.exit(1);
}
}
function getContainerTool() {
// Check for Docker first (primary tool)
try {
execSync('docker --version', { stdio: 'ignore' });
return 'docker';
} catch (error) {
// Fall back to Apple's container command
try {
execSync('container --version', { stdio: 'ignore' });
return 'container';
} catch (error) {
console.error('Error: No container tool found. Please install Docker or Apple Container Tools to build container images.');
process.exit(1);
}
}
}
function getVersion() {
try {
// Try to get version from git tag
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
if (gitTag) {
return { version: gitTag.replace(/^v/, ''), isRelease: true };
}
} catch (error) {
// Git command failed, ignore
}
// Development build - use package.json version with -dev suffix
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
return { version: `${packageJson.version}-dev`, isRelease: false };
}
function getHostArchitecture() {
// Map Node.js architecture names to container architecture names
const archMap = {
'arm64': 'arm64',
'arm': 'arm64',
'x64': 'amd64'
};
return archMap[process.arch] || 'arm64';
}
function showHelp() {
const hostArch = getHostArchitecture();
console.log(`Build multi-architecture container images for JMESPath Playground
Usage:
build-image.js [OPTIONS]
Options:
--all-arch Build for both arm64 and amd64 (default: build for host architecture only)
--arch <arch> Target architecture (arm64 or amd64). Can be specified multiple times.
--help, -h Show this help message and exit
Examples:
build-image.js # Builds for ${hostArch} only (host architecture)
build-image.js --all-arch # Builds for both arm64 and amd64
build-image.js --arch arm64 # Builds for arm64 only
build-image.js --arch arm64 --arch amd64 # Explicitly specify both
build-image.js -h # Show help`);
}
function main() {
const { values } = parseArgs({
options: {
help: {
type: 'boolean',
short: 'h',
description: 'Show help'
},
'all-arch': {
type: 'boolean',
description: 'Build for both arm64 and amd64'
},
arch: {
type: 'string',
multiple: true,
description: 'Target architecture (arm64 or amd64)'
}
},
strict: true,
allowPositionals: false
});
if (values.help) {
showHelp();
process.exit(0);
}
const containerTool = getContainerTool();
const { version, isRelease } = getVersion();
let architectures;
if (values['all-arch']) {
architectures = ['arm64', 'amd64'];
} else if (values.arch && values.arch.length > 0) {
architectures = values.arch;
} else {
architectures = [getHostArchitecture()];
}
console.log(`Building ${isRelease ? 'release' : 'development'} version: ${version}`);
console.log(`Target architectures: ${architectures.join(', ')}`);
// Build container image
const tags = isRelease
? [
`-t skoszewski/jmespath-playground:${version}`,
`-t skoszewski/jmespath-playground:latest`
].join(' ')
: [
`-t skoszewski/jmespath-playground:dev`,
`-t skoszewski/jmespath-playground:latest`
].join(' ');
const archFlags = architectures.map(arch => `--arch ${arch}`).join(' ');
const buildCommand = `${containerTool} build ${archFlags} --build-arg VERSION="${version}" --build-arg IS_RELEASE="${isRelease}" ${tags} .`;
execCommand(buildCommand, 'Building container image');
console.log('Container image build completed successfully!');
// Show usage instructions
console.log(`\nUsage examples:`);
console.log(` build-image.js # Builds for host architecture only`);
console.log(` build-image.js --all-arch # Builds for both arm64 and amd64`);
console.log(` build-image.js --arch arm64 # Builds for arm64 only`);
console.log(` build-image.js --arch arm64 --arch amd64 # Explicitly specify both`);
if (isRelease) {
console.log(`\nTo run the container:`);
console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 skoszewski/jmespath-playground:${version}`);
if (containerTool === 'docker') {
console.log(`\nTo push to Docker Hub:`);
console.log(` docker push skoszewski/jmespath-playground:${version}`);
console.log(` docker push skoszewski/jmespath-playground:latest`);
}
} else {
console.log(`\nTo run the container:`);
console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 skoszewski/jmespath-playground:dev`);
}
}
if (require.main === module) {
main();
}

View File

@@ -1,63 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Docker Image Build Script
set -e
echo "🐳 JMESPath Testing Tool - Docker Image Build"
echo "=============================================="
echo ""
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo "❌ Docker not found. Please install Docker to build container images."
exit 1
fi
# Determine version information for Docker build
VERSION=$(git tag --points-at HEAD 2>/dev/null | sed 's/^v//' | head -n 1)
if [ -n "$VERSION" ]; then
# We're at a tagged commit - release build
echo "📦 Building release version: $VERSION"
docker build \
--build-arg VERSION="$VERSION" \
--build-arg IS_RELEASE="true" \
-t skoszewski/jmespath-playground:$VERSION \
-t skoszewski/jmespath-playground:latest .
echo "✅ Built Docker images: skoszewski/jmespath-playground:$VERSION, skoszewski/jmespath-playground:latest"
echo ""
echo "To run the release container:"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:$VERSION"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:latest"
echo ""
echo "To push to Docker Hub:"
echo " docker push skoszewski/jmespath-playground:$VERSION"
echo " docker push skoszewski/jmespath-playground:latest"
else
# Development build
PACKAGE_VERSION=$(grep '"version"' package.json | cut -d'"' -f4)
DEV_VERSION="${PACKAGE_VERSION}-dev"
echo "📦 Building development version: $DEV_VERSION"
docker build \
--build-arg VERSION="$DEV_VERSION" \
--build-arg IS_RELEASE="false" \
-t skoszewski/jmespath-playground:dev \
-t skoszewski/jmespath-playground:latest .
echo "✅ Built Docker images: skoszewski/jmespath-playground:dev, skoszewski/jmespath-playground:latest"
echo ""
echo "To run the development container:"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:dev"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:latest"
echo ""
echo "To push to Docker Hub:"
echo " docker push skoszewski/jmespath-playground:dev"
echo " docker push skoszewski/jmespath-playground:latest"
fi
echo ""
echo "🎉 Docker image build completed successfully!"

View File

@@ -1,39 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Build Script
set -e
echo "🚀 JMESPath Testing Tool - Build Script"
echo "======================================="
echo ""
# Check Node.js version
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version | sed 's/v//')
MAJOR_VERSION=$(echo $NODE_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge 24 ]; then
echo "✅ Node.js $NODE_VERSION (compatible with v24+ requirement)"
else
echo "❌ Node.js $NODE_VERSION found, but v24+ is required"
exit 1
fi
else
echo "❌ Node.js not found. Please install Node.js 24 LTS or higher."
exit 1
fi
# Build the React application
echo "📦 Installing dependencies..."
npm install
echo "🔨 Building production bundle..."
npm run build
echo "✅ Build completed successfully!"
echo ""
echo "To run the application:"
echo " npm run server # Run integrated server locally"
echo ""
echo "To build Docker image:"
echo " scripts/build-image.sh # Build Docker container image"

View File

@@ -1,30 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Development Script
set -e
echo "🚀 JMESPath Testing Tool - Development"
echo "====================================="
echo ""
# Check Node.js
if command -v node &> /dev/null; then
echo "✅ Node.js version: $(node --version)"
else
echo "❌ Node.js not found. Please install Node.js 24 LTS."
exit 1
fi
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
# Start development server
echo "🚀 Starting development server..."
echo " The app will open at http://localhost:3000"
echo " Press Ctrl+C to stop the server"
echo ""
npm start

View File

@@ -2,63 +2,57 @@
/**
* JMESPath Playground Upload Script (JavaScript)
* Usage: node upload.js [-u URL] "json_file.json"
* Usage: node upload.js [-u URL] [-k API_KEY] "json_file.json"
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const { URL } = require('url');
const { parseArgs } = require('util');
function showUsage() {
const scriptName = path.basename(process.argv[1]);
console.log(`Usage: node ${scriptName} [-u|--url URL] <json_file>`);
console.log(`Usage: node ${scriptName} [-u|--url URL] [-k|--key API_KEY] <json_file>`);
console.log('');
console.log('Options:');
console.log(' -u, --url URL API URL (default: http://localhost:3000)');
console.log(' -k, --key API_KEY API key (not required for localhost)');
console.log(' -h, --help Show this help message');
console.log('');
console.log('Example:');
console.log('Examples:');
console.log(` node ${scriptName} data.json`);
console.log(` node ${scriptName} -u http://example.com:3000 data.json`);
console.log(` node ${scriptName} -u http://example.com:3000 -k your-api-key data.json`);
}
function parseArguments() {
const args = process.argv.slice(2);
let apiUrl = 'http://localhost:3000';
let jsonFile = '';
function getArguments() {
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
url: { type: 'string', short: 'u', default: 'http://localhost:3000' },
key: { type: 'string', short: 'k' },
help: { type: 'boolean', short: 'h' }
},
allowPositionals: true
});
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '-u' || arg === '--url') {
if (i + 1 >= args.length) {
console.error('Error: URL argument required for -u/--url option');
process.exit(1);
}
apiUrl = args[i + 1];
i++; // Skip next argument
} else if (arg === '-h' || arg === '--help') {
if (values.help) {
showUsage();
process.exit(0);
} else if (arg.startsWith('-')) {
console.error(`Error: Unknown option ${arg}`);
showUsage();
process.exit(1);
} else {
if (jsonFile) {
console.error('Error: Multiple JSON files specified');
process.exit(1);
}
jsonFile = arg;
}
}
if (!jsonFile) {
if (positionals.length !== 1) {
console.error('Error: JSON file required');
showUsage();
process.exit(1);
}
return { apiUrl, jsonFile };
return {
apiUrl: values.url,
apiKey: values.key || '',
jsonFile: positionals[0]
};
}
async function validateJsonFile(jsonFile) {
@@ -80,28 +74,84 @@ async function validateJsonFile(jsonFile) {
}
}
async function uploadData(apiUrl, jsonFile, jsonData) {
console.log('Uploading sample data to JMESPath Playground...');
console.log(`JSON file: ${jsonFile}`);
console.log(`API URL: ${apiUrl}`);
console.log('');
function isLocalhost(url) {
try {
const response = await fetch(`${apiUrl}/api/v1/upload`, {
method: 'POST',
headers: {
const parsed = new URL(url);
const hostname = parsed.hostname;
return hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('127.') ||
hostname === '::1';
} catch {
return false;
}
}
function makeRequest(url, options) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const isHttps = parsedUrl.protocol === 'https:';
const client = isHttps ? https : http;
const requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = client.request(requestOptions, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
json: () => Promise.resolve(JSON.parse(data))
});
});
});
req.on('error', reject);
if (options.body) {
req.write(options.body);
}
req.end();
});
}
async function uploadData(apiUrl, apiKey, jsonFile, jsonData) {
try {
const headers = {
'Content-Type': 'application/json',
},
};
// Only send API key for non-localhost requests
const isLocal = isLocalhost(apiUrl);
if (!isLocal && apiKey) {
headers['X-API-Key'] = apiKey;
} else if (!isLocal && !apiKey) {
console.error('Error: API key required for non-localhost URLs');
console.error('Use -k/--key option to specify API key');
process.exit(1);
}
const response = await makeRequest(`${apiUrl}/api/v1/upload`, {
method: 'POST',
headers: headers,
body: jsonData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.error || 'Upload failed'}`);
}
console.log('Sample data uploaded successfully!');
console.log(`Open ${apiUrl} in your browser to see the reload button.`);
console.log('You can then enter your JMESPath expression in the web interface.');
const result = await response.json();
console.log(JSON.stringify(result));
} catch (error) {
console.error('Error uploading data:', error.message);
@@ -110,9 +160,9 @@ async function uploadData(apiUrl, jsonFile, jsonData) {
}
async function main() {
const { apiUrl, jsonFile } = parseArguments();
const { apiUrl, apiKey, jsonFile } = getArguments();
const jsonData = await validateJsonFile(jsonFile);
await uploadData(apiUrl, jsonFile, jsonData);
await uploadData(apiUrl, apiKey, jsonFile, jsonData);
}
// Run the script

View File

@@ -1,82 +0,0 @@
#!/bin/bash
# JMESPath Playground Upload Script
# Usage: ./upload.sh [-u URL] "json_file.json"
show_usage() {
echo "Usage: $0 [-u|--url URL] <json_file>"
echo ""
echo "Options:"
echo " -u, --url URL API URL (default: http://localhost:3000)"
echo " -h, --help Show this help message"
echo ""
echo "Example:"
echo " $0 data.json"
echo " $0 -u http://example.com:3000 data.json"
}
# Parse command line options
API_URL="http://localhost:3000"
JSON_FILE=""
while [[ $# -gt 0 ]]; do
case $1 in
-u|--url)
API_URL="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
-*)
echo "Error: Unknown option $1"
show_usage
exit 1
;;
*)
if [ -z "$JSON_FILE" ]; then
JSON_FILE="$1"
else
echo "Error: Multiple JSON files specified"
exit 1
fi
shift
;;
esac
done
if [ -z "$JSON_FILE" ]; then
echo "Error: JSON file required"
show_usage
exit 1
fi
if [ ! -f "$JSON_FILE" ]; then
echo "Error: JSON file '$JSON_FILE' not found"
exit 1
fi
# Validate JSON with jq if available
if command -v jq >/dev/null 2>&1; then
if ! jq . "$JSON_FILE" >/dev/null 2>&1; then
echo "Error: '$JSON_FILE' contains invalid JSON"
exit 1
fi
fi
echo "Uploading sample data to JMESPath Playground..."
echo "JSON file: $JSON_FILE"
echo "API URL: $API_URL"
echo
# Upload the JSON data
curl -s -X POST \
-H "Content-Type: application/json" \
--data @"$JSON_FILE" \
"$API_URL/api/v1/upload"
echo
echo "Sample data uploaded successfully!"
echo "Open $API_URL in your browser to see the reload button."
echo "You can then enter your JMESPath expression in the web interface."

View File

@@ -22,15 +22,15 @@ try {
console.log(`✅ Building release version ${version} (tagged: ${gitTag})`);
isRelease = true;
} else {
// We're not at a tagged commit - add -dev suffix
version = `${version}-dev`;
console.log(`📦 Building development version ${version}`);
// We're not at a tagged commit - use unknown version
version = 'unknown';
console.log(`📦 Building development version with unknown version`);
isRelease = false;
}
} catch (error) {
// Git command failed (maybe not in a git repo)
version = `${version}-dev`;
console.log(`⚠️ Cannot determine git status, using development version ${version}`);
version = 'unknown';
console.log(`⚠️ Cannot determine git status, using unknown version`);
isRelease = false;
}

402
server.js
View File

@@ -1,7 +1,9 @@
const express = require('express');
const path = require('path');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const express = require("express");
const path = require("path");
const crypto = require("crypto");
const os = require("os");
const { v4: uuidv4 } = require("uuid");
const { parseArgs } = require("util");
// Environment configuration
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100;
@@ -9,54 +11,29 @@ const MAX_SAMPLE_SIZE = parseInt(process.env.MAX_SAMPLE_SIZE) || 1024 * 1024; //
const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour
// Utility functions for encryption
function generateSalt() {
return crypto.randomBytes(16);
}
function isLocalhostRequest(req) {
// Get client IP with fallback options
const forwarded = req.get('X-Forwarded-For');
const ip = forwarded ? forwarded.split(',')[0].trim() :
req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'127.0.0.1';
const host = req.get('host') || '';
// Check for localhost IP addresses (IPv4 and IPv6)
const localhostIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost'];
const isLocalIP = localhostIPs.includes(ip) || ip.startsWith('127.') || ip === '::1';
// Check for localhost hostnames
const isLocalHost = host.startsWith('localhost:') || host.startsWith('127.0.0.1:') || host === 'localhost' || host === '127.0.0.1';
return isLocalIP || isLocalHost;
}
function encrypt(data, key) {
try {
const algorithm = 'aes-256-gcm';
const algorithm = "aes-256-gcm";
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.setAAD(Buffer.from('session-data'));
cipher.setAAD(Buffer.from("session-data"));
let encrypted = cipher.update(JSON.stringify(data), 'utf8');
let encrypted = cipher.update(JSON.stringify(data), "utf8");
encrypted = Buffer.concat([encrypted, cipher.final()]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
data: encrypted.toString('hex'),
tag: authTag.toString('hex')
iv: iv.toString("hex"),
data: encrypted.toString("hex"),
tag: authTag.toString("hex"),
};
} catch (error) {
console.error('⚠️ Encryption exception:', {
console.error("⚠️ Encryption exception:", {
message: error.message,
algorithm: 'aes-256-gcm',
keyLength: key ? key.length : 'undefined',
timestamp: new Date().toISOString()
algorithm: "aes-256-gcm",
keyLength: key ? key.length : "undefined",
timestamp: new Date().toISOString(),
});
throw new Error(`Encryption failed: ${error.message}`);
}
@@ -64,39 +41,40 @@ function encrypt(data, key) {
function decrypt(encryptedObj, key) {
try {
const algorithm = 'aes-256-gcm';
const iv = Buffer.from(encryptedObj.iv, 'hex');
const algorithm = "aes-256-gcm";
const iv = Buffer.from(encryptedObj.iv, "hex");
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAAD(Buffer.from('session-data'));
decipher.setAuthTag(Buffer.from(encryptedObj.tag, 'hex'));
decipher.setAAD(Buffer.from("session-data"));
decipher.setAuthTag(Buffer.from(encryptedObj.tag, "hex"));
let decrypted = decipher.update(Buffer.from(encryptedObj.data, 'hex'), null, 'utf8');
decrypted += decipher.final('utf8');
let decrypted = decipher.update(
Buffer.from(encryptedObj.data, "hex"),
null,
"utf8",
);
decrypted += decipher.final("utf8");
return JSON.parse(decrypted);
} catch (error) {
console.error('⚠️ Decryption exception:', {
console.error("⚠️ Decryption exception:", {
message: error.message,
algorithm: 'aes-256-gcm',
keyLength: key ? key.length : 'undefined',
algorithm: "aes-256-gcm",
keyLength: key ? key.length : "undefined",
hasIV: !!encryptedObj.iv,
hasTag: !!encryptedObj.tag,
hasData: !!encryptedObj.data,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
throw new Error(`Decryption failed: ${error.message}`);
}
}
// For localhost requests, use a consistent API key so sessions persist
const LOCALHOST_API_KEY = 'localhost0123456789abcdef0123456789';
function isValidApiKey(apiKey) {
return typeof apiKey === 'string' && /^[0-9a-f]{32}$/i.test(apiKey);
return typeof apiKey === "string" && /^[0-9a-f]{32}$/i.test(apiKey);
}
function getSessionId(apiKey) {
return crypto.createHash('sha256').update(apiKey).digest('hex');
return crypto.createHash("sha256").update(apiKey).digest("hex");
}
function generateSalt() {
@@ -104,19 +82,38 @@ function generateSalt() {
}
function deriveKey(apiKey, salt) {
return crypto.pbkdf2Sync(apiKey, salt, 10000, 32, 'sha256');
return crypto.pbkdf2Sync(apiKey, salt, 100000, 32, "sha256");
}
// Create Express app
function createApp() {
function createApp(devMode = false) {
const app = express();
// Trust proxy to get real client IP (needed for localhost detection)
app.set('trust proxy', true);
app.set("trust proxy", true);
// Middleware
app.use(express.json({ limit: MAX_SAMPLE_SIZE }));
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, "build")));
// Dev mode request logging middleware
if (devMode) {
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`📨 [${timestamp}] ${req.method} ${req.path}`);
if (req.method !== "GET" && Object.keys(req.body).length > 0) {
const bodySize = Buffer.byteLength(JSON.stringify(req.body), "utf8");
console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`);
}
const originalJson = res.json;
res.json = function (data) {
console.log(` ✓ Response: ${res.statusCode}`);
return originalJson.call(this, data);
};
next();
});
}
// Session storage
const sessions = new Map();
@@ -127,7 +124,9 @@ function createApp() {
for (const [sessionId, session] of sessions.entries()) {
if (now - session.createdAt > MAX_SESSION_TTL) {
sessions.delete(sessionId);
console.log(`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`);
console.log(
`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`,
);
}
}
}
@@ -136,22 +135,15 @@ function createApp() {
setInterval(cleanupExpiredSessions, 5 * 60 * 1000);
// API endpoints
app.post('/api/v1/upload', (req, res) => {
app.post("/api/v1/upload", (req, res) => {
try {
// Check if request is from localhost - if so, skip API key validation
const isFromLocalhost = isLocalhostRequest(req);
let apiKey = req.headers['x-api-key'];
const apiKey = req.headers["x-api-key"];
if (!isFromLocalhost) {
// Validate API key header for remote clients
// Validate API key header
if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' });
}
} else {
// For localhost requests, use consistent API key for session persistence
if (!apiKey || !isValidApiKey(apiKey)) {
apiKey = LOCALHOST_API_KEY;
}
return res
.status(403)
.json({ error: "Invalid or missing X-API-Key header" });
}
// Cleanup expired sessions before checking limits
@@ -160,26 +152,26 @@ function createApp() {
// Check session limits
if (sessions.size >= MAX_SESSIONS) {
return res.status(429).json({
error: 'Maximum number of sessions reached. Please try again later.',
error: "Maximum number of sessions reached. Please try again later.",
maxSessions: MAX_SESSIONS,
currentSessions: sessions.size
currentSessions: sessions.size,
});
}
const uploadedData = req.body;
// Validate that it's valid JSON
if (!uploadedData || typeof uploadedData !== 'object') {
return res.status(400).json({ error: 'Invalid JSON data' });
if (!uploadedData || typeof uploadedData !== "object") {
return res.status(400).json({ error: "Invalid JSON data" });
}
// Check data size
const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), 'utf8');
const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), "utf8");
if (dataSize > MAX_SAMPLE_SIZE) {
return res.status(413).json({
error: 'Sample data too large',
error: "Sample data too large",
maxSize: MAX_SAMPLE_SIZE,
receivedSize: dataSize
receivedSize: dataSize,
});
}
@@ -192,69 +184,66 @@ function createApp() {
const encryptedData = encrypt(uploadedData, key);
sessions.set(sessionId, {
salt: salt.toString('hex'),
salt: salt.toString("hex"),
encryptedData,
state: stateGuid,
createdAt: Date.now(),
accessed: false
accessed: false,
});
console.log(`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`);
console.log(
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
);
res.json({
message: 'Sample data uploaded successfully',
message: "Sample data uploaded successfully",
state: stateGuid,
sessionId: sessionId.substring(0, 8) + '...'
sessionId: sessionId.substring(0, 8) + "...",
});
} catch (error) {
console.error('⚠️ Upload endpoint exception occurred:', {
console.error("⚠️ Upload endpoint exception occurred:", {
message: error.message,
stack: error.stack,
sessionCount: sessions.size,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
// Provide more specific error messages based on error type
if (error.name === 'SyntaxError') {
if (error.name === "SyntaxError") {
return res.status(400).json({
error: 'Invalid JSON data format',
details: 'The uploaded data could not be parsed as valid JSON'
error: "Invalid JSON data format",
details: "The uploaded data could not be parsed as valid JSON",
});
} else if (error.message.includes('encrypt')) {
} else if (error.message.includes("encrypt")) {
return res.status(500).json({
error: 'Encryption failed',
details: 'Failed to encrypt session data. Please try again with a new API key.'
error: "Encryption failed",
details:
"Failed to encrypt session data. Please try again with a new API key.",
});
} else if (error.message.includes('PBKDF2')) {
} else if (error.message.includes("PBKDF2")) {
return res.status(500).json({
error: 'Key derivation failed',
details: 'Failed to derive encryption key from API key'
error: "Key derivation failed",
details: "Failed to derive encryption key from API key",
});
} else {
return res.status(500).json({
error: 'Upload processing failed',
details: 'An unexpected error occurred while processing your upload. Please try again.'
error: "Upload processing failed",
details:
"An unexpected error occurred while processing your upload. Please try again.",
});
}
}
});
app.get('/api/v1/sample', (req, res) => {
app.get("/api/v1/sample", (req, res) => {
try {
// Check if request is from localhost - if so, skip API key validation
const isFromLocalhost = isLocalhostRequest(req);
let apiKey = req.headers['x-api-key'];
const apiKey = req.headers["x-api-key"];
if (!isFromLocalhost) {
// Validate API key header for remote clients
// Validate API key header
if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' });
}
} else {
// For localhost requests, use consistent API key for session persistence
if (!apiKey || !isValidApiKey(apiKey)) {
apiKey = LOCALHOST_API_KEY;
}
return res
.status(403)
.json({ error: "Invalid or missing X-API-Key header" });
}
const sessionId = getSessionId(apiKey);
@@ -265,64 +254,62 @@ function createApp() {
}
// Decrypt data
const salt = Buffer.from(session.salt, 'hex');
const salt = Buffer.from(session.salt, "hex");
const key = deriveKey(apiKey, salt);
const decryptedData = decrypt(session.encryptedData, key);
// Remove session after first access (one-time use)
sessions.delete(sessionId);
console.log(`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`);
console.log(
`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`,
);
res.json(decryptedData);
} catch (error) {
console.error('⚠️ Sample retrieval exception occurred:', {
console.error("⚠️ Sample retrieval exception occurred:", {
message: error.message,
stack: error.stack,
sessionCount: sessions.size,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
// Provide more specific error messages based on error type
if (error.message.includes('decrypt')) {
if (error.message.includes("decrypt")) {
return res.status(500).json({
error: 'Decryption failed',
details: 'Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.'
error: "Decryption failed",
details:
"Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.",
});
} else if (error.message.includes('JSON')) {
} else if (error.message.includes("JSON")) {
return res.status(500).json({
error: 'Data corruption detected',
details: 'The stored session data appears to be corrupted and cannot be parsed.'
error: "Data corruption detected",
details:
"The stored session data appears to be corrupted and cannot be parsed.",
});
} else if (error.name === 'TypeError') {
} else if (error.name === "TypeError") {
return res.status(500).json({
error: 'Session data format error',
details: 'The session data format is invalid or corrupted.'
error: "Session data format error",
details: "The session data format is invalid or corrupted.",
});
} else {
return res.status(500).json({
error: 'Sample retrieval failed',
details: 'An unexpected error occurred while retrieving sample data. The session may have been corrupted.'
error: "Sample retrieval failed",
details:
"An unexpected error occurred while retrieving sample data. The session may have been corrupted.",
});
}
}
});
app.get('/api/v1/state', (req, res) => {
app.get("/api/v1/state", (req, res) => {
try {
// Check if request is from localhost - if so, skip API key validation
const isFromLocalhost = isLocalhostRequest(req);
let apiKey = req.headers['x-api-key'];
const apiKey = req.headers["x-api-key"];
if (!isFromLocalhost) {
// Validate API key header for remote clients
// Validate API key header
if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' });
}
} else {
// For localhost requests, use consistent API key for session persistence
if (!apiKey || !isValidApiKey(apiKey)) {
apiKey = LOCALHOST_API_KEY;
}
return res
.status(403)
.json({ error: "Invalid or missing X-API-Key header" });
}
const sessionId = getSessionId(apiKey);
@@ -335,50 +322,56 @@ function createApp() {
res.json({ state: session.state });
} catch (error) {
console.error('⚠️ State retrieval exception occurred:', {
console.error("⚠️ State retrieval exception occurred:", {
message: error.message,
stack: error.stack,
sessionCount: sessions.size,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
// Provide more specific error messages
if (error.message.includes('API key')) {
if (error.message.includes("API key")) {
return res.status(403).json({
error: 'API key processing failed',
details: 'Failed to process the provided API key'
error: "API key processing failed",
details: "Failed to process the provided API key",
});
} else {
return res.status(500).json({
error: 'State retrieval failed',
details: 'An unexpected error occurred while retrieving session state. Please try again.'
error: "State retrieval failed",
details:
"An unexpected error occurred while retrieving session state. Please try again.",
});
}
}
});
// Health endpoint (no auth required)
app.get('/api/v1/health', (req, res) => {
cleanupExpiredSessions(); // Cleanup on health check
// Status endpoint (no auth required) - detailed information
app.get("/api/v1/status", (req, res) => {
cleanupExpiredSessions(); // Cleanup on status check
res.json({
status: 'healthy',
status: "healthy",
sessions: {
current: sessions.size,
max: MAX_SESSIONS,
available: MAX_SESSIONS - sessions.size
available: MAX_SESSIONS - sessions.size,
},
limits: {
maxSessions: MAX_SESSIONS,
maxSampleSize: MAX_SAMPLE_SIZE,
maxSessionTTL: MAX_SESSION_TTL
maxSessionTTL: MAX_SESSION_TTL,
},
uptime: process.uptime()
uptime: process.uptime(),
});
});
// Health endpoint (no auth required) - simple OK response
app.get("/api/v1/health", (req, res) => {
res.type("text/plain").send("OK");
});
// Serve React app for all other routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "build", "index.html"));
});
return app;
@@ -386,37 +379,84 @@ function createApp() {
// Start server if this file is run directly
if (require.main === module) {
// Parse command line arguments
const args = process.argv.slice(2);
let listenAddr = process.env.LISTEN_ADDR || '127.0.0.1';
let listenPort = process.env.LISTEN_PORT || 3000;
const { values } = parseArgs({
options: {
"listen-addr": {
type: "string",
short: "h",
default: process.env.LISTEN_ADDR || "127.0.0.1",
},
port: {
type: "string",
short: "p",
default: process.env.LISTEN_PORT || "3000",
},
dev: {
type: "boolean",
default: process.env.DEV_MODE === "true" || false,
},
},
});
for (let i = 0; i < args.length; i++) {
if (args[i] === '-h' || args[i] === '--listen-addr') {
listenAddr = args[i + 1];
i++;
} else if (args[i] === '-p' || args[i] === '--port') {
listenPort = args[i + 1];
i++;
}
}
const app = createApp();
const PORT = parseInt(listenPort);
const HOST = listenAddr;
const DEV_MODE = values.dev;
const app = createApp(DEV_MODE);
const PORT = parseInt(values.port);
const HOST = values["listen-addr"];
app.listen(PORT, HOST, () => {
console.log(`🚀 JMESPath Playground Server running on http://${HOST}:${PORT}`);
console.log(`📊 Configuration:`);
console.log(`JMESPath Playground Server running`);
if (DEV_MODE) {
console.log(` 🔧 Development Mode Enabled`);
}
// Show actual accessible URLs
if (HOST === "0.0.0.0") {
console.log(` Listening on all interfaces:`);
const interfaces = os.networkInterfaces();
for (const [name, addrs] of Object.entries(interfaces)) {
for (const addr of addrs) {
if (addr.family === "IPv4" && !addr.internal) {
console.log(` http://${addr.address}:${PORT}`);
}
}
}
// Also show localhost for local access
console.log(` http://127.0.0.1:${PORT}`);
} else {
console.log(` http://${HOST}:${PORT}`);
}
console.log(`Configuration:`);
console.log(` Max Sessions: ${MAX_SESSIONS}`);
console.log(` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`);
console.log(` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`);
console.log(`🔗 API endpoints:`);
console.log(` POST http://${HOST}:${PORT}/api/v1/upload (requires X-API-Key)`);
console.log(` GET http://${HOST}:${PORT}/api/v1/sample (requires X-API-Key)`);
console.log(` GET http://${HOST}:${PORT}/api/v1/state (requires X-API-Key)`);
console.log(` GET http://${HOST}:${PORT}/api/v1/health (public)`);
console.log(`🔐 Security: AES-256-GCM encryption with PBKDF2 key derivation`);
console.log(
` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`,
);
console.log(
` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`,
);
console.log(
" Security: AES-256-GCM encryption with PBKDF2 (100k iterations)",
);
// Show base API URL
let apiBaseUrl;
if (HOST === "0.0.0.0") {
const interfaces = os.networkInterfaces();
let firstIP = "127.0.0.1";
outer: for (const addrs of Object.values(interfaces)) {
for (const addr of addrs) {
if (addr.family === "IPv4" && !addr.internal) {
firstIP = addr.address;
break outer;
}
}
}
apiBaseUrl = `http://${firstIP}:${PORT}/api/v1`;
} else {
apiBaseUrl = `http://${HOST}:${PORT}/api/v1`;
}
console.log(`API Base URL: ${apiBaseUrl}`);
});
}

View File

@@ -1,41 +1,25 @@
/* JMESPath Testing Tool Custom Styles */
:root {
/* Light theme colors */
--bg-primary-light: #ffffff;
--bg-secondary-light: #f8f9fa;
--text-primary-light: #212529;
--text-secondary-light: #495057;
--text-muted-light: #6c757d;
--border-light: #dee2e6;
--border-input-light: #ced4da;
/* Common variables */
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
--accent-color: #007bff;
--accent-shadow: rgba(0, 123, 255, 0.25);
/* Dark theme colors */
--bg-primary-dark: #1a1a1a;
--bg-secondary-dark: #2d2d2d;
--bg-card-dark: #323232;
--text-primary-dark: #ffffff;
--text-secondary-dark: #e9ecef;
--text-muted-dark: #adb5bd;
--border-dark: #495057;
--border-input-dark: #6c757d;
/* Brand colors */
--brand-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--brand-white: #ffffff;
--brand-dark: #212529;
--brand-warning: #ffc107;
/* State colors */
--success-bg-light: #d4edda;
--success-border-light: #c3e6cb;
--success-text-light: #155724;
--success-bg-dark: #1e4a1e;
--success-border-dark: #2c6d2c;
--success-text-dark: #d4edda;
/* Brand opacity levels */
--brand-white-60: rgba(255, 255, 255, 0.6);
--brand-white-10: rgba(255, 255, 255, 0.1);
--brand-warning-50: rgba(255, 193, 7, 0.5);
--brand-warning-10: rgba(255, 193, 7, 0.1);
--error-bg-light: #f8d7da;
--error-border-light: #f5c6cb;
--error-text-light: #721c24;
--error-bg-dark: #4a1e1e;
--error-border-dark: #6d2c2c;
--error-text-dark: #f8d7da;
/* Elevation and overlays */
--shadow-light: rgba(0, 0, 0, 0.1);
--focus-ring: rgba(0, 123, 255, 0.25);
/* Button variants */
--btn-success: #28a745;
@@ -68,23 +52,57 @@ body {
/* Header section styling - more compact */
.header-section {
/* Removed gradient background to fix text visibility */
background: var(--brand-gradient);
color: var(--brand-white);
padding: 1.2rem 0;
margin-bottom: 1rem;
transition: background-color 0.3s ease;
}
.header-section h2 {
color: var(--brand-white);
}
/* Ensure buttons in header are clearly visible against gradient */
.header-section .btn-light.active {
background-color: var(--brand-white);
color: var(--brand-dark) !important; /* Deep dark text for selected states */
border-color: var(--brand-white);
}
.header-section .btn-outline-light {
color: var(--brand-white);
border-color: var(--brand-white-60);
}
.header-section .btn-outline-light:hover {
background-color: var(--brand-white-10);
color: var(--brand-white);
}
.header-section .btn-outline-warning {
color: var(--brand-warning);
border-color: var(--brand-warning-50);
}
.header-section .btn-outline-warning:hover {
background-color: var(--brand-warning-10);
color: var(--brand-warning);
}
/* Custom card styling */
.card {
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px var(--shadow-light);
border-radius: 8px;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
background-color: var(--bg-secondary);
border-bottom: 2px solid var(--border);
font-weight: 600;
color: #212529;
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}
@@ -98,16 +116,10 @@ body {
.jmespath-input {
font-size: 14px;
padding: 10px;
background-color: var(--bg-primary-light);
border: 1px solid var(--border-input-light);
color: var(--text-secondary-light);
}
.json-input, .result-output {
font-size: 13px;
background-color: var(--bg-secondary-light);
border: 1px solid var(--border-light);
color: var(--text-secondary-light);
line-height: 1.4;
}
@@ -125,15 +137,6 @@ footer {
flex-shrink: 0;
}
footer a {
color: var(--text-muted-light);
transition: color var(--transition-fast);
}
footer a:hover {
color: var(--text-secondary-light);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.header-section {
@@ -158,519 +161,158 @@ footer a:hover {
}
}
/* Manual theme overrides */
.theme-light {
/* Force light theme regardless of system preference */
background-color: #ffffff !important;
color: #212529 !important;
}
.theme-light .header-section {
background-color: transparent !important;
border-bottom: none !important;
}
.theme-light .card {
background-color: #ffffff !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
color: #212529 !important;
}
.theme-light .card-header {
background-color: #f8f9fa !important;
border-bottom: 2px solid #dee2e6 !important;
color: #212529 !important;
}
.theme-light .jmespath-input {
background-color: #ffffff;
border: 1px solid #ced4da;
color: #495057;
}
.theme-light .json-input,
.theme-light .result-output {
background-color: #f8f9fa !important;
border: 1px solid #dee2e6 !important;
color: #495057 !important;
}
/* Success and Error state overrides - must come after base input rules */
.theme-light .jmespath-input.success {
background-color: #d4edda !important;
border-color: #c3e6cb !important;
color: #155724 !important;
}
.theme-light .jmespath-input.error {
background-color: #f8d7da !important;
border-color: #f5c6cb !important;
color: #721c24 !important;
}
.theme-light .text-muted {
color: #6c757d !important;
}
/* Bootstrap theme integration */
[data-bs-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #495057;
--text-muted: #6c757d;
--border: #dee2e6;
--border-input: #ced4da;
.theme-light .jmespath-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 0.2rem var(--accent-shadow);
}
--success-bg: #d4edda;
--success-border: #c3e6cb;
--success-text: #155724;
.theme-light .jmespath-input::placeholder {
color: var(--text-muted-light) !important;
--error-bg: #f8d7da;
--error-border: #f5c6cb;
--error-text: #721c24;
}
.theme-light .json-input::placeholder,
.theme-light .result-output::placeholder {
color: var(--text-muted-light) !important;
}
[data-bs-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-card: #323232;
--text-primary: #ffffff;
--text-secondary: #e9ecef;
--text-muted: #adb5bd;
--border: #495057;
--border-input: #6c757d;
.theme-light .json-input:focus,
.theme-light .result-output:focus {
background-color: var(--bg-primary-light) !important;
border-color: var(--accent-color) !important;
color: var(--text-secondary-light) !important;
box-shadow: 0 0 0 0.2rem var(--accent-shadow) !important;
}
.theme-light .output-section .form-control {
background-color: #f8f9fa !important;
}
.theme-light .alert-danger {
background-color: #f8d7da !important;
border-color: #f5c6cb !important;
color: #721c24 !important;
}
.theme-light .alert-success {
background-color: #d4edda !important;
border-color: #c3e6cb !important;
color: #155724 !important;
}
.theme-light .btn-primary {
background-color: var(--btn-primary) !important;
border-color: var(--btn-primary) !important;
color: var(--bg-primary-light) !important;
}
.theme-light .btn-outline-secondary {
color: var(--btn-secondary) !important;
border-color: var(--btn-secondary) !important;
}
.theme-light .btn-outline-secondary:hover {
background-color: var(--btn-secondary) !important;
border-color: var(--btn-secondary) !important;
color: var(--bg-primary-light) !important;
}
.theme-light .btn-outline-success {
color: var(--btn-success) !important;
border-color: var(--btn-success) !important;
}
--success-bg: #1e4a1e;
--success-border: #2c6d2c;
--success-text: #d4edda;
.theme-light .btn-outline-success:hover {
background-color: var(--btn-success) !important;
border-color: var(--btn-success) !important;
color: var(--bg-primary-light) !important;
--error-bg: #4a1e1e;
--error-border: #6d2c2c;
--error-text: #f8d7da;
}
.theme-light .btn-outline-info {
color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
/* Apply theme colors */
body {
background-color: var(--bg-primary);
color: var(--text-secondary);
}
.theme-light .btn-outline-info:hover {
background-color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
color: var(--bg-primary-light) !important;
.card {
background-color: var(--bg-primary);
border-color: var(--border);
color: var(--text-primary);
}
.theme-light .btn-outline-primary {
color: var(--btn-primary) !important;
border-color: var(--btn-primary) !important;
.card-header {
background-color: var(--bg-secondary);
border-bottom-color: var(--border);
color: var(--text-primary);
}
.theme-light .btn-outline-primary:hover {
background-color: var(--btn-primary) !important;
border-color: var(--btn-primary) !important;
color: var(--bg-primary-light) !important;
.jmespath-input {
background-color: var(--bg-primary);
border-color: var(--border-input);
color: var(--text-secondary);
}
.theme-light .btn-outline-danger {
color: var(--btn-danger) !important;
border-color: var(--btn-danger) !important;
.json-input, .result-output {
background-color: var(--bg-secondary);
border-color: var(--border);
color: var(--text-secondary);
}
.theme-light .btn-outline-danger:hover {
background-color: var(--btn-danger) !important;
border-color: var(--btn-danger) !important;
color: var(--bg-primary-light) !important;
footer {
background-color: var(--bg-secondary);
color: var(--text-secondary);
}
.theme-light footer {
background-color: #f8f9fa !important;
border-top: 1px solid #dee2e6 !important;
color: #212529 !important;
footer.bg-light {
background-color: var(--bg-secondary) !important;
}
.theme-light footer a {
color: #6c757d !important;
footer a {
color: var(--text-muted);
}
.theme-light footer a:hover {
color: #495057 !important;
footer a:hover {
color: var(--text-secondary);
}
/* Force dark theme regardless of system preference */
.theme-dark {
background-color: var(--bg-primary-dark) !important;
color: var(--text-secondary-dark) !important;
/* State styles */
.jmespath-input.success {
background-color: var(--success-bg) !important;
border-color: var(--success-border) !important;
color: var(--success-text) !important;
}
.theme-dark .header-section {
background-color: var(--bg-secondary-dark) !important;
border-bottom: 1px solid #404040 !important;
.jmespath-input.error {
background-color: var(--error-bg) !important;
border-color: var(--error-border) !important;
color: var(--error-text) !important;
}
.theme-dark .card {
background-color: var(--bg-secondary-dark) !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
color: var(--text-secondary-dark) !important;
.json-input.success {
background-color: var(--success-bg) !important;
border-color: var(--success-border) !important;
color: var(--success-text) !important;
}
.theme-dark .card-header {
background-color: var(--bg-card-dark) !important;
border-bottom: 2px solid #505050 !important;
color: var(--text-primary-dark) !important;
.json-input.error {
background-color: var(--error-bg) !important;
border-color: var(--error-border) !important;
color: var(--error-text) !important;
}
.theme-dark .jmespath-input {
background-color: var(--bg-card-dark);
border: 1px solid #505050;
color: var(--text-primary-dark);
/* Focus states */
.jmespath-input:focus {
border-color: var(--accent-color, #007bff);
box-shadow: 0 0 0 0.2rem var(--focus-ring);
}
/* Success and Error state overrides - must come after base input rules */
.theme-dark .jmespath-input.success {
background-color: #1e4a1e !important;
border-color: #2c6d2c !important;
color: #d4edda !important;
.json-input:focus,
.result-output:focus {
background-color: var(--bg-primary);
border-color: var(--accent-color, #007bff);
color: var(--text-secondary);
box-shadow: 0 0 0 0.2rem var(--focus-ring);
}
.theme-dark .jmespath-input.error {
background-color: #4a1e1e !important;
border-color: #6d2c2c !important;
color: #f8d7da !important;
/* Placeholder colors */
.jmespath-input::placeholder,
.json-input::placeholder,
.result-output::placeholder {
color: var(--text-muted);
}
.theme-dark .jmespath-input::placeholder {
color: var(--text-muted-dark) !important;
/* Alert styles */
.alert-danger {
background-color: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
.theme-dark .jmespath-input:focus {
border-color: var(--accent-color);
.alert-success {
background-color: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
.theme-dark .json-input,
.theme-dark .result-output {
background-color: #2a2a2a !important;
border: 1px solid #505050 !important;
color: var(--text-secondary-dark) !important;
/* Code block styles */
pre.bg-light {
background-color: var(--bg-secondary) !important;
color: var(--text-secondary) !important;
border-color: var(--border) !important;
}
.theme-dark .json-input::placeholder,
.theme-dark .result-output::placeholder {
color: var(--text-muted-dark) !important;
code {
color: var(--text-secondary);
}
.theme-dark .json-input:focus,
.theme-dark .result-output:focus {
background-color: var(--bg-card-dark) !important;
border-color: var(--accent-color) !important;
color: var(--text-primary-dark) !important;
}
.theme-dark .output-section .form-control {
background-color: var(--bg-secondary-dark) !important;
}
.theme-dark .alert-danger {
background-color: #3d1a1a !important;
border-color: #dc3545 !important;
color: #f8d7da !important;
}
.theme-dark .alert-success {
background-color: #1e4a1e !important;
border-color: #2c6d2c !important;
color: #d4edda !important;
}
.theme-dark .text-muted {
color: var(--text-muted-dark) !important;
}
.theme-dark footer {
background-color: var(--bg-secondary-dark) !important;
border-top: 1px solid #404040 !important;
color: var(--text-secondary-dark) !important;
}
.theme-dark footer a {
color: var(--text-muted-dark) !important;
}
.theme-dark footer a:hover {
color: var(--text-secondary-dark) !important;
}
.theme-dark .btn-primary {
background-color: var(--btn-primary) !important;
border-color: var(--btn-primary) !important;
color: var(--bg-primary-light) !important;
}
.theme-dark .btn-outline-secondary {
color: var(--btn-secondary) !important;
border-color: var(--btn-secondary) !important;
}
.theme-dark .btn-outline-secondary:hover {
background-color: var(--btn-secondary) !important;
border-color: var(--btn-secondary) !important;
color: var(--bg-primary-light) !important;
}
.theme-dark .btn-outline-success {
color: var(--btn-success) !important;
border-color: var(--btn-success) !important;
}
.theme-dark .btn-outline-success:hover {
background-color: var(--btn-success) !important;
border-color: var(--btn-success) !important;
color: var(--bg-primary-light) !important;
}
.theme-dark .btn-outline-info {
color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
}
.theme-dark .btn-outline-info:hover {
background-color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
color: var(--bg-primary-light) !important;
}
.theme-light .btn-outline-info {
color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
}
.theme-light .btn-outline-info:hover {
background-color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
color: var(--bg-primary-light) !important;
}
.theme-dark .btn-outline-info {
color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
}
.theme-dark .btn-outline-info:hover {
background-color: var(--btn-info) !important;
border-color: var(--btn-info) !important;
color: var(--bg-primary-light) !important;
}
.theme-dark .btn-outline-primary {
color: var(--btn-primary) !important;
border-color: var(--btn-primary) !important;
}
.theme-dark .btn-outline-primary:hover {
background-color: var(--btn-primary) !important;
border-color: var(--btn-primary) !important;
color: var(--bg-primary-light) !important;
}
.theme-dark .btn-outline-danger {
color: var(--btn-danger) !important;
border-color: var(--btn-danger) !important;
}
.theme-dark .btn-outline-danger:hover {
background-color: var(--btn-danger) !important;
border-color: var(--btn-danger) !important;
color: var(--bg-primary-light) !important;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
body:not(.theme-light):not(.theme-dark) {
background-color: var(--bg-primary-dark);
color: var(--text-secondary-dark);
}
body:not(.theme-light):not(.theme-dark) .header-section {
background-color: var(--bg-secondary-dark);
border-bottom: 1px solid var(--border-dark);
}
body:not(.theme-light):not(.theme-dark) .card {
background-color: var(--bg-secondary-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
color: var(--text-secondary-dark);
}
body:not(.theme-light):not(.theme-dark) .card-header {
background-color: var(--bg-card-dark);
border-bottom: 2px solid var(--border-dark);
color: var(--text-primary-dark);
}
body:not(.theme-light):not(.theme-dark) .jmespath-input {
background-color: var(--bg-card-dark) !important;
border: 1px solid var(--border-input-dark) !important;
color: var(--text-primary-dark) !important;
}
body:not(.theme-light):not(.theme-dark) .jmespath-input.success {
background-color: var(--success-bg-dark) !important;
border-color: var(--success-border-dark) !important;
color: var(--success-text-dark) !important;
}
body:not(.theme-light):not(.theme-dark) .jmespath-input.error {
background-color: var(--error-bg-dark) !important;
border-color: var(--error-border-dark) !important;
color: var(--error-text-dark) !important;
}
body:not(.theme-light):not(.theme-dark) .jmespath-input::placeholder {
color: var(--text-muted-dark);
}
body:not(.theme-light):not(.theme-dark) .jmespath-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 0.2rem var(--accent-shadow);
}
body:not(.theme-light):not(.theme-dark) .json-input,
body:not(.theme-light):not(.theme-dark) .result-output {
background-color: #2a2a2a;
border: 1px solid var(--border-input-dark);
color: var(--text-secondary-dark);
}
body:not(.theme-light):not(.theme-dark) .json-input::placeholder,
body:not(.theme-light):not(.theme-dark) .result-output::placeholder {
color: var(--text-muted-dark);
}
body:not(.theme-light):not(.theme-dark) .json-input:focus,
body:not(.theme-light):not(.theme-dark) .result-output:focus {
background-color: #323232;
border-color: var(--accent-color);
color: var(--text-primary-dark);
box-shadow: 0 0 0 0.2rem var(--accent-shadow);
}
body:not(.theme-light):not(.theme-dark) .alert-danger {
background-color: var(--error-bg-dark);
border-color: var(--error-border-dark);
color: var(--error-text-dark);
}
body:not(.theme-light):not(.theme-dark) .alert-success {
background-color: var(--success-bg-dark);
border-color: var(--success-border-dark);
color: var(--success-text-dark);
}
body:not(.theme-light):not(.theme-dark) .text-muted {
color: var(--text-muted-dark) !important;
}
body:not(.theme-light):not(.theme-dark) footer.bg-light {
background-color: var(--bg-secondary-dark) !important;
border-top: 1px solid var(--border-dark) !important;
color: var(--text-secondary-dark) !important;
}
body:not(.theme-light):not(.theme-dark) footer .text-muted {
color: var(--text-muted-dark) !important;
}
body:not(.theme-light):not(.theme-dark) footer a {
color: var(--text-muted-dark) !important;
}
body:not(.theme-light):not(.theme-dark) footer a:hover {
color: var(--text-secondary-dark) !important;
}
/* Bootstrap dark mode overrides */
body:not(.theme-light):not(.theme-dark) .btn-outline-info {
color: var(--btn-info);
border-color: var(--btn-info);
}
body:not(.theme-light):not(.theme-dark) .btn-outline-info:hover {
background-color: var(--btn-info);
border-color: var(--btn-info);
color: var(--bg-primary-light);
}
body:not(.theme-light):not(.theme-dark) .btn-outline-success {
color: var(--btn-success);
border-color: var(--btn-success);
}
body:not(.theme-light):not(.theme-dark) .btn-outline-success:hover {
background-color: var(--btn-success);
border-color: var(--btn-success);
color: var(--bg-primary-light);
}
.btn-outline-info {
color: #17a2b8;
border-color: #17a2b8;
}
.btn-outline-info:hover {
background-color: #17a2b8;
border-color: #17a2b8;
color: #fff;
}
.btn-outline-primary {
color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:hover {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:hover {
background-color: #6c757d;
border-color: var(--btn-secondary);
color: #fff;
}
.btn-outline-danger {
color: var(--btn-danger);
border-color: var(--btn-danger);
}
.btn-outline-danger:hover {
background-color: var(--btn-danger);
border-color: var(--btn-danger);
color: #fff;
}
}

View File

@@ -1,16 +1,16 @@
import React, { useState, useEffect } from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import MainPage from './components/MainPage';
import ApiKeyPage from './components/ApiKeyPage';
import './App.css';
import React, { useState, useEffect } from "react";
import Header from "./components/Header";
import Footer from "./components/Footer";
import MainPage from "./components/MainPage";
import ApiKeyPage from "./components/ApiKeyPage";
import "./App.css";
// Utility function to generate a cryptographically secure API key
function generateApiKey() {
const array = new Uint8Array(16);
// Use crypto.getRandomValues if available (browser), fallback for tests
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
crypto.getRandomValues(array);
} else {
// Fallback for test environments - not cryptographically secure
@@ -19,76 +19,83 @@ function generateApiKey() {
}
}
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
}
// JMESPath Testing Tool - Main Application Component
function App() {
const [currentPage, setCurrentPage] = useState('main'); // 'main' or 'apikey'
const [currentPage, setCurrentPage] = useState("main"); // 'main' or 'apikey'
const [theme, setTheme] = useState(() => {
// Load theme from localStorage or default to 'auto'
return localStorage.getItem('theme') || 'auto';
return localStorage.getItem("theme") || "auto";
});
const [shellType, setShellType] = useState(() => {
// Load shell type from localStorage or default to 'bash'
return localStorage.getItem("jmespath-shell-type") || "bash";
});
const [showReloadButton, setShowReloadButton] = useState(false);
const [currentStateGuid, setCurrentStateGuid] = useState(null);
const [sampleData, setSampleData] = useState(null);
const [jmespathExpression, setJmespathExpression] =
useState("people[0].name");
const [jsonData, setJsonData] = useState(`{
"people": [
{
"name": "John Doe",
"age": 30,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"city": "Los Angeles"
}
],
"total": 2
}`);
const [apiKey, setApiKey] = useState(() => {
// Load API key from localStorage or generate new one
const stored = localStorage.getItem('jmespath-api-key');
const stored = localStorage.getItem("jmespath-api-key");
if (stored && /^[0-9a-f]{32}$/i.test(stored)) {
return stored;
}
const newKey = generateApiKey();
localStorage.setItem('jmespath-api-key', newKey);
localStorage.setItem("jmespath-api-key", newKey);
return newKey;
});
// Theme management
useEffect(() => {
// Apply theme to document
const applyTheme = (selectedTheme) => {
const root = document.documentElement;
const body = document.body;
const effectiveTheme =
selectedTheme === "auto"
? window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: selectedTheme;
// Clear existing theme classes from both html and body
root.className = '';
body.classList.remove('theme-light', 'theme-dark');
if (selectedTheme === 'light') {
body.classList.add('theme-light');
} else if (selectedTheme === 'dark') {
body.classList.add('theme-dark');
}
// 'auto' uses CSS media queries (no class needed)
document.documentElement.setAttribute("data-bs-theme", effectiveTheme);
};
applyTheme(theme);
// Save theme preference
localStorage.setItem('theme', theme);
localStorage.setItem("theme", theme);
}, [theme]);
// Check if we're running on localhost
const isRunningOnLocalhost = () => {
const hostname = window.location.hostname;
return hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('127.') ||
hostname === '::1';
};
// Shell type management
useEffect(() => {
localStorage.setItem("jmespath-shell-type", shellType);
}, [shellType]);
// Get headers for API requests (omit API key for localhost)
// Get headers for API requests
const getApiHeaders = () => {
const headers = {
'Accept': 'application/json'
return {
Accept: "application/json",
"X-API-Key": apiKey,
};
// Only send API key for non-localhost requests
if (!isRunningOnLocalhost()) {
headers['X-API-Key'] = apiKey;
}
return headers;
};
// Load sample data from API on startup and setup periodic state checking
@@ -104,8 +111,8 @@ function App() {
// Check if state has changed (new data uploaded)
const checkStateChange = async () => {
try {
const response = await fetch('/api/v1/state', {
headers: getApiHeaders()
const response = await fetch("/api/v1/state", {
headers: getApiHeaders(),
});
if (response.ok) {
@@ -116,7 +123,6 @@ function App() {
}
} catch (error) {
// Silently handle state check errors
console.log('State check failed:', error);
}
};
@@ -124,20 +130,19 @@ function App() {
const loadSampleData = async () => {
try {
setShowReloadButton(false);
const response = await fetch('/api/v1/sample', {
headers: getApiHeaders()
const response = await fetch("/api/v1/sample", {
headers: getApiHeaders(),
});
if (response.ok) {
const data = await response.json();
if (data) {
setSampleData(data);
console.log('Sample data loaded:', data);
setJsonData(JSON.stringify(data, null, 2));
}
// Update current state GUID
const stateResponse = await fetch('/api/v1/state', {
headers: getApiHeaders()
const stateResponse = await fetch("/api/v1/state", {
headers: getApiHeaders(),
});
if (stateResponse.ok) {
const stateData = await stateResponse.json();
@@ -145,7 +150,7 @@ function App() {
}
}
} catch (error) {
console.error('Failed to load sample data:', error);
console.error("Failed to load sample data:", error);
}
};
@@ -153,7 +158,7 @@ function App() {
const regenerateApiKey = () => {
const newKey = generateApiKey();
setApiKey(newKey);
localStorage.setItem('jmespath-api-key', newKey);
localStorage.setItem("jmespath-api-key", newKey);
setShowReloadButton(false);
setCurrentStateGuid(null);
};
@@ -167,7 +172,7 @@ function App() {
};
return (
<div className="container-fluid vh-100 d-flex flex-column">
<div className="vh-100 d-flex flex-column">
<Header
theme={theme}
onThemeChange={handleThemeChange}
@@ -176,18 +181,26 @@ function App() {
/>
{/* Main Content Section - flex-grow to fill space */}
<div className="container-fluid flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}>
{currentPage === 'main' ? (
<div
className="container-fluid flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
{currentPage === "main" ? (
<MainPage
apiKey={apiKey}
showReloadButton={showReloadButton}
onReloadSampleData={loadSampleData}
initialSampleData={sampleData}
jmespathExpression={jmespathExpression}
setJmespathExpression={setJmespathExpression}
jsonData={jsonData}
setJsonData={setJsonData}
/>
) : (
<ApiKeyPage
apiKey={apiKey}
onRegenerateApiKey={regenerateApiKey}
shellType={shellType}
onShellTypeChange={setShellType}
/>
)}
</div>

View File

@@ -1,13 +1,27 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
import { vi } from 'vitest';
import '@testing-library/jest-dom';
// Mock localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => { store[key] = value.toString(); }),
clear: vi.fn(() => { store = {}; }),
removeItem: vi.fn((key) => { delete store[key]; })
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Mock fetch for API calls
global.fetch = jest.fn();
global.fetch = vi.fn();
describe('App Component', () => {
beforeEach(() => {
fetch.mockClear();
vi.clearAllMocks();
// Mock successful API responses
fetch.mockImplementation((url) => {
if (url.includes('/api/v1/sample')) {
@@ -55,8 +69,23 @@ describe('App Component', () => {
test('renders version number', () => {
render(<App />);
const versionText = screen.getByText(/v1\.1\.7-dev/);
// Version can be either v1.2.3 format (release), v1.2.3-dev/test format (legacy dev), or "unknown" format (new dev)
const versionText = screen.getByText(/(v\d+\.\d+\.\d+(-dev|-test)?|unknown)/);
expect(versionText).toBeInTheDocument();
// Check if it's a dev/test/unknown build
const isDevBuild = versionText.textContent.includes('-dev') ||
versionText.textContent.includes('-test') ||
versionText.textContent.includes('unknown');
// Additional validations can be added here based on build type
if (isDevBuild) {
// Dev/test/unknown specific validations
expect(versionText.textContent).toMatch(/(v\d+\.\d+\.\d+-(dev|test)|unknown)/);
} else {
// Release build validations - just check that version pattern exists in the text
expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+/);
}
});
test('renders all toolbar buttons', () => {
@@ -259,7 +288,7 @@ describe('App Component', () => {
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/api/v1/sample', expect.objectContaining({
headers: expect.objectContaining({
'X-API-Key': expect.any(String)
'Accept': 'application/json'
})
}));
});

View File

@@ -1,102 +0,0 @@
import React, { useState } from 'react';
function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopyToClipboard = async () => {
try {
await navigator.clipboard.writeText(apiKey);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = apiKey;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
}
};
return (
<div className="row justify-content-center">
<div className="col-md-8">
<div className="card">
<div className="card-header">
<h5 className="mb-0">🔐 API Key Management</h5>
</div>
<div className="card-body">
<div className="mb-4">
<label className="form-label fw-bold">Your API Key:</label>
<div className="input-group">
<input
type="text"
className="form-control font-monospace"
value={apiKey}
readOnly
/>
<button
className={`btn ${copySuccess ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={handleCopyToClipboard}
title="Copy API key to clipboard"
>
{copySuccess ? '✓ Copied!' : '📋 Copy'}
</button>
<button
className="btn btn-outline-primary"
onClick={onRegenerateApiKey}
title="Generate new API key"
>
🔄 Regenerate
</button>
</div>
<div className="form-text">
This API key is used to encrypt and authenticate data uploads from remote clients.
<strong>Note:</strong> Requests from localhost (127.0.0.1) do not require an API key.
</div>
</div>
<div className="mb-4">
<h6>📡 Remote Data Upload API</h6>
<p className="text-muted">
External tools can upload sample data remotely using the REST API.
For remote clients, the API key is required for authentication:
</p>
<pre className="bg-light p-3 rounded border">
<code>{`curl -s -X POST \\
-H "Content-Type: application/json" \\
-H "Accept: application/json" \\
-H "X-API-Key: ${apiKey}" \\
--data @{{JSON_FILE_NAME}} \\
"${window.location.origin}/api/v1/upload"`}</code>
</pre>
<div className="form-text">
Replace <code>{'{{JSON_FILE_NAME}}'}</code> with the path to your JSON file containing the sample data.
<br />
<strong>For localhost clients:</strong> The X-API-Key header is optional and can be omitted.
</div>
</div>
<div className="alert alert-info">
<h6 className="alert-heading"> How it works:</h6>
<ul className="mb-0">
<li>Remote clients require API key authentication for security</li>
<li>Localhost clients (127.0.0.1) can access the API without authentication</li>
<li>Your data is encrypted using AES-256-GCM with PBKDF2 key derivation</li>
<li>Data is automatically cleared after first retrieval (one-time use)</li>
<li>Sessions expire after 1 hour for security</li>
<li>Maximum 100 concurrent sessions supported</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
}
export default ApiKeyPage;

View File

@@ -0,0 +1,146 @@
import React, { useState } from 'react';
function CodeBlock({ code }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
return (
<div className="position-relative">
<pre className="bg-light p-3 pe-5 rounded border shadow-sm">
<code className="d-block" style={{ whiteSpace: 'pre-wrap' }}>{code}</code>
</pre>
<button
className={`btn btn-sm ${copySuccess ? 'btn-success' : 'btn-outline-secondary'} position-absolute top-0 end-0 m-2`}
onClick={handleCopy}
title="Copy code to clipboard"
style={{ opacity: 0.8 }}
>
{copySuccess ? '✓' : '📋'}
</button>
</div>
);
}
function ApiKeyPage({ apiKey, onRegenerateApiKey, shellType, onShellTypeChange }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopyToClipboard = async () => {
try {
await navigator.clipboard.writeText(apiKey);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = apiKey;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
}
};
return (
<div className="row justify-content-center">
<div className="col-md-8">
<div className="card">
<div className="card-header">
<h5 className="mb-0">🔐 API Key Management</h5>
</div>
<div className="card-body">
<div className="mb-4">
<label className="form-label fw-bold">Your API Key:</label>
<div className="input-group">
<input
type="text"
className="form-control font-monospace"
value={apiKey}
readOnly
/>
<button
className={`btn ${copySuccess ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={handleCopyToClipboard}
title="Copy API key to clipboard"
>
{copySuccess ? '✓ Copied!' : '📋 Copy'}
</button>
<button
className="btn btn-outline-primary"
onClick={onRegenerateApiKey}
title="Generate new API key"
>
🔄 Regenerate
</button>
</div>
<div className="form-text">
This API key is used to encrypt and authenticate data uploads.
</div>
</div>
<div className="mb-4">
<div className="d-flex justify-content-between align-items-center mb-2">
<h6 className="mb-0">📡 Remote Data Upload API</h6>
<div className="btn-group btn-group-sm" role="group" aria-label="Shell type selector">
<input
type="radio"
className="btn-check"
name="shellType"
id="shellBash"
autoComplete="off"
checked={shellType === 'bash'}
onChange={() => onShellTypeChange('bash')}
/>
<label className="btn btn-outline-secondary" htmlFor="shellBash">UNIX (Bash)</label>
<input
type="radio"
className="btn-check"
name="shellType"
id="shellPowerShell"
autoComplete="off"
checked={shellType === 'powershell'}
onChange={() => onShellTypeChange('powershell')}
/>
<label className="btn btn-outline-secondary" htmlFor="shellPowerShell">Windows (PowerShell)</label>
</div>
</div>
<p className="text-muted">
External tools can upload sample data remotely using the REST API.
The API key is required for authentication. Define two
environment variables in your {shellType === 'bash' ? <code>.bashrc</code> : <code>PowerShell profile</code>}.
</p>
<CodeBlock
code={shellType === 'bash'
? `export JMESPATH_PLAYGROUND_API_URL=${window.location.origin}\nexport JMESPATH_PLAYGROUND_API_KEY=${apiKey}`
: `$env:JMESPATH_PLAYGROUND_API_URL = "${window.location.origin}"\n$env:JMESPATH_PLAYGROUND_API_KEY = "${apiKey}"`}
/>
<p className="text-muted">Then, use the following {shellType === 'bash' ? <code>curl</code> : <code>PowerShell</code>} command to upload your data:</p>
<CodeBlock
code={shellType === 'bash'
? `curl -s -X POST \\\n -H "Content-Type: application/json" \\\n -H "Accept: application/json" \\\n -H "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY" \\\n --data @__JSON_FILE_NAME__ \\\n "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload"`
: `Invoke-RestMethod -Uri "$env:JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \\\n -Method Post \\\n -ContentType "application/json" \\\n -Headers @{\n "X-API-Key" = $env:JMESPATH_PLAYGROUND_API_KEY\n "Accept" = "application/json"\n } \\\n -InFile __JSON_FILE_NAME__`}
/>
<div className="form-text">
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
JSON file containing the sample data. {shellType === 'bash' && <span>or use <code>-</code> to read from standard input.</span>}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default ApiKeyPage;

View File

@@ -8,7 +8,7 @@ function Footer() {
<div className="row">
<div className="col-md-6">
<p className="mb-0 text-muted small">
<strong>JMESPath Testing Tool</strong> v{VERSION} - Created for testing and validating JMESPath expressions
<strong>JMESPath Testing Tool</strong> {VERSION === 'unknown' ? VERSION : `v${VERSION}`} - Created for testing and validating JMESPath expressions
</p>
</div>
<div className="col-md-6 text-md-end">

View File

@@ -1,21 +1,20 @@
import React from 'react';
import { VERSION } from '../version';
function Header({ theme, onThemeChange, currentPage, onPageChange }) {
return (
<div className="header-section py-2">
<div className="container">
<div className="row">
<div className="header-section">
<div className="container-fluid px-4">
<div className="row align-items-center">
<div className="col-12 text-center position-relative">
<h2 className="mb-1">JMESPath Testing Tool</h2>
{/* Right side controls - better positioning */}
<div className="position-absolute top-0 end-0 d-flex align-items-center gap-2">
<div className="position-absolute top-50 end-0 translate-middle-y d-flex align-items-center gap-2 me-4">
{/* API Key Management Button - more prominent */}
<button
type="button"
className={`btn btn-sm ${
currentPage === 'apikey'
? 'btn-warning fw-bold'
? 'btn-warning fw-bold text-dark'
: 'btn-outline-warning'
}`}
onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')}
@@ -29,8 +28,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button"
className={`btn ${
theme === 'auto'
? 'btn-primary'
: 'btn-outline-secondary'
? 'btn-light active'
: 'btn-outline-light'
}`}
onClick={() => onThemeChange('auto')}
title="Auto (follow system)"
@@ -41,8 +40,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button"
className={`btn ${
theme === 'light'
? 'btn-primary'
: 'btn-outline-secondary'
? 'btn-light active'
: 'btn-outline-light'
}`}
onClick={() => onThemeChange('light')}
title="Light theme"
@@ -53,8 +52,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button"
className={`btn ${
theme === 'dark'
? 'btn-primary'
: 'btn-outline-secondary'
? 'btn-light active'
: 'btn-outline-light'
}`}
onClick={() => onThemeChange('dark')}
title="Dark theme"

View File

@@ -1,41 +1,24 @@
import React, { useState, useEffect } from 'react';
import jmespath from 'jmespath';
function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleData }) {
const [jmespathExpression, setJmespathExpression] = useState('people[0].name');
const [jsonData, setJsonData] = useState(`{
"people": [
{
"name": "John Doe",
"age": 30,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"city": "Los Angeles"
}
],
"total": 2
}`);
const [result, setResult] = useState('');
const [error, setError] = useState('');
const [jsonError, setJsonError] = useState('');
// Use initial sample data when provided
useEffect(() => {
if (initialSampleData) {
setJsonData(JSON.stringify(initialSampleData, null, 2));
}
}, [initialSampleData]);
import React, { useState, useEffect } from "react";
import jmespath from "jmespath";
function MainPage({
showReloadButton,
onReloadSampleData,
jmespathExpression,
setJmespathExpression,
jsonData,
setJsonData,
}) {
const [result, setResult] = useState("");
const [error, setError] = useState("");
const [jsonError, setJsonError] = useState("");
const [copySuccess, setCopySuccess] = useState(false);
const evaluateExpression = () => {
try {
// Clear previous errors
setError('');
setJsonError('');
setError("");
setJsonError("");
// Validate and parse JSON
let parsedData;
@@ -43,7 +26,7 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
parsedData = JSON.parse(jsonData);
} catch (jsonErr) {
setJsonError(`Invalid JSON: ${jsonErr.message}`);
setResult('');
setResult("");
return;
}
@@ -52,13 +35,13 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
// Format the result
if (queryResult === null || queryResult === undefined) {
setResult('null');
setResult("null");
} else {
setResult(JSON.stringify(queryResult, null, 2));
}
} catch (jmesErr) {
setError(`JMESPath Error: ${jmesErr.message}`);
setResult('');
setResult("");
}
};
@@ -88,30 +71,50 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
};
const clearAll = () => {
setJmespathExpression('');
setJsonData('');
setResult('');
setError('');
setJsonError('');
setJmespathExpression("");
setJsonData("");
setResult("");
setError("");
setJsonError("");
};
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(result);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error("Failed to copy!", err);
}
};
const downloadResult = () => {
const blob = new Blob([result], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "result.json";
a.click();
URL.revokeObjectURL(url);
};
const loadSample = () => {
const sampleData = {
"users": [
{"name": "Alice", "age": 30, "city": "New York"},
{"name": "Bob", "age": 25, "city": "San Francisco"},
{"name": "Charlie", "age": 35, "city": "Chicago"}
users: [
{ name: "Alice", age: 30, city: "New York" },
{ name: "Bob", age: 25, city: "San Francisco" },
{ name: "Charlie", age: 35, city: "Chicago" },
],
"total": 3
total: 3,
};
setJsonData(JSON.stringify(sampleData, null, 2));
setJmespathExpression('users[?age > `30`].name');
setJmespathExpression("users[?age > `30`].name");
};
const loadFromDisk = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
@@ -122,7 +125,7 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
const parsed = JSON.parse(content);
setJsonData(JSON.stringify(parsed, null, 2));
} catch (error) {
alert('Invalid JSON file');
alert("Invalid JSON file");
}
};
reader.readAsText(file);
@@ -132,9 +135,9 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
};
const loadLogFile = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.log,.jsonl,.ndjson';
const input = document.createElement("input");
input.type = "file";
input.accept = ".log,.jsonl,.ndjson";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
@@ -142,12 +145,12 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
reader.onload = (e) => {
try {
const content = e.target.result;
const lines = content.trim().split('\n');
const logs = lines.map(line => JSON.parse(line));
const lines = content.trim().split("\n");
const logs = lines.map((line) => JSON.parse(line));
setJsonData(JSON.stringify(logs, null, 2));
setJmespathExpression('[*].message');
setJmespathExpression("[*].message");
} catch (error) {
alert('Invalid JSON Lines file');
alert("Invalid JSON Lines file");
}
};
reader.readAsText(file);
@@ -162,8 +165,9 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
<div className="row mb-2">
<div className="col-12">
<p className="text-muted text-center mb-2 small">
Validate and test JMESPath expressions against JSON data in real-time.
Enter your JMESPath query and JSON data below to see the results instantly.
Validate and test JMESPath expressions against JSON data in
real-time. Enter your JMESPath query and JSON data below to see the
results instantly.
</p>
</div>
</div>
@@ -172,11 +176,54 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
<div className="row mb-2">
<div className="col-12">
<div className="card">
<div className="card-header d-flex justify-content-between align-items-center py-2">
<div className="card-header py-2">
<h6 className="mb-0">
<i className="bi bi-search me-2"></i>
JMESPath Expression
</h6>
</div>
<div className="card-body">
<input
type="text"
className={`form-control jmespath-input ${error ? "error" : "success"}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
<div
className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
>
<small className="mb-0">
{error || "Expression is correct"}
</small>
{showReloadButton && (
<button
className="btn btn-light btn-sm ms-2 border"
onClick={() => {
onReloadSampleData();
}}
title="New sample data is available"
>
<i className="bi bi-arrow-clockwise me-1"></i>
Reload Sample Data
</button>
)}
</div>
</div>
</div>
</div>
</div>
{/* Lower Middle Section: Input and Output Areas */}
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
{/* Left Panel: JSON Data Input */}
<div className="col-md-6">
<div className="card h-100 d-flex flex-column">
<div className="card-header d-flex justify-content-between align-items-center py-2">
<h6 className="mb-0">
<i className="bi bi-file-earmark-code me-2"></i>
JSON Data
</h6>
<div>
<button
className="btn btn-outline-success btn-sm me-2"
@@ -215,52 +262,16 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
</button>
</div>
</div>
<div className="card-body">
<input
type="text"
className={`form-control jmespath-input ${error ? 'error' : 'success'}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
<div className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? 'alert-danger' : 'alert-success'}`}>
<small className="mb-0">{error || 'Expression is correct'}</small>
{showReloadButton && (
<button
className="btn btn-light btn-sm ms-2 border"
onClick={() => {
onReloadSampleData();
}}
title="New sample data is available"
<div
className="card-body flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
<i className="bi bi-arrow-clockwise me-1"></i>
Reload Sample Data
</button>
)}
</div>
</div>
</div>
</div>
</div>
{/* Lower Middle Section: Input and Output Areas */}
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
{/* Left Panel: JSON Data Input */}
<div className="col-md-6">
<div className="card h-100 d-flex flex-column">
<div className="card-header py-2">
<h6 className="mb-0">
<i className="bi bi-file-earmark-code me-2"></i>
JSON Data
</h6>
</div>
<div className="card-body flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}>
<textarea
className={`form-control json-input flex-grow-1 ${jsonError ? 'error' : 'success'}`}
className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
value={jsonData}
onChange={handleJsonChange}
placeholder="Enter JSON data here..."
style={{ minHeight: 0, resize: 'none' }}
style={{ minHeight: 0, resize: "none" }}
/>
{jsonError && (
<div className="alert alert-danger mt-2 mb-0">
@@ -274,19 +285,42 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
{/* Right Panel: Results */}
<div className="col-md-6">
<div className="card h-100 d-flex flex-column">
<div className="card-header py-2">
<div className="card-header py-2 d-flex justify-content-between align-items-center">
<h6 className="mb-0">
<i className="bi bi-output me-2"></i>
Results
</h6>
<div>
<button
className={`btn btn-sm me-2 ${copySuccess ? "btn-success" : "btn-outline-secondary"}`}
onClick={copyToClipboard}
disabled={!result || result === "null"}
title="Copy result to clipboard"
>
<i className={`bi ${copySuccess ? "bi-check-lg" : "bi-clipboard"} me-1`}></i>
{copySuccess ? "Copied!" : "Copy"}
</button>
<button
className="btn btn-outline-secondary btn-sm"
onClick={downloadResult}
disabled={!result || result === "null"}
title="Download result as JSON file"
>
<i className="bi bi-download me-1"></i>
Download
</button>
</div>
<div className="card-body flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}>
</div>
<div
className="card-body flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
<textarea
className="form-control result-output flex-grow-1"
value={result}
readOnly
placeholder="Results will appear here..."
style={{ minHeight: 0, resize: 'none' }}
style={{ minHeight: 0, resize: "none" }}
/>
</div>
</div>

View File

@@ -8,12 +8,6 @@ code {
monospace;
}
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-section {
flex: 1;
min-height: 0;
@@ -52,13 +46,6 @@ code {
color: var(--success-text-light);
}
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
/* Dark mode support for error states */
@media (prefers-color-scheme: dark) {
.error {

View File

@@ -1,46 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
// Add TextEncoder/TextDecoder for Node.js compatibility
if (typeof TextEncoder === 'undefined') {
global.TextEncoder = require('util').TextEncoder;
}
if (typeof TextDecoder === 'undefined') {
global.TextDecoder = require('util').TextDecoder;
}
// Mock crypto.getRandomValues for test environment
if (typeof global.crypto === 'undefined') {
global.crypto = {
getRandomValues: (array) => {
// Simple predictable mock for testing
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
}
};
}
// Suppress console errors during tests
const originalError = console.error;
beforeAll(() => {
console.error = (...args) => {
if (
typeof args[0] === 'string' &&
(args[0].includes('Warning: ReactDOMTestUtils.act is deprecated') ||
args[0].includes('Warning: An update to App inside a test was not wrapped in act'))
) {
return;
}
originalError.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
});

25
vite.config.js Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:3000',
changeOrigin: true,
},
},
},
build: {
outDir: 'build',
},
test: {
globals: true,
environment: 'jsdom',
css: true,
},
});