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

This commit is contained in:
2025-07-22 07:11:59 +00:00
parent f10bf53522
commit bf088da9d5
38 changed files with 6398 additions and 9442 deletions

View File

@ -1,6 +0,0 @@
{
"presets": [
["@babel/preset-env", {"targets": {"node": "current"}}],
"@babel/preset-typescript"
]
}

176
.gitea/workflows/ci.yml Normal file
View 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

View File

@ -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
View File

@ -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 | ## Features
--- | --- |
Jest (Testing) | 29.7.0
Typescript | 5.6.2
GraphQL | 16.9.0
Type GraphQL | 2.0.0-rc.2
## Setup -**Diagram Creation**: Support for multiple diagram types (BPMN, UML, ER, Network, Architecture, Flowchart, etc.)
To create a new project run in the terminal: -**VSCode Integration**: Automatic opening of diagrams in VSCode with draw.io extension
``` -**Functional Programming**: Architecture based on pure functions without classes
npx @aleleba/create-node-ts-graphql-server server-app-name -**Automatic Generation**: Predefined templates for different diagram types
``` -**File Management**: Search and listing of existing diagrams
Then run: -**Automatic Configuration**: Automatic VSCode environment setup
```
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.
### For Development ## Supported Diagram Types
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.
You will find the controllers on: ### BPMN (Business Process Model and Notation)
``` - `bpmn-process`: Business processes
scr/controllers/ - `bpmn-collaboration`: Collaboration between participants
``` - `bpmn-choreography`: Message exchanges
You will find the models on:
``` ### UML (Unified Modeling Language)
scr/models - `uml-class`: Class diagrams
``` - `uml-sequence`: Sequence diagrams
You will find the GraphQL server, resolvers and schema definition on: - `uml-use-case`: Use case diagrams
``` - `uml-activity`: Activity diagrams
scr/GraphQL - `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: 2. **Install dependencies**:
``` ```bash
scr/routes npm install
``` ```
This will start the app in development mode, also use nodemon and webpack to real time coding! 3. **Build the project**:
Enjoy coding! ```bash
### For Production
In the terminal run:
```
npm run build npm run build
``` ```
It will create a build folder and run:
```
npm start
```
This will start the app.
## Cheers 4. **Configure in Cline**:
Hope you enjoy this proyect! Sincerely Alejandro Lembke Barrientos. 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.

View File

@ -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,
};

View File

@ -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
},
}
];

View File

@ -1,15 +1,25 @@
const { pathsToModuleNameMapper } = require('ts-jest'); export default {
const { compilerOptions } = require('./tsconfig'); preset: 'ts-jest/presets/default-esm',
extensionsToTreatAsEsm: ['.ts'],
const aliases = pathsToModuleNameMapper(compilerOptions.paths, { testEnvironment: 'node',
prefix: '<rootDir>' roots: ['<rootDir>/src/tests'],
}); testMatch: ['**/*.test.ts'],
transform: {
module.exports = { '^.+\\.ts$': ['ts-jest', {
testEnvironment: 'node', useESM: true
transform: { }]
"^.+\\.ts$": "ts-jest" },
},moduleNameMapper: { moduleNameMapper: {
...aliases, '^(\\.{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
View 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"
}
}
}
}

9381
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +1,61 @@
{ {
"name": "drawio-cline-mcp-server", "name": "drawio-mcp-server",
"version": "0.0.1", "version": "0.1.0",
"description": "Node with Typescript and GraphQL Server", "description": "MCP Server for Draw.io integration with Cline - Create and manage diagrams (BPMN, UML, ER, Network, Architecture) from VSCode",
"main": "index.js", "main": "build/index.js",
"type": "module",
"scripts": { "scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"start": "node build/index.js", "start": "node build/index.js",
"start:dev": "webpack-cli --config webpack.config.dev.ts", "dev": "tsc --watch",
"start:nodemon": "nodemon build/index.js",
"build": "webpack-cli --config webpack.config.ts",
"lint": "eslint ./ --ext .js --ext .ts", "lint": "eslint ./ --ext .js --ext .ts",
"lint:fix": "eslint ./ --ext .js --ext .ts --fix", "lint:fix": "eslint ./ --ext .js --ext .ts --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch"
"check-updates": "npx npm-check-updates -u && npm i --legacy-peer-deps"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/aleleba/node-ts-graphql-server.git" "url": "git+https://github.com/aleleba/drawio-mcp-server.git"
}, },
"keywords": [ "keywords": [
"node", "mcp",
"express", "drawio",
"typescript", "diagrams",
"graphql", "bpmn",
"server" "uml",
"er-diagram",
"network-diagram",
"architecture",
"vscode",
"cline"
], ],
"author": "Alejandro Lembke Barrientos", "author": "Alejandro Lembke Barrientos",
"license": "MIT", "license": "MIT",
"bugs": { "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": { "dependencies": {
"@graphql-tools/schema": "^10.0.25", "@modelcontextprotocol/sdk": "^0.5.0",
"body-parser": "^2.2.0", "fs-extra": "^11.2.0",
"class-validator": "^0.14.2", "glob": "^10.3.10",
"cookie-parse": "^0.4.0", "path": "^0.12.7",
"cookie-parser": "^1.4.7", "uuid": "^10.0.0",
"cors": "^2.8.5", "xml2js": "^0.6.2"
"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"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.0", "@types/fs-extra": "^11.0.4",
"@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/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.0.15", "@types/node": "^24.0.15",
"@types/supertest": "^6.0.3", "@types/uuid": "^10.0.0",
"@types/webpack": "^5.28.5", "@types/xml2js": "^0.4.14",
"@types/webpack-node-externals": "^3.0.4",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.38.0",
"babel-loader": "^10.0.0", "concurrently": "^9.2.0",
"clean-webpack-plugin": "^4.0.0",
"compression-webpack-plugin": "^11.1.0",
"eslint": "^9.31.0", "eslint": "^9.31.0",
"eslint-webpack-plugin": "^5.0.2",
"jest": "^30.0.4", "jest": "^30.0.4",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"resolve-ts-aliases": "^1.0.1",
"supertest": "^7.1.3",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"ts-loader": "^9.5.2", "typescript": "^5.8.3"
"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"
} }
} }

View File

@ -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!
}

View File

@ -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!
}

View File

@ -1 +0,0 @@
export * from './test.resolver';

View File

@ -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});
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;

View File

@ -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 });
};

View 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'];
};

View File

@ -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 tools and utilities
import ws from 'ws'; // yarn add ws import { createDiagram, validateCreateDiagramInput, getSupportedDiagramTypes, getDiagramTypeDescription } from './tools/create-diagram.js';
import express from 'express'; //express import { createFileConfig, findDiagramFiles, getDiagramFilesWithMetadata } from './utils/file-manager.js';
import cors from 'cors'; import { createVSCodeConfig, openDiagramInVSCode, setupVSCodeEnvironment } from './utils/vscode-integration.js';
import cookieParser from 'cookie-parser'; import { DiagramType, DiagramFormat } from './types/diagram-types.js';
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';
const app = express(), //creating app // Functional types and configurations
whitelist = config.WHITELIST_URLS, type ServerConfig = Readonly<{
corsOptions = { name: string;
origin: function (origin: string | undefined, callback: (arg0: Error | null, arg1?: boolean) => void) { version: string;
if (whitelist.indexOf(origin as string) !== -1 || !origin) { workspaceRoot: string;
callback(null, true); capabilities: Readonly<{
} else { resources: Record<string, unknown>;
callback(new Error('Not allowed by CORS')); tools: Record<string, unknown>;
} }>;
}, }>;
credentials: true
};
//Inicialization of services of express type ToolHandler = (args: any) => Promise<any>;
app type ResourceHandler = (uri: string) => Promise<any>;
.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){ type ToolHandlers = Readonly<{
app.get('/playground', expressPlayground({ create_diagram: ToolHandler;
endpoint: '/graphql', list_diagrams: ToolHandler;
subscriptionEndpoint: '/graphql', open_diagram_in_vscode: ToolHandler;
settings: { setup_vscode_environment: ToolHandler;
'request.credentials': 'include', //Include Credentials for playground get_diagram_types: ToolHandler;
}, }>;
}));
}
// DO NOT DO app.listen() unless we're testing this directly type ResourceHandlers = Readonly<{
if (require.main === module) { 'diagrams://workspace/list': ResourceHandler;
'diagrams://types/supported': ResourceHandler;
}>;
const server = app.listen(config.PORT, () => { // Pure function to create server configuration
// create and use the websocket server const createServerConfig = (workspaceRoot?: string): ServerConfig => ({
const wsServer = new ws.Server({ name: 'drawio-mcp-server',
server, version: '0.1.0',
path: '/graphql', workspaceRoot: workspaceRoot || process.env.WORKSPACE_ROOT || process.cwd(),
}); capabilities: {
resources: {},
tools: {},
},
});
useServer({ // Pure function to create tool definitions
schema, const createToolDefinitions = () => [
execute, {
subscribe, name: 'create_diagram',
// eslint-disable-next-line description: 'Create a new diagram of specified type (BPMN, UML, ER, Network, Architecture, etc.)',
onConnect: (ctx) => { inputSchema: {
//console.log('Connect'); type: 'object',
}, properties: {
// eslint-disable-next-line name: {
onSubscribe: (ctx, msg) => { type: 'string',
//console.log('Subscribe'); description: 'Name of the diagram file (without extension)'
}, },
// eslint-disable-next-line type: {
onNext: (ctx, msg, args, result) => { type: 'string',
//console.debug('Next'); enum: Object.values(DiagramType),
}, description: 'Type of diagram to create'
// eslint-disable-next-line },
onError: (ctx, msg, errors) => { format: {
//console.error('Error'); type: 'string',
}, enum: Object.values(DiagramFormat),
// eslint-disable-next-line default: 'drawio',
onComplete: (ctx, msg) => { description: 'File format for the diagram'
//console.log('Complete'); },
}, description: {
}, wsServer); 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'
}
},
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: {}
}
}
];
console.log(`Starting Express on port ${config.PORT} and iniciating server of web sockets`); // 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
});
// Instead do export the app: return {
export default app; content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
},
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)
}));
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
supportedTypes: typesWithDescriptions
}, null, 2)
}
]
};
}
});
// 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,
};
}
});
return server;
};
// 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;
};
// 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);

View File

@ -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`;
};

