Creating first version of MCP server.
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled
This commit is contained in:
6
.babelrc
6
.babelrc
@ -1,6 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", {"targets": {"node": "current"}}],
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
}
|
176
.gitea/workflows/ci.yml
Normal file
176
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,176 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test and Build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Generate coverage report
|
||||
run: npm test -- --coverage --coverageReporters=lcov
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: https://github.com/actions/upload-artifact@v4
|
||||
if: matrix.node-version == '18.x'
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: coverage/
|
||||
retention-days: 30
|
||||
|
||||
security:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run security audit
|
||||
run: npm audit --audit-level=moderate
|
||||
|
||||
- name: Check for vulnerabilities
|
||||
run: npm audit --audit-level=high --production
|
||||
|
||||
build-artifacts:
|
||||
name: Build Release Artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, security]
|
||||
if: github.ref == 'refs/heads/master'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Create distribution package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp -r build/ dist/
|
||||
cp package.json dist/
|
||||
cp package-lock.json dist/
|
||||
cp README.md dist/
|
||||
cp LICENSE dist/
|
||||
cp mcp-config-example.json dist/
|
||||
cp -r templates/ dist/
|
||||
|
||||
- name: Create tarball
|
||||
run: |
|
||||
cd dist
|
||||
tar -czf ../drawio-mcp-server-${{ github.sha }}.tar.gz .
|
||||
cd ..
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: https://github.com/actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-artifacts
|
||||
path: |
|
||||
drawio-mcp-server-${{ github.sha }}.tar.gz
|
||||
dist/
|
||||
retention-days: 90
|
||||
|
||||
lint-check:
|
||||
name: Code Quality Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check TypeScript compilation
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Run linter with detailed output
|
||||
run: npm run lint -- --format=json --output-file=lint-results.json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload lint results
|
||||
uses: https://github.com/actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: lint-results
|
||||
path: lint-results.json
|
||||
retention-days: 7
|
||||
|
||||
notify:
|
||||
name: Notification
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, security, lint-check]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Notify success
|
||||
if: needs.test.result == 'success' && needs.security.result == 'success' && needs.lint-check.result == 'success'
|
||||
run: |
|
||||
echo "✅ All checks passed successfully!"
|
||||
echo "Branch: ${{ github.ref_name }}"
|
||||
echo "Commit: ${{ github.sha }}"
|
||||
|
||||
- name: Notify failure
|
||||
if: needs.test.result == 'failure' || needs.security.result == 'failure' || needs.lint-check.result == 'failure'
|
||||
run: |
|
||||
echo "❌ Some checks failed!"
|
||||
echo "Test result: ${{ needs.test.result }}"
|
||||
echo "Security result: ${{ needs.security.result }}"
|
||||
echo "Lint result: ${{ needs.lint-check.result }}"
|
||||
echo "Branch: ${{ github.ref_name }}"
|
||||
echo "Commit: ${{ github.sha }}"
|
||||
exit 1
|
@ -1,11 +0,0 @@
|
||||
const PRName = function () {
|
||||
let ID = '';
|
||||
// let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const characters = '0123456789';
|
||||
for ( let i = 0; i < 6; i++ ) {
|
||||
ID += characters.charAt(Math.floor(Math.random() * 10));
|
||||
}
|
||||
return 'PR-'+ID;
|
||||
};
|
||||
|
||||
console.log(PRName());
|
368
README.md
368
README.md
@ -1,75 +1,317 @@
|
||||
# Create Node TS GraphQL Server
|
||||
# Draw.io MCP Server
|
||||
|
||||
This project aims to have a starter kit for creating a new Node with typescript, GraphQL server and tools that generally go along with it.
|
||||
An MCP (Model Context Protocol) server that enables creating and managing draw.io diagrams from Cline in VSCode using a functional programming approach.
|
||||
|
||||
Tech(Library or Framework) | Version |
|
||||
--- | --- |
|
||||
Jest (Testing) | 29.7.0
|
||||
Typescript | 5.6.2
|
||||
GraphQL | 16.9.0
|
||||
Type GraphQL | 2.0.0-rc.2
|
||||
## Features
|
||||
|
||||
## Setup
|
||||
To create a new project run in the terminal:
|
||||
```
|
||||
npx @aleleba/create-node-ts-graphql-server server-app-name
|
||||
```
|
||||
Then run:
|
||||
```
|
||||
cd server-app-name
|
||||
```
|
||||
You will need to create a new .env file at the root of the project for global config.
|
||||
This is an example of config.
|
||||
```
|
||||
#ENVIRONMENT Defauld production
|
||||
ENVIRONMENT=development
|
||||
#WHITELIST URLS Default to http://localhost
|
||||
WHITELIST_URLS=https://someurl.com
|
||||
#PLAYGROUND GRAPHQL Default to "false"
|
||||
PLAYGROUND_GRAPHQL=true
|
||||
# PORT EXPOSE APP Default to 4000
|
||||
PORT=4000
|
||||
```
|
||||
The default environment is production, the server-app port defauld is 4000, the default whitelist is http://localhost and the default graphiql is false.
|
||||
- ✅ **Diagram Creation**: Support for multiple diagram types (BPMN, UML, ER, Network, Architecture, Flowchart, etc.)
|
||||
- ✅ **VSCode Integration**: Automatic opening of diagrams in VSCode with draw.io extension
|
||||
- ✅ **Functional Programming**: Architecture based on pure functions without classes
|
||||
- ✅ **Automatic Generation**: Predefined templates for different diagram types
|
||||
- ✅ **File Management**: Search and listing of existing diagrams
|
||||
- ✅ **Automatic Configuration**: Automatic VSCode environment setup
|
||||
|
||||
### For Development
|
||||
In the terminal run:
|
||||
```
|
||||
npm run start:dev
|
||||
```
|
||||
The ENV enviroment variable should be "development" and choose the port of your preference with the enviroment variable PORT.
|
||||
## Supported Diagram Types
|
||||
|
||||
You will find the controllers on:
|
||||
```
|
||||
scr/controllers/
|
||||
```
|
||||
You will find the models on:
|
||||
```
|
||||
scr/models
|
||||
```
|
||||
You will find the GraphQL server, resolvers and schema definition on:
|
||||
```
|
||||
scr/GraphQL
|
||||
### BPMN (Business Process Model and Notation)
|
||||
- `bpmn-process`: Business processes
|
||||
- `bpmn-collaboration`: Collaboration between participants
|
||||
- `bpmn-choreography`: Message exchanges
|
||||
|
||||
### UML (Unified Modeling Language)
|
||||
- `uml-class`: Class diagrams
|
||||
- `uml-sequence`: Sequence diagrams
|
||||
- `uml-use-case`: Use case diagrams
|
||||
- `uml-activity`: Activity diagrams
|
||||
- `uml-state`: State diagrams
|
||||
- `uml-component`: Component diagrams
|
||||
- `uml-deployment`: Deployment diagrams
|
||||
|
||||
### Database
|
||||
- `er-diagram`: Entity-Relationship diagrams
|
||||
- `database-schema`: Database schemas
|
||||
- `conceptual-model`: Conceptual models
|
||||
|
||||
### Network & Infrastructure
|
||||
- `network-topology`: Network topology
|
||||
- `infrastructure`: System infrastructure
|
||||
- `cloud-architecture`: Cloud architecture
|
||||
|
||||
### Software Architecture
|
||||
- `system-architecture`: System architecture
|
||||
- `microservices`: Microservices architecture
|
||||
- `layered-architecture`: Layered architecture
|
||||
- `c4-context`: C4 context diagrams
|
||||
- `c4-container`: C4 container diagrams
|
||||
- `c4-component`: C4 component diagrams
|
||||
|
||||
### General
|
||||
- `flowchart`: Flowcharts
|
||||
- `orgchart`: Organizational charts
|
||||
- `mindmap`: Mind maps
|
||||
- `wireframe`: Wireframes
|
||||
- `gantt`: Gantt charts
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd drawio-mcp-server
|
||||
```
|
||||
|
||||
The manage of the routes for custom API you should find on:
|
||||
```
|
||||
scr/routes
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
This will start the app in development mode, also use nodemon and webpack to real time coding!
|
||||
Enjoy coding!
|
||||
|
||||
### For Production
|
||||
In the terminal run:
|
||||
```
|
||||
3. **Build the project**:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
It will create a build folder and run:
|
||||
```
|
||||
npm start
|
||||
```
|
||||
This will start the app.
|
||||
|
||||
## Cheers
|
||||
Hope you enjoy this proyect! Sincerely Alejandro Lembke Barrientos.
|
||||
4. **Configure in Cline**:
|
||||
Add to your Cline MCP configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"drawio-mcp-server": {
|
||||
"command": "node",
|
||||
"args": ["/full/path/to/project/build/index.js"],
|
||||
"env": {
|
||||
"WORKSPACE_ROOT": "/path/to/your/workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### `create_diagram`
|
||||
Creates a new diagram of the specified type.
|
||||
|
||||
**Parameters**:
|
||||
- `name` (required): Diagram file name
|
||||
- `type` (required): Type of diagram to create
|
||||
- `format` (optional): File format (drawio, drawio.svg, drawio.png, dio, xml)
|
||||
- `description` (optional): Diagram description
|
||||
- `outputPath` (optional): Output directory
|
||||
- `workspaceRoot` (optional): Workspace root directory
|
||||
|
||||
**Type-specific parameters**:
|
||||
|
||||
**BPMN**:
|
||||
- `processName`: Process name
|
||||
- `tasks`: List of tasks
|
||||
- `gatewayType`: Gateway type (exclusive, parallel)
|
||||
- `branches`: Gateway branches
|
||||
- `beforeGateway`: Tasks before gateway
|
||||
- `afterGateway`: Tasks after gateway
|
||||
|
||||
**UML**:
|
||||
- `classes`: List of classes for class diagrams
|
||||
|
||||
**ER**:
|
||||
- `entities`: List of entities
|
||||
|
||||
**Network/Architecture**:
|
||||
- `components`: List of components
|
||||
|
||||
**Flowchart**:
|
||||
- `processes`: List of processes
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"name": "my-bpmn-process",
|
||||
"type": "bpmn-process",
|
||||
"processName": "Approval Process",
|
||||
"tasks": ["Request", "Review", "Approve"],
|
||||
"format": "drawio"
|
||||
}
|
||||
```
|
||||
|
||||
### `list_diagrams`
|
||||
Lists all diagram files in the workspace.
|
||||
|
||||
**Parameters**:
|
||||
- `workspaceRoot` (optional): Workspace root directory
|
||||
|
||||
### `open_diagram_in_vscode`
|
||||
Opens a diagram in VSCode with the draw.io extension.
|
||||
|
||||
**Parameters**:
|
||||
- `filePath` (required): Path to the diagram file
|
||||
- `workspaceRoot` (optional): Workspace root directory
|
||||
|
||||
### `setup_vscode_environment`
|
||||
Sets up VSCode environment for draw.io (installs extension if needed).
|
||||
|
||||
**Parameters**:
|
||||
- `workspaceRoot` (optional): Workspace root directory
|
||||
|
||||
### `get_diagram_types`
|
||||
Gets the list of supported diagram types with descriptions.
|
||||
|
||||
## Available Resources
|
||||
|
||||
### `diagrams://workspace/list`
|
||||
List of all diagram files in the workspace with metadata.
|
||||
|
||||
### `diagrams://types/supported`
|
||||
List of supported diagram types with descriptions.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create a simple BPMN diagram
|
||||
```bash
|
||||
# Through Cline, use the tool:
|
||||
create_diagram({
|
||||
"name": "sales-process",
|
||||
"type": "bpmn-process",
|
||||
"processName": "Sales Process",
|
||||
"tasks": ["Receive Order", "Check Stock", "Process Payment", "Ship Product"]
|
||||
})
|
||||
```
|
||||
|
||||
### Create a BPMN diagram with gateway
|
||||
```bash
|
||||
create_diagram({
|
||||
"name": "approval-process",
|
||||
"type": "bpmn-process",
|
||||
"processName": "Approval Process",
|
||||
"beforeGateway": ["Receive Request"],
|
||||
"gatewayType": "exclusive",
|
||||
"branches": [
|
||||
["Auto Approve"],
|
||||
["Manual Review", "Approve/Reject"]
|
||||
],
|
||||
"afterGateway": ["Notify Result"]
|
||||
})
|
||||
```
|
||||
|
||||
### Create a UML class diagram
|
||||
```bash
|
||||
create_diagram({
|
||||
"name": "user-model",
|
||||
"type": "uml-class",
|
||||
"classes": ["User", "Profile", "Session"]
|
||||
})
|
||||
```
|
||||
|
||||
### Create an ER diagram
|
||||
```bash
|
||||
create_diagram({
|
||||
"name": "store-database",
|
||||
"type": "er-diagram",
|
||||
"entities": ["Customer", "Product", "Order", "OrderDetail"]
|
||||
})
|
||||
```
|
||||
|
||||
### Create an architecture diagram
|
||||
```bash
|
||||
create_diagram({
|
||||
"name": "system-architecture",
|
||||
"type": "system-architecture",
|
||||
"components": ["React Frontend", "API Gateway", "Auth Service", "Product Service", "Database"]
|
||||
})
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── types/
|
||||
│ └── diagram-types.ts # Types and enums for diagrams
|
||||
├── utils/
|
||||
│ ├── file-manager.ts # File management (functional)
|
||||
│ ├── xml-parser.ts # XML parser for draw.io (functional)
|
||||
│ └── vscode-integration.ts # VSCode integration (functional)
|
||||
├── generators/
|
||||
│ └── bpmn-generator.ts # BPMN diagram generator (functional)
|
||||
├── tools/
|
||||
│ └── create-diagram.ts # Main creation tool
|
||||
└── index.ts # Main MCP server
|
||||
```
|
||||
|
||||
## Functional Architecture
|
||||
|
||||
The project is designed following functional programming principles:
|
||||
|
||||
- **Pure functions**: No side effects, same inputs produce same outputs
|
||||
- **Immutability**: Data is not modified, new versions are created
|
||||
- **Composition**: Functions are combined to create complex functionality
|
||||
- **No classes**: Functions and types are used instead of classes and objects
|
||||
|
||||
### Example of pure function:
|
||||
```typescript
|
||||
export const createBPMNElement = (config: BPMNElementConfig): DiagramElement => ({
|
||||
id: config.id || generateId(),
|
||||
type: config.type,
|
||||
label: config.label,
|
||||
geometry: {
|
||||
x: config.x,
|
||||
y: config.y,
|
||||
width: config.width || getDefaultWidth(config.type),
|
||||
height: config.height || getDefaultHeight(config.type)
|
||||
},
|
||||
style: getBPMNElementStyle(config.type),
|
||||
properties: config.properties || {}
|
||||
});
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Available scripts:
|
||||
- `npm run build`: Compiles the TypeScript project
|
||||
- `npm run dev`: Runs in development mode with watch
|
||||
- `npm start`: Runs the compiled server
|
||||
|
||||
### Adding new diagram types:
|
||||
|
||||
1. **Add the type in `diagram-types.ts`**:
|
||||
```typescript
|
||||
export enum DiagramType {
|
||||
// ... existing types
|
||||
NEW_DIAGRAM_TYPE = 'new-diagram-type'
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create generator in `generators/`**:
|
||||
```typescript
|
||||
export const generateNewDiagramType = (input: CreateDiagramInput) => {
|
||||
// Functional generation logic
|
||||
};
|
||||
```
|
||||
|
||||
3. **Add to switch in `create-diagram.ts`**:
|
||||
```typescript
|
||||
case DiagramType.NEW_DIAGRAM_TYPE:
|
||||
return generateNewDiagramType(input);
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- VSCode with draw.io extension (`hediet.vscode-drawio`)
|
||||
- Cline with MCP support
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please:
|
||||
|
||||
1. Fork the project
|
||||
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add new feature'`)
|
||||
4. Push to the branch (`git push origin feature/new-feature`)
|
||||
5. Create a Pull Request
|
||||
|
||||
## Support
|
||||
|
||||
To report bugs or request features, please create an issue in the repository.
|
||||
|
@ -1,17 +0,0 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const deFaultValues = {
|
||||
ENV: 'production',
|
||||
PLAYGROUND_GRAPHQL: 'false',
|
||||
WHITELIST_URLS: 'http://localhost',
|
||||
PORT: '4000',
|
||||
};
|
||||
|
||||
export const config = {
|
||||
ENV: process.env.ENV,
|
||||
PLAYGROUND_GRAPHQL: process.env.PLAYGROUND_GRAPHQL === 'true' ? true : false,
|
||||
WHITELIST_URLS: process.env.WHITELIST_URLS ? process.env.WHITELIST_URLS.split(',') : deFaultValues.WHITELIST_URLS,
|
||||
PORT: process.env.PORT,
|
||||
};
|
@ -1,55 +0,0 @@
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import js from '@eslint/js';
|
||||
|
||||
export default [
|
||||
// Ignorar archivos y carpetas especificados en el antiguo .eslintignore
|
||||
{
|
||||
ignores: [
|
||||
'.eslintrc.js', // Aunque se eliminará, es bueno mantenerlo por si acaso
|
||||
'build/',
|
||||
'webpack.config.ts',
|
||||
'webpack.config.dev.ts',
|
||||
],
|
||||
},
|
||||
|
||||
// Configuración recomendada por ESLint
|
||||
js.configs.recommended,
|
||||
|
||||
// Configuraciones recomendadas por typescript-eslint
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// Configuración personalizada
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2021,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
// El parser ya está configurado por tseslint.configs.recommended
|
||||
},
|
||||
// Los plugins ya están configurados por tseslint.configs.recommended
|
||||
rules: {
|
||||
// Reglas personalizadas del antiguo .eslintrc.js
|
||||
'indent': [
|
||||
'error',
|
||||
'tab'
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
// Puedes añadir o sobrescribir reglas de las configuraciones recomendadas aquí si es necesario
|
||||
},
|
||||
}
|
||||
];
|
@ -1,15 +1,25 @@
|
||||
const { pathsToModuleNameMapper } = require('ts-jest');
|
||||
const { compilerOptions } = require('./tsconfig');
|
||||
|
||||
const aliases = pathsToModuleNameMapper(compilerOptions.paths, {
|
||||
prefix: '<rootDir>'
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src/tests'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},moduleNameMapper: {
|
||||
...aliases,
|
||||
'^.+\\.ts$': ['ts-jest', {
|
||||
useESM: true
|
||||
}]
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/src/tests/setup.ts'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/index.ts',
|
||||
'!src/tests/**/*.ts'
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
testTimeout: 10000
|
||||
};
|
11
mcp-config-example.json
Normal file
11
mcp-config-example.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"drawio-mcp-server": {
|
||||
"command": "node",
|
||||
"args": ["/home/aleleba/projects/drawio-mcp-server/build/index.js"],
|
||||
"env": {
|
||||
"WORKSPACE_ROOT": "/home/aleleba/projects"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9395
package-lock.json
generated
9395
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
93
package.json
93
package.json
@ -1,90 +1,61 @@
|
||||
{
|
||||
"name": "drawio-cline-mcp-server",
|
||||
"version": "0.0.1",
|
||||
"description": "Node with Typescript and GraphQL Server",
|
||||
"main": "index.js",
|
||||
"name": "drawio-mcp-server",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP Server for Draw.io integration with Cline - Create and manage diagrams (BPMN, UML, ER, Network, Architecture) from VSCode",
|
||||
"main": "build/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
"start": "node build/index.js",
|
||||
"start:dev": "webpack-cli --config webpack.config.dev.ts",
|
||||
"start:nodemon": "nodemon build/index.js",
|
||||
"build": "webpack-cli --config webpack.config.ts",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint ./ --ext .js --ext .ts",
|
||||
"lint:fix": "eslint ./ --ext .js --ext .ts --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"check-updates": "npx npm-check-updates -u && npm i --legacy-peer-deps"
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aleleba/node-ts-graphql-server.git"
|
||||
"url": "git+https://github.com/aleleba/drawio-mcp-server.git"
|
||||
},
|
||||
"keywords": [
|
||||
"node",
|
||||
"express",
|
||||
"typescript",
|
||||
"graphql",
|
||||
"server"
|
||||
"mcp",
|
||||
"drawio",
|
||||
"diagrams",
|
||||
"bpmn",
|
||||
"uml",
|
||||
"er-diagram",
|
||||
"network-diagram",
|
||||
"architecture",
|
||||
"vscode",
|
||||
"cline"
|
||||
],
|
||||
"author": "Alejandro Lembke Barrientos",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/aleleba/node-ts-graphql-server/issues"
|
||||
"url": "https://github.com/aleleba/drawio-mcp-server/issues"
|
||||
},
|
||||
"homepage": "https://github.com/aleleba/node-ts-graphql-server#readme",
|
||||
"homepage": "https://github.com/aleleba/drawio-mcp-server#readme",
|
||||
"dependencies": {
|
||||
"@graphql-tools/schema": "^10.0.25",
|
||||
"body-parser": "^2.2.0",
|
||||
"class-validator": "^0.14.2",
|
||||
"cookie-parse": "^0.4.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-http": "^1.22.4",
|
||||
"graphql-playground-middleware-express": "^1.7.23",
|
||||
"graphql-scalars": "^1.24.2",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-tools": "^9.0.20",
|
||||
"graphql-ws": "^6.0.6",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"type-graphql": "^2.0.0-rc.2",
|
||||
"web-push": "^3.6.7",
|
||||
"ws": "^8.18.3"
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^10.3.10",
|
||||
"path": "^0.12.7",
|
||||
"uuid": "^10.0.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.0",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@babel/register": "^7.27.1",
|
||||
"@types/body-parser": "^1.19.6",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.15",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"@types/webpack-node-externals": "^3.0.4",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"compression-webpack-plugin": "^11.1.0",
|
||||
"concurrently": "^9.2.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-webpack-plugin": "^5.0.2",
|
||||
"jest": "^30.0.4",
|
||||
"nodemon": "^3.1.10",
|
||||
"resolve-ts-aliases": "^1.0.1",
|
||||
"supertest": "^7.1.3",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.100.2",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
"webpack-shell-plugin-next": "^2.3.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
20
schema.gql
20
schema.gql
@ -1,20 +0,0 @@
|
||||
# -----------------------------------------------
|
||||
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
|
||||
# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!!
|
||||
# -----------------------------------------------
|
||||
|
||||
type Mutation {
|
||||
testMutation: TestMutation!
|
||||
}
|
||||
|
||||
type Query {
|
||||
test: Test!
|
||||
}
|
||||
|
||||
type Test {
|
||||
text: String!
|
||||
}
|
||||
|
||||
type TestMutation {
|
||||
text(text: String!): String!
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
# -----------------------------------------------
|
||||
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
|
||||
# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!!
|
||||
# -----------------------------------------------
|
||||
|
||||
type Mutation {
|
||||
testMutation: TestMutation!
|
||||
}
|
||||
|
||||
type Query {
|
||||
test: Test!
|
||||
}
|
||||
|
||||
type Test {
|
||||
text: String!
|
||||
}
|
||||
|
||||
type TestMutation {
|
||||
text(text: String!): String!
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './test.resolver';
|
@ -1,33 +0,0 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
'use strict';
|
||||
|
||||
import { Query, Resolver, Mutation, FieldResolver, Root, Arg } from 'type-graphql';
|
||||
import { Test, TestMutation } from '@GraphQL/schema';
|
||||
import { getTest, addText } from '@controllerGraphQL';
|
||||
|
||||
@Resolver(() => Test)
|
||||
export class TestResolverQuery {
|
||||
@Query(() => Test)
|
||||
async test() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@FieldResolver(() => String)
|
||||
async text() {
|
||||
return await getTest({});
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver(() => TestMutation)
|
||||
export class TestResolverMutation {
|
||||
@Mutation(() => TestMutation)
|
||||
async testMutation() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@FieldResolver(() => String)
|
||||
async text(@Arg('text') text?: string){
|
||||
return await addText({text});
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
import { buildSchemaSync } from "type-graphql"
|
||||
import {
|
||||
TestResolverQuery,
|
||||
TestResolverMutation
|
||||
} from "@GraphQL/resolvers";
|
||||
|
||||
export * from './test.schema'
|
||||
|
||||
const schema = buildSchemaSync({
|
||||
resolvers: [
|
||||
TestResolverQuery,
|
||||
TestResolverMutation,
|
||||
],
|
||||
emitSchemaFile: true,
|
||||
})
|
||||
export default schema
|
@ -1,16 +0,0 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
'use strict';
|
||||
|
||||
import { Field, ObjectType } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class Test {
|
||||
@Field(() => String)
|
||||
text?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class TestMutation {
|
||||
@Field(() => String)
|
||||
text?: string;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import express from 'express'; //express
|
||||
import { createHandler } from 'graphql-http/lib/use/express';
|
||||
import schema from '@src/GraphQL/schema';
|
||||
|
||||
|
||||
const server = express.Router();//Router de Express
|
||||
|
||||
server.use(
|
||||
'/',
|
||||
createHandler({
|
||||
schema,
|
||||
context(req) {
|
||||
const res = req.context.res
|
||||
return {req, res};
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// DO NOT DO app.listen() unless we're testing this directly
|
||||
if (require.main === module) {
|
||||
const app = express();
|
||||
app.listen((process.env.PORT || 4000), () => {
|
||||
console.log(`Iniciando Express en el puerto 4000`); /*${app.get('port')}*/
|
||||
});
|
||||
}
|
||||
|
||||
// Instead do export the app:
|
||||
export default server;
|
@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import { getTestModel, addTextModel } from '@models';
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const getTest = async ({}) => {
|
||||
return getTestModel();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const addText = async ({ text }: {text?: string}) => {
|
||||
return addTextModel({ text });
|
||||
};
|
437
src/generators/bpmn-generator.ts
Normal file
437
src/generators/bpmn-generator.ts
Normal file
@ -0,0 +1,437 @@
|
||||
import { DiagramData, DiagramElement, DiagramConnection, DiagramType, DiagramFormat } from '../types/diagram-types.js';
|
||||
import { generateId, getDefaultStyle } from '../utils/xml-parser.js';
|
||||
|
||||
// BPMN Element Types
|
||||
export enum BPMNElementType {
|
||||
START_EVENT = 'bpmn-start-event',
|
||||
END_EVENT = 'bpmn-end-event',
|
||||
TASK = 'bpmn-task',
|
||||
USER_TASK = 'bpmn-user-task',
|
||||
SERVICE_TASK = 'bpmn-service-task',
|
||||
GATEWAY = 'bpmn-gateway',
|
||||
EXCLUSIVE_GATEWAY = 'bpmn-exclusive-gateway',
|
||||
PARALLEL_GATEWAY = 'bpmn-parallel-gateway',
|
||||
SUBPROCESS = 'bpmn-subprocess',
|
||||
POOL = 'bpmn-pool',
|
||||
LANE = 'bpmn-lane'
|
||||
}
|
||||
|
||||
// BPMN Configuration
|
||||
export interface BPMNConfig {
|
||||
processName: string;
|
||||
description?: string;
|
||||
includePool?: boolean;
|
||||
lanes?: string[];
|
||||
}
|
||||
|
||||
// BPMN Element Configuration
|
||||
export interface BPMNElementConfig {
|
||||
id?: string;
|
||||
type: BPMNElementType;
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
// BPMN Connection Configuration
|
||||
export interface BPMNConnectionConfig {
|
||||
id?: string;
|
||||
source: string;
|
||||
target: string;
|
||||
label?: string;
|
||||
type?: 'sequence' | 'message' | 'association';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default BPMN configuration
|
||||
*/
|
||||
export const createBPMNConfig = (processName: string): BPMNConfig => ({
|
||||
processName,
|
||||
includePool: false,
|
||||
lanes: []
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate basic BPMN process diagram
|
||||
*/
|
||||
export const generateBasicBPMNProcess = (
|
||||
config: BPMNConfig,
|
||||
elements: BPMNElementConfig[],
|
||||
connections: BPMNConnectionConfig[]
|
||||
): DiagramData => {
|
||||
const diagramElements = elements.map(createBPMNElement);
|
||||
const diagramConnections = connections.map(createBPMNConnection);
|
||||
|
||||
// Add pool if requested
|
||||
if (config.includePool) {
|
||||
const poolElement = createPoolElement(config);
|
||||
diagramElements.unshift(poolElement);
|
||||
}
|
||||
|
||||
return {
|
||||
elements: diagramElements,
|
||||
connections: diagramConnections,
|
||||
metadata: {
|
||||
type: DiagramType.BPMN_PROCESS,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create BPMN element from configuration
|
||||
*/
|
||||
export const createBPMNElement = (config: BPMNElementConfig): DiagramElement => ({
|
||||
id: config.id || generateId(),
|
||||
type: config.type,
|
||||
label: config.label,
|
||||
geometry: {
|
||||
x: config.x,
|
||||
y: config.y,
|
||||
width: config.width || getDefaultWidth(config.type),
|
||||
height: config.height || getDefaultHeight(config.type)
|
||||
},
|
||||
style: getBPMNElementStyle(config.type),
|
||||
properties: config.properties || {}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create BPMN connection from configuration
|
||||
*/
|
||||
export const createBPMNConnection = (config: BPMNConnectionConfig): DiagramConnection => ({
|
||||
id: config.id || generateId(),
|
||||
source: config.source,
|
||||
target: config.target,
|
||||
label: config.label || '',
|
||||
style: getBPMNConnectionStyle(config.type || 'sequence'),
|
||||
properties: { type: config.type || 'sequence' }
|
||||
});
|
||||
|
||||
/**
|
||||
* Create pool element
|
||||
*/
|
||||
const createPoolElement = (config: BPMNConfig): DiagramElement => ({
|
||||
id: generateId(),
|
||||
type: BPMNElementType.POOL,
|
||||
label: config.processName,
|
||||
geometry: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 800,
|
||||
height: 400
|
||||
},
|
||||
style: getBPMNElementStyle(BPMNElementType.POOL),
|
||||
properties: { isPool: true }
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate simple linear BPMN process
|
||||
*/
|
||||
export const generateLinearBPMNProcess = (
|
||||
processName: string,
|
||||
tasks: string[]
|
||||
): DiagramData => {
|
||||
const elements: BPMNElementConfig[] = [];
|
||||
const connections: BPMNConnectionConfig[] = [];
|
||||
|
||||
let currentX = 100;
|
||||
const y = 200;
|
||||
const spacing = 150;
|
||||
|
||||
// Start event
|
||||
const startId = generateId();
|
||||
elements.push({
|
||||
id: startId,
|
||||
type: BPMNElementType.START_EVENT,
|
||||
label: 'Start',
|
||||
x: currentX,
|
||||
y: y
|
||||
});
|
||||
|
||||
let previousId = startId;
|
||||
currentX += spacing;
|
||||
|
||||
// Tasks
|
||||
for (const taskName of tasks) {
|
||||
const taskId = generateId();
|
||||
elements.push({
|
||||
id: taskId,
|
||||
type: BPMNElementType.TASK,
|
||||
label: taskName,
|
||||
x: currentX,
|
||||
y: y
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: previousId,
|
||||
target: taskId
|
||||
});
|
||||
|
||||
previousId = taskId;
|
||||
currentX += spacing;
|
||||
}
|
||||
|
||||
// End event
|
||||
const endId = generateId();
|
||||
elements.push({
|
||||
id: endId,
|
||||
type: BPMNElementType.END_EVENT,
|
||||
label: 'End',
|
||||
x: currentX,
|
||||
y: y
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: previousId,
|
||||
target: endId
|
||||
});
|
||||
|
||||
const config = createBPMNConfig(processName);
|
||||
return generateBasicBPMNProcess(config, elements, connections);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate BPMN process with gateway
|
||||
*/
|
||||
export const generateBPMNProcessWithGateway = (
|
||||
processName: string,
|
||||
beforeGateway: string[],
|
||||
gatewayType: 'exclusive' | 'parallel',
|
||||
branches: string[][],
|
||||
afterGateway: string[]
|
||||
): DiagramData => {
|
||||
const elements: BPMNElementConfig[] = [];
|
||||
const connections: BPMNConnectionConfig[] = [];
|
||||
|
||||
let currentX = 100;
|
||||
const baseY = 200;
|
||||
const spacing = 150;
|
||||
const branchSpacing = 100;
|
||||
|
||||
// Start event
|
||||
const startId = generateId();
|
||||
elements.push({
|
||||
id: startId,
|
||||
type: BPMNElementType.START_EVENT,
|
||||
label: 'Start',
|
||||
x: currentX,
|
||||
y: baseY
|
||||
});
|
||||
|
||||
let previousId = startId;
|
||||
currentX += spacing;
|
||||
|
||||
// Tasks before gateway
|
||||
for (const taskName of beforeGateway) {
|
||||
const taskId = generateId();
|
||||
elements.push({
|
||||
id: taskId,
|
||||
type: BPMNElementType.TASK,
|
||||
label: taskName,
|
||||
x: currentX,
|
||||
y: baseY
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: previousId,
|
||||
target: taskId
|
||||
});
|
||||
|
||||
previousId = taskId;
|
||||
currentX += spacing;
|
||||
}
|
||||
|
||||
// Split gateway
|
||||
const splitGatewayId = generateId();
|
||||
const gatewayElementType = gatewayType === 'exclusive'
|
||||
? BPMNElementType.EXCLUSIVE_GATEWAY
|
||||
: BPMNElementType.PARALLEL_GATEWAY;
|
||||
|
||||
elements.push({
|
||||
id: splitGatewayId,
|
||||
type: gatewayElementType,
|
||||
label: '',
|
||||
x: currentX,
|
||||
y: baseY
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: previousId,
|
||||
target: splitGatewayId
|
||||
});
|
||||
|
||||
currentX += spacing;
|
||||
|
||||
// Branches
|
||||
const branchEndIds: string[] = [];
|
||||
branches.forEach((branch, branchIndex) => {
|
||||
const branchY = baseY + (branchIndex - (branches.length - 1) / 2) * branchSpacing;
|
||||
let branchX = currentX;
|
||||
let branchPreviousId = splitGatewayId;
|
||||
|
||||
for (const taskName of branch) {
|
||||
const taskId = generateId();
|
||||
elements.push({
|
||||
id: taskId,
|
||||
type: BPMNElementType.TASK,
|
||||
label: taskName,
|
||||
x: branchX,
|
||||
y: branchY
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: branchPreviousId,
|
||||
target: taskId
|
||||
});
|
||||
|
||||
branchPreviousId = taskId;
|
||||
branchX += spacing;
|
||||
}
|
||||
|
||||
branchEndIds.push(branchPreviousId);
|
||||
});
|
||||
|
||||
// Find the maximum X position
|
||||
const maxBranchX = currentX + (Math.max(...branches.map(b => b.length)) * spacing);
|
||||
|
||||
// Join gateway
|
||||
const joinGatewayId = generateId();
|
||||
elements.push({
|
||||
id: joinGatewayId,
|
||||
type: gatewayElementType,
|
||||
label: '',
|
||||
x: maxBranchX,
|
||||
y: baseY
|
||||
});
|
||||
|
||||
// Connect branches to join gateway
|
||||
branchEndIds.forEach(endId => {
|
||||
connections.push({
|
||||
source: endId,
|
||||
target: joinGatewayId
|
||||
});
|
||||
});
|
||||
|
||||
currentX = maxBranchX + spacing;
|
||||
previousId = joinGatewayId;
|
||||
|
||||
// Tasks after gateway
|
||||
for (const taskName of afterGateway) {
|
||||
const taskId = generateId();
|
||||
elements.push({
|
||||
id: taskId,
|
||||
type: BPMNElementType.TASK,
|
||||
label: taskName,
|
||||
x: currentX,
|
||||
y: baseY
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: previousId,
|
||||
target: taskId
|
||||
});
|
||||
|
||||
previousId = taskId;
|
||||
currentX += spacing;
|
||||
}
|
||||
|
||||
// End event
|
||||
const endId = generateId();
|
||||
elements.push({
|
||||
id: endId,
|
||||
type: BPMNElementType.END_EVENT,
|
||||
label: 'End',
|
||||
x: currentX,
|
||||
y: baseY
|
||||
});
|
||||
|
||||
connections.push({
|
||||
source: previousId,
|
||||
target: endId
|
||||
});
|
||||
|
||||
const config = createBPMNConfig(processName);
|
||||
return generateBasicBPMNProcess(config, elements, connections);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get default width for BPMN element type
|
||||
*/
|
||||
const getDefaultWidth = (type: BPMNElementType): number => {
|
||||
const widthMap: Record<BPMNElementType, number> = {
|
||||
[BPMNElementType.START_EVENT]: 36,
|
||||
[BPMNElementType.END_EVENT]: 36,
|
||||
[BPMNElementType.TASK]: 100,
|
||||
[BPMNElementType.USER_TASK]: 100,
|
||||
[BPMNElementType.SERVICE_TASK]: 100,
|
||||
[BPMNElementType.GATEWAY]: 50,
|
||||
[BPMNElementType.EXCLUSIVE_GATEWAY]: 50,
|
||||
[BPMNElementType.PARALLEL_GATEWAY]: 50,
|
||||
[BPMNElementType.SUBPROCESS]: 120,
|
||||
[BPMNElementType.POOL]: 800,
|
||||
[BPMNElementType.LANE]: 800
|
||||
};
|
||||
|
||||
return widthMap[type] || 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get default height for BPMN element type
|
||||
*/
|
||||
const getDefaultHeight = (type: BPMNElementType): number => {
|
||||
const heightMap: Record<BPMNElementType, number> = {
|
||||
[BPMNElementType.START_EVENT]: 36,
|
||||
[BPMNElementType.END_EVENT]: 36,
|
||||
[BPMNElementType.TASK]: 80,
|
||||
[BPMNElementType.USER_TASK]: 80,
|
||||
[BPMNElementType.SERVICE_TASK]: 80,
|
||||
[BPMNElementType.GATEWAY]: 50,
|
||||
[BPMNElementType.EXCLUSIVE_GATEWAY]: 50,
|
||||
[BPMNElementType.PARALLEL_GATEWAY]: 50,
|
||||
[BPMNElementType.SUBPROCESS]: 100,
|
||||
[BPMNElementType.POOL]: 400,
|
||||
[BPMNElementType.LANE]: 200
|
||||
};
|
||||
|
||||
return heightMap[type] || 80;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get BPMN element style
|
||||
*/
|
||||
const getBPMNElementStyle = (type: BPMNElementType): string => {
|
||||
const styleMap: Record<BPMNElementType, string> = {
|
||||
[BPMNElementType.START_EVENT]: 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
[BPMNElementType.END_EVENT]: 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=3;',
|
||||
[BPMNElementType.TASK]: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
[BPMNElementType.USER_TASK]: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;',
|
||||
[BPMNElementType.SERVICE_TASK]: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
[BPMNElementType.GATEWAY]: 'rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
[BPMNElementType.EXCLUSIVE_GATEWAY]: 'rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
[BPMNElementType.PARALLEL_GATEWAY]: 'rhombus;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
[BPMNElementType.SUBPROCESS]: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;dashed=1;',
|
||||
[BPMNElementType.POOL]: 'swimlane;html=1;childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=1;startSize=20;horizontalStack=0;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;',
|
||||
[BPMNElementType.LANE]: 'swimlane;html=1;childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=1;startSize=20;horizontalStack=0;fillColor=#ffffff;strokeColor=#000000;'
|
||||
};
|
||||
|
||||
return styleMap[type] || getDefaultStyle(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get BPMN connection style
|
||||
*/
|
||||
const getBPMNConnectionStyle = (type: string): string => {
|
||||
const styleMap: Record<string, string> = {
|
||||
'sequence': 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;',
|
||||
'message': 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=classic;',
|
||||
'association': 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=none;'
|
||||
};
|
||||
|
||||
return styleMap[type] || styleMap['sequence'];
|
||||
};
|
558
src/index.ts
558
src/index.ts
@ -1,91 +1,499 @@
|
||||
'use strict';
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
import 'reflect-metadata';
|
||||
import ws from 'ws'; // yarn add ws
|
||||
import express from 'express'; //express
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { useServer } from 'graphql-ws/use/ws';
|
||||
import { execute, subscribe } from 'graphql';
|
||||
import GraphQLserver from '@GraphQL/server';// Server of GraphQL,
|
||||
import expressPlayground from 'graphql-playground-middleware-express';
|
||||
import schema from '@GraphQL/schema';
|
||||
import { config } from '@config';
|
||||
import apiRouter from '@routes';
|
||||
// Import tools and utilities
|
||||
import { createDiagram, validateCreateDiagramInput, getSupportedDiagramTypes, getDiagramTypeDescription } from './tools/create-diagram.js';
|
||||
import { createFileConfig, findDiagramFiles, getDiagramFilesWithMetadata } from './utils/file-manager.js';
|
||||
import { createVSCodeConfig, openDiagramInVSCode, setupVSCodeEnvironment } from './utils/vscode-integration.js';
|
||||
import { DiagramType, DiagramFormat } from './types/diagram-types.js';
|
||||
|
||||
const app = express(), //creating app
|
||||
whitelist = config.WHITELIST_URLS,
|
||||
corsOptions = {
|
||||
origin: function (origin: string | undefined, callback: (arg0: Error | null, arg1?: boolean) => void) {
|
||||
if (whitelist.indexOf(origin as string) !== -1 || !origin) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
// Functional types and configurations
|
||||
type ServerConfig = Readonly<{
|
||||
name: string;
|
||||
version: string;
|
||||
workspaceRoot: string;
|
||||
capabilities: Readonly<{
|
||||
resources: Record<string, unknown>;
|
||||
tools: Record<string, unknown>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
type ToolHandler = (args: any) => Promise<any>;
|
||||
type ResourceHandler = (uri: string) => Promise<any>;
|
||||
|
||||
type ToolHandlers = Readonly<{
|
||||
create_diagram: ToolHandler;
|
||||
list_diagrams: ToolHandler;
|
||||
open_diagram_in_vscode: ToolHandler;
|
||||
setup_vscode_environment: ToolHandler;
|
||||
get_diagram_types: ToolHandler;
|
||||
}>;
|
||||
|
||||
type ResourceHandlers = Readonly<{
|
||||
'diagrams://workspace/list': ResourceHandler;
|
||||
'diagrams://types/supported': ResourceHandler;
|
||||
}>;
|
||||
|
||||
// Pure function to create server configuration
|
||||
const createServerConfig = (workspaceRoot?: string): ServerConfig => ({
|
||||
name: 'drawio-mcp-server',
|
||||
version: '0.1.0',
|
||||
workspaceRoot: workspaceRoot || process.env.WORKSPACE_ROOT || process.cwd(),
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
// Pure function to create tool definitions
|
||||
const createToolDefinitions = () => [
|
||||
{
|
||||
name: 'create_diagram',
|
||||
description: 'Create a new diagram of specified type (BPMN, UML, ER, Network, Architecture, etc.)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the diagram file (without extension)'
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: Object.values(DiagramType),
|
||||
description: 'Type of diagram to create'
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: Object.values(DiagramFormat),
|
||||
default: 'drawio',
|
||||
description: 'File format for the diagram'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of the diagram'
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description: 'Output directory path (relative to workspace)'
|
||||
},
|
||||
// BPMN specific parameters
|
||||
processName: {
|
||||
type: 'string',
|
||||
description: 'Name of the BPMN process'
|
||||
},
|
||||
tasks: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of tasks for BPMN process'
|
||||
},
|
||||
gatewayType: {
|
||||
type: 'string',
|
||||
enum: ['exclusive', 'parallel'],
|
||||
description: 'Type of gateway for BPMN process'
|
||||
},
|
||||
branches: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
},
|
||||
description: 'Branches for BPMN gateway'
|
||||
},
|
||||
beforeGateway: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Tasks before gateway'
|
||||
},
|
||||
afterGateway: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Tasks after gateway'
|
||||
},
|
||||
// UML specific parameters
|
||||
classes: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of classes for UML class diagram'
|
||||
},
|
||||
// ER specific parameters
|
||||
entities: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of entities for ER diagram'
|
||||
},
|
||||
// Network/Architecture specific parameters
|
||||
components: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of components for network or architecture diagram'
|
||||
},
|
||||
// Flowchart specific parameters
|
||||
processes: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of processes for flowchart'
|
||||
}
|
||||
},
|
||||
credentials: true
|
||||
required: ['name', 'type']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'list_diagrams',
|
||||
description: 'List all diagram files in the workspace',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspaceRoot: {
|
||||
type: 'string',
|
||||
description: 'Workspace root directory (optional)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open_diagram_in_vscode',
|
||||
description: 'Open a diagram file in VSCode with draw.io extension',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description: 'Path to the diagram file'
|
||||
},
|
||||
workspaceRoot: {
|
||||
type: 'string',
|
||||
description: 'Workspace root directory (optional)'
|
||||
}
|
||||
},
|
||||
required: ['filePath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'setup_vscode_environment',
|
||||
description: 'Setup VSCode environment for draw.io (install extension if needed)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspaceRoot: {
|
||||
type: 'string',
|
||||
description: 'Workspace root directory (optional)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_diagram_types',
|
||||
description: 'Get list of supported diagram types with descriptions',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Pure function to create resource definitions
|
||||
const createResourceDefinitions = () => [
|
||||
{
|
||||
uri: 'diagrams://workspace/list',
|
||||
name: 'Workspace Diagrams',
|
||||
mimeType: 'application/json',
|
||||
description: 'List of all diagram files in the workspace'
|
||||
},
|
||||
{
|
||||
uri: 'diagrams://types/supported',
|
||||
name: 'Supported Diagram Types',
|
||||
mimeType: 'application/json',
|
||||
description: 'List of supported diagram types and their descriptions'
|
||||
}
|
||||
];
|
||||
|
||||
// Pure function to create tool handlers
|
||||
const createToolHandlers = (config: ServerConfig): ToolHandlers => ({
|
||||
create_diagram: async (args: any) => {
|
||||
if (!validateCreateDiagramInput(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid create_diagram arguments'
|
||||
);
|
||||
}
|
||||
|
||||
const result = await createDiagram({
|
||||
...args,
|
||||
workspaceRoot: args.workspaceRoot || config.workspaceRoot
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
//Inicialization of services of express
|
||||
app
|
||||
.use(cookieParser())
|
||||
.use(express.urlencoded({limit: '500mb', extended: true}))
|
||||
.use(express.json({limit: '500mb'}))
|
||||
.use(cors(corsOptions))
|
||||
.use(apiRouter)//Routes de App
|
||||
.use('/graphql', GraphQLserver);//Server of Graphql
|
||||
|
||||
if(config.PLAYGROUND_GRAPHQL === true){
|
||||
app.get('/playground', expressPlayground({
|
||||
endpoint: '/graphql',
|
||||
subscriptionEndpoint: '/graphql',
|
||||
settings: {
|
||||
'request.credentials': 'include', //Include Credentials for playground
|
||||
},
|
||||
|
||||
list_diagrams: async (args: any) => {
|
||||
const workspaceRoot = args?.workspaceRoot || config.workspaceRoot;
|
||||
const fileConfig = createFileConfig(workspaceRoot);
|
||||
const diagrams = await getDiagramFilesWithMetadata(fileConfig)();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: diagrams.length,
|
||||
diagrams: diagrams.map(d => ({
|
||||
path: d.relativePath,
|
||||
format: d.format,
|
||||
size: d.stats.size,
|
||||
modified: d.stats.mtime
|
||||
}))
|
||||
}, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
open_diagram_in_vscode: async (args: any) => {
|
||||
if (!args?.filePath) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'filePath is required'
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceRoot = args.workspaceRoot || config.workspaceRoot;
|
||||
const vscodeConfig = createVSCodeConfig(workspaceRoot);
|
||||
|
||||
try {
|
||||
await openDiagramInVSCode(vscodeConfig)(args.filePath);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `Successfully opened diagram: ${args.filePath}`
|
||||
}, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
message: `Failed to open diagram: ${error}`
|
||||
}, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
setup_vscode_environment: async (args: any) => {
|
||||
const workspaceRoot = args?.workspaceRoot || config.workspaceRoot;
|
||||
const vscodeConfig = createVSCodeConfig(workspaceRoot);
|
||||
|
||||
const result = await setupVSCodeEnvironment(vscodeConfig)();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
get_diagram_types: async () => {
|
||||
const types = getSupportedDiagramTypes();
|
||||
const typesWithDescriptions = types.map(type => ({
|
||||
type,
|
||||
description: getDiagramTypeDescription(type)
|
||||
}));
|
||||
}
|
||||
|
||||
// DO NOT DO app.listen() unless we're testing this directly
|
||||
if (require.main === module) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
supportedTypes: typesWithDescriptions
|
||||
}, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const server = app.listen(config.PORT, () => {
|
||||
// create and use the websocket server
|
||||
const wsServer = new ws.Server({
|
||||
server,
|
||||
path: '/graphql',
|
||||
// Pure function to create resource handlers
|
||||
const createResourceHandlers = (config: ServerConfig): ResourceHandlers => ({
|
||||
'diagrams://workspace/list': async () => {
|
||||
const fileConfig = createFileConfig(config.workspaceRoot);
|
||||
const diagrams = await getDiagramFilesWithMetadata(fileConfig)();
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: 'diagrams://workspace/list',
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(diagrams, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
'diagrams://types/supported': async () => {
|
||||
const types = getSupportedDiagramTypes();
|
||||
const typesWithDescriptions = types.map(type => ({
|
||||
type,
|
||||
description: getDiagramTypeDescription(type)
|
||||
}));
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: 'diagrams://types/supported',
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(typesWithDescriptions, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to setup tool request handlers
|
||||
const setupToolRequestHandlers = (server: Server, toolHandlers: ToolHandlers) => {
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: createToolDefinitions()
|
||||
}));
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const handler = toolHandlers[request.params.name as keyof ToolHandlers];
|
||||
if (!handler) {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
`Unknown tool: ${request.params.name}`
|
||||
);
|
||||
}
|
||||
|
||||
return await handler(request.params.arguments);
|
||||
} catch (error) {
|
||||
console.error(`Error in tool ${request.params.name}:`, error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useServer({
|
||||
schema,
|
||||
execute,
|
||||
subscribe,
|
||||
// eslint-disable-next-line
|
||||
onConnect: (ctx) => {
|
||||
//console.log('Connect');
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
onSubscribe: (ctx, msg) => {
|
||||
//console.log('Subscribe');
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
onNext: (ctx, msg, args, result) => {
|
||||
//console.debug('Next');
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
onError: (ctx, msg, errors) => {
|
||||
//console.error('Error');
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
onComplete: (ctx, msg) => {
|
||||
//console.log('Complete');
|
||||
},
|
||||
}, wsServer);
|
||||
return server;
|
||||
};
|
||||
|
||||
console.log(`Starting Express on port ${config.PORT} and iniciating server of web sockets`);
|
||||
// Pure function to setup resource request handlers
|
||||
const setupResourceRequestHandlers = (server: Server, resourceHandlers: ResourceHandlers) => {
|
||||
// List available resources
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: createResourceDefinitions()
|
||||
}));
|
||||
|
||||
// Handle resource requests
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
|
||||
try {
|
||||
const handler = resourceHandlers[uri as keyof ResourceHandlers];
|
||||
if (!handler) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
`Unknown resource URI: ${uri}`
|
||||
);
|
||||
}
|
||||
|
||||
return await handler(uri);
|
||||
} catch (error) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to read resource: ${error}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
return server;
|
||||
};
|
||||
|
||||
// Instead do export the app:
|
||||
export default app;
|
||||
// Pure function to setup error handling
|
||||
const setupErrorHandling = (server: Server) => {
|
||||
server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
// Pure function to create and configure server
|
||||
const createMCPServer = (config: ServerConfig): Server => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: config.name,
|
||||
version: config.version,
|
||||
},
|
||||
{
|
||||
capabilities: config.capabilities,
|
||||
}
|
||||
);
|
||||
|
||||
const toolHandlers = createToolHandlers(config);
|
||||
const resourceHandlers = createResourceHandlers(config);
|
||||
|
||||
// Compose server setup using function composition
|
||||
const serverWithTools = setupToolRequestHandlers(server, toolHandlers);
|
||||
const serverWithResources = setupResourceRequestHandlers(serverWithTools, resourceHandlers);
|
||||
const serverWithErrorHandling = setupErrorHandling(serverWithResources);
|
||||
|
||||
return serverWithErrorHandling;
|
||||
};
|
||||
|
||||
// Pure function to run the server
|
||||
const runServer = async (server: Server): Promise<void> => {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('Draw.io MCP server running on stdio');
|
||||
};
|
||||
|
||||
// Main execution - functional composition
|
||||
const main = async (): Promise<void> => {
|
||||
const config = createServerConfig();
|
||||
const server = createMCPServer(config);
|
||||
await runServer(server);
|
||||
};
|
||||
|
||||
// Start the server
|
||||
main().catch(console.error);
|
||||
|
@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
export const getTestModel = async () => {
|
||||
return 'This is the text response for Test Query from a model';
|
||||
};
|
||||
|
||||
export const addTextModel = async ({ text }: {text?: string}) => {
|
||||
return `Simulate to insert some text: ${text} from a model`;
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// use this to set API REST
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';//bodyParser conversionde Api REST,
|
||||
|
||||
const apiRouter = express.Router();//Router de Express
|
||||
|
||||
apiRouter
|
||||
.use(bodyParser.json())
|
||||
.use(bodyParser.urlencoded({extended: false}));
|
||||
|
||||
export default apiRouter;
|
453
src/tests/generators/bpmn-generator.test.ts
Normal file
453
src/tests/generators/bpmn-generator.test.ts
Normal file
@ -0,0 +1,453 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
|
||||
describe('BPMN Generator Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('generateLinearBPMNProcess', () => {
|
||||
it('should generate a linear BPMN process with start, tasks, and end events', () => {
|
||||
const processName = 'Test Process';
|
||||
const tasks = ['Task 1', 'Task 2', 'Task 3'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'bpmn-start-event',
|
||||
label: 'Start',
|
||||
geometry: { x: 100, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
properties: { isStartEvent: true }
|
||||
},
|
||||
{
|
||||
id: 'task-1',
|
||||
type: 'bpmn-task',
|
||||
label: 'Task 1',
|
||||
geometry: { x: 200, y: 100, width: 120, height: 80 },
|
||||
style: 'rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isTask: true }
|
||||
},
|
||||
{
|
||||
id: 'end-1',
|
||||
type: 'bpmn-end-event',
|
||||
label: 'End',
|
||||
geometry: { x: 400, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
properties: { isEndEvent: true }
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
{
|
||||
id: 'flow-1',
|
||||
source: 'start-1',
|
||||
target: 'task-1',
|
||||
label: '',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;',
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 'flow-2',
|
||||
source: 'task-1',
|
||||
target: 'end-1',
|
||||
label: '',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;',
|
||||
properties: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(mockResult).toHaveProperty('elements');
|
||||
expect(mockResult).toHaveProperty('connections');
|
||||
expect(Array.isArray(mockResult.elements)).toBe(true);
|
||||
expect(Array.isArray(mockResult.connections)).toBe(true);
|
||||
});
|
||||
|
||||
it('should include start event in generated process', () => {
|
||||
const processName = 'Test Process';
|
||||
const tasks = ['Task 1'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'bpmn-start-event',
|
||||
label: 'Start',
|
||||
geometry: { x: 100, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
properties: { isStartEvent: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const startEvents = mockResult.elements.filter((el: any) => el.type === 'bpmn-start-event');
|
||||
expect(startEvents.length).toBe(1);
|
||||
expect(startEvents[0].properties.isStartEvent).toBe(true);
|
||||
});
|
||||
|
||||
it('should include end event in generated process', () => {
|
||||
const processName = 'Test Process';
|
||||
const tasks = ['Task 1'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'end-1',
|
||||
type: 'bpmn-end-event',
|
||||
label: 'End',
|
||||
geometry: { x: 400, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
properties: { isEndEvent: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const endEvents = mockResult.elements.filter((el: any) => el.type === 'bpmn-end-event');
|
||||
expect(endEvents.length).toBe(1);
|
||||
expect(endEvents[0].properties.isEndEvent).toBe(true);
|
||||
});
|
||||
|
||||
it('should include all specified tasks', () => {
|
||||
const processName = 'Test Process';
|
||||
const tasks = ['Task 1', 'Task 2', 'Task 3'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'task-1',
|
||||
type: 'bpmn-task',
|
||||
label: 'Task 1',
|
||||
geometry: { x: 200, y: 100, width: 120, height: 80 },
|
||||
style: 'rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isTask: true }
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
type: 'bpmn-task',
|
||||
label: 'Task 2',
|
||||
geometry: { x: 350, y: 100, width: 120, height: 80 },
|
||||
style: 'rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isTask: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const taskElements = mockResult.elements.filter((el: any) => el.type === 'bpmn-task');
|
||||
expect(taskElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should create connections between elements', () => {
|
||||
const processName = 'Test Process';
|
||||
const tasks = ['Task 1'];
|
||||
|
||||
const mockResult = {
|
||||
connections: [
|
||||
{
|
||||
id: 'flow-1',
|
||||
source: 'start-1',
|
||||
target: 'task-1',
|
||||
label: '',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;',
|
||||
properties: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(mockResult.connections.length).toBeGreaterThan(0);
|
||||
expect(mockResult.connections[0]).toHaveProperty('source');
|
||||
expect(mockResult.connections[0]).toHaveProperty('target');
|
||||
});
|
||||
|
||||
it('should handle empty task list', () => {
|
||||
const processName = 'Empty Process';
|
||||
const tasks: string[] = [];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'bpmn-start-event',
|
||||
label: 'Start',
|
||||
geometry: { x: 100, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
properties: { isStartEvent: true }
|
||||
},
|
||||
{
|
||||
id: 'end-1',
|
||||
type: 'bpmn-end-event',
|
||||
label: 'End',
|
||||
geometry: { x: 200, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
properties: { isEndEvent: true }
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
{
|
||||
id: 'flow-1',
|
||||
source: 'start-1',
|
||||
target: 'end-1',
|
||||
label: '',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;',
|
||||
properties: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(mockResult.elements.length).toBe(2); // Only start and end
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateBPMNProcessWithGateway', () => {
|
||||
it('should generate BPMN process with exclusive gateway', () => {
|
||||
const processName = 'Gateway Process';
|
||||
const beforeGateway = ['Initial Task'];
|
||||
const gatewayType = 'exclusive';
|
||||
const branches = [['Branch 1 Task'], ['Branch 2 Task']];
|
||||
const afterGateway = ['Final Task'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'gateway-1',
|
||||
type: 'bpmn-gateway',
|
||||
label: 'Gateway',
|
||||
geometry: { x: 250, y: 100, width: 80, height: 80 },
|
||||
style: 'rhombus;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
properties: { isGateway: true, gatewayType: 'exclusive' }
|
||||
}
|
||||
],
|
||||
connections: []
|
||||
};
|
||||
|
||||
expect(mockResult).toHaveProperty('elements');
|
||||
expect(mockResult).toHaveProperty('connections');
|
||||
});
|
||||
|
||||
it('should include gateway element', () => {
|
||||
const processName = 'Gateway Process';
|
||||
const beforeGateway = ['Initial Task'];
|
||||
const gatewayType = 'exclusive';
|
||||
const branches = [['Branch 1 Task'], ['Branch 2 Task']];
|
||||
const afterGateway = ['Final Task'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'gateway-1',
|
||||
type: 'bpmn-gateway',
|
||||
label: 'Gateway',
|
||||
geometry: { x: 250, y: 100, width: 80, height: 80 },
|
||||
style: 'rhombus;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
properties: { isGateway: true, gatewayType: 'exclusive' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const gateways = mockResult.elements.filter((el: any) => el.type === 'bpmn-gateway');
|
||||
expect(gateways.length).toBe(1);
|
||||
expect(gateways[0].properties.isGateway).toBe(true);
|
||||
expect(gateways[0].properties.gatewayType).toBe('exclusive');
|
||||
});
|
||||
|
||||
it('should generate parallel gateway', () => {
|
||||
const processName = 'Parallel Gateway Process';
|
||||
const beforeGateway = ['Initial Task'];
|
||||
const gatewayType = 'parallel';
|
||||
const branches = [['Branch 1 Task'], ['Branch 2 Task']];
|
||||
const afterGateway = ['Final Task'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'gateway-1',
|
||||
type: 'bpmn-gateway',
|
||||
label: 'Parallel Gateway',
|
||||
geometry: { x: 250, y: 100, width: 80, height: 80 },
|
||||
style: 'rhombus;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
properties: { isGateway: true, gatewayType: 'parallel' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const gateways = mockResult.elements.filter((el: any) => el.type === 'bpmn-gateway');
|
||||
expect(gateways[0].properties.gatewayType).toBe('parallel');
|
||||
});
|
||||
|
||||
it('should handle multiple branches', () => {
|
||||
const processName = 'Multi-branch Process';
|
||||
const beforeGateway = ['Initial Task'];
|
||||
const gatewayType = 'exclusive';
|
||||
const branches = [
|
||||
['Branch 1 Task A', 'Branch 1 Task B'],
|
||||
['Branch 2 Task A'],
|
||||
['Branch 3 Task A', 'Branch 3 Task B', 'Branch 3 Task C']
|
||||
];
|
||||
const afterGateway = ['Final Task'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [],
|
||||
connections: []
|
||||
};
|
||||
|
||||
expect(mockResult).toHaveProperty('elements');
|
||||
expect(mockResult).toHaveProperty('connections');
|
||||
});
|
||||
|
||||
it('should handle empty before gateway tasks', () => {
|
||||
const processName = 'No Before Tasks';
|
||||
const beforeGateway: string[] = [];
|
||||
const gatewayType = 'exclusive';
|
||||
const branches = [['Branch 1'], ['Branch 2']];
|
||||
const afterGateway = ['Final Task'];
|
||||
|
||||
const mockResult = {
|
||||
elements: [],
|
||||
connections: []
|
||||
};
|
||||
|
||||
expect(mockResult).toHaveProperty('elements');
|
||||
expect(mockResult).toHaveProperty('connections');
|
||||
});
|
||||
|
||||
it('should handle empty after gateway tasks', () => {
|
||||
const processName = 'No After Tasks';
|
||||
const beforeGateway = ['Initial Task'];
|
||||
const gatewayType = 'exclusive';
|
||||
const branches = [['Branch 1'], ['Branch 2']];
|
||||
const afterGateway: string[] = [];
|
||||
|
||||
const mockResult = {
|
||||
elements: [],
|
||||
connections: []
|
||||
};
|
||||
|
||||
expect(mockResult).toHaveProperty('elements');
|
||||
expect(mockResult).toHaveProperty('connections');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BPMN Element Types', () => {
|
||||
it('should validate BPMN start event properties', () => {
|
||||
const startEvent = {
|
||||
id: 'start-1',
|
||||
type: 'bpmn-start-event',
|
||||
label: 'Start',
|
||||
geometry: { x: 100, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
properties: { isStartEvent: true }
|
||||
};
|
||||
|
||||
expect(startEvent.type).toBe('bpmn-start-event');
|
||||
expect(startEvent.properties.isStartEvent).toBe(true);
|
||||
expect(startEvent.style).toContain('ellipse');
|
||||
});
|
||||
|
||||
it('should validate BPMN task properties', () => {
|
||||
const task = {
|
||||
id: 'task-1',
|
||||
type: 'bpmn-task',
|
||||
label: 'Process Task',
|
||||
geometry: { x: 200, y: 100, width: 120, height: 80 },
|
||||
style: 'rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isTask: true }
|
||||
};
|
||||
|
||||
expect(task.type).toBe('bpmn-task');
|
||||
expect(task.properties.isTask).toBe(true);
|
||||
expect(task.style).toContain('rounded=1');
|
||||
});
|
||||
|
||||
it('should validate BPMN end event properties', () => {
|
||||
const endEvent = {
|
||||
id: 'end-1',
|
||||
type: 'bpmn-end-event',
|
||||
label: 'End',
|
||||
geometry: { x: 400, y: 100, width: 50, height: 50 },
|
||||
style: 'ellipse;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
properties: { isEndEvent: true }
|
||||
};
|
||||
|
||||
expect(endEvent.type).toBe('bpmn-end-event');
|
||||
expect(endEvent.properties.isEndEvent).toBe(true);
|
||||
expect(endEvent.style).toContain('ellipse');
|
||||
});
|
||||
|
||||
it('should validate BPMN gateway properties', () => {
|
||||
const gateway = {
|
||||
id: 'gateway-1',
|
||||
type: 'bpmn-gateway',
|
||||
label: 'Decision Gateway',
|
||||
geometry: { x: 250, y: 100, width: 80, height: 80 },
|
||||
style: 'rhombus;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
properties: { isGateway: true, gatewayType: 'exclusive' }
|
||||
};
|
||||
|
||||
expect(gateway.type).toBe('bpmn-gateway');
|
||||
expect(gateway.properties.isGateway).toBe(true);
|
||||
expect(gateway.properties.gatewayType).toBe('exclusive');
|
||||
expect(gateway.style).toContain('rhombus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BPMN Connection Types', () => {
|
||||
it('should validate sequence flow properties', () => {
|
||||
const sequenceFlow = {
|
||||
id: 'flow-1',
|
||||
source: 'start-1',
|
||||
target: 'task-1',
|
||||
label: '',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
expect(sequenceFlow).toHaveProperty('source');
|
||||
expect(sequenceFlow).toHaveProperty('target');
|
||||
expect(sequenceFlow.style).toContain('edgeStyle=orthogonalEdgeStyle');
|
||||
});
|
||||
|
||||
it('should validate conditional flow properties', () => {
|
||||
const conditionalFlow = {
|
||||
id: 'flow-conditional',
|
||||
source: 'gateway-1',
|
||||
target: 'task-1',
|
||||
label: 'Yes',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;',
|
||||
properties: { isConditional: true }
|
||||
};
|
||||
|
||||
expect(conditionalFlow.properties.isConditional).toBe(true);
|
||||
expect(conditionalFlow.label).toBe('Yes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layout and Positioning', () => {
|
||||
it('should position elements with proper spacing', () => {
|
||||
const elements = [
|
||||
{ id: 'start-1', geometry: { x: 100, y: 100, width: 50, height: 50 } },
|
||||
{ id: 'task-1', geometry: { x: 200, y: 100, width: 120, height: 80 } },
|
||||
{ id: 'end-1', geometry: { x: 400, y: 100, width: 50, height: 50 } }
|
||||
];
|
||||
|
||||
// Check horizontal spacing
|
||||
const spacing1 = elements[1].geometry.x - (elements[0].geometry.x + elements[0].geometry.width);
|
||||
const spacing2 = elements[2].geometry.x - (elements[1].geometry.x + elements[1].geometry.width);
|
||||
|
||||
expect(spacing1).toBeGreaterThan(0);
|
||||
expect(spacing2).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should align elements vertically', () => {
|
||||
const elements = [
|
||||
{ id: 'start-1', geometry: { x: 100, y: 100, width: 50, height: 50 } },
|
||||
{ id: 'task-1', geometry: { x: 200, y: 100, width: 120, height: 80 } },
|
||||
{ id: 'end-1', geometry: { x: 400, y: 100, width: 50, height: 50 } }
|
||||
];
|
||||
|
||||
// All elements should have the same y coordinate for horizontal alignment
|
||||
const yCoordinates = elements.map(el => el.geometry.y);
|
||||
const uniqueY = [...new Set(yCoordinates)];
|
||||
|
||||
expect(uniqueY.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
320
src/tests/integration/end-to-end.test.ts
Normal file
320
src/tests/integration/end-to-end.test.ts
Normal file
@ -0,0 +1,320 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
|
||||
describe('End-to-End Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Complete Diagram Creation Workflow', () => {
|
||||
it('should create a diagram from start to finish', async () => {
|
||||
const input = {
|
||||
name: 'integration-test',
|
||||
type: 'flowchart' as const,
|
||||
format: 'drawio' as const,
|
||||
description: 'Integration test diagram',
|
||||
processes: ['Start', 'Process 1', 'Decision', 'Process 2', 'End']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/integration-test.drawio',
|
||||
message: 'Successfully created flowchart diagram: integration-test',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.filePath).toBe('/test/workspace/integration-test.drawio');
|
||||
expect(mockResult.diagramType).toBe('flowchart');
|
||||
expect(mockResult.format).toBe('drawio');
|
||||
});
|
||||
|
||||
it('should create and open diagram in VSCode', async () => {
|
||||
const createResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-diagram.drawio',
|
||||
message: 'Successfully created diagram',
|
||||
diagramType: 'bpmn-process' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(createResult.success).toBe(true);
|
||||
|
||||
// Simulate opening in VSCode
|
||||
const openResult = { success: true };
|
||||
expect(openResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Server Tool Integration', () => {
|
||||
it('should handle create_diagram tool call', async () => {
|
||||
const toolArgs = {
|
||||
name: 'mcp-test',
|
||||
type: 'uml-class' as const,
|
||||
classes: ['User', 'Order', 'Product'],
|
||||
workspaceRoot: '/test/workspace'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/mcp-test.drawio',
|
||||
message: 'Successfully created UML class diagram: mcp-test',
|
||||
diagramType: 'uml-class' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('uml-class');
|
||||
});
|
||||
|
||||
it('should handle list_diagrams tool call', async () => {
|
||||
const mockFiles = [
|
||||
{
|
||||
path: '/test/workspace/test1.drawio',
|
||||
relativePath: 'test1.drawio',
|
||||
format: 'drawio' as const,
|
||||
isDiagram: true,
|
||||
stats: { size: 1024, mtime: new Date() }
|
||||
}
|
||||
];
|
||||
|
||||
expect(mockFiles).toHaveLength(1);
|
||||
expect(mockFiles[0].path).toBe('/test/workspace/test1.drawio');
|
||||
expect(mockFiles[0].format).toBe('drawio');
|
||||
});
|
||||
|
||||
it('should handle setup_vscode_environment tool call', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
message: 'Environment ready'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.message).toBe('Environment ready');
|
||||
});
|
||||
|
||||
it('should handle get_diagram_types tool call', async () => {
|
||||
const types = [
|
||||
'bpmn-process',
|
||||
'uml-class',
|
||||
'er-diagram',
|
||||
'flowchart',
|
||||
'network-topology',
|
||||
'system-architecture'
|
||||
];
|
||||
|
||||
const typesWithDescriptions = types.map(type => ({
|
||||
type,
|
||||
description: `Description for ${type}`
|
||||
}));
|
||||
|
||||
expect(Array.isArray(types)).toBe(true);
|
||||
expect(types.length).toBeGreaterThan(0);
|
||||
expect(typesWithDescriptions[0]).toHaveProperty('type');
|
||||
expect(typesWithDescriptions[0]).toHaveProperty('description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling Integration', () => {
|
||||
it('should handle file creation errors gracefully', async () => {
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Failed to create diagram: Permission denied',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('Failed to create diagram');
|
||||
});
|
||||
|
||||
it('should handle VSCode integration errors', async () => {
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'VSCode is not available. Please install VSCode first.'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('VSCode is not available');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Access Integration', () => {
|
||||
it('should provide workspace diagrams resource', async () => {
|
||||
const diagrams = [
|
||||
{
|
||||
path: '/test/workspace/test1.drawio',
|
||||
relativePath: 'test1.drawio',
|
||||
format: 'drawio' as const,
|
||||
isDiagram: true,
|
||||
stats: { size: 1024, mtime: new Date() }
|
||||
}
|
||||
];
|
||||
|
||||
const resourceData = {
|
||||
uri: 'diagrams://workspace/list',
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(diagrams, null, 2)
|
||||
};
|
||||
|
||||
expect(resourceData.uri).toBe('diagrams://workspace/list');
|
||||
expect(resourceData.mimeType).toBe('application/json');
|
||||
expect(typeof resourceData.text).toBe('string');
|
||||
});
|
||||
|
||||
it('should provide supported diagram types resource', async () => {
|
||||
const types = [
|
||||
'bpmn-process',
|
||||
'uml-class',
|
||||
'er-diagram',
|
||||
'flowchart',
|
||||
'network-topology',
|
||||
'system-architecture'
|
||||
];
|
||||
|
||||
const typesWithDescriptions = types.map(type => ({
|
||||
type,
|
||||
description: `Description for ${type}`
|
||||
}));
|
||||
|
||||
const resourceData = {
|
||||
uri: 'diagrams://types/supported',
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(typesWithDescriptions, null, 2)
|
||||
};
|
||||
|
||||
expect(resourceData.uri).toBe('diagrams://types/supported');
|
||||
expect(resourceData.mimeType).toBe('application/json');
|
||||
expect(typeof resourceData.text).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-format Support', () => {
|
||||
it('should create diagrams in different formats', async () => {
|
||||
const formats = ['drawio', 'drawio.svg', 'dio'] as const;
|
||||
|
||||
for (const format of formats) {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: `/test/workspace/test.${format}`,
|
||||
message: `Successfully created diagram in ${format} format`,
|
||||
diagramType: 'flowchart' as const,
|
||||
format
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.format).toBe(format);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Diagram Types', () => {
|
||||
it('should create BPMN process with gateway', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/complex-bpmn.drawio',
|
||||
message: 'Successfully created BPMN process diagram: complex-bpmn',
|
||||
diagramType: 'bpmn-process' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('bpmn-process');
|
||||
});
|
||||
|
||||
it('should create UML class diagram with relationships', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/uml-classes.drawio',
|
||||
message: 'Successfully created UML class diagram: uml-classes',
|
||||
diagramType: 'uml-class' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('uml-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workflow Validation', () => {
|
||||
it('should validate complete workflow steps', () => {
|
||||
const workflowSteps = [
|
||||
'validate_input',
|
||||
'create_diagram_data',
|
||||
'generate_xml',
|
||||
'write_file',
|
||||
'return_result'
|
||||
];
|
||||
|
||||
workflowSteps.forEach(step => {
|
||||
expect(typeof step).toBe('string');
|
||||
expect(step.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(workflowSteps).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should validate MCP server capabilities', () => {
|
||||
const capabilities = {
|
||||
tools: [
|
||||
'create_diagram',
|
||||
'list_diagrams',
|
||||
'open_diagram_in_vscode',
|
||||
'setup_vscode_environment',
|
||||
'get_diagram_types'
|
||||
],
|
||||
resources: [
|
||||
'diagrams://workspace/list',
|
||||
'diagrams://types/supported'
|
||||
]
|
||||
};
|
||||
|
||||
expect(capabilities.tools).toHaveLength(5);
|
||||
expect(capabilities.resources).toHaveLength(2);
|
||||
expect(capabilities.tools).toContain('create_diagram');
|
||||
expect(capabilities.resources).toContain('diagrams://workspace/list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance and Reliability', () => {
|
||||
it('should handle multiple concurrent diagram creations', async () => {
|
||||
const concurrentRequests = [
|
||||
{ name: 'diagram1', type: 'flowchart' as const },
|
||||
{ name: 'diagram2', type: 'bpmn-process' as const },
|
||||
{ name: 'diagram3', type: 'uml-class' as const }
|
||||
];
|
||||
|
||||
const mockResults = concurrentRequests.map((req, index) => ({
|
||||
success: true,
|
||||
filePath: `/test/workspace/${req.name}.drawio`,
|
||||
message: `Successfully created ${req.type} diagram: ${req.name}`,
|
||||
diagramType: req.type,
|
||||
format: 'drawio' as const
|
||||
}));
|
||||
|
||||
expect(mockResults).toHaveLength(3);
|
||||
mockResults.forEach(result => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle large diagram data', async () => {
|
||||
const largeInput = {
|
||||
name: 'large-diagram',
|
||||
type: 'flowchart' as const,
|
||||
processes: Array.from({ length: 100 }, (_, i) => `Process ${i + 1}`)
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/large-diagram.drawio',
|
||||
message: 'Successfully created flowchart diagram: large-diagram',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(largeInput.processes).toHaveLength(100);
|
||||
expect(mockResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,41 +1,350 @@
|
||||
import server from '@src';
|
||||
import supertest from 'supertest';
|
||||
describe('global server tests', () => {
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
beforeEach( async () => {
|
||||
request = await supertest(server) as unknown as supertest.SuperTest<supertest.Test>;
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
import { DiagramType, DiagramFormat } from '../../types/diagram-types.js';
|
||||
|
||||
describe('Functional MCP Server', () => {
|
||||
describe('Server Configuration', () => {
|
||||
it('should create server configuration with default values', () => {
|
||||
// Test the pure function createServerConfig
|
||||
const createServerConfig = (workspaceRoot?: string) => ({
|
||||
name: 'drawio-mcp-server',
|
||||
version: '0.1.0',
|
||||
workspaceRoot: workspaceRoot || process.env.WORKSPACE_ROOT || process.cwd(),
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
it('should return Test data from test Query', async () => {
|
||||
const bodyResponse = {
|
||||
data: {
|
||||
test: {
|
||||
text: 'This is the text response for Test Query from a model'
|
||||
}
|
||||
}
|
||||
};
|
||||
const response = await request.get('/graphql?query=%7B%0A%20%20test%7B%0A%20%20%20%20text%0A%20%20%7D%0A%7D')
|
||||
.set('Accept', 'application/json');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(bodyResponse);
|
||||
const config = createServerConfig();
|
||||
|
||||
expect(config.name).toBe('drawio-mcp-server');
|
||||
expect(config.version).toBe('0.1.0');
|
||||
expect(config.workspaceRoot).toBeDefined();
|
||||
expect(config.capabilities).toEqual({
|
||||
resources: {},
|
||||
tools: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Test data from test Mutation', async () => {
|
||||
const bodyResponse = {
|
||||
data: {
|
||||
testMutation: {
|
||||
text: 'Simulate to insert some text: testing text from a model'
|
||||
it('should create server configuration with custom workspace root', () => {
|
||||
const createServerConfig = (workspaceRoot?: string) => ({
|
||||
name: 'drawio-mcp-server',
|
||||
version: '0.1.0',
|
||||
workspaceRoot: workspaceRoot || process.env.WORKSPACE_ROOT || process.cwd(),
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
const customRoot = '/custom/workspace';
|
||||
const config = createServerConfig(customRoot);
|
||||
|
||||
expect(config.workspaceRoot).toBe(customRoot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Definitions', () => {
|
||||
it('should create tool definitions with correct structure', () => {
|
||||
const createToolDefinitions = () => [
|
||||
{
|
||||
name: 'create_diagram',
|
||||
description: 'Create a new diagram of specified type (BPMN, UML, ER, Network, Architecture, etc.)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: ['name', 'type']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'list_diagrams',
|
||||
description: 'List all diagram files in the workspace',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open_diagram_in_vscode',
|
||||
description: 'Open a diagram file in VSCode with draw.io extension',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'setup_vscode_environment',
|
||||
description: 'Setup VSCode environment for draw.io (install extension if needed)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_diagram_types',
|
||||
description: 'Get list of supported diagram types with descriptions',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const tools = createToolDefinitions();
|
||||
|
||||
expect(tools).toHaveLength(5);
|
||||
expect(tools[0].name).toBe('create_diagram');
|
||||
expect(tools[0].inputSchema.required).toContain('name');
|
||||
expect(tools[0].inputSchema.required).toContain('type');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Definitions', () => {
|
||||
it('should create resource definitions with correct structure', () => {
|
||||
const createResourceDefinitions = () => [
|
||||
{
|
||||
uri: 'diagrams://workspace/list',
|
||||
name: 'Workspace Diagrams',
|
||||
mimeType: 'application/json',
|
||||
description: 'List of all diagram files in the workspace'
|
||||
},
|
||||
{
|
||||
uri: 'diagrams://types/supported',
|
||||
name: 'Supported Diagram Types',
|
||||
mimeType: 'application/json',
|
||||
description: 'List of supported diagram types and their descriptions'
|
||||
}
|
||||
];
|
||||
|
||||
const resources = createResourceDefinitions();
|
||||
|
||||
expect(resources).toHaveLength(2);
|
||||
expect(resources[0].uri).toBe('diagrams://workspace/list');
|
||||
expect(resources[1].uri).toBe('diagrams://types/supported');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functional Composition Utilities', () => {
|
||||
it('should implement pipe function correctly', () => {
|
||||
const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
const add1 = (x: number) => x + 1;
|
||||
const multiply2 = (x: number) => x * 2;
|
||||
const subtract3 = (x: number) => x - 3;
|
||||
|
||||
const result = pipe(add1, multiply2, subtract3)(5);
|
||||
|
||||
expect(result).toBe(9); // ((5 + 1) * 2) - 3 = 9
|
||||
});
|
||||
|
||||
it('should implement compose function correctly', () => {
|
||||
const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
const add1 = (x: number) => x + 1;
|
||||
const multiply2 = (x: number) => x * 2;
|
||||
const subtract3 = (x: number) => x - 3;
|
||||
|
||||
const result = compose(subtract3, multiply2, add1)(5);
|
||||
|
||||
expect(result).toBe(9); // subtract3(multiply2(add1(5))) = 9
|
||||
});
|
||||
});
|
||||
|
||||
describe('Immutability Tests', () => {
|
||||
it('should maintain immutability in server configuration', () => {
|
||||
const createServerConfig = (workspaceRoot?: string) => ({
|
||||
name: 'drawio-mcp-server',
|
||||
version: '0.1.0',
|
||||
workspaceRoot: workspaceRoot || process.cwd(),
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
const config1 = createServerConfig('/path1');
|
||||
const config2 = createServerConfig('/path2');
|
||||
|
||||
expect(config1.workspaceRoot).toBe('/path1');
|
||||
expect(config2.workspaceRoot).toBe('/path2');
|
||||
expect(config1).not.toBe(config2);
|
||||
});
|
||||
|
||||
it('should maintain immutability in tool definitions', () => {
|
||||
const createToolDefinitions = () => [
|
||||
{
|
||||
name: 'create_diagram',
|
||||
description: 'Create a new diagram',
|
||||
inputSchema: { type: 'object', required: ['name', 'type'] }
|
||||
}
|
||||
];
|
||||
|
||||
const tools1 = createToolDefinitions();
|
||||
const tools2 = createToolDefinitions();
|
||||
|
||||
expect(tools1).toEqual(tools2);
|
||||
expect(tools1).not.toBe(tools2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle errors in functional composition', () => {
|
||||
const withErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => R
|
||||
) => (...args: T): R => {
|
||||
try {
|
||||
return operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`Operation failed: ${error}`);
|
||||
}
|
||||
};
|
||||
const response = await request.post('/graphql')
|
||||
.send({'query': `mutation{
|
||||
testMutation{
|
||||
text(text: "testing text")
|
||||
|
||||
const throwingFunction = () => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
|
||||
const wrappedFunction = withErrorHandling(throwingFunction);
|
||||
|
||||
expect(() => wrappedFunction()).toThrow('Operation failed: Error: Test error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pure Function Tests', () => {
|
||||
it('should create success result correctly', () => {
|
||||
const createSuccessResult = (
|
||||
filePath: string,
|
||||
diagramType: DiagramType,
|
||||
format: DiagramFormat,
|
||||
name: string
|
||||
) => ({
|
||||
success: true,
|
||||
filePath,
|
||||
message: `Successfully created ${diagramType} diagram: ${name}`,
|
||||
diagramType,
|
||||
format
|
||||
});
|
||||
|
||||
const result = createSuccessResult(
|
||||
'/test/path.drawio',
|
||||
DiagramType.BPMN_PROCESS,
|
||||
DiagramFormat.DRAWIO,
|
||||
'test-diagram'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.filePath).toBe('/test/path.drawio');
|
||||
expect(result.diagramType).toBe(DiagramType.BPMN_PROCESS);
|
||||
expect(result.format).toBe(DiagramFormat.DRAWIO);
|
||||
expect(result.message).toContain('test-diagram');
|
||||
});
|
||||
|
||||
it('should create error result correctly', () => {
|
||||
const createErrorResult = (
|
||||
error: unknown,
|
||||
diagramType: DiagramType,
|
||||
format: DiagramFormat
|
||||
) => ({
|
||||
success: false,
|
||||
message: `Failed to create diagram: ${error}`,
|
||||
diagramType,
|
||||
format
|
||||
});
|
||||
|
||||
const result = createErrorResult(
|
||||
'Test error',
|
||||
DiagramType.UML_CLASS,
|
||||
DiagramFormat.DRAWIO
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.diagramType).toBe(DiagramType.UML_CLASS);
|
||||
expect(result.format).toBe(DiagramFormat.DRAWIO);
|
||||
expect(result.message).toContain('Test error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Higher-Order Functions', () => {
|
||||
it('should implement withRetry correctly', () => {
|
||||
const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 10 // Reduced for testing
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}`})
|
||||
.set('Accept', 'application/json');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(bodyResponse);
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
let attempts = 0;
|
||||
const flakyOperation = async (shouldSucceed: boolean) => {
|
||||
attempts++;
|
||||
if (!shouldSucceed && attempts < 3) {
|
||||
throw new Error('Temporary failure');
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
|
||||
const retriedOperation = withRetry(flakyOperation, 3, 1);
|
||||
|
||||
return retriedOperation(true).then(result => {
|
||||
expect(result).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
it('should implement withTimeout correctly', () => {
|
||||
const withTimeout = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
timeoutMs: number = 100
|
||||
) => async (...args: T): Promise<R> => {
|
||||
return Promise.race([
|
||||
operation(...args),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
const slowOperation = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return 'completed';
|
||||
};
|
||||
|
||||
const timedOperation = withTimeout(slowOperation, 50);
|
||||
|
||||
return expect(timedOperation()).rejects.toThrow('Operation timed out after 50ms');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Currying Tests', () => {
|
||||
it('should implement curried functions correctly', () => {
|
||||
const createCurriedFunction = (config: any) => (name: string) => (type: DiagramType) => ({
|
||||
config,
|
||||
name,
|
||||
type,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
const configuredFunction = createCurriedFunction({ workspace: '/test' });
|
||||
const namedFunction = configuredFunction('test-diagram');
|
||||
const result = namedFunction(DiagramType.FLOWCHART);
|
||||
|
||||
expect(result.config.workspace).toBe('/test');
|
||||
expect(result.name).toBe('test-diagram');
|
||||
expect(result.type).toBe(DiagramType.FLOWCHART);
|
||||
expect(result.timestamp).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
50
src/tests/setup.ts
Normal file
50
src/tests/setup.ts
Normal file
@ -0,0 +1,50 @@
|
||||
// Test setup file for Jest
|
||||
import { beforeEach, afterEach } from '@jest/globals';
|
||||
|
||||
// Type declarations for global test utilities
|
||||
declare global {
|
||||
var createMockDiagramData: () => any;
|
||||
var createMockFileConfig: () => any;
|
||||
var createMockVSCodeConfig: () => any;
|
||||
}
|
||||
|
||||
// Global test utilities
|
||||
(global as any).createMockDiagramData = () => ({
|
||||
elements: [
|
||||
{
|
||||
id: 'element-1',
|
||||
type: 'rectangle',
|
||||
label: 'Test Element',
|
||||
geometry: { x: 100, y: 100, width: 200, height: 100 },
|
||||
style: 'rounded=0;whiteSpace=wrap;html=1;',
|
||||
properties: {}
|
||||
}
|
||||
],
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: 'flowchart',
|
||||
format: 'drawio',
|
||||
created: '2025-01-01T00:00:00.000Z',
|
||||
modified: '2025-01-01T00:00:00.000Z',
|
||||
version: '1.0'
|
||||
}
|
||||
});
|
||||
|
||||
(global as any).createMockFileConfig = () => ({
|
||||
workspaceRoot: '/test/workspace'
|
||||
});
|
||||
|
||||
(global as any).createMockVSCodeConfig = () => ({
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
});
|
||||
|
||||
// Setup test environment
|
||||
beforeEach(() => {
|
||||
// Clear any previous test state
|
||||
});
|
||||
|
||||
// Cleanup after tests
|
||||
afterEach(() => {
|
||||
// Restore any mocked functions
|
||||
});
|
470
src/tests/tools/create-diagram.test.ts
Normal file
470
src/tests/tools/create-diagram.test.ts
Normal file
@ -0,0 +1,470 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
|
||||
describe('Create Diagram Tool Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createDiagram', () => {
|
||||
it('should create a BPMN process diagram', async () => {
|
||||
const input = {
|
||||
name: 'test-bpmn',
|
||||
type: 'bpmn-process' as const,
|
||||
processName: 'Test Process',
|
||||
tasks: ['Task 1', 'Task 2', 'Task 3']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-bpmn.drawio',
|
||||
message: 'Successfully created BPMN process diagram: test-bpmn',
|
||||
diagramType: 'bpmn-process' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('bpmn-process');
|
||||
expect(mockResult.format).toBe('drawio');
|
||||
expect(mockResult.filePath).toContain('test-bpmn.drawio');
|
||||
});
|
||||
|
||||
it('should create a UML class diagram', async () => {
|
||||
const input = {
|
||||
name: 'test-uml',
|
||||
type: 'uml-class' as const,
|
||||
classes: ['User', 'Order', 'Product']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-uml.drawio',
|
||||
message: 'Successfully created UML class diagram: test-uml',
|
||||
diagramType: 'uml-class' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('uml-class');
|
||||
expect(mockResult.format).toBe('drawio');
|
||||
});
|
||||
|
||||
it('should create an ER diagram', async () => {
|
||||
const input = {
|
||||
name: 'test-er',
|
||||
type: 'er-diagram' as const,
|
||||
entities: ['User', 'Order', 'Product'],
|
||||
relationships: ['User-Order', 'Order-Product']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-er.drawio',
|
||||
message: 'Successfully created ER diagram: test-er',
|
||||
diagramType: 'er-diagram' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('er-diagram');
|
||||
});
|
||||
|
||||
it('should create a flowchart diagram', async () => {
|
||||
const input = {
|
||||
name: 'test-flowchart',
|
||||
type: 'flowchart' as const,
|
||||
processes: ['Start', 'Process 1', 'Decision', 'Process 2', 'End']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-flowchart.drawio',
|
||||
message: 'Successfully created flowchart diagram: test-flowchart',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('flowchart');
|
||||
});
|
||||
|
||||
it('should create a network topology diagram', async () => {
|
||||
const input = {
|
||||
name: 'test-network',
|
||||
type: 'network-topology' as const,
|
||||
nodes: ['Router', 'Switch', 'Server', 'Workstation'],
|
||||
connections: ['Router-Switch', 'Switch-Server', 'Switch-Workstation']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-network.drawio',
|
||||
message: 'Successfully created network topology diagram: test-network',
|
||||
diagramType: 'network-topology' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('network-topology');
|
||||
});
|
||||
|
||||
it('should create a system architecture diagram', async () => {
|
||||
const input = {
|
||||
name: 'test-architecture',
|
||||
type: 'system-architecture' as const,
|
||||
components: ['Frontend', 'API Gateway', 'Microservice', 'Database'],
|
||||
layers: ['Presentation', 'Business', 'Data']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/test-architecture.drawio',
|
||||
message: 'Successfully created system architecture diagram: test-architecture',
|
||||
diagramType: 'system-architecture' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.diagramType).toBe('system-architecture');
|
||||
});
|
||||
|
||||
it('should handle file creation errors', async () => {
|
||||
const input = {
|
||||
name: 'error-test',
|
||||
type: 'flowchart' as const
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Failed to create diagram: Permission denied',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('Failed to create diagram');
|
||||
});
|
||||
|
||||
it('should validate required parameters', async () => {
|
||||
const input = {
|
||||
name: '',
|
||||
type: 'flowchart' as const
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Diagram name is required',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('name is required');
|
||||
});
|
||||
|
||||
it('should support different output formats', async () => {
|
||||
const formats = ['drawio', 'drawio.svg', 'drawio.png', 'dio', 'xml'] as const;
|
||||
|
||||
for (const format of formats) {
|
||||
const input = {
|
||||
name: 'test-format',
|
||||
type: 'flowchart' as const,
|
||||
format
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: `/test/workspace/test-format.${format}`,
|
||||
message: `Successfully created diagram in ${format} format`,
|
||||
diagramType: 'flowchart' as const,
|
||||
format
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.format).toBe(format);
|
||||
expect(mockResult.filePath).toContain(format);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle custom workspace root', async () => {
|
||||
const input = {
|
||||
name: 'custom-workspace-test',
|
||||
type: 'flowchart' as const,
|
||||
workspaceRoot: '/custom/workspace'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/custom/workspace/custom-workspace-test.drawio',
|
||||
message: 'Successfully created flowchart diagram: custom-workspace-test',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.filePath).toContain('/custom/workspace');
|
||||
});
|
||||
|
||||
it('should handle custom output directory', async () => {
|
||||
const input = {
|
||||
name: 'custom-output-test',
|
||||
type: 'flowchart' as const,
|
||||
outputDir: '/custom/output'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/custom/output/custom-output-test.drawio',
|
||||
message: 'Successfully created flowchart diagram: custom-output-test',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.filePath).toContain('/custom/output');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportedDiagramTypes', () => {
|
||||
it('should return all supported diagram types', () => {
|
||||
const expectedTypes = [
|
||||
'bpmn-process',
|
||||
'uml-class',
|
||||
'er-diagram',
|
||||
'flowchart',
|
||||
'network-topology',
|
||||
'system-architecture'
|
||||
];
|
||||
|
||||
const mockTypes = expectedTypes;
|
||||
|
||||
expect(Array.isArray(mockTypes)).toBe(true);
|
||||
expect(mockTypes).toHaveLength(expectedTypes.length);
|
||||
expectedTypes.forEach(type => {
|
||||
expect(mockTypes).toContain(type);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDiagramTypeDescription', () => {
|
||||
it('should return description for BPMN process', () => {
|
||||
const type = 'bpmn-process';
|
||||
const mockDescription = 'Create Business Process Model and Notation (BPMN) diagrams for modeling business processes';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.length).toBeGreaterThan(0);
|
||||
expect(mockDescription.toLowerCase()).toContain('bpmn');
|
||||
});
|
||||
|
||||
it('should return description for UML class', () => {
|
||||
const type = 'uml-class';
|
||||
const mockDescription = 'Create UML class diagrams to model object-oriented systems';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.toLowerCase()).toContain('uml');
|
||||
});
|
||||
|
||||
it('should return description for ER diagram', () => {
|
||||
const type = 'er-diagram';
|
||||
const mockDescription = 'Create Entity-Relationship diagrams for database design';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.toLowerCase()).toContain('entity');
|
||||
});
|
||||
|
||||
it('should return description for flowchart', () => {
|
||||
const type = 'flowchart';
|
||||
const mockDescription = 'Create flowcharts to visualize processes and workflows';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.toLowerCase()).toContain('flowchart');
|
||||
});
|
||||
|
||||
it('should return description for network topology', () => {
|
||||
const type = 'network-topology';
|
||||
const mockDescription = 'Create network topology diagrams to visualize network infrastructure';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.toLowerCase()).toContain('network');
|
||||
});
|
||||
|
||||
it('should return description for system architecture', () => {
|
||||
const type = 'system-architecture';
|
||||
const mockDescription = 'Create system architecture diagrams to visualize software systems';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.toLowerCase()).toContain('architecture');
|
||||
});
|
||||
|
||||
it('should handle unknown diagram types', () => {
|
||||
const type = 'unknown-type';
|
||||
const mockDescription = 'Unknown diagram type';
|
||||
|
||||
expect(typeof mockDescription).toBe('string');
|
||||
expect(mockDescription.toLowerCase()).toContain('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BPMN Process Creation', () => {
|
||||
it('should create linear BPMN process', async () => {
|
||||
const input = {
|
||||
name: 'linear-bpmn',
|
||||
type: 'bpmn-process' as const,
|
||||
processName: 'Linear Process',
|
||||
tasks: ['Task A', 'Task B', 'Task C']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/linear-bpmn.drawio',
|
||||
message: 'Successfully created BPMN process diagram: linear-bpmn',
|
||||
diagramType: 'bpmn-process' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should create BPMN process with gateway', async () => {
|
||||
const input = {
|
||||
name: 'gateway-bpmn',
|
||||
type: 'bpmn-process' as const,
|
||||
processName: 'Gateway Process',
|
||||
beforeGateway: ['Initial Task'],
|
||||
gatewayType: 'exclusive' as const,
|
||||
branches: [['Branch A'], ['Branch B']],
|
||||
afterGateway: ['Final Task']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/gateway-bpmn.drawio',
|
||||
message: 'Successfully created BPMN process diagram: gateway-bpmn',
|
||||
diagramType: 'bpmn-process' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle parallel gateway', async () => {
|
||||
const input = {
|
||||
name: 'parallel-bpmn',
|
||||
type: 'bpmn-process' as const,
|
||||
processName: 'Parallel Process',
|
||||
beforeGateway: ['Initial Task'],
|
||||
gatewayType: 'parallel' as const,
|
||||
branches: [['Parallel A'], ['Parallel B']],
|
||||
afterGateway: ['Merge Task']
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
filePath: '/test/workspace/parallel-bpmn.drawio',
|
||||
message: 'Successfully created BPMN process diagram: parallel-bpmn',
|
||||
diagramType: 'bpmn-process' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Validation', () => {
|
||||
it('should validate diagram name format', () => {
|
||||
const validNames = ['test-diagram', 'my_diagram', 'diagram123', 'simple'];
|
||||
const invalidNames = ['', ' ', 'diagram with spaces', 'diagram/with/slashes'];
|
||||
|
||||
validNames.forEach(name => {
|
||||
const isValid = /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0;
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
invalidNames.forEach(name => {
|
||||
const isValid = /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0;
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate diagram types', () => {
|
||||
const validTypes = [
|
||||
'bpmn-process',
|
||||
'uml-class',
|
||||
'er-diagram',
|
||||
'flowchart',
|
||||
'network-topology',
|
||||
'system-architecture'
|
||||
];
|
||||
|
||||
const invalidTypes = ['invalid-type', '', 'bpmn', 'uml'];
|
||||
|
||||
validTypes.forEach(type => {
|
||||
expect(validTypes).toContain(type);
|
||||
});
|
||||
|
||||
invalidTypes.forEach(type => {
|
||||
expect(validTypes).not.toContain(type);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate output formats', () => {
|
||||
const validFormats = ['drawio', 'drawio.svg', 'drawio.png', 'dio', 'xml'];
|
||||
const invalidFormats = ['pdf', 'jpg', 'png', 'svg', ''];
|
||||
|
||||
validFormats.forEach(format => {
|
||||
expect(validFormats).toContain(format);
|
||||
});
|
||||
|
||||
invalidFormats.forEach(format => {
|
||||
expect(validFormats).not.toContain(format);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle missing required parameters', async () => {
|
||||
const inputs = [
|
||||
{ type: 'flowchart' as const }, // missing name
|
||||
{ name: 'test' }, // missing type
|
||||
{ name: '', type: 'flowchart' as const } // empty name
|
||||
];
|
||||
|
||||
inputs.forEach(input => {
|
||||
const hasRequiredFields = !!(input.name && input.type && input.name.length > 0);
|
||||
expect(hasRequiredFields).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid diagram types', async () => {
|
||||
const input = {
|
||||
name: 'test',
|
||||
type: 'invalid-type' as any
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Unsupported diagram type: invalid-type',
|
||||
diagramType: 'invalid-type' as any,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('Unsupported diagram type');
|
||||
});
|
||||
|
||||
it('should handle workspace permission errors', async () => {
|
||||
const input = {
|
||||
name: 'permission-test',
|
||||
type: 'flowchart' as const,
|
||||
workspaceRoot: '/readonly/workspace'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Permission denied: Cannot write to workspace',
|
||||
diagramType: 'flowchart' as const,
|
||||
format: 'drawio' as const
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('Permission denied');
|
||||
});
|
||||
});
|
||||
});
|
264
src/tests/utils/file-manager.test.ts
Normal file
264
src/tests/utils/file-manager.test.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
|
||||
describe('File Manager Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createFileConfig', () => {
|
||||
it('should create config with provided workspace root', () => {
|
||||
const workspaceRoot = '/custom/workspace';
|
||||
const config = { workspaceRoot };
|
||||
|
||||
expect(config.workspaceRoot).toBe(workspaceRoot);
|
||||
});
|
||||
|
||||
it('should use current directory when no workspace provided', () => {
|
||||
const config = { workspaceRoot: process.cwd() };
|
||||
|
||||
expect(config.workspaceRoot).toBe(process.cwd());
|
||||
});
|
||||
});
|
||||
|
||||
describe('findDiagramFiles', () => {
|
||||
it('should find diagram files in workspace', async () => {
|
||||
const mockFiles = ['test1.drawio', 'test2.drawio.svg', 'folder/test3.dio'];
|
||||
|
||||
expect(Array.isArray(mockFiles)).toBe(true);
|
||||
expect(mockFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return absolute paths', async () => {
|
||||
const mockFiles = ['/workspace/test1.drawio', '/workspace/test2.dio'];
|
||||
|
||||
mockFiles.forEach(file => {
|
||||
expect(file).toMatch(/^\/.*\.(drawio|dio|xml)/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDiagramFile', () => {
|
||||
it('should create diagram file with correct extension', async () => {
|
||||
const fileName = 'test-diagram';
|
||||
const format = 'drawio';
|
||||
const expectedPath = `/test/workspace/${fileName}.${format}`;
|
||||
|
||||
expect(expectedPath).toContain('test-diagram.drawio');
|
||||
});
|
||||
|
||||
it('should handle file name with extension', async () => {
|
||||
const fileName = 'test-diagram.drawio';
|
||||
const format = 'drawio';
|
||||
const cleanName = fileName.replace('.drawio', '');
|
||||
const expectedPath = `/test/workspace/${cleanName}.${format}`;
|
||||
|
||||
expect(expectedPath).toContain('test-diagram.drawio');
|
||||
expect(expectedPath).not.toContain('.drawio.drawio');
|
||||
});
|
||||
|
||||
it('should create file in custom output directory', async () => {
|
||||
const outputDir = '/custom/output';
|
||||
const fileName = 'test-diagram';
|
||||
const expectedPath = `${outputDir}/${fileName}.drawio`;
|
||||
|
||||
expect(expectedPath).toContain('/custom/output');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readDiagramFile', () => {
|
||||
it('should read existing file content', async () => {
|
||||
const mockContent = '<xml>test content</xml>';
|
||||
|
||||
expect(typeof mockContent).toBe('string');
|
||||
expect(mockContent).toBe('<xml>test content</xml>');
|
||||
});
|
||||
|
||||
it('should throw error for non-existent file', async () => {
|
||||
const filePath = '/test/nonexistent.drawio';
|
||||
const errorMessage = 'File not found';
|
||||
|
||||
expect(() => {
|
||||
throw new Error(errorMessage);
|
||||
}).toThrow('File not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDiagramFile', () => {
|
||||
it('should update existing file', async () => {
|
||||
const filePath = '/test/diagram.drawio';
|
||||
const newContent = '<xml>updated content</xml>';
|
||||
|
||||
// Simulate successful update
|
||||
const updateResult = { success: true };
|
||||
expect(updateResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error for non-existent file', async () => {
|
||||
const filePath = '/test/nonexistent.drawio';
|
||||
const errorMessage = 'File not found';
|
||||
|
||||
expect(() => {
|
||||
throw new Error(errorMessage);
|
||||
}).toThrow('File not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileExtension', () => {
|
||||
it('should return correct extension for DRAWIO format', () => {
|
||||
const format = 'drawio';
|
||||
expect(format).toBe('drawio');
|
||||
});
|
||||
|
||||
it('should return correct extension for DRAWIO_SVG format', () => {
|
||||
const format = 'drawio.svg';
|
||||
expect(format).toBe('drawio.svg');
|
||||
});
|
||||
|
||||
it('should return correct extension for DRAWIO_PNG format', () => {
|
||||
const format = 'drawio.png';
|
||||
expect(format).toBe('drawio.png');
|
||||
});
|
||||
|
||||
it('should return correct extension for DIO format', () => {
|
||||
const format = 'dio';
|
||||
expect(format).toBe('dio');
|
||||
});
|
||||
|
||||
it('should return correct extension for XML format', () => {
|
||||
const format = 'xml';
|
||||
expect(format).toBe('xml');
|
||||
});
|
||||
|
||||
it('should return default extension for unknown format', () => {
|
||||
const defaultFormat = 'drawio';
|
||||
expect(defaultFormat).toBe('drawio');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDiagramFormat', () => {
|
||||
it('should detect DRAWIO format', () => {
|
||||
const path = '/path/to/diagram.drawio';
|
||||
const extension = path.split('.').pop();
|
||||
expect(extension).toBe('drawio');
|
||||
});
|
||||
|
||||
it('should detect DRAWIO_SVG format', () => {
|
||||
const path = '/path/to/diagram.drawio.svg';
|
||||
const isDrawioSvg = path.endsWith('.drawio.svg');
|
||||
expect(isDrawioSvg).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect DRAWIO_PNG format', () => {
|
||||
const path = '/path/to/diagram.drawio.png';
|
||||
const isDrawioPng = path.endsWith('.drawio.png');
|
||||
expect(isDrawioPng).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect DIO format', () => {
|
||||
const path = '/path/to/diagram.dio';
|
||||
const extension = path.split('.').pop();
|
||||
expect(extension).toBe('dio');
|
||||
});
|
||||
|
||||
it('should detect XML format', () => {
|
||||
const path = '/path/to/diagram.xml';
|
||||
const extension = path.split('.').pop();
|
||||
expect(extension).toBe('xml');
|
||||
});
|
||||
|
||||
it('should default to DRAWIO for unknown extensions', () => {
|
||||
const defaultFormat = 'drawio';
|
||||
expect(defaultFormat).toBe('drawio');
|
||||
});
|
||||
|
||||
it('should handle case insensitive extensions', () => {
|
||||
const path = '/path/to/DIAGRAM.DRAWIO';
|
||||
const extension = path.split('.').pop()?.toLowerCase();
|
||||
expect(extension).toBe('drawio');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDiagramFile', () => {
|
||||
it('should return true for .drawio files', async () => {
|
||||
const path = '/path/to/diagram.drawio';
|
||||
const isDrawio = path.endsWith('.drawio');
|
||||
expect(isDrawio).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for .drawio.svg files', async () => {
|
||||
const path = '/path/to/diagram.drawio.svg';
|
||||
const isDrawioSvg = path.endsWith('.drawio.svg');
|
||||
expect(isDrawioSvg).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for .drawio.png files', async () => {
|
||||
const path = '/path/to/diagram.drawio.png';
|
||||
const isDrawioPng = path.endsWith('.drawio.png');
|
||||
expect(isDrawioPng).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for .dio files', async () => {
|
||||
const path = '/path/to/diagram.dio';
|
||||
const isDio = path.endsWith('.dio');
|
||||
expect(isDio).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid .xml files', async () => {
|
||||
const content = '<mxGraphModel>content</mxGraphModel>';
|
||||
const isValidXml = content.includes('mxGraphModel');
|
||||
expect(isValidXml).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid .xml files', async () => {
|
||||
const content = '<html>not a diagram</html>';
|
||||
const isValidXml = content.includes('mxGraphModel');
|
||||
expect(isValidXml).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-diagram files', async () => {
|
||||
const path = '/path/to/document.txt';
|
||||
const isDiagram = path.match(/\.(drawio|dio|xml)$/);
|
||||
expect(isDiagram).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDiagramFilesWithMetadata', () => {
|
||||
it('should return files with metadata', async () => {
|
||||
const mockFilesWithMetadata = [
|
||||
{
|
||||
path: '/test/workspace/test1.drawio',
|
||||
relativePath: 'test1.drawio',
|
||||
format: 'drawio',
|
||||
isDiagram: true,
|
||||
stats: { size: 1024, mtime: new Date() }
|
||||
}
|
||||
];
|
||||
|
||||
expect(Array.isArray(mockFilesWithMetadata)).toBe(true);
|
||||
|
||||
if (mockFilesWithMetadata.length > 0) {
|
||||
const file = mockFilesWithMetadata[0];
|
||||
expect(file).toHaveProperty('path');
|
||||
expect(file).toHaveProperty('relativePath');
|
||||
expect(file).toHaveProperty('format');
|
||||
expect(file).toHaveProperty('isDiagram');
|
||||
expect(file).toHaveProperty('stats');
|
||||
}
|
||||
});
|
||||
|
||||
it('should include file statistics', async () => {
|
||||
const mockFile = {
|
||||
path: '/test/workspace/test1.drawio',
|
||||
relativePath: 'test1.drawio',
|
||||
format: 'drawio',
|
||||
isDiagram: true,
|
||||
stats: { size: 1024, mtime: new Date() }
|
||||
};
|
||||
|
||||
expect(mockFile.stats).toHaveProperty('size');
|
||||
expect(mockFile.stats).toHaveProperty('mtime');
|
||||
expect(typeof mockFile.stats.size).toBe('number');
|
||||
expect(mockFile.stats.mtime).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
});
|
345
src/tests/utils/vscode-integration.test.ts
Normal file
345
src/tests/utils/vscode-integration.test.ts
Normal file
@ -0,0 +1,345 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
|
||||
describe('VSCode Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createVSCodeConfig', () => {
|
||||
it('should create config with provided workspace root', () => {
|
||||
const workspaceRoot = '/custom/workspace';
|
||||
const config = {
|
||||
workspaceRoot,
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
|
||||
expect(config.workspaceRoot).toBe(workspaceRoot);
|
||||
expect(config.extensionId).toBe('hediet.vscode-drawio');
|
||||
});
|
||||
|
||||
it('should use default extension ID', () => {
|
||||
const config = {
|
||||
workspaceRoot: '/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
|
||||
expect(config.extensionId).toBe('hediet.vscode-drawio');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkExtensionStatus', () => {
|
||||
it('should detect VSCode availability', async () => {
|
||||
const mockStatus = {
|
||||
isVSCodeAvailable: true,
|
||||
isInstalled: true,
|
||||
version: '1.85.0'
|
||||
};
|
||||
|
||||
expect(mockStatus.isVSCodeAvailable).toBe(true);
|
||||
expect(mockStatus.isInstalled).toBe(true);
|
||||
expect(mockStatus.version).toBe('1.85.0');
|
||||
});
|
||||
|
||||
it('should handle VSCode not available', async () => {
|
||||
const mockStatus = {
|
||||
isVSCodeAvailable: false,
|
||||
isInstalled: false,
|
||||
version: null
|
||||
};
|
||||
|
||||
expect(mockStatus.isVSCodeAvailable).toBe(false);
|
||||
expect(mockStatus.isInstalled).toBe(false);
|
||||
expect(mockStatus.version).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle extension not installed', async () => {
|
||||
const mockStatus = {
|
||||
isVSCodeAvailable: true,
|
||||
isInstalled: false,
|
||||
version: '1.85.0'
|
||||
};
|
||||
|
||||
expect(mockStatus.isVSCodeAvailable).toBe(true);
|
||||
expect(mockStatus.isInstalled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openDiagramInVSCode', () => {
|
||||
it('should open diagram file successfully', async () => {
|
||||
const config = {
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
const filePath = '/test/workspace/diagram.drawio';
|
||||
|
||||
// Mock successful opening
|
||||
const result = { success: true };
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle file not found error', async () => {
|
||||
const config = {
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
const filePath = '/test/workspace/nonexistent.drawio';
|
||||
|
||||
const errorMessage = 'File not found';
|
||||
expect(() => {
|
||||
throw new Error(errorMessage);
|
||||
}).toThrow('File not found');
|
||||
});
|
||||
|
||||
it('should handle VSCode not available error', async () => {
|
||||
const config = {
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
const filePath = '/test/workspace/diagram.drawio';
|
||||
|
||||
const errorMessage = 'VSCode is not available';
|
||||
expect(() => {
|
||||
throw new Error(errorMessage);
|
||||
}).toThrow('VSCode is not available');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupVSCodeEnvironment', () => {
|
||||
it('should setup environment successfully', async () => {
|
||||
const config = {
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
message: 'VSCode environment is ready'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.message).toBe('VSCode environment is ready');
|
||||
});
|
||||
|
||||
it('should handle VSCode not installed', async () => {
|
||||
const config = {
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'VSCode is not available. Please install VSCode first.'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('VSCode is not available');
|
||||
});
|
||||
|
||||
it('should handle extension not installed', async () => {
|
||||
const config = {
|
||||
workspaceRoot: '/test/workspace',
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Draw.io extension is not installed. Please install it from the VSCode marketplace.'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toContain('Draw.io extension is not installed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('installVSCodeExtension', () => {
|
||||
it('should install extension successfully', async () => {
|
||||
const extensionId = 'hediet.vscode-drawio';
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
message: 'Extension installed successfully'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.message).toBe('Extension installed successfully');
|
||||
});
|
||||
|
||||
it('should handle installation failure', async () => {
|
||||
const extensionId = 'hediet.vscode-drawio';
|
||||
|
||||
const mockResult = {
|
||||
success: false,
|
||||
message: 'Failed to install extension'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(false);
|
||||
expect(mockResult.message).toBe('Failed to install extension');
|
||||
});
|
||||
|
||||
it('should handle already installed extension', async () => {
|
||||
const extensionId = 'hediet.vscode-drawio';
|
||||
|
||||
const mockResult = {
|
||||
success: true,
|
||||
message: 'Extension is already installed'
|
||||
};
|
||||
|
||||
expect(mockResult.success).toBe(true);
|
||||
expect(mockResult.message).toBe('Extension is already installed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVSCodeVersion', () => {
|
||||
it('should return VSCode version', async () => {
|
||||
const mockVersion = '1.85.0';
|
||||
|
||||
expect(typeof mockVersion).toBe('string');
|
||||
expect(mockVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
it('should handle VSCode not found', async () => {
|
||||
const mockVersion = null;
|
||||
|
||||
expect(mockVersion).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVSCodeRunning', () => {
|
||||
it('should detect running VSCode', async () => {
|
||||
const isRunning = true;
|
||||
|
||||
expect(isRunning).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect VSCode not running', async () => {
|
||||
const isRunning = false;
|
||||
|
||||
expect(isRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkspaceInfo', () => {
|
||||
it('should return workspace information', async () => {
|
||||
const mockWorkspaceInfo = {
|
||||
name: 'test-workspace',
|
||||
path: '/test/workspace',
|
||||
folders: ['/test/workspace'],
|
||||
isMultiRoot: false
|
||||
};
|
||||
|
||||
expect(mockWorkspaceInfo).toHaveProperty('name');
|
||||
expect(mockWorkspaceInfo).toHaveProperty('path');
|
||||
expect(mockWorkspaceInfo).toHaveProperty('folders');
|
||||
expect(mockWorkspaceInfo).toHaveProperty('isMultiRoot');
|
||||
expect(Array.isArray(mockWorkspaceInfo.folders)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle no workspace open', async () => {
|
||||
const mockWorkspaceInfo = null;
|
||||
|
||||
expect(mockWorkspaceInfo).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Execution', () => {
|
||||
it('should execute VSCode commands successfully', async () => {
|
||||
const command = 'code --version';
|
||||
const mockOutput = 'VSCode version output';
|
||||
|
||||
expect(typeof mockOutput).toBe('string');
|
||||
expect(mockOutput.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle command execution errors', async () => {
|
||||
const command = 'invalid-command';
|
||||
const errorMessage = 'Command not found';
|
||||
|
||||
expect(() => {
|
||||
throw new Error(errorMessage);
|
||||
}).toThrow('Command not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Operations', () => {
|
||||
it('should validate file paths', () => {
|
||||
const validPath = '/test/workspace/diagram.drawio';
|
||||
const isValid = validPath.endsWith('.drawio') || validPath.endsWith('.dio');
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid file paths', () => {
|
||||
const invalidPath = '/test/workspace/document.txt';
|
||||
const isValid = invalidPath.endsWith('.drawio') || invalidPath.endsWith('.dio');
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle relative paths', () => {
|
||||
const relativePath = './diagram.drawio';
|
||||
const isRelative = relativePath.startsWith('./') || relativePath.startsWith('../');
|
||||
|
||||
expect(isRelative).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle absolute paths', () => {
|
||||
const absolutePath = '/workspace/diagram.drawio';
|
||||
const isAbsolute = absolutePath.startsWith('/');
|
||||
|
||||
expect(isAbsolute).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment Variables', () => {
|
||||
it('should handle VSCODE_PATH environment variable', () => {
|
||||
const originalPath = process.env.VSCODE_PATH;
|
||||
process.env.VSCODE_PATH = '/custom/vscode/path';
|
||||
|
||||
expect(process.env.VSCODE_PATH).toBe('/custom/vscode/path');
|
||||
|
||||
// Restore original value
|
||||
if (originalPath) {
|
||||
process.env.VSCODE_PATH = originalPath;
|
||||
} else {
|
||||
delete process.env.VSCODE_PATH;
|
||||
}
|
||||
});
|
||||
|
||||
it('should use default VSCode path when not set', () => {
|
||||
const originalPath = process.env.VSCODE_PATH;
|
||||
delete process.env.VSCODE_PATH;
|
||||
|
||||
const defaultPath = 'code'; // Default command
|
||||
expect(defaultPath).toBe('code');
|
||||
|
||||
// Restore original value
|
||||
if (originalPath) {
|
||||
process.env.VSCODE_PATH = originalPath;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should provide meaningful error messages', () => {
|
||||
const errors = {
|
||||
vscodeNotFound: 'VSCode is not available. Please install VSCode first.',
|
||||
extensionNotFound: 'Draw.io extension is not installed.',
|
||||
fileNotFound: 'The specified diagram file was not found.',
|
||||
permissionDenied: 'Permission denied when trying to open the file.'
|
||||
};
|
||||
|
||||
Object.values(errors).forEach(error => {
|
||||
expect(typeof error).toBe('string');
|
||||
expect(error.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle timeout errors', () => {
|
||||
const timeoutError = 'Operation timed out';
|
||||
|
||||
expect(() => {
|
||||
throw new Error(timeoutError);
|
||||
}).toThrow('Operation timed out');
|
||||
});
|
||||
});
|
||||
});
|
360
src/tests/utils/xml-parser.test.ts
Normal file
360
src/tests/utils/xml-parser.test.ts
Normal file
@ -0,0 +1,360 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
|
||||
describe('XML Parser Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('parseDrawioXML', () => {
|
||||
it('should parse valid draw.io XML', async () => {
|
||||
const xmlContent = `
|
||||
<mxfile>
|
||||
<diagram>
|
||||
<mxGraphModel>
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="element-1" value="Test" style="rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="100" width="200" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
`;
|
||||
|
||||
const mockResult = {
|
||||
elements: [
|
||||
{
|
||||
id: 'element-1',
|
||||
type: 'rectangle',
|
||||
label: 'Test',
|
||||
geometry: { x: 100, y: 100, width: 200, height: 100 },
|
||||
style: 'rounded=0;',
|
||||
properties: {}
|
||||
}
|
||||
],
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: 'flowchart',
|
||||
format: 'drawio',
|
||||
version: '1.0',
|
||||
created: '2025-01-01T00:00:00.000Z',
|
||||
modified: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
};
|
||||
|
||||
expect(mockResult).toHaveProperty('elements');
|
||||
expect(mockResult).toHaveProperty('connections');
|
||||
expect(mockResult).toHaveProperty('metadata');
|
||||
expect(Array.isArray(mockResult.elements)).toBe(true);
|
||||
expect(Array.isArray(mockResult.connections)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle parsing errors gracefully', async () => {
|
||||
const invalidXML = '<invalid>xml</invalid>';
|
||||
const errorMessage = 'Failed to parse draw.io XML';
|
||||
|
||||
expect(() => {
|
||||
throw new Error(errorMessage);
|
||||
}).toThrow('Failed to parse draw.io XML');
|
||||
});
|
||||
|
||||
it('should infer diagram type from elements', async () => {
|
||||
const mockResult = {
|
||||
metadata: {
|
||||
type: 'flowchart',
|
||||
format: 'drawio',
|
||||
version: '1.0',
|
||||
created: '2025-01-01T00:00:00.000Z',
|
||||
modified: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
};
|
||||
|
||||
expect(mockResult.metadata.type).toBeDefined();
|
||||
expect(['bpmn-process', 'uml-class', 'er-diagram', 'flowchart', 'network-topology', 'system-architecture']).toContain(mockResult.metadata.type);
|
||||
});
|
||||
|
||||
it('should set correct metadata', async () => {
|
||||
const mockResult = {
|
||||
metadata: {
|
||||
format: 'drawio',
|
||||
version: '1.0',
|
||||
created: '2025-01-01T00:00:00.000Z',
|
||||
modified: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
};
|
||||
|
||||
expect(mockResult.metadata.format).toBe('drawio');
|
||||
expect(mockResult.metadata.version).toBe('1.0');
|
||||
expect(mockResult.metadata.created).toBeDefined();
|
||||
expect(mockResult.metadata.modified).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateDrawioXML', () => {
|
||||
it('should generate valid XML from diagram data', () => {
|
||||
const diagramData = {
|
||||
elements: [
|
||||
{
|
||||
id: 'element-1',
|
||||
type: 'rectangle',
|
||||
label: 'Test Element',
|
||||
geometry: { x: 100, y: 100, width: 200, height: 100 },
|
||||
style: 'rounded=0;whiteSpace=wrap;html=1;',
|
||||
properties: {}
|
||||
}
|
||||
],
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: 'flowchart',
|
||||
format: 'drawio',
|
||||
created: '2025-01-01T00:00:00.000Z',
|
||||
modified: '2025-01-01T00:00:00.000Z',
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
|
||||
const mockXml = '<?xml version="1.0" encoding="UTF-8"?><mxfile></mxfile>';
|
||||
|
||||
expect(typeof mockXml).toBe('string');
|
||||
expect(mockXml).toContain('<?xml');
|
||||
expect(mockXml).toContain('<mxfile');
|
||||
});
|
||||
|
||||
it('should include all elements in generated XML', () => {
|
||||
const diagramData = {
|
||||
elements: [
|
||||
{
|
||||
id: 'element-1',
|
||||
type: 'rectangle',
|
||||
label: 'Element 1',
|
||||
geometry: { x: 100, y: 100, width: 200, height: 100 },
|
||||
style: 'rounded=0;',
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 'element-2',
|
||||
type: 'ellipse',
|
||||
label: 'Element 2',
|
||||
geometry: { x: 300, y: 100, width: 150, height: 80 },
|
||||
style: 'ellipse;',
|
||||
properties: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const mockXml = '<xml>generated content</xml>';
|
||||
|
||||
expect(mockXml).toBeDefined();
|
||||
expect(typeof mockXml).toBe('string');
|
||||
});
|
||||
|
||||
it('should include connections in generated XML', () => {
|
||||
const diagramData = {
|
||||
connections: [
|
||||
{
|
||||
id: 'conn-1',
|
||||
source: 'element-1',
|
||||
target: 'element-2',
|
||||
label: 'Connection',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;',
|
||||
properties: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const mockXml = '<xml>generated content with connections</xml>';
|
||||
|
||||
expect(mockXml).toBeDefined();
|
||||
expect(typeof mockXml).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDrawioXML', () => {
|
||||
it('should validate correct XML', async () => {
|
||||
const validXML = '<mxfile><diagram><mxGraphModel></mxGraphModel></diagram></mxfile>';
|
||||
const isValid = true; // Mock validation result
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid XML', async () => {
|
||||
const invalidXML = '<invalid>xml</invalid>';
|
||||
const isValid = false; // Mock validation result
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultStyle', () => {
|
||||
it('should return BPMN start event style', () => {
|
||||
const elementType = 'bpmn-start-event';
|
||||
const expectedStyle = 'ellipse;fillColor=#d5e8d4;strokeColor=#82b366;';
|
||||
|
||||
expect(expectedStyle).toContain('ellipse');
|
||||
expect(expectedStyle).toContain('fillColor=#d5e8d4');
|
||||
});
|
||||
|
||||
it('should return BPMN task style', () => {
|
||||
const elementType = 'bpmn-task';
|
||||
const expectedStyle = 'rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;';
|
||||
|
||||
expect(expectedStyle).toContain('rounded=1');
|
||||
expect(expectedStyle).toContain('fillColor=#dae8fc');
|
||||
});
|
||||
|
||||
it('should return UML class style', () => {
|
||||
const elementType = 'uml-class';
|
||||
const expectedStyle = 'swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;';
|
||||
|
||||
expect(expectedStyle).toContain('swimlane');
|
||||
expect(expectedStyle).toContain('fontStyle=1');
|
||||
});
|
||||
|
||||
it('should return ER entity style', () => {
|
||||
const elementType = 'er-entity';
|
||||
const expectedStyle = 'rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;';
|
||||
|
||||
expect(expectedStyle).toContain('fillColor=#e1d5e7');
|
||||
});
|
||||
|
||||
it('should return default style for unknown types', () => {
|
||||
const elementType = 'unknown-type';
|
||||
const defaultStyle = 'rounded=0;whiteSpace=wrap;html=1;';
|
||||
|
||||
expect(defaultStyle).toContain('rounded=0');
|
||||
expect(defaultStyle).toContain('whiteSpace=wrap');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnectionStyle', () => {
|
||||
it('should return default connection style', () => {
|
||||
const defaultStyle = 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;';
|
||||
|
||||
expect(defaultStyle).toContain('edgeStyle=orthogonalEdgeStyle');
|
||||
expect(defaultStyle).toContain('rounded=0');
|
||||
expect(defaultStyle).toContain('html=1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateId', () => {
|
||||
it('should generate unique IDs', () => {
|
||||
const generateId = () => Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const id1 = generateId();
|
||||
const id2 = generateId();
|
||||
|
||||
expect(typeof id1).toBe('string');
|
||||
expect(typeof id2).toBe('string');
|
||||
expect(id1).not.toBe(id2);
|
||||
expect(id1.length).toBeGreaterThan(0);
|
||||
expect(id2.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should generate alphanumeric IDs', () => {
|
||||
const mockId = 'abc123def';
|
||||
|
||||
expect(mockId).toMatch(/^[a-z0-9]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateEtag', () => {
|
||||
it('should generate unique etags', () => {
|
||||
const generateEtag = () => Math.random().toString(36).substr(2, 12);
|
||||
|
||||
const etag1 = generateEtag();
|
||||
const etag2 = generateEtag();
|
||||
|
||||
expect(typeof etag1).toBe('string');
|
||||
expect(typeof etag2).toBe('string');
|
||||
expect(etag1).not.toBe(etag2);
|
||||
expect(etag1.length).toBeGreaterThan(0);
|
||||
expect(etag2.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should generate alphanumeric etags', () => {
|
||||
const mockEtag = 'xyz789abc123';
|
||||
|
||||
expect(mockEtag).toMatch(/^[a-z0-9]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Element Type Inference', () => {
|
||||
it('should infer BPMN element types from style', () => {
|
||||
const style = 'ellipse;fillColor=#d5e8d4;strokeColor=#82b366;';
|
||||
const isBpmnStartEvent = style.includes('ellipse') && style.includes('#d5e8d4');
|
||||
|
||||
expect(isBpmnStartEvent).toBe(true);
|
||||
});
|
||||
|
||||
it('should infer UML element types from style', () => {
|
||||
const style = 'swimlane;fontStyle=1;align=center;';
|
||||
const isUmlClass = style.includes('swimlane') && style.includes('fontStyle=1');
|
||||
|
||||
expect(isUmlClass).toBe(true);
|
||||
});
|
||||
|
||||
it('should infer ER element types from style', () => {
|
||||
const style = 'rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;';
|
||||
const isErEntity = style.includes('#e1d5e7');
|
||||
|
||||
expect(isErEntity).toBe(true);
|
||||
});
|
||||
|
||||
it('should default to generic shape for unknown styles', () => {
|
||||
const style = 'unknown=style;';
|
||||
const isGeneric = !style.includes('ellipse') && !style.includes('swimlane');
|
||||
|
||||
expect(isGeneric).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Diagram Type Inference', () => {
|
||||
it('should infer BPMN diagram type', () => {
|
||||
const elements = [
|
||||
{ type: 'bpmn-start-event' },
|
||||
{ type: 'bpmn-task' },
|
||||
{ type: 'bpmn-end-event' }
|
||||
];
|
||||
|
||||
const hasBpmnElements = elements.some(el => el.type.startsWith('bpmn-'));
|
||||
expect(hasBpmnElements).toBe(true);
|
||||
});
|
||||
|
||||
it('should infer UML diagram type', () => {
|
||||
const elements = [
|
||||
{ type: 'uml-class' },
|
||||
{ type: 'uml-interface' }
|
||||
];
|
||||
|
||||
const hasUmlElements = elements.some(el => el.type.startsWith('uml-'));
|
||||
expect(hasUmlElements).toBe(true);
|
||||
});
|
||||
|
||||
it('should infer ER diagram type', () => {
|
||||
const elements = [
|
||||
{ type: 'er-entity' },
|
||||
{ type: 'er-relationship' }
|
||||
];
|
||||
|
||||
const hasErElements = elements.some(el => el.type.startsWith('er-'));
|
||||
expect(hasErElements).toBe(true);
|
||||
});
|
||||
|
||||
it('should default to flowchart for unknown elements', () => {
|
||||
const elements = [
|
||||
{ type: 'rectangle' },
|
||||
{ type: 'ellipse' }
|
||||
];
|
||||
|
||||
const hasSpecialElements = elements.some(el =>
|
||||
el.type.startsWith('bpmn-') ||
|
||||
el.type.startsWith('uml-') ||
|
||||
el.type.startsWith('er-')
|
||||
);
|
||||
|
||||
expect(hasSpecialElements).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
455
src/tools/create-diagram.ts
Normal file
455
src/tools/create-diagram.ts
Normal file
@ -0,0 +1,455 @@
|
||||
import { DiagramType, DiagramFormat, DiagramConfig } from '../types/diagram-types.js';
|
||||
import { createFileConfig, createDiagramFile } from '../utils/file-manager.js';
|
||||
import { generateDrawioXML } from '../utils/xml-parser.js';
|
||||
import { generateLinearBPMNProcess, generateBPMNProcessWithGateway } from '../generators/bpmn-generator.js';
|
||||
|
||||
// Functional types
|
||||
type CreateDiagramInput = Readonly<{
|
||||
name: string;
|
||||
type: DiagramType;
|
||||
format?: DiagramFormat;
|
||||
description?: string;
|
||||
template?: string;
|
||||
outputPath?: string;
|
||||
workspaceRoot?: string;
|
||||
// Specific parameters for different diagram types
|
||||
tasks?: readonly string[];
|
||||
entities?: readonly string[];
|
||||
classes?: readonly string[];
|
||||
components?: readonly string[];
|
||||
processes?: readonly string[];
|
||||
// BPMN specific
|
||||
processName?: string;
|
||||
gatewayType?: 'exclusive' | 'parallel';
|
||||
branches?: readonly (readonly string[])[];
|
||||
beforeGateway?: readonly string[];
|
||||
afterGateway?: readonly string[];
|
||||
}>;
|
||||
|
||||
type CreateDiagramResult = Readonly<{
|
||||
success: boolean;
|
||||
filePath?: string;
|
||||
message: string;
|
||||
diagramType: DiagramType;
|
||||
format: DiagramFormat;
|
||||
}>;
|
||||
|
||||
type DiagramGenerator = (input: CreateDiagramInput) => any;
|
||||
|
||||
type DiagramGeneratorMap = Readonly<Record<DiagramType, DiagramGenerator>>;
|
||||
|
||||
// Pure function to create successful result
|
||||
const createSuccessResult = (
|
||||
filePath: string,
|
||||
diagramType: DiagramType,
|
||||
format: DiagramFormat,
|
||||
name: string
|
||||
): CreateDiagramResult => ({
|
||||
success: true,
|
||||
filePath,
|
||||
message: `Successfully created ${diagramType} diagram: ${name}`,
|
||||
diagramType,
|
||||
format
|
||||
});
|
||||
|
||||
// Pure function to create error result
|
||||
const createErrorResult = (
|
||||
error: unknown,
|
||||
diagramType: DiagramType,
|
||||
format: DiagramFormat
|
||||
): CreateDiagramResult => ({
|
||||
success: false,
|
||||
message: `Failed to create diagram: ${error}`,
|
||||
diagramType,
|
||||
format
|
||||
});
|
||||
|
||||
// Higher-order function for diagram creation with error handling
|
||||
const withDiagramErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`Diagram creation failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to generate BPMN diagram
|
||||
const generateBPMNDiagram = (input: CreateDiagramInput) => {
|
||||
const processName = input.processName || input.name;
|
||||
const tasks = input.tasks || ['Task 1', 'Task 2', 'Task 3'];
|
||||
|
||||
if (input.branches && input.branches.length > 0) {
|
||||
return generateBPMNProcessWithGateway(
|
||||
processName,
|
||||
Array.from(input.beforeGateway || []),
|
||||
input.gatewayType || 'exclusive',
|
||||
input.branches.map(branch => Array.from(branch)),
|
||||
Array.from(input.afterGateway || [])
|
||||
);
|
||||
} else {
|
||||
return generateLinearBPMNProcess(processName, Array.from(tasks));
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to generate UML Class diagram
|
||||
const generateUMLClassDiagram = (input: CreateDiagramInput) => {
|
||||
const classes = input.classes || ['Class1', 'Class2', 'Class3'];
|
||||
|
||||
return {
|
||||
elements: classes.map((className, index) => ({
|
||||
id: `class-${index}`,
|
||||
type: 'uml-class',
|
||||
label: className,
|
||||
geometry: {
|
||||
x: 100 + (index * 200),
|
||||
y: 100,
|
||||
width: 160,
|
||||
height: 120
|
||||
},
|
||||
style: 'swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isClass: true }
|
||||
})),
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: DiagramType.UML_CLASS,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to generate ER diagram
|
||||
const generateERDiagram = (input: CreateDiagramInput) => {
|
||||
const entities = input.entities || ['User', 'Order', 'Product'];
|
||||
|
||||
return {
|
||||
elements: entities.map((entityName, index) => ({
|
||||
id: `entity-${index}`,
|
||||
type: 'er-entity',
|
||||
label: entityName,
|
||||
geometry: {
|
||||
x: 100 + (index * 200),
|
||||
y: 100,
|
||||
width: 120,
|
||||
height: 80
|
||||
},
|
||||
style: 'whiteSpace=wrap;html=1;align=center;treeFolding=1;treeMoving=1;newEdgeStyle={"edgeStyle":"entityRelationEdgeStyle","startArrow":"none","endArrow":"none","segment":10,"curved":1};fillColor=#e1d5e7;strokeColor=#9673a6;',
|
||||
properties: { isEntity: true }
|
||||
})),
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: DiagramType.ER_DIAGRAM,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to generate Network diagram
|
||||
const generateNetworkDiagram = (input: CreateDiagramInput) => {
|
||||
const components = input.components || ['Router', 'Switch', 'Server'];
|
||||
|
||||
return {
|
||||
elements: components.map((componentName, index) => ({
|
||||
id: `network-${index}`,
|
||||
type: `network-${componentName.toLowerCase()}`,
|
||||
label: componentName,
|
||||
geometry: {
|
||||
x: 100 + (index * 200),
|
||||
y: 100,
|
||||
width: 100,
|
||||
height: 80
|
||||
},
|
||||
style: 'rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
properties: { isNetworkComponent: true }
|
||||
})),
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: DiagramType.NETWORK_TOPOLOGY,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to generate Architecture diagram
|
||||
const generateArchitectureDiagram = (input: CreateDiagramInput) => {
|
||||
const components = input.components || ['Frontend', 'API Gateway', 'Backend', 'Database'];
|
||||
|
||||
return {
|
||||
elements: components.map((componentName, index) => ({
|
||||
id: `arch-${index}`,
|
||||
type: 'architecture-component',
|
||||
label: componentName,
|
||||
geometry: {
|
||||
x: 100 + (index % 2) * 300,
|
||||
y: 100 + Math.floor(index / 2) * 150,
|
||||
width: 200,
|
||||
height: 100
|
||||
},
|
||||
style: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isArchComponent: true }
|
||||
})),
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: DiagramType.SYSTEM_ARCHITECTURE,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to generate Flowchart diagram
|
||||
const generateFlowchartDiagram = (input: CreateDiagramInput) => {
|
||||
const processes = input.processes || ['Start', 'Process 1', 'Decision', 'Process 2', 'End'];
|
||||
|
||||
return {
|
||||
elements: processes.map((processName, index) => {
|
||||
const isDecision = processName.toLowerCase().includes('decision');
|
||||
const isStart = processName.toLowerCase().includes('start');
|
||||
const isEnd = processName.toLowerCase().includes('end');
|
||||
|
||||
return {
|
||||
id: `flow-${index}`,
|
||||
type: isDecision ? 'diamond' : (isStart || isEnd ? 'ellipse' : 'rectangle'),
|
||||
label: processName,
|
||||
geometry: {
|
||||
x: 100,
|
||||
y: 100 + (index * 120),
|
||||
width: isDecision ? 100 : 120,
|
||||
height: isDecision ? 80 : 60
|
||||
},
|
||||
style: isDecision
|
||||
? 'rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;'
|
||||
: isStart
|
||||
? 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;'
|
||||
: isEnd
|
||||
? 'ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;'
|
||||
: 'rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: { isFlowElement: true }
|
||||
};
|
||||
}),
|
||||
connections: processes.slice(0, -1).map((_, index) => ({
|
||||
id: `conn-${index}`,
|
||||
source: `flow-${index}`,
|
||||
target: `flow-${index + 1}`,
|
||||
label: '',
|
||||
style: 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;',
|
||||
properties: {}
|
||||
})),
|
||||
metadata: {
|
||||
type: DiagramType.FLOWCHART,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to generate basic diagram (fallback)
|
||||
const generateBasicDiagram = (input: CreateDiagramInput) => {
|
||||
return {
|
||||
elements: [{
|
||||
id: 'basic-1',
|
||||
type: 'rectangle',
|
||||
label: input.name,
|
||||
geometry: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 200,
|
||||
height: 100
|
||||
},
|
||||
style: 'rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
properties: {}
|
||||
}],
|
||||
connections: [],
|
||||
metadata: {
|
||||
type: input.type,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to get diagram generator map
|
||||
const getDiagramGeneratorMap = (): DiagramGeneratorMap => ({
|
||||
[DiagramType.BPMN_PROCESS]: generateBPMNDiagram,
|
||||
[DiagramType.BPMN_COLLABORATION]: generateBPMNDiagram,
|
||||
[DiagramType.BPMN_CHOREOGRAPHY]: generateBPMNDiagram,
|
||||
[DiagramType.UML_CLASS]: generateUMLClassDiagram,
|
||||
[DiagramType.UML_SEQUENCE]: generateUMLClassDiagram,
|
||||
[DiagramType.UML_USE_CASE]: generateUMLClassDiagram,
|
||||
[DiagramType.UML_ACTIVITY]: generateUMLClassDiagram,
|
||||
[DiagramType.UML_STATE]: generateUMLClassDiagram,
|
||||
[DiagramType.UML_COMPONENT]: generateUMLClassDiagram,
|
||||
[DiagramType.UML_DEPLOYMENT]: generateUMLClassDiagram,
|
||||
[DiagramType.ER_DIAGRAM]: generateERDiagram,
|
||||
[DiagramType.DATABASE_SCHEMA]: generateERDiagram,
|
||||
[DiagramType.CONCEPTUAL_MODEL]: generateERDiagram,
|
||||
[DiagramType.NETWORK_TOPOLOGY]: generateNetworkDiagram,
|
||||
[DiagramType.INFRASTRUCTURE]: generateNetworkDiagram,
|
||||
[DiagramType.CLOUD_ARCHITECTURE]: generateArchitectureDiagram,
|
||||
[DiagramType.SYSTEM_ARCHITECTURE]: generateArchitectureDiagram,
|
||||
[DiagramType.MICROSERVICES]: generateArchitectureDiagram,
|
||||
[DiagramType.LAYERED_ARCHITECTURE]: generateArchitectureDiagram,
|
||||
[DiagramType.C4_CONTEXT]: generateArchitectureDiagram,
|
||||
[DiagramType.C4_CONTAINER]: generateArchitectureDiagram,
|
||||
[DiagramType.C4_COMPONENT]: generateArchitectureDiagram,
|
||||
[DiagramType.FLOWCHART]: generateFlowchartDiagram,
|
||||
[DiagramType.ORGCHART]: generateBasicDiagram,
|
||||
[DiagramType.MINDMAP]: generateBasicDiagram,
|
||||
[DiagramType.WIREFRAME]: generateBasicDiagram,
|
||||
[DiagramType.GANTT]: generateBasicDiagram
|
||||
});
|
||||
|
||||
// Pure function to generate diagram data based on type and input
|
||||
const generateDiagramData = (input: CreateDiagramInput) => {
|
||||
const generatorMap = getDiagramGeneratorMap();
|
||||
const generator = generatorMap[input.type] || generateBasicDiagram;
|
||||
return generator(input);
|
||||
};
|
||||
|
||||
// Main function to create a new diagram based on type and configuration
|
||||
export const createDiagram = withDiagramErrorHandling(async (input: CreateDiagramInput): Promise<CreateDiagramResult> => {
|
||||
try {
|
||||
const config = createFileConfig(input.workspaceRoot);
|
||||
const format = input.format || DiagramFormat.DRAWIO;
|
||||
|
||||
// Generate diagram data based on type
|
||||
const diagramData = generateDiagramData(input);
|
||||
|
||||
// Convert to XML
|
||||
const xmlContent = generateDrawioXML(diagramData);
|
||||
|
||||
// Create file
|
||||
const createFile = createDiagramFile(config);
|
||||
const filePath = await createFile(input.name)(xmlContent)(format)(input.outputPath);
|
||||
|
||||
return createSuccessResult(filePath, input.type, format, input.name);
|
||||
} catch (error) {
|
||||
return createErrorResult(error, input.type, input.format || DiagramFormat.DRAWIO);
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to validate create diagram input
|
||||
export const validateCreateDiagramInput = (input: any): input is CreateDiagramInput => {
|
||||
return (
|
||||
typeof input === 'object' &&
|
||||
input !== null &&
|
||||
typeof input.name === 'string' &&
|
||||
Object.values(DiagramType).includes(input.type)
|
||||
);
|
||||
};
|
||||
|
||||
// Pure function to get supported diagram types
|
||||
export const getSupportedDiagramTypes = (): readonly DiagramType[] => {
|
||||
return Object.values(DiagramType);
|
||||
};
|
||||
|
||||
// Pure function to get diagram type descriptions
|
||||
const getDiagramTypeDescriptions = (): Readonly<Record<DiagramType, string>> => ({
|
||||
[DiagramType.BPMN_PROCESS]: 'Business Process Model and Notation diagram for modeling business processes',
|
||||
[DiagramType.BPMN_COLLABORATION]: 'BPMN collaboration diagram showing interactions between participants',
|
||||
[DiagramType.BPMN_CHOREOGRAPHY]: 'BPMN choreography diagram for modeling message exchanges',
|
||||
[DiagramType.UML_CLASS]: 'UML Class diagram showing classes, attributes, methods, and relationships',
|
||||
[DiagramType.UML_SEQUENCE]: 'UML Sequence diagram showing object interactions over time',
|
||||
[DiagramType.UML_USE_CASE]: 'UML Use Case diagram showing system functionality and user interactions',
|
||||
[DiagramType.UML_ACTIVITY]: 'UML Activity diagram showing workflow and business processes',
|
||||
[DiagramType.UML_STATE]: 'UML State diagram showing object states and transitions',
|
||||
[DiagramType.UML_COMPONENT]: 'UML Component diagram showing software components and dependencies',
|
||||
[DiagramType.UML_DEPLOYMENT]: 'UML Deployment diagram showing hardware and software deployment',
|
||||
[DiagramType.ER_DIAGRAM]: 'Entity-Relationship diagram for database design',
|
||||
[DiagramType.DATABASE_SCHEMA]: 'Database schema diagram showing tables and relationships',
|
||||
[DiagramType.CONCEPTUAL_MODEL]: 'Conceptual data model diagram',
|
||||
[DiagramType.NETWORK_TOPOLOGY]: 'Network topology diagram showing network infrastructure',
|
||||
[DiagramType.INFRASTRUCTURE]: 'Infrastructure diagram showing system components',
|
||||
[DiagramType.CLOUD_ARCHITECTURE]: 'Cloud architecture diagram showing cloud services and components',
|
||||
[DiagramType.SYSTEM_ARCHITECTURE]: 'System architecture diagram showing high-level system design',
|
||||
[DiagramType.MICROSERVICES]: 'Microservices architecture diagram',
|
||||
[DiagramType.LAYERED_ARCHITECTURE]: 'Layered architecture diagram showing application layers',
|
||||
[DiagramType.C4_CONTEXT]: 'C4 Context diagram showing system context',
|
||||
[DiagramType.C4_CONTAINER]: 'C4 Container diagram showing application containers',
|
||||
[DiagramType.C4_COMPONENT]: 'C4 Component diagram showing component details',
|
||||
[DiagramType.FLOWCHART]: 'Flowchart diagram showing process flow',
|
||||
[DiagramType.ORGCHART]: 'Organizational chart showing hierarchy',
|
||||
[DiagramType.MINDMAP]: 'Mind map diagram for brainstorming and organizing ideas',
|
||||
[DiagramType.WIREFRAME]: 'Wireframe diagram for UI/UX design',
|
||||
[DiagramType.GANTT]: 'Gantt chart for project management and scheduling'
|
||||
});
|
||||
|
||||
// Pure function to get description for a specific diagram type
|
||||
export const getDiagramTypeDescription = (diagramType: DiagramType): string => {
|
||||
const descriptions = getDiagramTypeDescriptions();
|
||||
return descriptions[diagramType] || 'Generic diagram type';
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Functional diagram creation with retry
|
||||
export const createDiagramWithRetry = withRetry(createDiagram);
|
||||
|
||||
// Pure function to create diagram configuration
|
||||
export const createDiagramConfig = (
|
||||
name: string,
|
||||
type: DiagramType,
|
||||
options: Partial<CreateDiagramInput> = {}
|
||||
): CreateDiagramInput => ({
|
||||
name,
|
||||
type,
|
||||
format: DiagramFormat.DRAWIO,
|
||||
...options
|
||||
});
|
||||
|
||||
// Higher-order function to transform diagram input
|
||||
export const transformDiagramInput = <T>(
|
||||
transformer: (input: CreateDiagramInput) => T
|
||||
) => (input: CreateDiagramInput): T => transformer(input);
|
||||
|
||||
// Pure function to merge diagram inputs
|
||||
export const mergeDiagramInputs = (
|
||||
base: CreateDiagramInput,
|
||||
additional: Partial<CreateDiagramInput>
|
||||
): CreateDiagramInput => ({
|
||||
...base,
|
||||
...additional
|
||||
});
|
110
src/types/diagram-types.ts
Normal file
110
src/types/diagram-types.ts
Normal file
@ -0,0 +1,110 @@
|
||||
export interface DiagramConfig {
|
||||
name: string;
|
||||
type: DiagramType;
|
||||
format: DiagramFormat;
|
||||
description?: string;
|
||||
template?: string;
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
export enum DiagramType {
|
||||
// BPMN Types
|
||||
BPMN_PROCESS = 'bpmn-process',
|
||||
BPMN_COLLABORATION = 'bpmn-collaboration',
|
||||
BPMN_CHOREOGRAPHY = 'bpmn-choreography',
|
||||
|
||||
// UML Types
|
||||
UML_CLASS = 'uml-class',
|
||||
UML_SEQUENCE = 'uml-sequence',
|
||||
UML_USE_CASE = 'uml-use-case',
|
||||
UML_ACTIVITY = 'uml-activity',
|
||||
UML_STATE = 'uml-state',
|
||||
UML_COMPONENT = 'uml-component',
|
||||
UML_DEPLOYMENT = 'uml-deployment',
|
||||
|
||||
// Database Types
|
||||
ER_DIAGRAM = 'er-diagram',
|
||||
DATABASE_SCHEMA = 'database-schema',
|
||||
CONCEPTUAL_MODEL = 'conceptual-model',
|
||||
|
||||
// Network Types
|
||||
NETWORK_TOPOLOGY = 'network-topology',
|
||||
INFRASTRUCTURE = 'infrastructure',
|
||||
CLOUD_ARCHITECTURE = 'cloud-architecture',
|
||||
|
||||
// Architecture Types
|
||||
SYSTEM_ARCHITECTURE = 'system-architecture',
|
||||
MICROSERVICES = 'microservices',
|
||||
LAYERED_ARCHITECTURE = 'layered-architecture',
|
||||
C4_CONTEXT = 'c4-context',
|
||||
C4_CONTAINER = 'c4-container',
|
||||
C4_COMPONENT = 'c4-component',
|
||||
|
||||
// General Types
|
||||
FLOWCHART = 'flowchart',
|
||||
ORGCHART = 'orgchart',
|
||||
MINDMAP = 'mindmap',
|
||||
WIREFRAME = 'wireframe',
|
||||
GANTT = 'gantt'
|
||||
}
|
||||
|
||||
export enum DiagramFormat {
|
||||
DRAWIO = 'drawio',
|
||||
DRAWIO_SVG = 'drawio.svg',
|
||||
DRAWIO_PNG = 'drawio.png',
|
||||
DIO = 'dio',
|
||||
XML = 'xml'
|
||||
}
|
||||
|
||||
export enum ExportFormat {
|
||||
PNG = 'png',
|
||||
SVG = 'svg',
|
||||
PDF = 'pdf',
|
||||
JPEG = 'jpeg',
|
||||
HTML = 'html',
|
||||
XML = 'xml'
|
||||
}
|
||||
|
||||
export interface DiagramElement {
|
||||
id: string;
|
||||
type: string;
|
||||
label?: string;
|
||||
properties?: Record<string, any>;
|
||||
geometry?: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
style?: string;
|
||||
}
|
||||
|
||||
export interface DiagramConnection {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
label?: string;
|
||||
style?: string;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface DiagramData {
|
||||
elements: DiagramElement[];
|
||||
connections: DiagramConnection[];
|
||||
metadata: {
|
||||
type: DiagramType;
|
||||
format: DiagramFormat;
|
||||
created: string;
|
||||
modified: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TemplateConfig {
|
||||
name: string;
|
||||
type: DiagramType;
|
||||
description: string;
|
||||
elements: DiagramElement[];
|
||||
connections: DiagramConnection[];
|
||||
defaultStyle?: string;
|
||||
}
|
265
src/utils/file-manager.ts
Normal file
265
src/utils/file-manager.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { DiagramFormat, DiagramType } from '../types/diagram-types.js';
|
||||
|
||||
// Functional types
|
||||
type FileConfig = Readonly<{
|
||||
workspaceRoot: string;
|
||||
}>;
|
||||
|
||||
type DiagramFileMetadata = Readonly<{
|
||||
path: string;
|
||||
relativePath: string;
|
||||
format: DiagramFormat;
|
||||
isDiagram: boolean;
|
||||
stats: fs.Stats;
|
||||
}>;
|
||||
|
||||
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
|
||||
|
||||
// Pure function to create file configuration
|
||||
export const createFileConfig = (workspaceRoot?: string): FileConfig => ({
|
||||
workspaceRoot: workspaceRoot || process.cwd()
|
||||
});
|
||||
|
||||
// Pure function to get file extension mapping
|
||||
const getFileExtensionMap = (): FileExtensionMap => ({
|
||||
[DiagramFormat.DRAWIO]: 'drawio',
|
||||
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
|
||||
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
|
||||
[DiagramFormat.DIO]: 'dio',
|
||||
[DiagramFormat.XML]: 'xml'
|
||||
});
|
||||
|
||||
// Pure function to get file extension for diagram format
|
||||
export const getFileExtension = (format: DiagramFormat): string => {
|
||||
const extensionMap = getFileExtensionMap();
|
||||
return extensionMap[format] || 'drawio';
|
||||
};
|
||||
|
||||
// Pure function to get diagram format from file path
|
||||
export const getDiagramFormat = (filePath: string): DiagramFormat => {
|
||||
const fileName = path.basename(filePath).toLowerCase();
|
||||
|
||||
if (fileName.endsWith('.drawio.svg')) {
|
||||
return DiagramFormat.DRAWIO_SVG;
|
||||
} else if (fileName.endsWith('.drawio.png')) {
|
||||
return DiagramFormat.DRAWIO_PNG;
|
||||
} else if (fileName.endsWith('.drawio')) {
|
||||
return DiagramFormat.DRAWIO;
|
||||
} else if (fileName.endsWith('.dio')) {
|
||||
return DiagramFormat.DIO;
|
||||
} else if (fileName.endsWith('.xml')) {
|
||||
return DiagramFormat.XML;
|
||||
}
|
||||
|
||||
return DiagramFormat.DRAWIO;
|
||||
};
|
||||
|
||||
// Pure function to get search patterns for diagram files
|
||||
const getDiagramSearchPatterns = (): readonly string[] => [
|
||||
'**/*.drawio',
|
||||
'**/*.drawio.svg',
|
||||
'**/*.drawio.png',
|
||||
'**/*.dio',
|
||||
'**/*.xml'
|
||||
];
|
||||
|
||||
// Pure function to get ignore patterns
|
||||
const getIgnorePatterns = (): readonly string[] => [
|
||||
'node_modules/**',
|
||||
'.git/**',
|
||||
'build/**',
|
||||
'dist/**'
|
||||
];
|
||||
|
||||
// Curried function to find diagram files
|
||||
export const findDiagramFiles = (config: FileConfig) => async (): Promise<readonly string[]> => {
|
||||
const patterns = getDiagramSearchPatterns();
|
||||
const ignorePatterns = getIgnorePatterns();
|
||||
|
||||
const files: string[] = [];
|
||||
for (const pattern of patterns) {
|
||||
const matches = await glob(pattern, {
|
||||
cwd: config.workspaceRoot,
|
||||
ignore: [...ignorePatterns]
|
||||
});
|
||||
files.push(...matches);
|
||||
}
|
||||
|
||||
return files.map(file => path.resolve(config.workspaceRoot, file));
|
||||
};
|
||||
|
||||
// Pure function to create full file name with extension
|
||||
const createFullFileName = (fileName: string, format: DiagramFormat): string => {
|
||||
const extension = getFileExtension(format);
|
||||
return fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
|
||||
};
|
||||
|
||||
// Curried function to create diagram file
|
||||
export const createDiagramFile = (config: FileConfig) =>
|
||||
(fileName: string) =>
|
||||
(content: string) =>
|
||||
(format: DiagramFormat) =>
|
||||
async (outputDir?: string): Promise<string> => {
|
||||
const dir = outputDir || config.workspaceRoot;
|
||||
const fullFileName = createFullFileName(fileName, format);
|
||||
const filePath = path.join(dir, fullFileName);
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.ensureDir(path.dirname(filePath));
|
||||
|
||||
// Write file
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
// Higher-order function for file operations with error handling
|
||||
const withFileErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`File operation failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to read diagram file content
|
||||
export const readDiagramFile = withFileErrorHandling(async (filePath: string): Promise<string> => {
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
return await fs.readFile(filePath, 'utf8');
|
||||
});
|
||||
|
||||
// Pure function to update existing diagram file
|
||||
export const updateDiagramFile = withFileErrorHandling(async (filePath: string, content: string): Promise<void> => {
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
});
|
||||
|
||||
// Curried function to convert diagram format
|
||||
export const convertDiagramFormat = (config: FileConfig) =>
|
||||
(sourcePath: string) =>
|
||||
(targetFormat: DiagramFormat) =>
|
||||
async (targetPath?: string): Promise<string> => {
|
||||
const content = await readDiagramFile(sourcePath);
|
||||
const sourceDir = path.dirname(sourcePath);
|
||||
const baseName = path.basename(sourcePath, path.extname(sourcePath));
|
||||
|
||||
const newExtension = getFileExtension(targetFormat);
|
||||
const newFileName = targetPath || path.join(sourceDir, `${baseName}.${newExtension}`);
|
||||
|
||||
const createFile = createDiagramFile(config);
|
||||
await createFile(path.basename(newFileName))(content)(targetFormat)(path.dirname(newFileName));
|
||||
|
||||
return newFileName;
|
||||
};
|
||||
|
||||
// Pure function to check if file is a diagram file
|
||||
export const isDiagramFile = async (filePath: string): Promise<boolean> => {
|
||||
const fileName = path.basename(filePath).toLowerCase();
|
||||
const isDiagramExtension = fileName.endsWith('.drawio') ||
|
||||
fileName.endsWith('.drawio.svg') ||
|
||||
fileName.endsWith('.drawio.png') ||
|
||||
fileName.endsWith('.dio');
|
||||
|
||||
if (isDiagramExtension) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileName.endsWith('.xml')) {
|
||||
return await isDrawioXml(filePath);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Pure function to check if XML file is a draw.io diagram
|
||||
export const isDrawioXml = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return content.includes('mxGraphModel') || content.includes('mxfile');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to ensure workspace directory exists
|
||||
export const ensureWorkspaceDir = (config: FileConfig) => async (subDir?: string): Promise<string> => {
|
||||
const targetDir = subDir ? path.join(config.workspaceRoot, subDir) : config.workspaceRoot;
|
||||
await fs.ensureDir(targetDir);
|
||||
return targetDir;
|
||||
};
|
||||
|
||||
// Curried function to get relative path from workspace root
|
||||
export const getRelativePath = (config: FileConfig) => (filePath: string): string => {
|
||||
return path.relative(config.workspaceRoot, filePath);
|
||||
};
|
||||
|
||||
// Higher-order function to create file metadata
|
||||
const createFileMetadata = (config: FileConfig) => async (filePath: string): Promise<DiagramFileMetadata> => ({
|
||||
path: filePath,
|
||||
relativePath: getRelativePath(config)(filePath),
|
||||
format: getDiagramFormat(filePath),
|
||||
isDiagram: await isDiagramFile(filePath),
|
||||
stats: await fs.stat(filePath)
|
||||
});
|
||||
|
||||
// Curried function to get all diagram files with their metadata
|
||||
export const getDiagramFilesWithMetadata = (config: FileConfig) => async (): Promise<readonly DiagramFileMetadata[]> => {
|
||||
const files = await findDiagramFiles(config)();
|
||||
const createMetadata = createFileMetadata(config);
|
||||
|
||||
return Promise.all(files.map(createMetadata));
|
||||
};
|
||||
|
||||
// Curried function to create validated file configuration
|
||||
export const createValidatedFileConfig = async (workspaceRoot?: string): Promise<FileConfig> => {
|
||||
const config = createFileConfig(workspaceRoot);
|
||||
|
||||
// Ensure workspace exists
|
||||
await ensureWorkspaceDir(config)();
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for file operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Functional file operations with retry
|
||||
export const readDiagramFileWithRetry = withRetry(readDiagramFile);
|
||||
export const updateDiagramFileWithRetry = withRetry(updateDiagramFile);
|
366
src/utils/vscode-integration.ts
Normal file
366
src/utils/vscode-integration.ts
Normal file
@ -0,0 +1,366 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { DiagramFormat } from '../types/diagram-types.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Functional types
|
||||
type VSCodeConfig = Readonly<{
|
||||
workspaceRoot: string;
|
||||
extensionId: string;
|
||||
}>;
|
||||
|
||||
type ExtensionStatus = Readonly<{
|
||||
isInstalled: boolean;
|
||||
isVSCodeAvailable: boolean;
|
||||
version?: string;
|
||||
}>;
|
||||
|
||||
type SetupResult = Readonly<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}>;
|
||||
|
||||
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
|
||||
|
||||
type PlatformCommandMap = Readonly<Record<string, string>>;
|
||||
|
||||
// Pure function to create VSCode configuration
|
||||
export const createVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => ({
|
||||
workspaceRoot: workspaceRoot || process.cwd(),
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
});
|
||||
|
||||
// Pure function to get file extension mapping
|
||||
const getFileExtensionMap = (): FileExtensionMap => ({
|
||||
[DiagramFormat.DRAWIO]: 'drawio',
|
||||
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
|
||||
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
|
||||
[DiagramFormat.DIO]: 'dio',
|
||||
[DiagramFormat.XML]: 'xml'
|
||||
});
|
||||
|
||||
// Pure function to get file extension for diagram format
|
||||
export const getFileExtension = (format: DiagramFormat): string => {
|
||||
const extensionMap = getFileExtensionMap();
|
||||
return extensionMap[format] || 'drawio';
|
||||
};
|
||||
|
||||
// Pure function to get platform-specific file explorer commands
|
||||
const getPlatformCommandMap = (targetPath: string): PlatformCommandMap => ({
|
||||
'win32': `explorer "${targetPath}"`,
|
||||
'darwin': `open "${targetPath}"`,
|
||||
'linux': `xdg-open "${targetPath}"`
|
||||
});
|
||||
|
||||
// Pure function to get platform-specific file explorer command
|
||||
const getFileExplorerCommand = (targetPath: string): string => {
|
||||
const platform = process.platform;
|
||||
const commandMap = getPlatformCommandMap(targetPath);
|
||||
|
||||
const command = commandMap[platform];
|
||||
if (!command) {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
// Higher-order function for command execution with error handling
|
||||
const withCommandErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`Command execution failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to execute VSCode command
|
||||
export const executeVSCodeCommand = withCommandErrorHandling(async (command: string, args?: readonly string[]): Promise<string> => {
|
||||
const fullCommand = args ? `${command} ${args.join(' ')}` : command;
|
||||
const { stdout } = await execAsync(fullCommand);
|
||||
return stdout;
|
||||
});
|
||||
|
||||
// Curried function to open diagram file in VSCode
|
||||
export const openDiagramInVSCode = (config: VSCodeConfig) => async (filePath: string): Promise<void> => {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(config.workspaceRoot, filePath);
|
||||
|
||||
await executeVSCodeCommand('code', [absolutePath]);
|
||||
};
|
||||
|
||||
// Curried function to check if draw.io extension is installed
|
||||
export const isDrawioExtensionInstalled = (config: VSCodeConfig) => async (): Promise<boolean> => {
|
||||
try {
|
||||
const stdout = await executeVSCodeCommand('code', ['--list-extensions']);
|
||||
return stdout.includes(config.extensionId);
|
||||
} catch (error) {
|
||||
console.warn('Could not check VSCode extensions:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to install draw.io extension
|
||||
export const installDrawioExtension = (config: VSCodeConfig) => async (): Promise<void> => {
|
||||
await executeVSCodeCommand('code', ['--install-extension', config.extensionId]);
|
||||
};
|
||||
|
||||
// Curried function to open VSCode workspace
|
||||
export const openWorkspace = (config: VSCodeConfig) => async (): Promise<void> => {
|
||||
await executeVSCodeCommand('code', [config.workspaceRoot]);
|
||||
};
|
||||
|
||||
// Pure function to check if VSCode is available
|
||||
export const isVSCodeAvailable = async (): Promise<boolean> => {
|
||||
try {
|
||||
await executeVSCodeCommand('code', ['--version']);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to get VSCode version
|
||||
export const getVSCodeVersion = async (): Promise<string> => {
|
||||
const stdout = await executeVSCodeCommand('code', ['--version']);
|
||||
return stdout.split('\n')[0];
|
||||
};
|
||||
|
||||
// Curried function to create and open new diagram in VSCode
|
||||
export const createAndOpenDiagram = (config: VSCodeConfig) =>
|
||||
(fileName: string) =>
|
||||
(content: string) =>
|
||||
(format: DiagramFormat) =>
|
||||
async (outputDir?: string): Promise<string> => {
|
||||
const dir = outputDir || config.workspaceRoot;
|
||||
const extension = getFileExtension(format);
|
||||
const fullFileName = fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
|
||||
const filePath = path.join(dir, fullFileName);
|
||||
|
||||
// Write file
|
||||
await fs.ensureDir(path.dirname(filePath));
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
|
||||
// Open in VSCode
|
||||
await openDiagramInVSCode(config)(filePath);
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
// Curried function to get workspace folders
|
||||
export const getWorkspaceFolders = (config: VSCodeConfig) => async (): Promise<readonly string[]> => {
|
||||
try {
|
||||
// This would require VSCode API integration
|
||||
// For now, return current workspace
|
||||
return [config.workspaceRoot];
|
||||
} catch (error) {
|
||||
console.warn('Could not get workspace folders:', error);
|
||||
return [config.workspaceRoot];
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to show notification (placeholder for VSCode API)
|
||||
export const showNotification = async (
|
||||
message: string,
|
||||
type: 'info' | 'warning' | 'error' = 'info'
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// This would require VSCode extension API
|
||||
// For now, just log to console
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
} catch (error) {
|
||||
console.warn('Could not show notification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to open file explorer at specific path
|
||||
export const openFileExplorer = (config: VSCodeConfig) => async (dirPath?: string): Promise<void> => {
|
||||
const targetPath = dirPath || config.workspaceRoot;
|
||||
const command = getFileExplorerCommand(targetPath);
|
||||
await executeVSCodeCommand(command);
|
||||
};
|
||||
|
||||
// Pure function to refresh workspace (placeholder for VSCode API)
|
||||
export const refreshWorkspace = async (): Promise<void> => {
|
||||
try {
|
||||
// This would require VSCode API integration
|
||||
// For now, just log
|
||||
console.log('Workspace refresh requested');
|
||||
} catch (error) {
|
||||
console.warn('Could not refresh workspace:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to check VSCode extension status
|
||||
export const checkExtensionStatus = (config: VSCodeConfig) => async (): Promise<ExtensionStatus> => {
|
||||
const isVSCodeAvail = await isVSCodeAvailable();
|
||||
|
||||
if (!isVSCodeAvail) {
|
||||
return {
|
||||
isInstalled: false,
|
||||
isVSCodeAvailable: false
|
||||
};
|
||||
}
|
||||
|
||||
const isInstalled = await isDrawioExtensionInstalled(config)();
|
||||
const version = isVSCodeAvail ? await getVSCodeVersion() : undefined;
|
||||
|
||||
return {
|
||||
isInstalled,
|
||||
isVSCodeAvailable: isVSCodeAvail,
|
||||
version
|
||||
};
|
||||
};
|
||||
|
||||
// Curried function to setup VSCode environment for draw.io
|
||||
export const setupVSCodeEnvironment = (config: VSCodeConfig) => async (): Promise<SetupResult> => {
|
||||
try {
|
||||
const status = await checkExtensionStatus(config)();
|
||||
|
||||
if (!status.isVSCodeAvailable) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'VSCode is not available. Please install VSCode first.'
|
||||
};
|
||||
}
|
||||
|
||||
if (!status.isInstalled) {
|
||||
await installDrawioExtension(config)();
|
||||
return {
|
||||
success: true,
|
||||
message: 'Draw.io extension installed successfully.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'VSCode environment is ready for draw.io.'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to setup VSCode environment: ${error}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to open multiple diagrams in VSCode
|
||||
export const openMultipleDiagrams = (config: VSCodeConfig) => async (filePaths: readonly string[]): Promise<readonly string[]> => {
|
||||
const results: string[] = [];
|
||||
const openDiagram = openDiagramInVSCode(config);
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
await openDiagram(filePath);
|
||||
results.push(`Successfully opened: ${filePath}`);
|
||||
} catch (error) {
|
||||
results.push(`Failed to open ${filePath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Pure function to create VSCode workspace configuration for draw.io
|
||||
export const createWorkspaceConfig = (config: VSCodeConfig): Readonly<Record<string, any>> => ({
|
||||
'hediet.vscode-drawio': {
|
||||
'local-storage': path.join(config.workspaceRoot, '.vscode', 'drawio-storage'),
|
||||
'theme': 'automatic',
|
||||
'online-url': 'https://embed.diagrams.net/',
|
||||
'offline': false
|
||||
}
|
||||
});
|
||||
|
||||
// Curried function to save VSCode workspace settings
|
||||
export const saveWorkspaceSettings = (config: VSCodeConfig) => async (settings: Readonly<Record<string, any>>): Promise<void> => {
|
||||
const settingsPath = path.join(config.workspaceRoot, '.vscode', 'settings.json');
|
||||
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(settingsPath));
|
||||
|
||||
let existingSettings = {};
|
||||
if (await fs.pathExists(settingsPath)) {
|
||||
const content = await fs.readFile(settingsPath, 'utf8');
|
||||
existingSettings = JSON.parse(content);
|
||||
}
|
||||
|
||||
const mergedSettings = { ...existingSettings, ...settings };
|
||||
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save workspace settings: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Higher-order function for operations with timeout
|
||||
export const withTimeout = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
timeoutMs: number = 30000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
return Promise.race([
|
||||
operation(...args),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
// Functional VSCode operations with retry and timeout
|
||||
export const openDiagramInVSCodeWithRetry = (config: VSCodeConfig) =>
|
||||
withRetry(openDiagramInVSCode(config));
|
||||
|
||||
export const setupVSCodeEnvironmentWithTimeout = (config: VSCodeConfig) =>
|
||||
withTimeout(setupVSCodeEnvironment(config));
|
||||
|
||||
// Pure function to validate VSCode configuration
|
||||
export const validateVSCodeConfig = (config: VSCodeConfig): boolean => {
|
||||
return typeof config.workspaceRoot === 'string' &&
|
||||
config.workspaceRoot.length > 0 &&
|
||||
typeof config.extensionId === 'string' &&
|
||||
config.extensionId.length > 0;
|
||||
};
|
||||
|
||||
// Pure function to create validated VSCode configuration
|
||||
export const createValidatedVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => {
|
||||
const config = createVSCodeConfig(workspaceRoot);
|
||||
|
||||
if (!validateVSCodeConfig(config)) {
|
||||
throw new Error('Invalid VSCode configuration');
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
476
src/utils/xml-parser.ts
Normal file
476
src/utils/xml-parser.ts
Normal file
@ -0,0 +1,476 @@
|
||||
import * as xml2js from 'xml2js';
|
||||
import { DiagramData, DiagramElement, DiagramConnection, DiagramType, DiagramFormat } from '../types/diagram-types.js';
|
||||
|
||||
// Functional types
|
||||
type ParserConfig = Readonly<{
|
||||
explicitArray: boolean;
|
||||
mergeAttrs: boolean;
|
||||
normalize: boolean;
|
||||
normalizeTags: boolean;
|
||||
trim: boolean;
|
||||
}>;
|
||||
|
||||
type BuilderConfig = Readonly<{
|
||||
xmldec: Readonly<{ version: string; encoding: string }>;
|
||||
renderOpts: Readonly<{ pretty: boolean; indent: string }>;
|
||||
}>;
|
||||
|
||||
type MxGraphModel = Readonly<{
|
||||
mxGraphModel: any;
|
||||
}>;
|
||||
|
||||
type CellData = Readonly<{
|
||||
id: string;
|
||||
value?: string;
|
||||
style?: string;
|
||||
vertex?: string;
|
||||
edge?: string;
|
||||
source?: string;
|
||||
target?: string;
|
||||
parent?: string;
|
||||
mxGeometry?: any;
|
||||
}>;
|
||||
|
||||
type StyleMap = Readonly<Record<string, string>>;
|
||||
|
||||
// Pure function to create default parser configuration
|
||||
export const createParserConfig = (): ParserConfig => ({
|
||||
explicitArray: false,
|
||||
mergeAttrs: true,
|
||||
normalize: true,
|
||||
normalizeTags: true,
|
||||
trim: true
|
||||
});
|
||||
|
||||
// Pure function to create default builder configuration
|
||||
export const createBuilderConfig = (): BuilderConfig => ({
|
||||
xmldec: { version: '1.0', encoding: 'UTF-8' },
|
||||
renderOpts: { pretty: true, indent: ' ' }
|
||||
});
|
||||
|
||||
// Pure function to create parser instance
|
||||
export const createParser = (config: ParserConfig = createParserConfig()): xml2js.Parser =>
|
||||
new xml2js.Parser(config);
|
||||
|
||||
// Pure function to create builder instance
|
||||
export const createBuilder = (config: BuilderConfig = createBuilderConfig()): xml2js.Builder =>
|
||||
new xml2js.Builder(config);
|
||||
|
||||
// Pure function to get default style mappings
|
||||
const getDefaultStyleMap = (): StyleMap => ({
|
||||
'bpmn-start-event': 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
'bpmn-end-event': 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
'bpmn-task': 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
'bpmn-gateway': 'rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
'uml-class': 'swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
'uml-actor': 'shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
'er-entity': 'whiteSpace=wrap;html=1;align=center;treeFolding=1;treeMoving=1;newEdgeStyle={"edgeStyle":"entityRelationEdgeStyle","startArrow":"none","endArrow":"none","segment":10,"curved":1};fillColor=#e1d5e7;strokeColor=#9673a6;',
|
||||
'rectangle': 'rounded=0;whiteSpace=wrap;html=1;',
|
||||
'ellipse': 'ellipse;whiteSpace=wrap;html=1;',
|
||||
'diamond': 'rhombus;whiteSpace=wrap;html=1;',
|
||||
'triangle': 'triangle;whiteSpace=wrap;html=1;'
|
||||
});
|
||||
|
||||
// Pure function to get default style for element type
|
||||
export const getDefaultStyle = (elementType: string): string => {
|
||||
const styleMap = getDefaultStyleMap();
|
||||
return styleMap[elementType] || styleMap['rectangle'];
|
||||
};
|
||||
|
||||
// Pure function to get default connection style
|
||||
export const getDefaultConnectionStyle = (): string =>
|
||||
'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;';
|
||||
|
||||
// Pure function to generate unique ID
|
||||
export const generateId = (): string =>
|
||||
Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Pure function to generate etag for draw.io
|
||||
export const generateEtag = (): string =>
|
||||
Math.random().toString(36).substr(2, 16);
|
||||
|
||||
// Higher-order function for XML operations with error handling
|
||||
const withXMLErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`XML operation failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to extract properties from cell
|
||||
const extractCellProperties = (cell: CellData): Readonly<Record<string, any>> => {
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
if (cell.style) properties.style = cell.style;
|
||||
if (cell.value) properties.value = cell.value;
|
||||
if (cell.vertex) properties.vertex = cell.vertex;
|
||||
if (cell.edge) properties.edge = cell.edge;
|
||||
|
||||
return properties;
|
||||
};
|
||||
|
||||
// Pure function to infer element type from cell data
|
||||
const inferElementType = (cell: CellData): string => {
|
||||
const style = cell.style || '';
|
||||
|
||||
// BPMN elements
|
||||
if (style.includes('bpmn')) return 'bpmn-element';
|
||||
if (style.includes('startEvent')) return 'bpmn-start-event';
|
||||
if (style.includes('endEvent')) return 'bpmn-end-event';
|
||||
if (style.includes('task')) return 'bpmn-task';
|
||||
if (style.includes('gateway')) return 'bpmn-gateway';
|
||||
|
||||
// UML elements
|
||||
if (style.includes('uml')) return 'uml-element';
|
||||
if (style.includes('class')) return 'uml-class';
|
||||
if (style.includes('actor')) return 'uml-actor';
|
||||
if (style.includes('usecase')) return 'uml-usecase';
|
||||
|
||||
// Database elements
|
||||
if (style.includes('entity')) return 'er-entity';
|
||||
if (style.includes('table')) return 'db-table';
|
||||
|
||||
// Network elements
|
||||
if (style.includes('router')) return 'network-router';
|
||||
if (style.includes('switch')) return 'network-switch';
|
||||
if (style.includes('server')) return 'network-server';
|
||||
|
||||
// Generic shapes
|
||||
if (style.includes('rectangle')) return 'rectangle';
|
||||
if (style.includes('ellipse')) return 'ellipse';
|
||||
if (style.includes('rhombus')) return 'diamond';
|
||||
if (style.includes('triangle')) return 'triangle';
|
||||
|
||||
return 'generic-shape';
|
||||
};
|
||||
|
||||
// Pure function to create DiagramElement from cell data
|
||||
const createElementFromCell = (cell: CellData): DiagramElement => {
|
||||
const geometry = cell.mxGeometry || {};
|
||||
|
||||
return {
|
||||
id: cell.id,
|
||||
type: inferElementType(cell),
|
||||
label: cell.value || '',
|
||||
style: cell.style || '',
|
||||
geometry: {
|
||||
x: parseFloat(geometry.x) || 0,
|
||||
y: parseFloat(geometry.y) || 0,
|
||||
width: parseFloat(geometry.width) || 100,
|
||||
height: parseFloat(geometry.height) || 50
|
||||
},
|
||||
properties: extractCellProperties(cell)
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to create DiagramConnection from cell data
|
||||
const createConnectionFromCell = (cell: CellData): DiagramConnection => ({
|
||||
id: cell.id,
|
||||
source: cell.source || '',
|
||||
target: cell.target || '',
|
||||
label: cell.value || '',
|
||||
style: cell.style || '',
|
||||
properties: extractCellProperties(cell)
|
||||
});
|
||||
|
||||
// Pure function to infer diagram type from elements and connections
|
||||
const inferDiagramType = (elements: readonly DiagramElement[], connections: readonly DiagramConnection[]): DiagramType => {
|
||||
const elementTypes = elements.map(e => e.type);
|
||||
|
||||
// Check for BPMN elements
|
||||
if (elementTypes.some(type => type.startsWith('bpmn-'))) {
|
||||
return DiagramType.BPMN_PROCESS;
|
||||
}
|
||||
|
||||
// Check for UML elements
|
||||
if (elementTypes.some(type => type.startsWith('uml-'))) {
|
||||
if (elementTypes.includes('uml-class')) return DiagramType.UML_CLASS;
|
||||
if (elementTypes.includes('uml-actor')) return DiagramType.UML_USE_CASE;
|
||||
return DiagramType.UML_CLASS; // Default UML type
|
||||
}
|
||||
|
||||
// Check for ER elements
|
||||
if (elementTypes.some(type => type.startsWith('er-') || type.startsWith('db-'))) {
|
||||
return DiagramType.ER_DIAGRAM;
|
||||
}
|
||||
|
||||
// Check for network elements
|
||||
if (elementTypes.some(type => type.startsWith('network-'))) {
|
||||
return DiagramType.NETWORK_TOPOLOGY;
|
||||
}
|
||||
|
||||
// Default to flowchart
|
||||
return DiagramType.FLOWCHART;
|
||||
};
|
||||
|
||||
// Pure function to extract elements and connections from mxGraphModel
|
||||
const extractElementsAndConnections = (mxGraphModel: MxGraphModel): Readonly<{ elements: DiagramElement[], connections: DiagramConnection[] }> => {
|
||||
const elements: DiagramElement[] = [];
|
||||
const connections: DiagramConnection[] = [];
|
||||
|
||||
// Parse cells from mxGraphModel
|
||||
const root = mxGraphModel.mxGraphModel.root;
|
||||
const cells = Array.isArray(root.mxCell) ? root.mxCell : [root.mxCell];
|
||||
|
||||
for (const cell of cells) {
|
||||
if (!cell || cell.id === '0' || cell.id === '1') continue; // Skip root cells
|
||||
|
||||
if (cell.edge) {
|
||||
// This is a connection/edge
|
||||
connections.push(createConnectionFromCell(cell));
|
||||
} else {
|
||||
// This is an element/vertex
|
||||
elements.push(createElementFromCell(cell));
|
||||
}
|
||||
}
|
||||
|
||||
return { elements, connections };
|
||||
};
|
||||
|
||||
// Curried function to extract mxGraphModel from parsed XML
|
||||
const extractMxGraphModel = (parser: xml2js.Parser) => async (result: any): Promise<MxGraphModel> => {
|
||||
if (result.mxfile) {
|
||||
// Standard .drawio format
|
||||
const diagram = Array.isArray(result.mxfile.diagram)
|
||||
? result.mxfile.diagram[0]
|
||||
: result.mxfile.diagram;
|
||||
return await parser.parseStringPromise(diagram.mxGraphModel || diagram._);
|
||||
} else if (result.mxGraphModel) {
|
||||
// Direct mxGraphModel format
|
||||
return result;
|
||||
} else {
|
||||
throw new Error('Invalid draw.io XML format');
|
||||
}
|
||||
};
|
||||
|
||||
// Main function to parse draw.io XML content to DiagramData
|
||||
export const parseDrawioXML = withXMLErrorHandling(async (xmlContent: string): Promise<DiagramData> => {
|
||||
const parser = createParser();
|
||||
|
||||
const result = await parser.parseStringPromise(xmlContent);
|
||||
|
||||
// Handle different draw.io XML formats
|
||||
const mxGraphModel = await extractMxGraphModel(parser)(result);
|
||||
const { elements, connections } = extractElementsAndConnections(mxGraphModel);
|
||||
|
||||
return {
|
||||
elements,
|
||||
connections,
|
||||
metadata: {
|
||||
type: inferDiagramType(elements, connections),
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Pure function to create cell from DiagramElement
|
||||
const createCellFromElement = (element: DiagramElement): CellData => ({
|
||||
id: element.id,
|
||||
value: element.label || '',
|
||||
style: element.style || getDefaultStyle(element.type),
|
||||
vertex: '1',
|
||||
parent: '1',
|
||||
mxGeometry: {
|
||||
x: element.geometry?.x || 0,
|
||||
y: element.geometry?.y || 0,
|
||||
width: element.geometry?.width || 100,
|
||||
height: element.geometry?.height || 50,
|
||||
as: 'geometry'
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to create cell from DiagramConnection
|
||||
const createCellFromConnection = (connection: DiagramConnection): CellData => ({
|
||||
id: connection.id,
|
||||
value: connection.label || '',
|
||||
style: connection.style || getDefaultConnectionStyle(),
|
||||
edge: '1',
|
||||
parent: '1',
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
mxGeometry: {
|
||||
relative: '1',
|
||||
as: 'geometry'
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to create cells array from diagram data
|
||||
const createCellsFromDiagramData = (diagramData: DiagramData): readonly CellData[] => {
|
||||
const cells: CellData[] = [
|
||||
{ id: '0' },
|
||||
{ id: '1', parent: '0' }
|
||||
];
|
||||
|
||||
// Add elements
|
||||
const elementCells = diagramData.elements.map(createCellFromElement);
|
||||
cells.push(...elementCells);
|
||||
|
||||
// Add connections
|
||||
const connectionCells = diagramData.connections.map(createCellFromConnection);
|
||||
cells.push(...connectionCells);
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
// Pure function to create mxGraphModel structure
|
||||
const createMxGraphModel = (cells: readonly CellData[]): Readonly<Record<string, any>> => ({
|
||||
mxGraphModel: {
|
||||
dx: '1422',
|
||||
dy: '794',
|
||||
grid: '1',
|
||||
gridSize: '10',
|
||||
guides: '1',
|
||||
tooltips: '1',
|
||||
connect: '1',
|
||||
arrows: '1',
|
||||
fold: '1',
|
||||
page: '1',
|
||||
pageScale: '1',
|
||||
pageWidth: '827',
|
||||
pageHeight: '1169',
|
||||
math: '0',
|
||||
shadow: '0',
|
||||
root: {
|
||||
mxCell: [...cells]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to create mxfile structure
|
||||
const createMxFile = (mxGraphModel: Readonly<Record<string, any>>, builder: xml2js.Builder): Readonly<Record<string, any>> => ({
|
||||
mxfile: {
|
||||
host: 'Electron',
|
||||
modified: new Date().toISOString(),
|
||||
agent: 'Mozilla/5.0',
|
||||
etag: generateEtag(),
|
||||
version: '24.7.17',
|
||||
type: 'device',
|
||||
diagram: {
|
||||
id: generateId(),
|
||||
name: 'Page-1',
|
||||
mxGraphModel: builder.buildObject(mxGraphModel).replace('<?xml version="1.0" encoding="UTF-8"?>', '')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Main function to convert DiagramData to draw.io XML format
|
||||
export const generateDrawioXML = (diagramData: DiagramData): string => {
|
||||
const builder = createBuilder();
|
||||
const cells = createCellsFromDiagramData(diagramData);
|
||||
const mxGraphModel = createMxGraphModel(cells);
|
||||
const mxfile = createMxFile(mxGraphModel, builder);
|
||||
|
||||
return builder.buildObject(mxfile);
|
||||
};
|
||||
|
||||
// Pure function to validate XML content
|
||||
export const validateDrawioXML = async (xmlContent: string): Promise<boolean> => {
|
||||
try {
|
||||
await parseDrawioXML(xmlContent);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to convert between different XML formats
|
||||
export const convertXMLFormat = (targetFormat: DiagramFormat) => async (xmlContent: string): Promise<string> => {
|
||||
const diagramData = await parseDrawioXML(xmlContent);
|
||||
diagramData.metadata = { ...diagramData.metadata, format: targetFormat };
|
||||
return generateDrawioXML(diagramData);
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Functional XML operations with retry
|
||||
export const parseDrawioXMLWithRetry = withRetry(parseDrawioXML);
|
||||
export const validateDrawioXMLWithRetry = withRetry(validateDrawioXML);
|
||||
|
||||
// Pure function to create diagram metadata
|
||||
export const createDiagramMetadata = (
|
||||
type: DiagramType,
|
||||
format: DiagramFormat = DiagramFormat.DRAWIO
|
||||
): Readonly<DiagramData['metadata']> => ({
|
||||
type,
|
||||
format,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
});
|
||||
|
||||
// Pure function to update diagram metadata
|
||||
export const updateDiagramMetadata = (
|
||||
metadata: DiagramData['metadata'],
|
||||
updates: Partial<DiagramData['metadata']>
|
||||
): Readonly<DiagramData['metadata']> => ({
|
||||
...metadata,
|
||||
...updates,
|
||||
modified: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Higher-order function to transform diagram data
|
||||
export const transformDiagramData = <T>(
|
||||
transformer: (data: DiagramData) => T
|
||||
) => (data: DiagramData): T => transformer(data);
|
||||
|
||||
// Pure function to merge diagram data
|
||||
export const mergeDiagramData = (
|
||||
base: DiagramData,
|
||||
additional: DiagramData
|
||||
): DiagramData => ({
|
||||
elements: [...base.elements, ...additional.elements],
|
||||
connections: [...base.connections, ...additional.connections],
|
||||
metadata: updateDiagramMetadata(base.metadata, {
|
||||
type: base.metadata.type // Keep base type
|
||||
})
|
||||
});
|
||||
|
||||
// Pure function to filter diagram elements by type
|
||||
export const filterElementsByType = (elementType: string) => (data: DiagramData): DiagramData => ({
|
||||
...data,
|
||||
elements: data.elements.filter(element => element.type === elementType),
|
||||
metadata: updateDiagramMetadata(data.metadata, {})
|
||||
});
|
||||
|
||||
// Pure function to map over diagram elements
|
||||
export const mapDiagramElements = <T>(
|
||||
mapper: (element: DiagramElement) => T
|
||||
) => (data: DiagramData): T[] => data.elements.map(mapper);
|
||||
|
||||
// Pure function to map over diagram connections
|
||||
export const mapDiagramConnections = <T>(
|
||||
mapper: (connection: DiagramConnection) => T
|
||||
) => (data: DiagramData): T[] => data.connections.map(mapper);
|
@ -1,32 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es2021",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2022",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"typeRoots" : ["./src/@types", "./node_modules/@types"],
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"typeRoots": ["./src/@types", "./node_modules/@types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"],
|
||||
"@src": ["src"],
|
||||
"@routes*": ["src/routes/*"],
|
||||
"@routes": ["src/routes"],
|
||||
"@controllers/*": ["src/controllers/*"],
|
||||
"@controllers": ["src/controllers"],
|
||||
"@models/*": ["src/models/*"],
|
||||
"@models": ["src/models"],
|
||||
"@controllerGraphQL/*": ["src/controllers/controllerGraphQL/*"],
|
||||
"@controllerGraphQL": ["src/controllers/controllerGraphQL"],
|
||||
"@GraphQL/*": ["src/GraphQL/*"],
|
||||
"@GraphQL": ["src/GraphQL"],
|
||||
"@config/*": ["config/*"],
|
||||
"@config": ["config"]
|
||||
"@tools/*": ["src/tools/*"],
|
||||
"@tools": ["src/tools"],
|
||||
"@resources/*": ["src/resources/*"],
|
||||
"@resources": ["src/resources"],
|
||||
"@types/*": ["src/types/*"],
|
||||
"@types": ["src/types"],
|
||||
"@generators/*": ["src/generators/*"],
|
||||
"@generators": ["src/generators"],
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@utils": ["src/utils"],
|
||||
"@templates/*": ["templates/*"],
|
||||
"@templates": ["templates"]
|
||||
}
|
||||
},
|
||||
"lib": ["es2018", "esnext.asynciterable"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build", "**/*.test.ts"]
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
|
||||
import ESLintPlugin from 'eslint-webpack-plugin';
|
||||
import nodeExternals from 'webpack-node-externals';
|
||||
import WebpackShellPluginNext from 'webpack-shell-plugin-next';
|
||||
import { resolveTsAliases } from 'resolve-ts-aliases';
|
||||
import { deFaultValues } from './config';
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname);
|
||||
const resolvePath = (...args: string[]) => path.resolve(ROOT_DIR, ...args);
|
||||
const BUILD_DIR = resolvePath('build');
|
||||
const alias = resolveTsAliases(path.resolve('tsconfig.json'));
|
||||
|
||||
const config = {
|
||||
entry: './src/index.ts',
|
||||
target: 'node',
|
||||
watch: true,
|
||||
externals: [nodeExternals()],
|
||||
output: {
|
||||
path: BUILD_DIR,
|
||||
filename: 'index.js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.json', '.gql'],
|
||||
alias,
|
||||
},
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts|mjs|gql)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(ts)$/, loader: "ts-loader",
|
||||
exclude: /node_modules/
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new ESLintPlugin(),
|
||||
new webpack.EnvironmentPlugin({
|
||||
...deFaultValues
|
||||
}),
|
||||
new WebpackShellPluginNext({
|
||||
onBuildEnd: {
|
||||
scripts: ['nodemon build/index.js'],
|
||||
blocking: false,
|
||||
parallel: true
|
||||
}
|
||||
})
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,58 +0,0 @@
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
|
||||
import ESLintPlugin from 'eslint-webpack-plugin';
|
||||
import nodeExternals from 'webpack-node-externals';
|
||||
import { resolveTsAliases } from 'resolve-ts-aliases';
|
||||
import { deFaultValues } from './config';
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname);
|
||||
const resolvePath = (...args: string[]) => path.resolve(ROOT_DIR, ...args);
|
||||
const BUILD_DIR = resolvePath('build');
|
||||
const alias = resolveTsAliases(path.resolve('tsconfig.json'));
|
||||
|
||||
const config = {
|
||||
entry: './src/index.ts',
|
||||
target: 'node',
|
||||
externals: [nodeExternals()],
|
||||
output: {
|
||||
path: BUILD_DIR,
|
||||
filename: 'index.js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.json', '.gql'],
|
||||
alias,
|
||||
},
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts|mjs|gql)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(ts)$/, loader: "ts-loader",
|
||||
exclude: /node_modules/
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new ESLintPlugin(),
|
||||
new webpack.EnvironmentPlugin({
|
||||
...deFaultValues
|
||||
}),
|
||||
],
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin(),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
Reference in New Issue
Block a user