View File

@ -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;

View 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);
});
});
});

View 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);
});
});
});

View File

@ -1,41 +1,350 @@
import server from '@src'; import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import supertest from 'supertest'; import { DiagramType, DiagramFormat } from '../../types/diagram-types.js';
describe('global server tests', () => {
let request: supertest.SuperTest<supertest.Test>;
beforeEach( async () => {
request = await supertest(server) as unknown as supertest.SuperTest<supertest.Test>;
});
it('should return Test data from test Query', async () => { describe('Functional MCP Server', () => {
const bodyResponse = { describe('Server Configuration', () => {
data: { it('should create server configuration with default values', () => {
test: { // Test the pure function createServerConfig
text: 'This is the text response for Test Query from a model' const createServerConfig = (workspaceRoot?: string) => ({
} name: 'drawio-mcp-server',
} version: '0.1.0',
}; workspaceRoot: workspaceRoot || process.env.WORKSPACE_ROOT || process.cwd(),
const response = await request.get('/graphql?query=%7B%0A%20%20test%7B%0A%20%20%20%20text%0A%20%20%7D%0A%7D') capabilities: {
.set('Accept', 'application/json'); resources: {},
expect(response.status).toEqual(200); tools: {},
expect(response.body).toEqual(bodyResponse); },
}); });
it('should return Test data from test Mutation', async () => { const config = createServerConfig();
const bodyResponse = {
data: { expect(config.name).toBe('drawio-mcp-server');
testMutation: { expect(config.version).toBe('0.1.0');
text: 'Simulate to insert some text: testing text from a model' expect(config.workspaceRoot).toBeDefined();
} expect(config.capabilities).toEqual({
} resources: {},
}; tools: {},
const response = await request.post('/graphql') });
.send({'query': `mutation{ });
testMutation{
text(text: "testing text") 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']
} }
}`}) },
.set('Accept', 'application/json'); {
expect(response.status).toEqual(200); name: 'list_diagrams',
expect(response.body).toEqual(bodyResponse); 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 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));
}
}
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
View 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
});

View 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');
});
});
});

View 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);
});
});
});

View 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');
});
});
});

View 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
View 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
View 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
View 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);

View 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
View 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);

View File

@ -1,32 +1,37 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "ESNext",
"esModuleInterop": true, "moduleResolution": "node",
"target": "es2021", "target": "ES2022",
"moduleResolution": "node", "esModuleInterop": true,
"sourceMap": true, "allowSyntheticDefaultImports": true,
"typeRoots" : ["./src/@types", "./node_modules/@types"], "sourceMap": true,
"strict": true, "outDir": "./build",
"forceConsistentCasingInFileNames": true, "rootDir": "./src",
"emitDecoratorMetadata": true, "strict": true,
"experimentalDecorators": true, "forceConsistentCasingInFileNames": true,
"baseUrl": ".", "skipLibCheck": true,
"paths": { "declaration": true,
"@src/*": ["src/*"], "declarationMap": true,
"@src": ["src"], "typeRoots": ["./src/@types", "./node_modules/@types"],
"@routes*": ["src/routes/*"], "baseUrl": ".",
"@routes": ["src/routes"], "paths": {
"@controllers/*": ["src/controllers/*"], "@src/*": ["src/*"],
"@controllers": ["src/controllers"], "@src": ["src"],
"@models/*": ["src/models/*"], "@tools/*": ["src/tools/*"],
"@models": ["src/models"], "@tools": ["src/tools"],
"@controllerGraphQL/*": ["src/controllers/controllerGraphQL/*"], "@resources/*": ["src/resources/*"],
"@controllerGraphQL": ["src/controllers/controllerGraphQL"], "@resources": ["src/resources"],
"@GraphQL/*": ["src/GraphQL/*"], "@types/*": ["src/types/*"],
"@GraphQL": ["src/GraphQL"], "@types": ["src/types"],
"@config/*": ["config/*"], "@generators/*": ["src/generators/*"],
"@config": ["config"] "@generators": ["src/generators"],
} "@utils/*": ["src/utils/*"],
}, "@utils": ["src/utils"],
"lib": ["es2018", "esnext.asynciterable"] "@templates/*": ["templates/*"],
"@templates": ["templates"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build", "**/*.test.ts"]
} }

View File

@ -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;

View File

@ -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;