@thierrynakoa/fire-flow 12.2.1 → 13.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +25 -0
- package/DOMINION-FLOW-OVERVIEW.md +182 -38
- package/README.md +399 -455
- package/TROUBLESHOOTING.md +264 -264
- package/agents/fire-debugger.md +54 -0
- package/agents/fire-executor.md +1610 -1033
- package/agents/fire-fact-checker.md +1 -1
- package/agents/fire-planner.md +85 -17
- package/agents/fire-project-researcher.md +1 -1
- package/agents/fire-researcher.md +4 -22
- package/agents/{fire-phoenix-analyst.md → fire-resurrection-analyst.md} +394 -394
- package/agents/fire-reviewer.md +552 -499
- package/agents/fire-verifier.md +114 -19
- package/bin/cli.js +18 -101
- package/commands/fire-0-orient.md +2 -2
- package/commands/fire-1a-new.md +50 -15
- package/commands/fire-1c-setup.md +33 -5
- package/commands/fire-1d-discuss.md +87 -1
- package/commands/fire-2-plan.md +556 -527
- package/commands/fire-3-execute.md +2046 -1356
- package/commands/fire-4-verify.md +975 -906
- package/commands/fire-5-handoff.md +46 -5
- package/commands/fire-6-resume.md +2 -31
- package/commands/fire-add-new-skill.md +138 -19
- package/commands/fire-autonomous.md +14 -2
- package/commands/fire-complete-milestone.md +1 -1
- package/commands/fire-cost.md +179 -183
- package/commands/fire-debug.md +1 -6
- package/commands/fire-loop-resume.md +2 -2
- package/commands/fire-loop-stop.md +1 -1
- package/commands/fire-loop.md +2 -15
- package/commands/fire-map-codebase.md +1 -1
- package/commands/fire-migrate-database.md +548 -0
- package/commands/fire-new-milestone.md +1 -1
- package/commands/fire-reflect.md +1 -2
- package/commands/fire-research.md +142 -21
- package/commands/{fire-phoenix.md → fire-resurrect.md} +859 -603
- package/commands/fire-scaffold.md +297 -0
- package/commands/fire-search.md +1 -2
- package/commands/fire-security-scan.md +483 -484
- package/commands/fire-setup.md +359 -0
- package/commands/fire-skill.md +770 -0
- package/commands/fire-skills-diff.md +506 -506
- package/commands/fire-skills-history.md +388 -388
- package/commands/fire-skills-rollback.md +7 -7
- package/commands/fire-skills-sync.md +470 -470
- package/commands/fire-test.md +5 -5
- package/commands/fire-todos.md +1 -1
- package/commands/fire-update.md +5 -5
- package/commands/fire-validate-skills.md +282 -0
- package/commands/fire-vuln-scan.md +492 -493
- package/hooks/run-hook.sh +8 -8
- package/hooks/run-session-end.sh +7 -7
- package/hooks/session-end.sh +90 -90
- package/hooks/session-start.sh +1 -1
- package/package.json +4 -24
- package/plugin.json +7 -7
- package/references/autonomy-levels.md +235 -0
- package/references/behavioral-directives.md +95 -3
- package/references/blocker-tracking.md +1 -1
- package/references/circuit-breaker.md +93 -2
- package/references/context-engineering.md +227 -9
- package/references/honesty-protocols.md +70 -1
- package/references/issue-to-pr-pipeline.md +149 -150
- package/references/metrics-and-trends.md +1 -2
- package/references/research-improvements.md +4 -108
- package/references/sdlc-mapping.md +73 -0
- package/references/state-machine.md +151 -0
- package/skills-library/AVAILABLE_TOOLS_REFERENCE.md +333 -0
- package/skills-library/SKILLS-INDEX.md +57 -558
- package/skills-library/SKILLS_LIBRARY_INDEX.md +532 -0
- package/skills-library/_general/api-patterns/api-field-name-mismatch.md +107 -0
- package/skills-library/_general/api-patterns/streaming-command-timeout.md +122 -0
- package/skills-library/_general/api-patterns/streaming-proxy-cors-bypass.md +102 -0
- package/skills-library/_general/automation/settings-gui-generator.md +172 -0
- package/skills-library/_general/database-solutions/data-type-mapping-reference.md +181 -0
- package/skills-library/_general/database-solutions/mysql-limit-offset-string-coercion.md +102 -0
- package/skills-library/_general/database-solutions/mysql-to-pg-migration.md +195 -0
- package/skills-library/_general/database-solutions/orm-schema-portability.md +193 -0
- package/skills-library/_general/database-solutions/persistent-analysis-storage.md +207 -0
- package/skills-library/_general/database-solutions/pg-to-mysql-schema-migration-methodology.md +190 -0
- package/skills-library/_general/database-solutions/sql-dialect-compatibility-matrix.md +306 -0
- package/skills-library/_general/database-solutions/sqlite-to-pg-migration.md +219 -0
- package/skills-library/_general/frontend/canvas-bubble-animation-grouping.md +270 -0
- package/skills-library/_general/frontend/color-token-migration.md +112 -0
- package/skills-library/_general/frontend/framer-motion-layoutid-grouping.md +150 -0
- package/skills-library/_general/frontend/pyqt6-settings-dialog.md +191 -0
- package/skills-library/_general/frontend/react-flow-animated-layout-switching.md +101 -0
- package/skills-library/_general/frontend/react-hooks-order-debugging.md +141 -0
- package/skills-library/_general/frontend/redux-localstorage-auth-desync.md +126 -0
- package/skills-library/_general/frontend/safari-csp-theme-color-debugging.md +124 -0
- package/skills-library/_general/frontend/safari-sw-cache-poisoning.md +138 -0
- package/skills-library/_general/frontend/svg-sparkline-no-charting-library.md +131 -0
- package/skills-library/_general/growth-marketing/oss-daily-growth-intelligence.md +224 -0
- package/skills-library/_general/integrations/claude-code-local-mcp-integration.md +250 -0
- package/skills-library/_general/integrations/mcp-composite-tool-orchestration.md +200 -0
- package/skills-library/_general/methodology/AGENT_SDK_STANDALONE_TOOLING.md +181 -0
- package/skills-library/_general/methodology/AGENT_TEAMS_GUIDE.md +169 -0
- package/skills-library/_general/methodology/ALAS_STATEFUL_EXECUTION.md +207 -0
- package/skills-library/_general/methodology/AUTO_REVIEWER_SUBAGENT.md +211 -0
- package/skills-library/_general/methodology/CONSISTENCY_CHECK_AMBIGUITY_GATE.md +96 -0
- package/skills-library/_general/methodology/DEAD_ENDS_SHELF.md +4 -4
- package/skills-library/_general/methodology/DISTILL_NOT_DUMP.md +108 -0
- package/skills-library/_general/methodology/EXECUTION_PROGRESS_MONITOR.md +157 -0
- package/skills-library/_general/methodology/HIERARCHICAL_REVIEW_MARS.md +122 -0
- package/skills-library/_general/methodology/MCP_INTER_AGENT_BRIDGE.md +207 -0
- package/skills-library/_general/methodology/MERMAID_WIZARD_DIAGRAMS.md +77 -0
- package/skills-library/_general/methodology/MISSING_DIMENSION_DETECTOR.md +89 -0
- package/skills-library/_general/methodology/MULTI_AGENT_COORDINATION.md +397 -0
- package/skills-library/_general/methodology/OBSERVATION_MASKING.md +100 -0
- package/skills-library/_general/methodology/PHOENIX_REBUILD_METHODOLOGY.md +82 -11
- package/skills-library/_general/methodology/REVIEW_BACKTRACK_PANEL.md +140 -0
- package/skills-library/_general/methodology/REVIEW_FIX_LOOP.md +117 -0
- package/skills-library/_general/methodology/VOTING_VERDICT_ARBITRATION.md +155 -0
- package/skills-library/_general/methodology/ZERO_FRICTION_CLI_SETUP.md +2 -2
- package/skills-library/_general/methodology/dead-code-activation.md +123 -0
- package/skills-library/_general/methodology/debug-swarm-researcher-escape-hatch.md +240 -240
- package/skills-library/_general/methodology/shell-autonomous-loop-fixplan.md +1 -1
- package/skills-library/_general/patterns-standards/GOF_DESIGN_PATTERNS_FOR_AI_AGENTS.md +5 -5
- package/skills-library/_general/patterns-standards/cascading-failure-diagnosis.md +119 -0
- package/skills-library/_general/patterns-standards/domain-specific-layout-algorithms.md +209 -0
- package/skills-library/_general/patterns-standards/python-desktop-app-architecture.md +399 -0
- package/skills-library/_general/patterns-standards/realtime-monitoring-dashboard.md +457 -0
- package/skills-library/_general/patterns-standards/togglable-processing-pipeline.md +169 -0
- package/skills-library/_general/performance/liveclock-extraction.md +112 -0
- package/skills-library/_general/performance/ref-based-canvas-animation.md +117 -0
- package/skills-library/_general/performance/use-visible-interval.md +131 -0
- package/skills-library/_general/testing/playwright-firefox-withcredentials-auth-issue.md +104 -0
- package/skills-library/_quarantine/README.md +30 -0
- package/skills-library/api-patterns/BROADCAST_SCHEDULER_SHARED_EXECUTE_FUNCTION.md +150 -0
- package/skills-library/api-patterns/ERROR_RESPONSE_STANDARDS.md +145 -0
- package/skills-library/api-patterns/EXPRESS_ROUTE_ORDERING_MIDDLEWARE_INTERCEPTION.md +326 -0
- package/skills-library/api-patterns/PAGINATION_PATTERNS.md +137 -0
- package/skills-library/api-patterns/PODCAST_PROGRESS_TRACKING_THREE_ROOT_CAUSES.md +277 -0
- package/skills-library/api-patterns/RATE_LIMITING_TOGGLE.md +155 -0
- package/skills-library/api-patterns/graphql-content-queries.md +708 -0
- package/skills-library/appointment-scheduler-design.md +423 -0
- package/skills-library/automation/AUTO_POPULATE_COMPLETE_GUIDE.md +631 -0
- package/skills-library/automation/CC_WORKFLOW_STUDIO.md +83 -0
- package/skills-library/automation/CLAUDE_CODE_SWARM_MODE.md +95 -0
- package/skills-library/automation/DAEMON_TRIGGER_FILE_IPC.md +195 -0
- package/skills-library/automation/scheduled-content-publishing.md +608 -0
- package/skills-library/awesome-workflows/Blogging-Platform-Instructions/view_commands.md +25 -0
- package/skills-library/awesome-workflows/CREDENTIAL-SECURITY-WORKFLOW.md +109 -0
- package/skills-library/awesome-workflows/DEBUGGING-WORKFLOW.md +124 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/README.md +31 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-principles-example.md +129 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-review-agent.md +107 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-review-claude-md-snippet.md +24 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-review-slash-command.md +38 -0
- package/skills-library/awesome-workflows/PARALLEL-RESEARCH-WORKFLOW.md +89 -0
- package/skills-library/awesome-workflows/PHASE-EXECUTION-WORKFLOW.md +97 -0
- package/skills-library/awesome-workflows/SESSION-HANDOFF-WORKFLOW.md +116 -0
- package/skills-library/cms-patterns/content-branch-preview.md +515 -0
- package/skills-library/cms-patterns/inline-visual-editing.md +666 -0
- package/skills-library/cms-patterns/mdx-component-content.md +649 -0
- package/skills-library/cms-patterns/media-manager-abstraction.md +827 -0
- package/skills-library/cms-patterns/schema-driven-form-generator.md +838 -0
- package/skills-library/complexity-metrics/complexity-divider.md +707 -0
- package/skills-library/complexity-metrics/work-with-complexity.md +193 -0
- package/skills-library/creative-multimedia/animation-stack-guide.md +577 -0
- package/skills-library/creative-multimedia/audio-enhancement-pipeline.md +625 -0
- package/skills-library/creative-multimedia/content-repurposing-pipeline.md +1146 -0
- package/skills-library/creative-multimedia/data-visualization-generator.md +862 -0
- package/skills-library/creative-multimedia/doc-to-podcast-pipeline.md +2184 -0
- package/skills-library/creative-multimedia/ffmpeg-command-generator.md +405 -0
- package/skills-library/creative-multimedia/image-optimization-pipeline.md +605 -0
- package/skills-library/creative-multimedia/multi-format-content-generator.md +1759 -0
- package/skills-library/creative-multimedia/og-image-generator.md +635 -0
- package/skills-library/creative-multimedia/podcast-audio-composition.md +1355 -0
- package/skills-library/creative-multimedia/podcast-quality-evaluation.md +1452 -0
- package/skills-library/creative-multimedia/podcast-script-generation.md +1841 -0
- package/skills-library/creative-multimedia/svg-generation.md +750 -0
- package/skills-library/creative-multimedia/text-to-speech-provider-selector.md +1414 -0
- package/skills-library/creative-multimedia/transcription-pipeline-selector.md +677 -0
- package/skills-library/creative-multimedia/video-streaming-setup.md +559 -0
- package/skills-library/database-solutions/AI_RESPONSE_DATABASE_CACHING.md +520 -0
- package/skills-library/database-solutions/CONDITIONAL_SQL_MIGRATION_PATTERN.md +119 -0
- package/skills-library/database-solutions/DATABASE_COLUMN_NAME_MISMATCH.md +393 -0
- package/skills-library/database-solutions/DATABASE_SCHEMA.md +394 -0
- package/skills-library/database-solutions/DATABASE_SCHEMA_VERIFICATION_GUIDE.md +348 -0
- package/skills-library/database-solutions/DATABASE_STRATEGY.md +71 -0
- package/skills-library/database-solutions/ES_MODULE_SEED_SCRIPT_PATTERN.md +52 -0
- package/skills-library/database-solutions/MIGRATION_GUIDE.md +3 -0
- package/skills-library/database-solutions/PLPGSQL_VARIABLE_CONFLICT_FIX.md +208 -0
- package/skills-library/database-solutions/POSTGRESQL_JSONB_DOUBLE_STRINGIFY_FIX.md +245 -0
- package/skills-library/database-solutions/POSTGRESQL_LICENSE_TABLE_DESIGN.md +393 -0
- package/skills-library/database-solutions/POSTGRESQL_UUID_DOCUMENT_RAG_DUAL_SCOPE.md +732 -0
- package/skills-library/database-solutions/POSTGRES_SQL_TEMPLATE_BINDING_ERROR.md +240 -0
- package/skills-library/database-solutions/PRISMA_DB_PUSH_DATA_LOSS_PREVENTION.md +141 -0
- package/skills-library/database-solutions/PRODUCTION_QUERY_OPTIMIZATION_RESTART_FIX.md +389 -0
- package/skills-library/database-solutions/RLS_SECURITY_GUIDE.md +107 -0
- package/skills-library/database-solutions/SCHEMA_ENHANCEMENTS_GUIDE.md +373 -0
- package/skills-library/database-solutions/SCHEMA_MIGRATION_GUIDE.md +368 -0
- package/skills-library/database-solutions/SCHEMA_VERIFICATION_QUICK_REFERENCE.md +104 -0
- package/skills-library/database-solutions/ai-erd-generator.md +1213 -0
- package/skills-library/database-solutions/content-publishing-states.md +631 -0
- package/skills-library/database-solutions/database-schema-designer.md +522 -0
- package/skills-library/database-solutions/er-diagram-components.md +569 -0
- package/skills-library/database-solutions/er-to-ddl-mapping.md +1405 -0
- package/skills-library/database-solutions/erd-creator-textbook-research.md +433 -0
- package/skills-library/database-solutions/erd-react-flow-architecture.md +1965 -0
- package/skills-library/database-solutions/mariadb-aggregate-function-replacement.md +145 -0
- package/skills-library/database-solutions/normalization-validator.md +778 -0
- package/skills-library/database-solutions/postgres-full-text-search-content.md +494 -0
- package/skills-library/database-solutions/postgresql-to-mysql-runtime-translation.md +286 -0
- package/skills-library/database-solutions/regex-alternation-ordering-sql-types.md +92 -0
- package/skills-library/database-solutions/reserved-word-context-aware-quoting.md +142 -0
- package/skills-library/database-solutions/sql-ddl-generator.md +756 -0
- package/skills-library/database-solutions/supabase-connection-pooler-fix.md +102 -0
- package/skills-library/deployment-security/CPANEL_NODE_DEPLOYMENT.md +166 -0
- package/skills-library/deployment-security/DEPLOYMENT.md +275 -0
- package/skills-library/deployment-security/DEPLOYMENT_CHECKLIST.md +363 -0
- package/skills-library/deployment-security/DEPLOYMENT_PLAN.md +669 -0
- package/skills-library/deployment-security/KNEX_DATABASE_ABSTRACTION.md +444 -0
- package/skills-library/deployment-security/LICENSE_KEY_SYSTEM.md +206 -0
- package/skills-library/deployment-security/NODE18_DEPENDENCY_COMPATIBILITY.md +284 -0
- package/skills-library/deployment-security/PHP_INSTALLER_WIZARD_GUIDE.md +315 -0
- package/skills-library/deployment-security/PM2_ENVIRONMENT_VARIABLE_CACHING.md +256 -0
- package/skills-library/deployment-security/PM2_MEMORY_EXHAUSTION_FIX.md +370 -0
- package/skills-library/deployment-security/PRODUCTION_DEPLOYMENT_GUIDE.md +592 -0
- package/skills-library/deployment-security/PRODUCTION_HARDENING_DOCUMENTATION.md +307 -0
- package/skills-library/deployment-security/PRODUCTION_RECOVERY_CHERRY_PICK_PATTERN.md +202 -0
- package/skills-library/deployment-security/PYINSTALLER_CUDA_WHISPER_BUNDLING.md +236 -0
- package/skills-library/deployment-security/SECURITY.md +41 -0
- package/skills-library/deployment-security/SMTP_SSL_HOSTNAME_MISMATCH_SHARED_HOSTING.md +220 -0
- package/skills-library/deployment-security/SPA_SEO_OPTIMIZATION_CPANEL.md +200 -0
- package/skills-library/deployment-security/SUPABASE_EDGE_FUNCTIONS.md +338 -0
- package/skills-library/deployment-security/VERCEL_GITHUB_DEPLOYMENT_GUIDE.md +858 -0
- package/skills-library/deployment-security/VPS_DEPLOYMENT_READINESS.md +356 -0
- package/skills-library/deployment-security/deployment-changes-not-applying.md +241 -0
- package/skills-library/deployment-security/env-file-management-production-local.md +203 -0
- package/skills-library/deployment-security/express-secure-file-downloads.md +413 -0
- package/skills-library/deployment-security/react-production-deployment-desktop-guide.md +2011 -0
- package/skills-library/deployment-security/self-hosted-supabase-coolify-guide.md +1684 -0
- package/skills-library/deployment-security/unique-features-ai-strategy-plaid-security.md +1613 -0
- package/skills-library/deployment-security/vps-deployment.md +135 -0
- package/skills-library/document-processing/WORD_EXPORT_MARKDOWN_FORMATTING.md +482 -0
- package/skills-library/document-processing/document-ai-landingai-integration.md +677 -0
- package/skills-library/document-processing/express-secure-file-downloads-mern.md +413 -0
- package/skills-library/document-processing/express-secure-file-downloads.md +413 -0
- package/skills-library/document-processing/md-to-word-converter.md +318 -0
- package/skills-library/document-processing/pdf-forms-integration/README.md +101 -0
- package/skills-library/document-processing/pdf-forms-integration/SKILL.md +662 -0
- package/skills-library/ecommerce/ADMIN_PRODUCTS_GUIDE.md +428 -0
- package/skills-library/ecommerce/ECOMMERCE_API_REFERENCE.md +776 -0
- package/skills-library/ecommerce/ECOMMERCE_COMPLETION_SUMMARY.md +673 -0
- package/skills-library/ecommerce/ECOMMERCE_IMPLEMENTATION_GUIDE.md +729 -0
- package/skills-library/ecommerce/ECOMMERCE_QUICK_REFERENCE.md +521 -0
- package/skills-library/ecommerce/ECOMMERCE_TESTING_CHECKLIST.md +565 -0
- package/skills-library/ecommerce/ECOMMERCE_WORKFLOW_GUIDE.md +1059 -0
- package/skills-library/ecommerce/PRODUCT_CREATION_EXPANDED.md +522 -0
- package/skills-library/ecommerce/agentic-commerce-protocol.md +203 -0
- package/skills-library/ecommerce/cart-abandonment-recovery.md +236 -0
- package/skills-library/ecommerce/cart-architecture-patterns.md +300 -0
- package/skills-library/ecommerce/cart-item-count-indicator.md +264 -0
- package/skills-library/ecommerce/checkout-ux-conversion.md +227 -0
- package/skills-library/ecommerce/composable-commerce-selection.md +166 -0
- package/skills-library/ecommerce/ecommerce-analytics-patterns.md +167 -0
- package/skills-library/ecommerce/fraud-detection-patterns.md +179 -0
- package/skills-library/ecommerce/inventory-stock-management.md +270 -0
- package/skills-library/ecommerce/order-saga-state-machine.md +336 -0
- package/skills-library/ecommerce/payment-provider-abstraction.md +245 -0
- package/skills-library/ecommerce/pci-compliance-checklist.md +192 -0
- package/skills-library/ecommerce/refund-chargeback-handling.md +177 -0
- package/skills-library/ecommerce/shipping-carrier-integration.md +218 -0
- package/skills-library/ecommerce/webhook-idempotency-patterns.md +253 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/ci.yml +558 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/prompt-gallery.yml +448 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/release.yml +42 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/test-reusable-ci.yml +25 -0
- package/skills-library/excalidraw-diagrams/CLAUDE.md +57 -0
- package/skills-library/excalidraw-diagrams/LICENSE +21 -0
- package/skills-library/excalidraw-diagrams/README.md +178 -0
- package/skills-library/excalidraw-diagrams/SKILL.md +715 -0
- package/skills-library/form-solutions/BUTTON_TYPE_FORM_SUBMISSION.md +336 -0
- package/skills-library/form-solutions/FILLABLE_PDF_IMPLEMENTATION.md +226 -0
- package/skills-library/form-solutions/SURVEYJS_QUESTIONNAIRE_SYSTEM.md +367 -0
- package/skills-library/form-solutions/tiptap-minimal-setup.md +690 -0
- package/skills-library/frontend/scholarly-classification-bubble-map.md +149 -0
- package/skills-library/infrastructure/ci-cd-pipeline-builder.md +517 -0
- package/skills-library/infrastructure/observability-designer.md +264 -0
- package/skills-library/infrastructure/performance-profiler.md +621 -0
- package/skills-library/installer-wizard-patterns.md +249 -0
- package/skills-library/integrations/CLAUDE_CODE_TOKEN_ANALYTICS.md +160 -0
- package/skills-library/integrations/CONFIGURABLE_AI_PROVIDER_SELECTION.md +728 -0
- package/skills-library/integrations/SOCKET_IO_BROADCAST_ALL_VS_ROOM.md +141 -0
- package/skills-library/integrations/VIRTUAL_MEETINGS_IMPLEMENTATION.md +374 -0
- package/skills-library/integrations/WORDPRESS_LEARNDASH_DATA_RECOVERY.md +53 -0
- package/skills-library/integrations/YOUTUBE_API_SETUP.md +141 -0
- package/skills-library/integrations/YOUTUBE_BOOKMARKING_EXPLANATION.md +252 -0
- package/skills-library/integrations/YOUTUBE_BOOKMARKING_SOLUTION.md +268 -0
- package/skills-library/integrations/YOUTUBE_OAUTH_SETUP_GUIDE.md +200 -0
- package/skills-library/integrations/YOUTUBE_VIDEO_FIX_COMPLETE.md +192 -0
- package/skills-library/integrations/ai-ml/GEMINI_AI_RAG_PIPELINE_COMPLETE_GUIDE.md +195 -0
- package/skills-library/integrations/ai-ml/GEMINI_IMAGE_GENERATION_SETUP.md +64 -0
- package/skills-library/integrations/cloudflare/cloudflare-turnstile-debugging.md +202 -0
- package/skills-library/integrations/cloudflare/cloudflare-turnstile-implementation.md +476 -0
- package/skills-library/integrations/cloudflare-turnstile-debugging.md +202 -0
- package/skills-library/integrations/cloudflare-turnstile-implementation.md +476 -0
- package/skills-library/integrations/ghost-creator-monetization-pattern.md +454 -0
- package/skills-library/integrations/headless-cms-architecture.md +484 -0
- package/skills-library/integrations/headless-cms-stack-selection.md +183 -0
- package/skills-library/integrations/payload-cms-patterns.md +674 -0
- package/skills-library/integrations/realtimestt-openwakeword-cuda-windows.md +229 -0
- package/skills-library/integrations/rss-podcast-integration.md +300 -0
- package/skills-library/integrations/wordpress/WORDPRESS_LEARNDASH_DATA_RECOVERY.md +53 -0
- package/skills-library/integrations/youtube/YOUTUBE_API_SETUP.md +141 -0
- package/skills-library/integrations/youtube/YOUTUBE_BOOKMARKING_EXPLANATION.md +252 -0
- package/skills-library/integrations/youtube/YOUTUBE_BOOKMARKING_SOLUTION.md +268 -0
- package/skills-library/integrations/youtube/YOUTUBE_OAUTH_SETUP_GUIDE.md +200 -0
- package/skills-library/integrations/youtube/YOUTUBE_VIDEO_FIX_COMPLETE.md +192 -0
- package/skills-library/marketing/campaign-analytics.md +97 -0
- package/skills-library/marketing/content-creator.md +105 -0
- package/skills-library/marketing/marketing-strategy-pmm.md +94 -0
- package/skills-library/marketing/social-media-analyzer.md +81 -0
- package/skills-library/methodology/ADVANCED_ORCHESTRATION_PATTERNS.md +401 -0
- package/skills-library/methodology/AGENT_SELF_IMPROVEMENT_LOOP.md +179 -0
- package/skills-library/methodology/BREATH_BASED_PARALLEL_EXECUTION.md +1 -1
- package/skills-library/methodology/CLEANSING_CYCLE.md +358 -0
- package/skills-library/methodology/CONFIDENCE_ANNOTATION_PATTERN.md +143 -0
- package/skills-library/methodology/CRITICAL_PATTERNS_DOCUMENTATION_COMPLETE.md +204 -0
- package/skills-library/methodology/DELIVERABLES_SUMMARY.md +341 -0
- package/skills-library/methodology/DIFFICULTY_AWARE_AGENT_ROUTING.md +252 -0
- package/skills-library/methodology/EVOLUTIONARY_SKILL_SYNTHESIS.md +219 -0
- package/skills-library/methodology/GLOMERULUS_DECISION_GATE.md +223 -0
- package/skills-library/methodology/HIBERNATION_SYSTEM.md +231 -0
- package/skills-library/methodology/INSTRUMENTATION_OVER_RESTRICTION.md +192 -0
- package/skills-library/methodology/MASTER_COMPLETION_SUMMARY.md +444 -0
- package/skills-library/methodology/MASTER_SESSION_COMPLETION.md +743 -0
- package/skills-library/methodology/MERN_QUICK_REFERENCE.md +358 -0
- package/skills-library/methodology/ORGAN_AGENT_MAPPING.md +177 -0
- package/skills-library/methodology/PARALLEL_WAVE_BASED_REFACTORING.md +440 -0
- package/skills-library/methodology/QUICK_REFERENCE.md +358 -0
- package/skills-library/methodology/SDFT_ONPOLICY_SELF_DISTILLATION.md +186 -0
- package/skills-library/methodology/SELF_QUESTIONING_TASK_GENERATION.md +270 -0
- package/skills-library/methodology/SESSION_COMPLETION_SUMMARY.md +304 -0
- package/skills-library/methodology/SESSION_SUMMARY.md +432 -0
- package/skills-library/methodology/WARRIOR_WORKFLOW_DEBUGGING_PROTOCOL.md +252 -0
- package/skills-library/methodology/tech-debt-tracker.md +570 -0
- package/skills-library/parallel-debug/SKILL.md +60 -0
- package/skills-library/patterns-standards/API_PATTERN_FIX_SUMMARY.md +236 -0
- package/skills-library/patterns-standards/BATCH_OPERATIONS_WITH_PROGRESS_MODAL.md +362 -0
- package/skills-library/patterns-standards/CRITICAL_CODING_PATTERNS.md +639 -0
- package/skills-library/patterns-standards/DARK_MODE_MODAL_VISIBILITY.md +258 -0
- package/skills-library/patterns-standards/ERROR_RESILIENCE_IMPLEMENTATION.md +375 -0
- package/skills-library/patterns-standards/ES_MODULE_IMPORT_HOISTING_DOTENV.md +298 -0
- package/skills-library/patterns-standards/NESTED_BACKDROP_FILTER_CSS_ARTIFACT_FIX.md +76 -0
- package/skills-library/patterns-standards/ORDERED_DETECTOR_PIPELINE_GRACEFUL_FALLBACK.md +333 -0
- package/skills-library/patterns-standards/PHASE_IMPORT_ERROR_DEBUGGING.md +271 -0
- package/skills-library/patterns-standards/PYNPUT_GLOBAL_HOTKEY_VK_MATCHING.md +252 -0
- package/skills-library/patterns-standards/REACT_USEEFFECT_CASCADE_RESET_FIX.md +132 -0
- package/skills-library/patterns-standards/SUBMENU_HOVER_DROPDOWN_PATTERN.md +225 -0
- package/skills-library/patterns-standards/TAILWIND_TEXT_VISIBILITY_OVERRIDE.md +322 -0
- package/skills-library/patterns-standards/THEME_AWARE_CSS_VARIABLES_PATTERN.md +209 -0
- package/skills-library/patterns-standards/THEME_USER_OBJECT_PROPERTY_NAMING.md +194 -0
- package/skills-library/patterns-standards/TOOLTIP_BLOCKING_CLICKS_FIX.md +267 -0
- package/skills-library/patterns-standards/claude-code-plugin-structure.md +235 -0
- package/skills-library/patterns-standards/react-i18next-setup.md +429 -0
- package/skills-library/patterns-standards/thesys-c1-generative-ui-integration.md +967 -0
- package/skills-library/plugin-development/CLAUDE_CODE_COMMAND_REGISTRATION_SILENT_FAILURE.md +315 -0
- package/skills-library/plugin-development/plugin-command-namespace-vs-global.md +390 -0
- package/skills-library/plugin-development/plugin-doc-auto-generation.md +172 -0
- package/skills-library/security/GITHUB_REPO_SECURITY_AUDIT.md +115 -0
- package/skills-library/security/admin-deletion-safety.md +396 -0
- package/skills-library/security/application-vuln-patterns.md +477 -0
- package/skills-library/security/env-secrets-manager.md +686 -0
- package/skills-library/security/secure-ai-application-templates.md +347 -0
- package/skills-library/security/sql-injection-prevention-postgresjs.md +151 -0
- package/skills-library/supabase-connection-pooler-fix.md +102 -0
- package/skills-library/system-context/POWERSHELL_BASH_INTEROP.md +82 -0
- package/skills-library/system-context/SERVICE_LIFECYCLE_MANAGEMENT.md +119 -0
- package/skills-library/system-context/SKILL.md +40 -0
- package/skills-library/system-context/WINDOWS_DEV_ENVIRONMENT.md +73 -0
- package/skills-library/testing/E2E_PLAYWRIGHT_PATTERNS.md +99 -0
- package/skills-library/testing/INTEGRATION_TEST_STRATEGY.md +82 -0
- package/skills-library/testing/RED_GREEN_BUGFIX_GATE.md +203 -0
- package/skills-library/testing/TEST_DATA_MANAGEMENT.md +69 -0
- package/skills-library/testing/VITEST_UNIT_TEST_PATTERNS.md +75 -0
- package/skills-library/testing/playwright-api-security-tests.md +202 -0
- package/skills-library/toolbox/SKILL.md +84 -0
- package/skills-library/toolbox/code-graph-and-web-scraping-mcps.md +237 -0
- package/skills-library/ui-ux-pro-max/ACCESSIBILITY_ESSENTIALS.md +115 -0
- package/skills-library/ui-ux-pro-max/DESIGN_SYSTEM_SCAFFOLDING.md +133 -0
- package/skills-library/ui-ux-pro-max/RESPONSIVE_LAYOUT_PATTERNS.md +119 -0
- package/skills-library/ui-ux-pro-max/SKILL.md +386 -0
- package/skills-library/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills-library/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills-library/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills-library/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills-library/ui-ux-pro-max/data/products.csv +97 -0
- package/skills-library/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills-library/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills-library/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills-library/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills-library/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills-library/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills-library/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills-library/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills-library/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills-library/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills-library/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills-library/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills-library/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills-library/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills-library/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills-library/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills-library/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills-library/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills-library/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills-library/wordpress-style-theme-components.md +1526 -0
- package/templates/ASSUMPTIONS.md +1 -1
- package/templates/DECISION_LOG.md +0 -1
- package/templates/phase-prompt.md +1 -1
- package/templates/phoenix-comparison.md +6 -6
- package/templates/skill-api-integration.md +106 -0
- package/templates/skill-architecture-pattern.md +92 -0
- package/templates/skill-debug-pattern.md +98 -0
- package/templates/skill-devops-recipe.md +107 -0
- package/templates/skill-general.md +65 -0
- package/templates/skill-ui-component.md +113 -0
- package/tools/uat-runner.py +179 -0
- package/version.json +7 -3
- package/workflows/handoff-session.md +2 -2
- package/workflows/new-project.md +2 -2
- package/workflows/plan-phase.md +1 -1
- package/.claude-plugin/plugin.json +0 -64
- package/skills-library/_general/methodology/LIVE_BREADCRUMB_PROTOCOL.md +0 -242
- package/skills-library/_general/methodology/llm-judge-memory-crud.md +0 -241
- package/skills-library/methodology/REFLEXION_MEMORY_PATTERN.md +0 -183
- package/skills-library/methodology/RESEARCH_BACKED_WORKFLOW_UPGRADE.md +0 -263
- package/skills-library/methodology/SABBATH_REST_PATTERN.md +0 -267
- package/skills-library/methodology/STONE_AND_SCAFFOLD.md +0 -220
- package/skills-library/specialists/api-architecture/api-designer.md +0 -49
- package/skills-library/specialists/api-architecture/graphql-architect.md +0 -49
- package/skills-library/specialists/api-architecture/mcp-developer.md +0 -51
- package/skills-library/specialists/api-architecture/microservices-architect.md +0 -50
- package/skills-library/specialists/api-architecture/websocket-engineer.md +0 -48
- package/skills-library/specialists/backend/django-expert.md +0 -52
- package/skills-library/specialists/backend/fastapi-expert.md +0 -52
- package/skills-library/specialists/backend/laravel-specialist.md +0 -52
- package/skills-library/specialists/backend/nestjs-expert.md +0 -51
- package/skills-library/specialists/backend/rails-expert.md +0 -53
- package/skills-library/specialists/backend/spring-boot-engineer.md +0 -56
- package/skills-library/specialists/data-ml/fine-tuning-expert.md +0 -48
- package/skills-library/specialists/data-ml/ml-pipeline.md +0 -47
- package/skills-library/specialists/data-ml/pandas-pro.md +0 -47
- package/skills-library/specialists/data-ml/rag-architect.md +0 -51
- package/skills-library/specialists/data-ml/spark-engineer.md +0 -47
- package/skills-library/specialists/frontend/angular-architect.md +0 -52
- package/skills-library/specialists/frontend/flutter-expert.md +0 -51
- package/skills-library/specialists/frontend/nextjs-developer.md +0 -54
- package/skills-library/specialists/frontend/react-native-expert.md +0 -50
- package/skills-library/specialists/frontend/vue-expert.md +0 -51
- package/skills-library/specialists/infrastructure/chaos-engineer.md +0 -74
- package/skills-library/specialists/infrastructure/cloud-architect.md +0 -70
- package/skills-library/specialists/infrastructure/database-optimizer.md +0 -64
- package/skills-library/specialists/infrastructure/devops-engineer.md +0 -70
- package/skills-library/specialists/infrastructure/kubernetes-specialist.md +0 -52
- package/skills-library/specialists/infrastructure/monitoring-expert.md +0 -70
- package/skills-library/specialists/infrastructure/sre-engineer.md +0 -70
- package/skills-library/specialists/infrastructure/terraform-engineer.md +0 -51
- package/skills-library/specialists/languages/cpp-pro.md +0 -74
- package/skills-library/specialists/languages/csharp-developer.md +0 -69
- package/skills-library/specialists/languages/dotnet-core-expert.md +0 -54
- package/skills-library/specialists/languages/golang-pro.md +0 -51
- package/skills-library/specialists/languages/java-architect.md +0 -49
- package/skills-library/specialists/languages/javascript-pro.md +0 -68
- package/skills-library/specialists/languages/kotlin-specialist.md +0 -68
- package/skills-library/specialists/languages/php-pro.md +0 -49
- package/skills-library/specialists/languages/python-pro.md +0 -52
- package/skills-library/specialists/languages/react-expert.md +0 -51
- package/skills-library/specialists/languages/rust-engineer.md +0 -50
- package/skills-library/specialists/languages/sql-pro.md +0 -56
- package/skills-library/specialists/languages/swift-expert.md +0 -69
- package/skills-library/specialists/languages/typescript-pro.md +0 -51
- package/skills-library/specialists/platform/atlassian-mcp.md +0 -52
- package/skills-library/specialists/platform/embedded-systems.md +0 -53
- package/skills-library/specialists/platform/game-developer.md +0 -53
- package/skills-library/specialists/platform/salesforce-developer.md +0 -53
- package/skills-library/specialists/platform/shopify-expert.md +0 -49
- package/skills-library/specialists/platform/wordpress-pro.md +0 -49
- package/skills-library/specialists/quality/code-documenter.md +0 -51
- package/skills-library/specialists/quality/code-reviewer.md +0 -67
- package/skills-library/specialists/quality/debugging-wizard.md +0 -51
- package/skills-library/specialists/quality/fullstack-guardian.md +0 -51
- package/skills-library/specialists/quality/legacy-modernizer.md +0 -50
- package/skills-library/specialists/quality/playwright-expert.md +0 -65
- package/skills-library/specialists/quality/spec-miner.md +0 -56
- package/skills-library/specialists/quality/test-master.md +0 -65
- package/skills-library/specialists/security/secure-code-guardian.md +0 -55
- package/skills-library/specialists/security/security-reviewer.md +0 -53
- package/skills-library/specialists/workflow/architecture-designer.md +0 -53
- package/skills-library/specialists/workflow/cli-developer.md +0 -70
- package/skills-library/specialists/workflow/feature-forge.md +0 -65
- package/skills-library/specialists/workflow/prompt-engineer.md +0 -54
- package/skills-library/specialists/workflow/the-fool.md +0 -62
- /package/skills-library/{performance → _general/performance}/cache-augmented-generation.md +0 -0
- /package/skills-library/{debugging → parallel-debug}/FAILURE_TAXONOMY_CLASSIFICATION.md +0 -0
- /package/skills-library/{debugging → parallel-debug}/THREE_AGENT_HYPOTHESIS_DEBUGGING.md +0 -0
|
@@ -0,0 +1,1965 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: erd-react-flow-architecture
|
|
3
|
+
category: database-solutions
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
contributed: 2026-03-09
|
|
6
|
+
contributor: fire-research
|
|
7
|
+
last_updated: 2026-03-09
|
|
8
|
+
tags: [react-flow, erd-editor, architecture, tauri, shadcn, typescript, chartdb, drawdb]
|
|
9
|
+
difficulty: hard
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# ERD React Flow Architecture
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Problem
|
|
16
|
+
|
|
17
|
+
Building a production-quality visual ERD editor with React Flow requires synthesizing patterns from multiple proven implementations. No single OSS project covers the full surface area: ChartDB has the best stack match but lacks multi-dialect DDL generation; DrawDB has gold-standard parsers but uses a custom canvas; React Flow's official DatabaseSchemaNode handles per-column handles but not Crow's Foot notation. A developer starting from scratch will spend weeks discovering patterns that are already solved across these projects.
|
|
18
|
+
|
|
19
|
+
## Solution Pattern
|
|
20
|
+
|
|
21
|
+
Combine the best of each reference implementation into a layered architecture:
|
|
22
|
+
|
|
23
|
+
1. **Data Model** (DrawDB pattern) — Dialect-neutral JSON as the single source of truth
|
|
24
|
+
2. **React Flow Visualization** (ChartDB + official DatabaseSchemaNode) — Custom node/edge types with per-column handles
|
|
25
|
+
3. **Parser/Generator** (DrawDB pattern) — Separate parser and generator per SQL dialect, all round-tripping through the JSON model
|
|
26
|
+
4. **Persistence** (Tauri + Dexie.js fallback) — File system for desktop, IndexedDB for web
|
|
27
|
+
5. **MCP Integration** (ERFlow pattern) — Expose schema operations as MCP tools for NL editing
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Recommended Stack
|
|
32
|
+
|
|
33
|
+
| Layer | Technology | Why |
|
|
34
|
+
|-------|-----------|-----|
|
|
35
|
+
| Desktop shell | Tauri 2.x | Rust backend, file system access, <10MB binary |
|
|
36
|
+
| UI framework | React 18+ (Vite) | ChartDB uses this exact stack |
|
|
37
|
+
| Canvas | React Flow (v12+) | Declarative node/edge system, built-in controls |
|
|
38
|
+
| Component library | shadcn/ui | ChartDB + NextERD both use it, Tailwind-based |
|
|
39
|
+
| State management | Zustand | React Flow's recommended store, ChartDB uses it |
|
|
40
|
+
| Local persistence (web) | Dexie.js (IndexedDB) | ChartDB pattern — instant save, no backend needed |
|
|
41
|
+
| Local persistence (desktop) | Tauri fs API | Native file dialogs, .erd.json files |
|
|
42
|
+
| DDL parsing | node-sql-parser | Bidirectional SQL-to-AST, multi-dialect |
|
|
43
|
+
| Type safety | TypeScript (strict) | All reference implementations use TS |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Data Model
|
|
48
|
+
|
|
49
|
+
The central JSON model is the most critical architectural decision. Every feature — rendering, parsing, exporting, undo/redo — reads from and writes to this model. DrawDB's approach of a dialect-neutral intermediate representation is the proven pattern.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// ============================================================
|
|
53
|
+
// Core ERD Data Model — dialect-neutral JSON
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
/** Top-level document — serialized as .erd.json */
|
|
57
|
+
export interface ERDDocument {
|
|
58
|
+
version: '1.0.0';
|
|
59
|
+
name: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
createdAt: string; // ISO 8601
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
/** Active notation mode affects rendering only, not the model */
|
|
64
|
+
notation: 'crowsfoot' | 'chen';
|
|
65
|
+
/** Target dialect for DDL export */
|
|
66
|
+
dialect: SQLDialect;
|
|
67
|
+
tables: Table[];
|
|
68
|
+
relationships: Relationship[];
|
|
69
|
+
/** Diagram metadata — positions, viewport, etc. */
|
|
70
|
+
diagram: DiagramMeta;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SQLDialect = 'postgresql' | 'mysql' | 'sqlite' | 'mariadb' | 'mssql';
|
|
74
|
+
|
|
75
|
+
export interface Table {
|
|
76
|
+
id: string; // nanoid or cuid
|
|
77
|
+
name: string;
|
|
78
|
+
schema?: string; // 'public', 'dbo', etc.
|
|
79
|
+
comment?: string;
|
|
80
|
+
columns: Column[];
|
|
81
|
+
indexes: Index[];
|
|
82
|
+
/** Chen-mode only: which columns to render as separate oval nodes */
|
|
83
|
+
chenAttributeDisplay?: 'inline' | 'ovals';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Column {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
type: string; // Raw SQL type string: 'VARCHAR(255)', 'INTEGER', etc.
|
|
90
|
+
isPrimaryKey: boolean;
|
|
91
|
+
isForeignKey: boolean;
|
|
92
|
+
isNullable: boolean;
|
|
93
|
+
isUnique: boolean;
|
|
94
|
+
isAutoIncrement: boolean;
|
|
95
|
+
defaultValue?: string;
|
|
96
|
+
comment?: string;
|
|
97
|
+
/** For composite PKs — order within the key */
|
|
98
|
+
pkOrdinal?: number;
|
|
99
|
+
/** FK reference (also represented in Relationship[], but stored here for column-level rendering) */
|
|
100
|
+
references?: {
|
|
101
|
+
tableId: string;
|
|
102
|
+
columnId: string;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Index {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
columns: string[]; // Column IDs
|
|
110
|
+
isUnique: boolean;
|
|
111
|
+
type?: 'btree' | 'hash' | 'gin' | 'gist';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface Relationship {
|
|
115
|
+
id: string;
|
|
116
|
+
name?: string; // e.g., 'fk_orders_customer'
|
|
117
|
+
sourceTableId: string;
|
|
118
|
+
sourceColumnId: string;
|
|
119
|
+
targetTableId: string;
|
|
120
|
+
targetColumnId: string;
|
|
121
|
+
/** Cardinality at source end */
|
|
122
|
+
sourceCardinality: Cardinality;
|
|
123
|
+
/** Cardinality at target end */
|
|
124
|
+
targetCardinality: Cardinality;
|
|
125
|
+
/** ON DELETE behavior */
|
|
126
|
+
onDelete: ReferentialAction;
|
|
127
|
+
/** ON UPDATE behavior */
|
|
128
|
+
onUpdate: ReferentialAction;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type Cardinality = 'exactly-one' | 'zero-or-one' | 'one-or-many' | 'zero-or-many';
|
|
132
|
+
|
|
133
|
+
export type ReferentialAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
|
134
|
+
|
|
135
|
+
export interface DiagramMeta {
|
|
136
|
+
/** Per-table positions on the canvas */
|
|
137
|
+
positions: Record<string, { x: number; y: number }>;
|
|
138
|
+
/** Viewport state for restoring pan/zoom */
|
|
139
|
+
viewport: {
|
|
140
|
+
x: number;
|
|
141
|
+
y: number;
|
|
142
|
+
zoom: number;
|
|
143
|
+
};
|
|
144
|
+
/** Grid snap settings */
|
|
145
|
+
gridSize: number;
|
|
146
|
+
snapToGrid: boolean;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### JSON Serialization Format (.erd.json)
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"version": "1.0.0",
|
|
155
|
+
"name": "E-Commerce Schema",
|
|
156
|
+
"notation": "crowsfoot",
|
|
157
|
+
"dialect": "postgresql",
|
|
158
|
+
"tables": [
|
|
159
|
+
{
|
|
160
|
+
"id": "tbl_001",
|
|
161
|
+
"name": "users",
|
|
162
|
+
"columns": [
|
|
163
|
+
{ "id": "col_001", "name": "id", "type": "UUID", "isPrimaryKey": true, "isForeignKey": false, "isNullable": false, "isUnique": true, "isAutoIncrement": false, "defaultValue": "gen_random_uuid()" },
|
|
164
|
+
{ "id": "col_002", "name": "email", "type": "VARCHAR(255)", "isPrimaryKey": false, "isForeignKey": false, "isNullable": false, "isUnique": true, "isAutoIncrement": false },
|
|
165
|
+
{ "id": "col_003", "name": "created_at", "type": "TIMESTAMPTZ", "isPrimaryKey": false, "isForeignKey": false, "isNullable": false, "isUnique": false, "isAutoIncrement": false, "defaultValue": "NOW()" }
|
|
166
|
+
],
|
|
167
|
+
"indexes": []
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"relationships": [],
|
|
171
|
+
"diagram": {
|
|
172
|
+
"positions": { "tbl_001": { "x": 100, "y": 200 } },
|
|
173
|
+
"viewport": { "x": 0, "y": 0, "zoom": 1 },
|
|
174
|
+
"gridSize": 20,
|
|
175
|
+
"snapToGrid": true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## React Flow Node Types
|
|
183
|
+
|
|
184
|
+
### Table Node (Crow's Foot Mode) — Primary
|
|
185
|
+
|
|
186
|
+
Based on React Flow's official `DatabaseSchemaNode` pattern, extended with PK/FK icons, nullable indicators, and per-column source/target handles for field-level connections.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
// components/nodes/TableNode.tsx
|
|
190
|
+
import { memo } from 'react';
|
|
191
|
+
import { NodeProps, Handle, Position } from '@xyflow/react';
|
|
192
|
+
import { KeyRound, Link2, CircleDot } from 'lucide-react';
|
|
193
|
+
import { cn } from '@/lib/utils';
|
|
194
|
+
import type { Table, Column } from '@/types/erd';
|
|
195
|
+
|
|
196
|
+
interface TableNodeData {
|
|
197
|
+
table: Table;
|
|
198
|
+
isSelected: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const TableNode = memo(({ data, selected }: NodeProps<TableNodeData>) => {
|
|
202
|
+
const { table } = data;
|
|
203
|
+
const pkColumns = table.columns.filter(c => c.isPrimaryKey);
|
|
204
|
+
const regularColumns = table.columns.filter(c => !c.isPrimaryKey);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
className={cn(
|
|
209
|
+
'rounded-lg border bg-card text-card-foreground shadow-sm min-w-[220px]',
|
|
210
|
+
'transition-shadow duration-200',
|
|
211
|
+
selected && 'ring-2 ring-primary shadow-lg',
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
{/* Table header */}
|
|
215
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-t-lg border-b">
|
|
216
|
+
<CircleDot className="h-4 w-4 text-muted-foreground" />
|
|
217
|
+
<span className="font-semibold text-sm">{table.name}</span>
|
|
218
|
+
<span className="ml-auto text-xs text-muted-foreground">
|
|
219
|
+
{table.columns.length} cols
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Primary key columns — always on top */}
|
|
224
|
+
{pkColumns.map((col) => (
|
|
225
|
+
<ColumnRow key={col.id} column={col} tableId={table.id} isPk />
|
|
226
|
+
))}
|
|
227
|
+
|
|
228
|
+
{/* Separator between PK and regular columns */}
|
|
229
|
+
{pkColumns.length > 0 && regularColumns.length > 0 && (
|
|
230
|
+
<div className="border-t border-dashed" />
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{/* Regular columns */}
|
|
234
|
+
{regularColumns.map((col) => (
|
|
235
|
+
<ColumnRow key={col.id} column={col} tableId={table.id} />
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
TableNode.displayName = 'TableNode';
|
|
242
|
+
|
|
243
|
+
/** Individual column row with per-column handles */
|
|
244
|
+
function ColumnRow({
|
|
245
|
+
column,
|
|
246
|
+
tableId,
|
|
247
|
+
isPk = false,
|
|
248
|
+
}: {
|
|
249
|
+
column: Column;
|
|
250
|
+
tableId: string;
|
|
251
|
+
isPk?: boolean;
|
|
252
|
+
}) {
|
|
253
|
+
// Handle IDs follow the pattern: {tableId}.{columnId}
|
|
254
|
+
// This enables field-level edge connections
|
|
255
|
+
const handleId = `${tableId}.${column.id}`;
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div className="relative flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted/30 group">
|
|
259
|
+
{/* Source handle — left side (target for incoming FKs) */}
|
|
260
|
+
<Handle
|
|
261
|
+
type="target"
|
|
262
|
+
position={Position.Left}
|
|
263
|
+
id={`${handleId}-target`}
|
|
264
|
+
className="!w-2 !h-2 !bg-primary/60 !border-primary"
|
|
265
|
+
style={{ top: '50%' }}
|
|
266
|
+
/>
|
|
267
|
+
|
|
268
|
+
{/* Column icon */}
|
|
269
|
+
{isPk ? (
|
|
270
|
+
<KeyRound className="h-3.5 w-3.5 text-amber-500 shrink-0" />
|
|
271
|
+
) : column.isForeignKey ? (
|
|
272
|
+
<Link2 className="h-3.5 w-3.5 text-blue-500 shrink-0" />
|
|
273
|
+
) : (
|
|
274
|
+
<span className="w-3.5 shrink-0" />
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Column name */}
|
|
278
|
+
<span className={cn('font-mono', isPk && 'font-semibold')}>
|
|
279
|
+
{column.name}
|
|
280
|
+
</span>
|
|
281
|
+
|
|
282
|
+
{/* Column type */}
|
|
283
|
+
<span className="ml-auto text-muted-foreground font-mono">
|
|
284
|
+
{column.type}
|
|
285
|
+
{!column.isNullable && (
|
|
286
|
+
<span className="text-red-400 ml-1" title="NOT NULL">*</span>
|
|
287
|
+
)}
|
|
288
|
+
</span>
|
|
289
|
+
|
|
290
|
+
{/* Source handle — right side (source for outgoing FKs) */}
|
|
291
|
+
<Handle
|
|
292
|
+
type="source"
|
|
293
|
+
position={Position.Right}
|
|
294
|
+
id={`${handleId}-source`}
|
|
295
|
+
className="!w-2 !h-2 !bg-primary/60 !border-primary"
|
|
296
|
+
style={{ top: '50%' }}
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Entity Node (Chen Mode)
|
|
304
|
+
|
|
305
|
+
In Chen notation, entities are rectangles and attributes are separate oval nodes connected by lines. The same data model renders differently.
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
// components/nodes/ChenEntityNode.tsx
|
|
309
|
+
import { memo } from 'react';
|
|
310
|
+
import { NodeProps, Handle, Position } from '@xyflow/react';
|
|
311
|
+
import { cn } from '@/lib/utils';
|
|
312
|
+
|
|
313
|
+
interface ChenEntityNodeData {
|
|
314
|
+
name: string;
|
|
315
|
+
isWeak: boolean;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export const ChenEntityNode = memo(({ data, selected }: NodeProps<ChenEntityNodeData>) => (
|
|
319
|
+
<div
|
|
320
|
+
className={cn(
|
|
321
|
+
'px-6 py-3 bg-card border-2 rounded-sm text-center font-semibold',
|
|
322
|
+
data.isWeak && 'border-double border-4',
|
|
323
|
+
selected && 'ring-2 ring-primary',
|
|
324
|
+
)}
|
|
325
|
+
>
|
|
326
|
+
{data.name}
|
|
327
|
+
{/* Handles on all 4 sides for flexible attribute/relationship connections */}
|
|
328
|
+
<Handle type="source" position={Position.Top} id="top" />
|
|
329
|
+
<Handle type="source" position={Position.Right} id="right" />
|
|
330
|
+
<Handle type="source" position={Position.Bottom} id="bottom" />
|
|
331
|
+
<Handle type="source" position={Position.Left} id="left" />
|
|
332
|
+
</div>
|
|
333
|
+
));
|
|
334
|
+
|
|
335
|
+
ChenEntityNode.displayName = 'ChenEntityNode';
|
|
336
|
+
|
|
337
|
+
// components/nodes/ChenAttributeNode.tsx
|
|
338
|
+
export const ChenAttributeNode = memo(({ data, selected }: NodeProps<{
|
|
339
|
+
name: string;
|
|
340
|
+
variant: 'simple' | 'key' | 'multivalued' | 'derived' | 'composite' | 'partial-key';
|
|
341
|
+
}>) => (
|
|
342
|
+
<div
|
|
343
|
+
className={cn(
|
|
344
|
+
'px-4 py-2 rounded-full bg-card text-center text-sm border',
|
|
345
|
+
data.variant === 'key' && 'font-bold [&>span]:underline',
|
|
346
|
+
data.variant === 'multivalued' && 'border-double border-4',
|
|
347
|
+
data.variant === 'derived' && 'border-dashed',
|
|
348
|
+
data.variant === 'partial-key' && 'font-bold [&>span]:underline [&>span]:decoration-dashed',
|
|
349
|
+
selected && 'ring-2 ring-primary',
|
|
350
|
+
)}
|
|
351
|
+
>
|
|
352
|
+
<span>{data.name}</span>
|
|
353
|
+
<Handle type="target" position={Position.Top} id="top" />
|
|
354
|
+
<Handle type="target" position={Position.Bottom} id="bottom" />
|
|
355
|
+
<Handle type="target" position={Position.Left} id="left" />
|
|
356
|
+
<Handle type="target" position={Position.Right} id="right" />
|
|
357
|
+
</div>
|
|
358
|
+
));
|
|
359
|
+
|
|
360
|
+
ChenAttributeNode.displayName = 'ChenAttributeNode';
|
|
361
|
+
|
|
362
|
+
// components/nodes/ChenRelationshipNode.tsx (Diamond shape)
|
|
363
|
+
export const ChenRelationshipNode = memo(({ data, selected }: NodeProps<{
|
|
364
|
+
name: string;
|
|
365
|
+
isIdentifying: boolean;
|
|
366
|
+
}>) => (
|
|
367
|
+
<div className="relative" style={{ width: 120, height: 80 }}>
|
|
368
|
+
<svg viewBox="0 0 120 80" className="absolute inset-0">
|
|
369
|
+
<polygon
|
|
370
|
+
points="60,2 118,40 60,78 2,40"
|
|
371
|
+
className={cn(
|
|
372
|
+
'fill-card stroke-foreground',
|
|
373
|
+
data.isIdentifying ? 'stroke-[3]' : 'stroke-[1.5]',
|
|
374
|
+
selected && 'stroke-primary',
|
|
375
|
+
)}
|
|
376
|
+
/>
|
|
377
|
+
{data.isIdentifying && (
|
|
378
|
+
<polygon
|
|
379
|
+
points="60,8 112,40 60,72 8,40"
|
|
380
|
+
className="fill-none stroke-foreground stroke-[1.5]"
|
|
381
|
+
/>
|
|
382
|
+
)}
|
|
383
|
+
</svg>
|
|
384
|
+
<span className="absolute inset-0 flex items-center justify-center text-xs font-medium">
|
|
385
|
+
{data.name}
|
|
386
|
+
</span>
|
|
387
|
+
<Handle type="source" position={Position.Top} id="top" style={{ left: '50%' }} />
|
|
388
|
+
<Handle type="source" position={Position.Right} id="right" style={{ top: '50%' }} />
|
|
389
|
+
<Handle type="source" position={Position.Bottom} id="bottom" style={{ left: '50%' }} />
|
|
390
|
+
<Handle type="source" position={Position.Left} id="left" style={{ top: '50%' }} />
|
|
391
|
+
</div>
|
|
392
|
+
));
|
|
393
|
+
|
|
394
|
+
ChenRelationshipNode.displayName = 'ChenRelationshipNode';
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Node Type Registration
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
// lib/node-types.ts
|
|
401
|
+
import { TableNode } from '@/components/nodes/TableNode';
|
|
402
|
+
import { ChenEntityNode, ChenAttributeNode, ChenRelationshipNode } from '@/components/nodes/ChenNodes';
|
|
403
|
+
|
|
404
|
+
export const crowsFootNodeTypes = {
|
|
405
|
+
table: TableNode,
|
|
406
|
+
} as const;
|
|
407
|
+
|
|
408
|
+
export const chenNodeTypes = {
|
|
409
|
+
entity: ChenEntityNode,
|
|
410
|
+
attribute: ChenAttributeNode,
|
|
411
|
+
relationship: ChenRelationshipNode,
|
|
412
|
+
} as const;
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## React Flow Edge Types
|
|
418
|
+
|
|
419
|
+
### Crow's Foot Edge — Custom SVG Markers
|
|
420
|
+
|
|
421
|
+
The key insight from `relliv/crows-foot-notations`: define 4 SVG marker symbols in a `<defs>` block, then reference them as `markerStart`/`markerEnd` on React Flow edges.
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
// components/edges/CrowsFootMarkers.tsx
|
|
425
|
+
/**
|
|
426
|
+
* SVG marker definitions for Crow's Foot notation.
|
|
427
|
+
* Render this ONCE inside the ReactFlow component.
|
|
428
|
+
*
|
|
429
|
+
* 4 cardinality markers:
|
|
430
|
+
* exactly-one: --|-- (single perpendicular line)
|
|
431
|
+
* zero-or-one: --O|-- (circle + perpendicular line)
|
|
432
|
+
* one-or-many: --<|-- (fork + perpendicular line)
|
|
433
|
+
* zero-or-many: --O<-- (circle + fork)
|
|
434
|
+
*/
|
|
435
|
+
export function CrowsFootMarkerDefs() {
|
|
436
|
+
return (
|
|
437
|
+
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
|
438
|
+
<defs>
|
|
439
|
+
{/* Exactly One: perpendicular line */}
|
|
440
|
+
<marker
|
|
441
|
+
id="cf-exactly-one"
|
|
442
|
+
viewBox="0 0 20 20"
|
|
443
|
+
refX="18"
|
|
444
|
+
refY="10"
|
|
445
|
+
markerWidth="20"
|
|
446
|
+
markerHeight="20"
|
|
447
|
+
orient="auto-start-reverse"
|
|
448
|
+
markerUnits="userSpaceOnUse"
|
|
449
|
+
>
|
|
450
|
+
<line x1="18" y1="2" x2="18" y2="18" stroke="currentColor" strokeWidth="2" />
|
|
451
|
+
<line x1="12" y1="2" x2="12" y2="18" stroke="currentColor" strokeWidth="2" />
|
|
452
|
+
</marker>
|
|
453
|
+
|
|
454
|
+
{/* Zero or One: circle + perpendicular line */}
|
|
455
|
+
<marker
|
|
456
|
+
id="cf-zero-or-one"
|
|
457
|
+
viewBox="0 0 30 20"
|
|
458
|
+
refX="28"
|
|
459
|
+
refY="10"
|
|
460
|
+
markerWidth="30"
|
|
461
|
+
markerHeight="20"
|
|
462
|
+
orient="auto-start-reverse"
|
|
463
|
+
markerUnits="userSpaceOnUse"
|
|
464
|
+
>
|
|
465
|
+
<circle cx="10" cy="10" r="6" fill="white" stroke="currentColor" strokeWidth="2" />
|
|
466
|
+
<line x1="28" y1="2" x2="28" y2="18" stroke="currentColor" strokeWidth="2" />
|
|
467
|
+
</marker>
|
|
468
|
+
|
|
469
|
+
{/* One or Many: fork (crow's foot) + perpendicular line */}
|
|
470
|
+
<marker
|
|
471
|
+
id="cf-one-or-many"
|
|
472
|
+
viewBox="0 0 24 20"
|
|
473
|
+
refX="22"
|
|
474
|
+
refY="10"
|
|
475
|
+
markerWidth="24"
|
|
476
|
+
markerHeight="20"
|
|
477
|
+
orient="auto-start-reverse"
|
|
478
|
+
markerUnits="userSpaceOnUse"
|
|
479
|
+
>
|
|
480
|
+
{/* Perpendicular line */}
|
|
481
|
+
<line x1="22" y1="2" x2="22" y2="18" stroke="currentColor" strokeWidth="2" />
|
|
482
|
+
{/* Fork: three lines diverging from right to left */}
|
|
483
|
+
<line x1="16" y1="10" x2="4" y2="2" stroke="currentColor" strokeWidth="1.5" />
|
|
484
|
+
<line x1="16" y1="10" x2="4" y2="10" stroke="currentColor" strokeWidth="1.5" />
|
|
485
|
+
<line x1="16" y1="10" x2="4" y2="18" stroke="currentColor" strokeWidth="1.5" />
|
|
486
|
+
</marker>
|
|
487
|
+
|
|
488
|
+
{/* Zero or Many: circle + fork */}
|
|
489
|
+
<marker
|
|
490
|
+
id="cf-zero-or-many"
|
|
491
|
+
viewBox="0 0 34 20"
|
|
492
|
+
refX="32"
|
|
493
|
+
refY="10"
|
|
494
|
+
markerWidth="34"
|
|
495
|
+
markerHeight="20"
|
|
496
|
+
orient="auto-start-reverse"
|
|
497
|
+
markerUnits="userSpaceOnUse"
|
|
498
|
+
>
|
|
499
|
+
{/* Circle (zero) */}
|
|
500
|
+
<circle cx="8" cy="10" r="6" fill="white" stroke="currentColor" strokeWidth="2" />
|
|
501
|
+
{/* Fork: three lines diverging */}
|
|
502
|
+
<line x1="26" y1="10" x2="16" y2="2" stroke="currentColor" strokeWidth="1.5" />
|
|
503
|
+
<line x1="26" y1="10" x2="16" y2="10" stroke="currentColor" strokeWidth="1.5" />
|
|
504
|
+
<line x1="26" y1="10" x2="16" y2="18" stroke="currentColor" strokeWidth="1.5" />
|
|
505
|
+
</marker>
|
|
506
|
+
</defs>
|
|
507
|
+
</svg>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Map cardinality enum to marker ID */
|
|
512
|
+
export function getMarkerId(cardinality: Cardinality): string {
|
|
513
|
+
const map: Record<Cardinality, string> = {
|
|
514
|
+
'exactly-one': 'url(#cf-exactly-one)',
|
|
515
|
+
'zero-or-one': 'url(#cf-zero-or-one)',
|
|
516
|
+
'one-or-many': 'url(#cf-one-or-many)',
|
|
517
|
+
'zero-or-many': 'url(#cf-zero-or-many)',
|
|
518
|
+
};
|
|
519
|
+
return map[cardinality];
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Crow's Foot Edge Component
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
// components/edges/CrowsFootEdge.tsx
|
|
527
|
+
import { memo } from 'react';
|
|
528
|
+
import { EdgeProps, getBezierPath } from '@xyflow/react';
|
|
529
|
+
import { getMarkerId } from './CrowsFootMarkers';
|
|
530
|
+
import type { Cardinality } from '@/types/erd';
|
|
531
|
+
|
|
532
|
+
interface CrowsFootEdgeData {
|
|
533
|
+
sourceCardinality: Cardinality;
|
|
534
|
+
targetCardinality: Cardinality;
|
|
535
|
+
relationshipName?: string;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export const CrowsFootEdge = memo(({
|
|
539
|
+
id,
|
|
540
|
+
sourceX, sourceY,
|
|
541
|
+
targetX, targetY,
|
|
542
|
+
sourcePosition, targetPosition,
|
|
543
|
+
data,
|
|
544
|
+
selected,
|
|
545
|
+
}: EdgeProps<CrowsFootEdgeData>) => {
|
|
546
|
+
const [edgePath, labelX, labelY] = getBezierPath({
|
|
547
|
+
sourceX, sourceY, targetX, targetY,
|
|
548
|
+
sourcePosition, targetPosition,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<>
|
|
553
|
+
{/* Invisible wider path for easier click/hover targeting */}
|
|
554
|
+
<path
|
|
555
|
+
d={edgePath}
|
|
556
|
+
fill="none"
|
|
557
|
+
stroke="transparent"
|
|
558
|
+
strokeWidth={20}
|
|
559
|
+
className="react-flow__edge-interaction"
|
|
560
|
+
/>
|
|
561
|
+
{/* Visible edge line */}
|
|
562
|
+
<path
|
|
563
|
+
id={id}
|
|
564
|
+
d={edgePath}
|
|
565
|
+
fill="none"
|
|
566
|
+
stroke={selected ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'}
|
|
567
|
+
strokeWidth={selected ? 2.5 : 1.5}
|
|
568
|
+
markerStart={data ? getMarkerId(data.sourceCardinality) : undefined}
|
|
569
|
+
markerEnd={data ? getMarkerId(data.targetCardinality) : undefined}
|
|
570
|
+
/>
|
|
571
|
+
{/* Relationship label */}
|
|
572
|
+
{data?.relationshipName && (
|
|
573
|
+
<foreignObject
|
|
574
|
+
width={120}
|
|
575
|
+
height={24}
|
|
576
|
+
x={labelX - 60}
|
|
577
|
+
y={labelY - 12}
|
|
578
|
+
requiredExtensions="http://www.w3.org/1999/xhtml"
|
|
579
|
+
>
|
|
580
|
+
<div className="text-[10px] text-muted-foreground text-center bg-background/80 rounded px-1">
|
|
581
|
+
{data.relationshipName}
|
|
582
|
+
</div>
|
|
583
|
+
</foreignObject>
|
|
584
|
+
)}
|
|
585
|
+
</>
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
CrowsFootEdge.displayName = 'CrowsFootEdge';
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Chen Notation Edge
|
|
593
|
+
|
|
594
|
+
```tsx
|
|
595
|
+
// components/edges/ChenEdge.tsx
|
|
596
|
+
// Simpler edge — cardinality labels as text, participation as line style
|
|
597
|
+
import { memo } from 'react';
|
|
598
|
+
import { EdgeProps, getStraightPath } from '@xyflow/react';
|
|
599
|
+
|
|
600
|
+
interface ChenEdgeData {
|
|
601
|
+
cardinality?: string; // '1', 'M', 'N'
|
|
602
|
+
isTotalParticipation: boolean;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export const ChenEdge = memo(({
|
|
606
|
+
sourceX, sourceY, targetX, targetY, data, id,
|
|
607
|
+
}: EdgeProps<ChenEdgeData>) => {
|
|
608
|
+
const [path, labelX, labelY] = getStraightPath({
|
|
609
|
+
sourceX, sourceY, targetX, targetY,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
<>
|
|
614
|
+
<path
|
|
615
|
+
id={id}
|
|
616
|
+
d={path}
|
|
617
|
+
fill="none"
|
|
618
|
+
stroke="hsl(var(--foreground))"
|
|
619
|
+
strokeWidth={data?.isTotalParticipation ? 3 : 1.5}
|
|
620
|
+
/>
|
|
621
|
+
{data?.cardinality && (
|
|
622
|
+
<text x={labelX} y={labelY - 8} textAnchor="middle" className="text-xs fill-foreground">
|
|
623
|
+
{data.cardinality}
|
|
624
|
+
</text>
|
|
625
|
+
)}
|
|
626
|
+
</>
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
ChenEdge.displayName = 'ChenEdge';
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Edge Type Registration
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
// lib/edge-types.ts
|
|
637
|
+
import { CrowsFootEdge } from '@/components/edges/CrowsFootEdge';
|
|
638
|
+
import { ChenEdge } from '@/components/edges/ChenEdge';
|
|
639
|
+
|
|
640
|
+
export const crowsFootEdgeTypes = {
|
|
641
|
+
crowsfoot: CrowsFootEdge,
|
|
642
|
+
} as const;
|
|
643
|
+
|
|
644
|
+
export const chenEdgeTypes = {
|
|
645
|
+
chen: ChenEdge,
|
|
646
|
+
} as const;
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Canvas Features
|
|
652
|
+
|
|
653
|
+
### Core React Flow Setup
|
|
654
|
+
|
|
655
|
+
```tsx
|
|
656
|
+
// components/ERDCanvas.tsx
|
|
657
|
+
import { useCallback } from 'react';
|
|
658
|
+
import {
|
|
659
|
+
ReactFlow,
|
|
660
|
+
Background,
|
|
661
|
+
Controls,
|
|
662
|
+
MiniMap,
|
|
663
|
+
Panel,
|
|
664
|
+
type OnConnect,
|
|
665
|
+
BackgroundVariant,
|
|
666
|
+
} from '@xyflow/react';
|
|
667
|
+
import '@xyflow/react/dist/style.css';
|
|
668
|
+
import { crowsFootNodeTypes } from '@/lib/node-types';
|
|
669
|
+
import { crowsFootEdgeTypes } from '@/lib/edge-types';
|
|
670
|
+
import { CrowsFootMarkerDefs } from '@/components/edges/CrowsFootMarkers';
|
|
671
|
+
import { useERDStore } from '@/store/erd-store';
|
|
672
|
+
|
|
673
|
+
export function ERDCanvas() {
|
|
674
|
+
const { nodes, edges, onNodesChange, onEdgesChange } = useERDStore();
|
|
675
|
+
|
|
676
|
+
const onConnect: OnConnect = useCallback((connection) => {
|
|
677
|
+
// Create relationship from connection
|
|
678
|
+
// Parse handle IDs to get table + column: "tbl_001.col_002-source"
|
|
679
|
+
useERDStore.getState().addRelationship(connection);
|
|
680
|
+
}, []);
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<div className="h-full w-full">
|
|
684
|
+
<CrowsFootMarkerDefs />
|
|
685
|
+
<ReactFlow
|
|
686
|
+
nodes={nodes}
|
|
687
|
+
edges={edges}
|
|
688
|
+
onNodesChange={onNodesChange}
|
|
689
|
+
onEdgesChange={onEdgesChange}
|
|
690
|
+
onConnect={onConnect}
|
|
691
|
+
nodeTypes={crowsFootNodeTypes}
|
|
692
|
+
edgeTypes={crowsFootEdgeTypes}
|
|
693
|
+
snapToGrid
|
|
694
|
+
snapGrid={[20, 20]}
|
|
695
|
+
fitView
|
|
696
|
+
minZoom={0.1}
|
|
697
|
+
maxZoom={4}
|
|
698
|
+
deleteKeyCode={['Backspace', 'Delete']}
|
|
699
|
+
multiSelectionKeyCode="Shift"
|
|
700
|
+
proOptions={{ hideAttribution: true }}
|
|
701
|
+
>
|
|
702
|
+
<Background variant={BackgroundVariant.Dots} gap={20} size={1} />
|
|
703
|
+
<Controls showInteractive={false} />
|
|
704
|
+
<MiniMap
|
|
705
|
+
nodeStrokeWidth={3}
|
|
706
|
+
zoomable
|
|
707
|
+
pannable
|
|
708
|
+
className="!bg-muted/50 !border-border"
|
|
709
|
+
/>
|
|
710
|
+
<Panel position="top-right">
|
|
711
|
+
{/* Toolbar: add table, import, export, undo/redo, notation toggle */}
|
|
712
|
+
</Panel>
|
|
713
|
+
</ReactFlow>
|
|
714
|
+
</div>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Zustand Store with Undo/Redo
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
// store/erd-store.ts
|
|
723
|
+
import { create } from 'zustand';
|
|
724
|
+
import { temporal } from 'zundo';
|
|
725
|
+
import {
|
|
726
|
+
type Node, type Edge,
|
|
727
|
+
applyNodeChanges, applyEdgeChanges,
|
|
728
|
+
type NodeChange, type EdgeChange,
|
|
729
|
+
} from '@xyflow/react';
|
|
730
|
+
import { nanoid } from 'nanoid';
|
|
731
|
+
import type { ERDDocument, Table, Relationship } from '@/types/erd';
|
|
732
|
+
|
|
733
|
+
interface ERDState {
|
|
734
|
+
document: ERDDocument;
|
|
735
|
+
nodes: Node[];
|
|
736
|
+
edges: Edge[];
|
|
737
|
+
// React Flow handlers
|
|
738
|
+
onNodesChange: (changes: NodeChange[]) => void;
|
|
739
|
+
onEdgesChange: (changes: EdgeChange[]) => void;
|
|
740
|
+
// ERD operations
|
|
741
|
+
addTable: (table: Table) => void;
|
|
742
|
+
updateTable: (tableId: string, updates: Partial<Table>) => void;
|
|
743
|
+
removeTable: (tableId: string) => void;
|
|
744
|
+
addRelationship: (connection: any) => void;
|
|
745
|
+
removeRelationship: (relId: string) => void;
|
|
746
|
+
// I/O
|
|
747
|
+
loadDocument: (doc: ERDDocument) => void;
|
|
748
|
+
toDocument: () => ERDDocument;
|
|
749
|
+
// Sync: convert ERDDocument <-> React Flow nodes/edges
|
|
750
|
+
syncFromModel: () => void;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Zustand store wrapped with zundo for undo/redo.
|
|
755
|
+
* Ctrl+Z / Ctrl+Shift+Z call useTemporalStore().undo() / .redo()
|
|
756
|
+
*/
|
|
757
|
+
export const useERDStore = create<ERDState>()(
|
|
758
|
+
temporal(
|
|
759
|
+
(set, get) => ({
|
|
760
|
+
document: createEmptyDocument(),
|
|
761
|
+
nodes: [],
|
|
762
|
+
edges: [],
|
|
763
|
+
|
|
764
|
+
onNodesChange: (changes) => {
|
|
765
|
+
set({ nodes: applyNodeChanges(changes, get().nodes) });
|
|
766
|
+
// Sync position changes back to document.diagram.positions
|
|
767
|
+
for (const change of changes) {
|
|
768
|
+
if (change.type === 'position' && change.position) {
|
|
769
|
+
get().document.diagram.positions[change.id] = change.position;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
onEdgesChange: (changes) => {
|
|
775
|
+
set({ edges: applyEdgeChanges(changes, get().edges) });
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
addTable: (table) => {
|
|
779
|
+
const doc = get().document;
|
|
780
|
+
// Immutable update — required for zundo temporal middleware undo/redo
|
|
781
|
+
set({ document: { ...doc, tables: [...doc.tables, table] } });
|
|
782
|
+
get().syncFromModel();
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
updateTable: (tableId, updates) => {
|
|
786
|
+
const doc = get().document;
|
|
787
|
+
// Immutable update — map produces new array, preserving zundo snapshots
|
|
788
|
+
set({
|
|
789
|
+
document: {
|
|
790
|
+
...doc,
|
|
791
|
+
tables: doc.tables.map(t =>
|
|
792
|
+
t.id === tableId ? { ...t, ...updates } : t
|
|
793
|
+
),
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
get().syncFromModel();
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
removeTable: (tableId) => {
|
|
800
|
+
const doc = get().document;
|
|
801
|
+
// Immutable update — filter + destructure, never mutate doc in-place
|
|
802
|
+
const { [tableId]: _, ...remainingPositions } = doc.diagram.positions;
|
|
803
|
+
set({
|
|
804
|
+
document: {
|
|
805
|
+
...doc,
|
|
806
|
+
tables: doc.tables.filter(t => t.id !== tableId),
|
|
807
|
+
relationships: doc.relationships.filter(
|
|
808
|
+
r => r.sourceTableId !== tableId && r.targetTableId !== tableId
|
|
809
|
+
),
|
|
810
|
+
diagram: { ...doc.diagram, positions: remainingPositions },
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
get().syncFromModel();
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
addRelationship: (connection) => {
|
|
817
|
+
// Parse handle IDs: "tbl_001.col_002-source" -> tableId="tbl_001", columnId="col_002"
|
|
818
|
+
const parseHandle = (handleId: string) => {
|
|
819
|
+
const [combined] = handleId.split('-');
|
|
820
|
+
const [tableId, columnId] = combined.split('.');
|
|
821
|
+
return { tableId, columnId };
|
|
822
|
+
};
|
|
823
|
+
const source = parseHandle(connection.sourceHandle);
|
|
824
|
+
const target = parseHandle(connection.targetHandle);
|
|
825
|
+
const rel: Relationship = {
|
|
826
|
+
id: nanoid(),
|
|
827
|
+
sourceTableId: source.tableId,
|
|
828
|
+
sourceColumnId: source.columnId,
|
|
829
|
+
targetTableId: target.tableId,
|
|
830
|
+
targetColumnId: target.columnId,
|
|
831
|
+
sourceCardinality: 'exactly-one',
|
|
832
|
+
targetCardinality: 'zero-or-many',
|
|
833
|
+
onDelete: 'NO ACTION',
|
|
834
|
+
onUpdate: 'NO ACTION',
|
|
835
|
+
};
|
|
836
|
+
const doc = get().document;
|
|
837
|
+
doc.relationships.push(rel);
|
|
838
|
+
set({ document: { ...doc } });
|
|
839
|
+
get().syncFromModel();
|
|
840
|
+
},
|
|
841
|
+
|
|
842
|
+
removeRelationship: (relId) => {
|
|
843
|
+
const doc = get().document;
|
|
844
|
+
doc.relationships = doc.relationships.filter(r => r.id !== relId);
|
|
845
|
+
set({ document: { ...doc } });
|
|
846
|
+
get().syncFromModel();
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
loadDocument: (doc) => {
|
|
850
|
+
set({ document: doc });
|
|
851
|
+
get().syncFromModel();
|
|
852
|
+
},
|
|
853
|
+
|
|
854
|
+
toDocument: () => {
|
|
855
|
+
const state = get();
|
|
856
|
+
// Sync current positions back to document
|
|
857
|
+
for (const node of state.nodes) {
|
|
858
|
+
if (node.position) {
|
|
859
|
+
state.document.diagram.positions[node.id] = node.position;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
state.document.updatedAt = new Date().toISOString();
|
|
863
|
+
return structuredClone(state.document);
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
syncFromModel: () => {
|
|
867
|
+
const doc = get().document;
|
|
868
|
+
const nodes: Node[] = doc.tables.map(table => ({
|
|
869
|
+
id: table.id,
|
|
870
|
+
type: 'table',
|
|
871
|
+
position: doc.diagram.positions[table.id] ?? { x: 0, y: 0 },
|
|
872
|
+
data: { table, isSelected: false },
|
|
873
|
+
}));
|
|
874
|
+
const edges: Edge[] = doc.relationships.map(rel => ({
|
|
875
|
+
id: rel.id,
|
|
876
|
+
source: rel.sourceTableId,
|
|
877
|
+
target: rel.targetTableId,
|
|
878
|
+
sourceHandle: `${rel.sourceTableId}.${rel.sourceColumnId}-source`,
|
|
879
|
+
targetHandle: `${rel.targetTableId}.${rel.targetColumnId}-target`,
|
|
880
|
+
type: 'crowsfoot',
|
|
881
|
+
data: {
|
|
882
|
+
sourceCardinality: rel.sourceCardinality,
|
|
883
|
+
targetCardinality: rel.targetCardinality,
|
|
884
|
+
relationshipName: rel.name,
|
|
885
|
+
},
|
|
886
|
+
}));
|
|
887
|
+
set({ nodes, edges });
|
|
888
|
+
},
|
|
889
|
+
}),
|
|
890
|
+
{
|
|
891
|
+
// zundo: limit undo history to 100 steps
|
|
892
|
+
limit: 100,
|
|
893
|
+
// Only track meaningful changes (not every mouse move)
|
|
894
|
+
partialize: (state) => ({
|
|
895
|
+
document: state.document,
|
|
896
|
+
}),
|
|
897
|
+
}
|
|
898
|
+
)
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
function createEmptyDocument(): ERDDocument {
|
|
902
|
+
return {
|
|
903
|
+
version: '1.0.0',
|
|
904
|
+
name: 'Untitled',
|
|
905
|
+
createdAt: new Date().toISOString(),
|
|
906
|
+
updatedAt: new Date().toISOString(),
|
|
907
|
+
notation: 'crowsfoot',
|
|
908
|
+
dialect: 'postgresql',
|
|
909
|
+
tables: [],
|
|
910
|
+
relationships: [],
|
|
911
|
+
diagram: {
|
|
912
|
+
positions: {},
|
|
913
|
+
viewport: { x: 0, y: 0, zoom: 1 },
|
|
914
|
+
gridSize: 20,
|
|
915
|
+
snapToGrid: true,
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Auto-Layout (dagre)
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
// lib/auto-layout.ts
|
|
925
|
+
import dagre from '@dagrejs/dagre';
|
|
926
|
+
import type { Node, Edge } from '@xyflow/react';
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Auto-layout tables using dagre graph algorithm.
|
|
930
|
+
* ChartDB and Liam ERD both use dagre for auto-layout.
|
|
931
|
+
*/
|
|
932
|
+
export function autoLayout(
|
|
933
|
+
nodes: Node[],
|
|
934
|
+
edges: Edge[],
|
|
935
|
+
direction: 'TB' | 'LR' = 'LR',
|
|
936
|
+
): Node[] {
|
|
937
|
+
const g = new dagre.graphlib.Graph();
|
|
938
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
939
|
+
g.setGraph({
|
|
940
|
+
rankdir: direction,
|
|
941
|
+
nodesep: 60, // horizontal spacing
|
|
942
|
+
ranksep: 100, // vertical spacing between ranks
|
|
943
|
+
edgesep: 20,
|
|
944
|
+
marginx: 40,
|
|
945
|
+
marginy: 40,
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Estimate node dimensions (table height = header + rows * 28px)
|
|
949
|
+
for (const node of nodes) {
|
|
950
|
+
const colCount = node.data?.table?.columns?.length ?? 4;
|
|
951
|
+
const height = 40 + colCount * 28; // header + rows
|
|
952
|
+
g.setNode(node.id, { width: 240, height });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
for (const edge of edges) {
|
|
956
|
+
g.setEdge(edge.source, edge.target);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
dagre.layout(g);
|
|
960
|
+
|
|
961
|
+
return nodes.map(node => {
|
|
962
|
+
const pos = g.node(node.id);
|
|
963
|
+
return {
|
|
964
|
+
...node,
|
|
965
|
+
position: { x: pos.x - 120, y: pos.y - (pos.height / 2) },
|
|
966
|
+
};
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Performance: Virtual Rendering for 100+ Tables
|
|
972
|
+
|
|
973
|
+
JointJS insight: when table count exceeds ~50, render only visible nodes. React Flow v12 handles this natively with `nodeExtent` and built-in viewport culling. Additional optimizations:
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
// Performance patterns for large schemas
|
|
977
|
+
|
|
978
|
+
// 1. Memoize node components (already done with memo())
|
|
979
|
+
// 2. Use nodeTypes/edgeTypes outside of the component to prevent re-registration
|
|
980
|
+
// 3. Debounce position sync (don't write to store on every pixel of drag)
|
|
981
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
982
|
+
|
|
983
|
+
const debouncedSync = useDebouncedCallback(() => {
|
|
984
|
+
useERDStore.getState().syncPositionsToDocument();
|
|
985
|
+
}, 200);
|
|
986
|
+
|
|
987
|
+
// 4. For 100+ tables, collapse columns by default and expand on click
|
|
988
|
+
// 5. Use React Flow's `hidden` prop to hide tables not matching a filter
|
|
989
|
+
// 6. Batch node updates: applyNodeChanges handles arrays efficiently
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Parser/Generator Architecture (DrawDB Pattern)
|
|
995
|
+
|
|
996
|
+
DrawDB's architecture is the gold standard: separate parser and generator for each SQL dialect, all converting to/from a central JSON model. This enables round-tripping: `DDL -> JSON -> DDL` without information loss.
|
|
997
|
+
|
|
998
|
+
```
|
|
999
|
+
+----------------+ +---------------+ +--------------------+
|
|
1000
|
+
| MySQL DDL |---->| |---->| MySQL DDL |
|
|
1001
|
+
+----------------+ | | +--------------------+
|
|
1002
|
+
+----------------+ | Central | +--------------------+
|
|
1003
|
+
| PostgreSQL |---->| JSON |---->| PostgreSQL DDL |
|
|
1004
|
+
+----------------+ | Model | +--------------------+
|
|
1005
|
+
+----------------+ | (ERDDocument) | +--------------------+
|
|
1006
|
+
| SQLite DDL |---->| |---->| SQLite DDL |
|
|
1007
|
+
+----------------+ | | +--------------------+
|
|
1008
|
+
+----------------+ | | +--------------------+
|
|
1009
|
+
| Prisma .psl |---->| |---->| DBML |
|
|
1010
|
+
+----------------+ +---------------+ +--------------------+
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
### Parser Interface
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
// lib/parsers/types.ts
|
|
1017
|
+
|
|
1018
|
+
/** All parsers convert their input format to ERDDocument */
|
|
1019
|
+
export interface DDLParser {
|
|
1020
|
+
dialect: SQLDialect;
|
|
1021
|
+
/** Parse DDL string into document model */
|
|
1022
|
+
parse(ddl: string): ERDDocument;
|
|
1023
|
+
/** Supported file extensions */
|
|
1024
|
+
extensions: string[];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** All generators convert ERDDocument to their output format */
|
|
1028
|
+
export interface DDLGenerator {
|
|
1029
|
+
dialect: SQLDialect;
|
|
1030
|
+
/** Generate DDL string from document model */
|
|
1031
|
+
generate(doc: ERDDocument): string;
|
|
1032
|
+
/** File extension for export */
|
|
1033
|
+
extension: string;
|
|
1034
|
+
}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
### PostgreSQL Parser (using node-sql-parser)
|
|
1038
|
+
|
|
1039
|
+
```typescript
|
|
1040
|
+
// lib/parsers/postgres-parser.ts
|
|
1041
|
+
import { Parser } from 'node-sql-parser';
|
|
1042
|
+
import type { ERDDocument, Table, Column, Relationship } from '@/types/erd';
|
|
1043
|
+
import { nanoid } from 'nanoid';
|
|
1044
|
+
|
|
1045
|
+
const sqlParser = new Parser();
|
|
1046
|
+
|
|
1047
|
+
export function parsePostgres(ddl: string): Partial<ERDDocument> {
|
|
1048
|
+
const ast = sqlParser.astify(ddl, { database: 'PostgresQL' });
|
|
1049
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1050
|
+
|
|
1051
|
+
const tables: Table[] = [];
|
|
1052
|
+
const relationships: Relationship[] = [];
|
|
1053
|
+
|
|
1054
|
+
for (const stmt of statements) {
|
|
1055
|
+
if (stmt.type !== 'create' || stmt.keyword !== 'table') continue;
|
|
1056
|
+
|
|
1057
|
+
const tableName = stmt.table?.[0]?.table ?? 'unknown';
|
|
1058
|
+
const tableId = nanoid();
|
|
1059
|
+
const columns: Column[] = [];
|
|
1060
|
+
|
|
1061
|
+
// Extract columns
|
|
1062
|
+
for (const def of stmt.create_definitions ?? []) {
|
|
1063
|
+
if (def.resource === 'column') {
|
|
1064
|
+
const colId = nanoid();
|
|
1065
|
+
columns.push({
|
|
1066
|
+
id: colId,
|
|
1067
|
+
name: def.column?.column ?? 'unknown',
|
|
1068
|
+
type: formatColumnType(def),
|
|
1069
|
+
isPrimaryKey: false, // Set below from constraints
|
|
1070
|
+
isForeignKey: false,
|
|
1071
|
+
isNullable: !hasConstraint(def, 'not null'),
|
|
1072
|
+
isUnique: hasConstraint(def, 'unique'),
|
|
1073
|
+
isAutoIncrement: hasConstraint(def, 'auto_increment'),
|
|
1074
|
+
defaultValue: extractDefault(def),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// PRIMARY KEY constraint
|
|
1079
|
+
if (def.resource === 'constraint' && def.constraint_type === 'primary key') {
|
|
1080
|
+
for (const keyCol of def.definition ?? []) {
|
|
1081
|
+
const col = columns.find(c => c.name === keyCol.column);
|
|
1082
|
+
if (col) col.isPrimaryKey = true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// FOREIGN KEY constraint
|
|
1087
|
+
if (def.resource === 'constraint' && def.constraint_type === 'REFERENCES') {
|
|
1088
|
+
const sourceCol = columns.find(c => c.name === def.definition?.[0]?.column);
|
|
1089
|
+
if (sourceCol) {
|
|
1090
|
+
sourceCol.isForeignKey = true;
|
|
1091
|
+
// Relationship will be resolved after all tables are parsed
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
tables.push({
|
|
1097
|
+
id: tableId,
|
|
1098
|
+
name: tableName,
|
|
1099
|
+
columns,
|
|
1100
|
+
indexes: [],
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Second pass: resolve FK relationships across tables
|
|
1105
|
+
resolveRelationships(tables, statements, relationships);
|
|
1106
|
+
|
|
1107
|
+
return { tables, relationships };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function formatColumnType(def: any): string {
|
|
1111
|
+
const dt = def.definition?.dataType ?? 'TEXT';
|
|
1112
|
+
const length = def.definition?.length;
|
|
1113
|
+
return length ? `${dt}(${length})` : dt;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function hasConstraint(def: any, type: string): boolean {
|
|
1117
|
+
return def.definition?.constraint?.some?.((c: any) =>
|
|
1118
|
+
c.type?.toLowerCase() === type
|
|
1119
|
+
) ?? false;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function extractDefault(def: any): string | undefined {
|
|
1123
|
+
const d = def.definition?.constraint?.find?.((c: any) => c.type === 'default');
|
|
1124
|
+
return d?.value?.value?.toString();
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function resolveRelationships(
|
|
1128
|
+
tables: Table[],
|
|
1129
|
+
statements: any[],
|
|
1130
|
+
relationships: Relationship[]
|
|
1131
|
+
): void {
|
|
1132
|
+
// Build name -> id lookup
|
|
1133
|
+
const tableByName = new Map(tables.map(t => [t.name, t]));
|
|
1134
|
+
|
|
1135
|
+
for (const stmt of statements) {
|
|
1136
|
+
if (stmt.type !== 'create' || stmt.keyword !== 'table') continue;
|
|
1137
|
+
const srcTableName = stmt.table?.[0]?.table;
|
|
1138
|
+
const srcTable = tableByName.get(srcTableName);
|
|
1139
|
+
if (!srcTable) continue;
|
|
1140
|
+
|
|
1141
|
+
for (const def of stmt.create_definitions ?? []) {
|
|
1142
|
+
if (def.resource === 'constraint' && def.constraint_type === 'REFERENCES') {
|
|
1143
|
+
const srcColName = def.definition?.[0]?.column;
|
|
1144
|
+
const tgtTableName = def.reference_definition?.table?.[0]?.table;
|
|
1145
|
+
const tgtColName = def.reference_definition?.definition?.[0]?.column;
|
|
1146
|
+
|
|
1147
|
+
const tgtTable = tableByName.get(tgtTableName);
|
|
1148
|
+
const srcCol = srcTable.columns.find(c => c.name === srcColName);
|
|
1149
|
+
const tgtCol = tgtTable?.columns.find(c => c.name === tgtColName);
|
|
1150
|
+
|
|
1151
|
+
if (tgtTable && srcCol && tgtCol) {
|
|
1152
|
+
srcCol.references = { tableId: tgtTable.id, columnId: tgtCol.id };
|
|
1153
|
+
relationships.push({
|
|
1154
|
+
id: nanoid(),
|
|
1155
|
+
name: def.constraint ?? `fk_${srcTableName}_${srcColName}`,
|
|
1156
|
+
sourceTableId: srcTable.id,
|
|
1157
|
+
sourceColumnId: srcCol.id,
|
|
1158
|
+
targetTableId: tgtTable.id,
|
|
1159
|
+
targetColumnId: tgtCol.id,
|
|
1160
|
+
sourceCardinality: 'zero-or-many',
|
|
1161
|
+
targetCardinality: 'exactly-one',
|
|
1162
|
+
onDelete: extractAction(def, 'on_delete') ?? 'NO ACTION',
|
|
1163
|
+
onUpdate: extractAction(def, 'on_update') ?? 'NO ACTION',
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function extractAction(def: any, key: string): ReferentialAction | undefined {
|
|
1172
|
+
const action = def.reference_definition?.[key];
|
|
1173
|
+
if (!action) return undefined;
|
|
1174
|
+
return action.toUpperCase().replace(' ', '_') as ReferentialAction;
|
|
1175
|
+
}
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### PostgreSQL Generator
|
|
1179
|
+
|
|
1180
|
+
```typescript
|
|
1181
|
+
// lib/generators/postgres-generator.ts
|
|
1182
|
+
import type { ERDDocument, Table, Column, Relationship } from '@/types/erd';
|
|
1183
|
+
|
|
1184
|
+
export function generatePostgres(doc: ERDDocument): string {
|
|
1185
|
+
const lines: string[] = [];
|
|
1186
|
+
lines.push('-- Generated by ERD Editor');
|
|
1187
|
+
lines.push(`-- Dialect: PostgreSQL`);
|
|
1188
|
+
lines.push(`-- Date: ${new Date().toISOString()}\n`);
|
|
1189
|
+
|
|
1190
|
+
for (const table of doc.tables) {
|
|
1191
|
+
lines.push(generateCreateTable(table));
|
|
1192
|
+
lines.push('');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Foreign key constraints as ALTER TABLE (safer for circular references)
|
|
1196
|
+
for (const rel of doc.relationships) {
|
|
1197
|
+
const srcTable = doc.tables.find(t => t.id === rel.sourceTableId);
|
|
1198
|
+
const tgtTable = doc.tables.find(t => t.id === rel.targetTableId);
|
|
1199
|
+
const srcCol = srcTable?.columns.find(c => c.id === rel.sourceColumnId);
|
|
1200
|
+
const tgtCol = tgtTable?.columns.find(c => c.id === rel.targetColumnId);
|
|
1201
|
+
if (srcTable && tgtTable && srcCol && tgtCol) {
|
|
1202
|
+
lines.push(
|
|
1203
|
+
`ALTER TABLE "${srcTable.name}" ADD CONSTRAINT "${rel.name ?? `fk_${srcTable.name}_${srcCol.name}`}"`,
|
|
1204
|
+
` FOREIGN KEY ("${srcCol.name}") REFERENCES "${tgtTable.name}" ("${tgtCol.name}")`,
|
|
1205
|
+
` ON DELETE ${rel.onDelete} ON UPDATE ${rel.onUpdate};`,
|
|
1206
|
+
''
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return lines.join('\n');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function generateCreateTable(table: Table): string {
|
|
1215
|
+
const lines: string[] = [];
|
|
1216
|
+
lines.push(`CREATE TABLE "${table.name}" (`);
|
|
1217
|
+
|
|
1218
|
+
const colDefs: string[] = table.columns.map(col => {
|
|
1219
|
+
const parts: string[] = [` "${col.name}"`];
|
|
1220
|
+
parts.push(col.type);
|
|
1221
|
+
if (!col.isNullable) parts.push('NOT NULL');
|
|
1222
|
+
if (col.isUnique && !col.isPrimaryKey) parts.push('UNIQUE');
|
|
1223
|
+
if (col.defaultValue) parts.push(`DEFAULT ${col.defaultValue}`);
|
|
1224
|
+
return parts.join(' ');
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// Primary key constraint
|
|
1228
|
+
const pkCols = table.columns.filter(c => c.isPrimaryKey);
|
|
1229
|
+
if (pkCols.length > 0) {
|
|
1230
|
+
colDefs.push(
|
|
1231
|
+
` PRIMARY KEY (${pkCols.map(c => `"${c.name}"`).join(', ')})`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
lines.push(colDefs.join(',\n'));
|
|
1236
|
+
lines.push(');');
|
|
1237
|
+
|
|
1238
|
+
// Table comment
|
|
1239
|
+
if (table.comment) {
|
|
1240
|
+
lines.push(`COMMENT ON TABLE "${table.name}" IS '${table.comment.replace(/'/g, "''")}';`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return lines.join('\n');
|
|
1244
|
+
}
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
### MySQL Generator (dialect differences)
|
|
1248
|
+
|
|
1249
|
+
```typescript
|
|
1250
|
+
// lib/generators/mysql-generator.ts
|
|
1251
|
+
import type { ERDDocument, Table } from '@/types/erd';
|
|
1252
|
+
|
|
1253
|
+
export function generateMySql(doc: ERDDocument): string {
|
|
1254
|
+
const lines: string[] = [];
|
|
1255
|
+
lines.push('-- Generated by ERD Editor');
|
|
1256
|
+
lines.push(`-- Dialect: MySQL\n`);
|
|
1257
|
+
|
|
1258
|
+
for (const table of doc.tables) {
|
|
1259
|
+
lines.push(generateMySqlTable(table));
|
|
1260
|
+
lines.push('');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return lines.join('\n');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function generateMySqlTable(table: Table): string {
|
|
1267
|
+
const lines: string[] = [];
|
|
1268
|
+
// MySQL: backtick quoting, no schema prefix, ENGINE specification
|
|
1269
|
+
lines.push(`CREATE TABLE \`${table.name}\` (`);
|
|
1270
|
+
|
|
1271
|
+
const colDefs: string[] = table.columns.map(col => {
|
|
1272
|
+
const parts: string[] = [` \`${col.name}\``];
|
|
1273
|
+
// Type mapping: PostgreSQL -> MySQL
|
|
1274
|
+
parts.push(pgTypeToMySQL(col.type));
|
|
1275
|
+
if (col.isAutoIncrement) parts.push('AUTO_INCREMENT');
|
|
1276
|
+
if (!col.isNullable) parts.push('NOT NULL');
|
|
1277
|
+
if (col.isUnique && !col.isPrimaryKey) parts.push('UNIQUE');
|
|
1278
|
+
if (col.defaultValue) parts.push(`DEFAULT ${mysqlDefault(col.defaultValue)}`);
|
|
1279
|
+
return parts.join(' ');
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
const pkCols = table.columns.filter(c => c.isPrimaryKey);
|
|
1283
|
+
if (pkCols.length > 0) {
|
|
1284
|
+
colDefs.push(
|
|
1285
|
+
` PRIMARY KEY (${pkCols.map(c => `\`${c.name}\``).join(', ')})`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
lines.push(colDefs.join(',\n'));
|
|
1290
|
+
lines.push(') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;');
|
|
1291
|
+
|
|
1292
|
+
return lines.join('\n');
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Map PostgreSQL types to MySQL equivalents */
|
|
1296
|
+
function pgTypeToMySQL(pgType: string): string {
|
|
1297
|
+
const map: Record<string, string> = {
|
|
1298
|
+
'UUID': 'CHAR(36)',
|
|
1299
|
+
'TIMESTAMPTZ': 'DATETIME',
|
|
1300
|
+
'TIMESTAMP WITH TIME ZONE': 'DATETIME',
|
|
1301
|
+
'TEXT': 'TEXT',
|
|
1302
|
+
'JSONB': 'JSON',
|
|
1303
|
+
'BOOLEAN': 'TINYINT(1)',
|
|
1304
|
+
'SERIAL': 'INT',
|
|
1305
|
+
'BIGSERIAL': 'BIGINT',
|
|
1306
|
+
'REAL': 'FLOAT',
|
|
1307
|
+
'DOUBLE PRECISION': 'DOUBLE',
|
|
1308
|
+
};
|
|
1309
|
+
const upper = pgType.toUpperCase();
|
|
1310
|
+
return map[upper] ?? pgType;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function mysqlDefault(value: string): string {
|
|
1314
|
+
// Convert PG functions to MySQL equivalents
|
|
1315
|
+
if (value === 'gen_random_uuid()') return '(UUID())';
|
|
1316
|
+
if (value === 'NOW()') return 'CURRENT_TIMESTAMP';
|
|
1317
|
+
return value;
|
|
1318
|
+
}
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
### Generator Registry
|
|
1322
|
+
|
|
1323
|
+
```typescript
|
|
1324
|
+
// lib/generators/index.ts
|
|
1325
|
+
import { generatePostgres } from './postgres-generator';
|
|
1326
|
+
import { generateMySql } from './mysql-generator';
|
|
1327
|
+
import type { ERDDocument, SQLDialect } from '@/types/erd';
|
|
1328
|
+
|
|
1329
|
+
type GeneratorFn = (doc: ERDDocument) => string;
|
|
1330
|
+
|
|
1331
|
+
const generators: Record<SQLDialect, GeneratorFn> = {
|
|
1332
|
+
postgresql: generatePostgres,
|
|
1333
|
+
mysql: generateMySql,
|
|
1334
|
+
sqlite: generateSqlite, // Same pattern, different type mapping
|
|
1335
|
+
mariadb: generateMySql, // MariaDB is MySQL-compatible with minor differences
|
|
1336
|
+
mssql: generateMssql, // T-SQL square bracket quoting, IDENTITY instead of AUTO_INCREMENT
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
export function generateDDL(doc: ERDDocument, dialect?: SQLDialect): string {
|
|
1340
|
+
const target = dialect ?? doc.dialect;
|
|
1341
|
+
const gen = generators[target];
|
|
1342
|
+
if (!gen) throw new Error(`No generator for dialect: ${target}`);
|
|
1343
|
+
return gen(doc);
|
|
1344
|
+
}
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
---
|
|
1348
|
+
|
|
1349
|
+
## Import Flows
|
|
1350
|
+
|
|
1351
|
+
### DDL Import (node-sql-parser)
|
|
1352
|
+
|
|
1353
|
+
```typescript
|
|
1354
|
+
// lib/import/ddl-import.ts
|
|
1355
|
+
import { parsePostgres } from '@/lib/parsers/postgres-parser';
|
|
1356
|
+
import type { ERDDocument, SQLDialect } from '@/types/erd';
|
|
1357
|
+
import { autoLayout } from '@/lib/auto-layout';
|
|
1358
|
+
import { useERDStore } from '@/store/erd-store';
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Import DDL string: parse -> generate positions -> load into store.
|
|
1362
|
+
* Flow: SQL DDL -> node-sql-parser AST -> ERDDocument -> React Flow nodes/edges
|
|
1363
|
+
*/
|
|
1364
|
+
export async function importDDL(ddl: string, dialect: SQLDialect): Promise<void> {
|
|
1365
|
+
const parsers: Record<string, (ddl: string) => Partial<ERDDocument>> = {
|
|
1366
|
+
postgresql: parsePostgres,
|
|
1367
|
+
mysql: parseMySql,
|
|
1368
|
+
sqlite: parseSqlite,
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
const parser = parsers[dialect];
|
|
1372
|
+
if (!parser) throw new Error(`No parser for dialect: ${dialect}`);
|
|
1373
|
+
|
|
1374
|
+
const partial = parser(ddl);
|
|
1375
|
+
const doc: ERDDocument = {
|
|
1376
|
+
version: '1.0.0',
|
|
1377
|
+
name: 'Imported Schema',
|
|
1378
|
+
notation: 'crowsfoot',
|
|
1379
|
+
dialect,
|
|
1380
|
+
createdAt: new Date().toISOString(),
|
|
1381
|
+
updatedAt: new Date().toISOString(),
|
|
1382
|
+
tables: partial.tables ?? [],
|
|
1383
|
+
relationships: partial.relationships ?? [],
|
|
1384
|
+
diagram: {
|
|
1385
|
+
positions: {},
|
|
1386
|
+
viewport: { x: 0, y: 0, zoom: 1 },
|
|
1387
|
+
gridSize: 20,
|
|
1388
|
+
snapToGrid: true,
|
|
1389
|
+
},
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
// Auto-layout since imported DDL has no position data
|
|
1393
|
+
const store = useERDStore.getState();
|
|
1394
|
+
store.loadDocument(doc);
|
|
1395
|
+
const laid = autoLayout(store.nodes, store.edges);
|
|
1396
|
+
// Write positions back
|
|
1397
|
+
for (const node of laid) {
|
|
1398
|
+
doc.diagram.positions[node.id] = node.position;
|
|
1399
|
+
}
|
|
1400
|
+
store.loadDocument(doc);
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
### Schema Import (ChartDB "Smart Query" Pattern)
|
|
1405
|
+
|
|
1406
|
+
ChartDB's killer feature: paste a single SQL query against your live database, get the full schema as JSON. The query uses `information_schema` to extract tables, columns, constraints, and relationships in one shot.
|
|
1407
|
+
|
|
1408
|
+
```sql
|
|
1409
|
+
-- PostgreSQL "Smart Query" — paste into any SQL client connected to your DB
|
|
1410
|
+
-- Returns full schema as JSON
|
|
1411
|
+
SELECT json_build_object(
|
|
1412
|
+
'tables', (
|
|
1413
|
+
SELECT json_agg(json_build_object(
|
|
1414
|
+
'name', t.table_name,
|
|
1415
|
+
'schema', t.table_schema,
|
|
1416
|
+
'columns', (
|
|
1417
|
+
SELECT json_agg(json_build_object(
|
|
1418
|
+
'name', c.column_name,
|
|
1419
|
+
'type', c.data_type ||
|
|
1420
|
+
CASE WHEN c.character_maximum_length IS NOT NULL
|
|
1421
|
+
THEN '(' || c.character_maximum_length || ')'
|
|
1422
|
+
ELSE '' END,
|
|
1423
|
+
'isNullable', c.is_nullable = 'YES',
|
|
1424
|
+
'default', c.column_default,
|
|
1425
|
+
'isPrimaryKey', EXISTS (
|
|
1426
|
+
SELECT 1 FROM information_schema.key_column_usage kcu
|
|
1427
|
+
JOIN information_schema.table_constraints tc
|
|
1428
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1429
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
1430
|
+
AND kcu.table_name = c.table_name
|
|
1431
|
+
AND kcu.column_name = c.column_name
|
|
1432
|
+
)
|
|
1433
|
+
) ORDER BY c.ordinal_position)
|
|
1434
|
+
FROM information_schema.columns c
|
|
1435
|
+
WHERE c.table_name = t.table_name
|
|
1436
|
+
AND c.table_schema = t.table_schema
|
|
1437
|
+
),
|
|
1438
|
+
'foreignKeys', (
|
|
1439
|
+
SELECT json_agg(json_build_object(
|
|
1440
|
+
'column', kcu.column_name,
|
|
1441
|
+
'referencedTable', ccu.table_name,
|
|
1442
|
+
'referencedColumn', ccu.column_name
|
|
1443
|
+
))
|
|
1444
|
+
FROM information_schema.key_column_usage kcu
|
|
1445
|
+
JOIN information_schema.table_constraints tc
|
|
1446
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1447
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
1448
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
1449
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
1450
|
+
AND kcu.table_name = t.table_name
|
|
1451
|
+
AND kcu.table_schema = t.table_schema
|
|
1452
|
+
)
|
|
1453
|
+
))
|
|
1454
|
+
FROM information_schema.tables t
|
|
1455
|
+
WHERE t.table_schema = 'public'
|
|
1456
|
+
AND t.table_type = 'BASE TABLE'
|
|
1457
|
+
)
|
|
1458
|
+
) AS schema_json;
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
```sql
|
|
1462
|
+
-- MySQL equivalent "Smart Query"
|
|
1463
|
+
SELECT JSON_OBJECT(
|
|
1464
|
+
'tables', (
|
|
1465
|
+
SELECT JSON_ARRAYAGG(JSON_OBJECT(
|
|
1466
|
+
'name', t.TABLE_NAME,
|
|
1467
|
+
'columns', (
|
|
1468
|
+
SELECT JSON_ARRAYAGG(JSON_OBJECT(
|
|
1469
|
+
'name', c.COLUMN_NAME,
|
|
1470
|
+
'type', c.COLUMN_TYPE,
|
|
1471
|
+
'isNullable', c.IS_NULLABLE = 'YES',
|
|
1472
|
+
'default', c.COLUMN_DEFAULT,
|
|
1473
|
+
'isPrimaryKey', c.COLUMN_KEY = 'PRI'
|
|
1474
|
+
) ORDER BY c.ORDINAL_POSITION)
|
|
1475
|
+
FROM information_schema.COLUMNS c
|
|
1476
|
+
WHERE c.TABLE_NAME = t.TABLE_NAME
|
|
1477
|
+
AND c.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
1478
|
+
)
|
|
1479
|
+
))
|
|
1480
|
+
FROM information_schema.TABLES t
|
|
1481
|
+
WHERE t.TABLE_SCHEMA = DATABASE()
|
|
1482
|
+
AND t.TABLE_TYPE = 'BASE TABLE'
|
|
1483
|
+
)
|
|
1484
|
+
) AS schema_json;
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
### File Import (Prisma Schema)
|
|
1488
|
+
|
|
1489
|
+
```typescript
|
|
1490
|
+
// lib/import/prisma-import.ts
|
|
1491
|
+
// Liam ERD pattern: parse Prisma .prisma files into ERDDocument
|
|
1492
|
+
import { nanoid } from 'nanoid';
|
|
1493
|
+
import type { ERDDocument, Table, Column, Relationship } from '@/types/erd';
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Minimal Prisma parser — extracts models and relations.
|
|
1497
|
+
* For production, use @mrleebo/prisma-ast or prisma-schema-parser.
|
|
1498
|
+
*/
|
|
1499
|
+
export function parsePrismaSchema(schema: string): Partial<ERDDocument> {
|
|
1500
|
+
const tables: Table[] = [];
|
|
1501
|
+
const relationships: Relationship[] = [];
|
|
1502
|
+
|
|
1503
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1504
|
+
let match;
|
|
1505
|
+
|
|
1506
|
+
while ((match = modelRegex.exec(schema)) !== null) {
|
|
1507
|
+
const modelName = match[1];
|
|
1508
|
+
const body = match[2];
|
|
1509
|
+
const tableId = nanoid();
|
|
1510
|
+
const columns: Column[] = [];
|
|
1511
|
+
|
|
1512
|
+
for (const line of body.split('\n')) {
|
|
1513
|
+
const trimmed = line.trim();
|
|
1514
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
|
|
1515
|
+
|
|
1516
|
+
// Field line: name Type modifiers
|
|
1517
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\?)?(\[\])?\s*(.*)/);
|
|
1518
|
+
if (!fieldMatch) continue;
|
|
1519
|
+
|
|
1520
|
+
const [, name, type, optional, array, modifiers] = fieldMatch;
|
|
1521
|
+
|
|
1522
|
+
// Skip relation fields (they have @relation)
|
|
1523
|
+
if (modifiers.includes('@relation')) continue;
|
|
1524
|
+
|
|
1525
|
+
// Skip Prisma-only types (other model references without @relation)
|
|
1526
|
+
const prismaTypes = new Set([
|
|
1527
|
+
'String', 'Int', 'Float', 'Boolean', 'DateTime',
|
|
1528
|
+
'Json', 'BigInt', 'Decimal', 'Bytes',
|
|
1529
|
+
]);
|
|
1530
|
+
if (!prismaTypes.has(type)) continue;
|
|
1531
|
+
|
|
1532
|
+
columns.push({
|
|
1533
|
+
id: nanoid(),
|
|
1534
|
+
name: extractMapName(modifiers) ?? name,
|
|
1535
|
+
type: prismaTypeToSQL(type),
|
|
1536
|
+
isPrimaryKey: modifiers.includes('@id'),
|
|
1537
|
+
isForeignKey: false,
|
|
1538
|
+
isNullable: !!optional,
|
|
1539
|
+
isUnique: modifiers.includes('@unique'),
|
|
1540
|
+
isAutoIncrement: modifiers.includes('autoincrement()'),
|
|
1541
|
+
defaultValue: extractPrismaDefault(modifiers),
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
tables.push({
|
|
1546
|
+
id: tableId,
|
|
1547
|
+
name: extractTableMap(body) ?? modelName.toLowerCase() + 's',
|
|
1548
|
+
columns,
|
|
1549
|
+
indexes: [],
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return { tables, relationships };
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function prismaTypeToSQL(prismaType: string): string {
|
|
1557
|
+
const map: Record<string, string> = {
|
|
1558
|
+
'String': 'TEXT',
|
|
1559
|
+
'Int': 'INTEGER',
|
|
1560
|
+
'Float': 'REAL',
|
|
1561
|
+
'Boolean': 'BOOLEAN',
|
|
1562
|
+
'DateTime': 'TIMESTAMPTZ',
|
|
1563
|
+
'Json': 'JSONB',
|
|
1564
|
+
'BigInt': 'BIGINT',
|
|
1565
|
+
'Decimal': 'DECIMAL',
|
|
1566
|
+
'Bytes': 'BYTEA',
|
|
1567
|
+
};
|
|
1568
|
+
return map[prismaType] ?? 'TEXT';
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function extractMapName(modifiers: string): string | undefined {
|
|
1572
|
+
const match = modifiers.match(/@map\("(\w+)"\)/);
|
|
1573
|
+
return match?.[1];
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function extractTableMap(body: string): string | undefined {
|
|
1577
|
+
const match = body.match(/@@map\("(\w+)"\)/);
|
|
1578
|
+
return match?.[1];
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function extractPrismaDefault(modifiers: string): string | undefined {
|
|
1582
|
+
const match = modifiers.match(/@default\(([^)]+)\)/);
|
|
1583
|
+
if (!match) return undefined;
|
|
1584
|
+
const val = match[1];
|
|
1585
|
+
if (val === 'now()') return 'NOW()';
|
|
1586
|
+
if (val === 'cuid()') return 'gen_random_uuid()';
|
|
1587
|
+
if (val === 'uuid()') return 'gen_random_uuid()';
|
|
1588
|
+
if (val === 'autoincrement()') return undefined; // Handled by isAutoIncrement
|
|
1589
|
+
return val;
|
|
1590
|
+
}
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
---
|
|
1594
|
+
|
|
1595
|
+
## Export Flows
|
|
1596
|
+
|
|
1597
|
+
### DDL Export (Multi-Dialect)
|
|
1598
|
+
|
|
1599
|
+
```typescript
|
|
1600
|
+
// Already covered in Generator Registry above.
|
|
1601
|
+
// Usage from UI:
|
|
1602
|
+
function handleExportDDL(dialect: SQLDialect) {
|
|
1603
|
+
const doc = useERDStore.getState().toDocument();
|
|
1604
|
+
const ddl = generateDDL(doc, dialect);
|
|
1605
|
+
|
|
1606
|
+
// Desktop (Tauri): save to file
|
|
1607
|
+
// Web: download as .sql file
|
|
1608
|
+
downloadFile(ddl, `${doc.name}.sql`, 'text/sql');
|
|
1609
|
+
}
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
### Image Export (PNG/SVG)
|
|
1613
|
+
|
|
1614
|
+
```typescript
|
|
1615
|
+
// lib/export/image-export.ts
|
|
1616
|
+
import { toPng, toSvg } from '@xyflow/react';
|
|
1617
|
+
|
|
1618
|
+
export async function exportAsPng(filename: string): Promise<void> {
|
|
1619
|
+
const dataUrl = await toPng(
|
|
1620
|
+
document.querySelector('.react-flow') as HTMLElement,
|
|
1621
|
+
{
|
|
1622
|
+
backgroundColor: '#ffffff',
|
|
1623
|
+
quality: 1,
|
|
1624
|
+
width: 4096, // High-res export
|
|
1625
|
+
height: 2048,
|
|
1626
|
+
}
|
|
1627
|
+
);
|
|
1628
|
+
downloadDataUrl(dataUrl, `${filename}.png`);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export async function exportAsSvg(filename: string): Promise<void> {
|
|
1632
|
+
const svg = await toSvg(
|
|
1633
|
+
document.querySelector('.react-flow') as HTMLElement,
|
|
1634
|
+
{ backgroundColor: '#ffffff' }
|
|
1635
|
+
);
|
|
1636
|
+
downloadDataUrl(svg, `${filename}.svg`);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function downloadDataUrl(dataUrl: string, filename: string): void {
|
|
1640
|
+
const link = document.createElement('a');
|
|
1641
|
+
link.href = dataUrl;
|
|
1642
|
+
link.download = filename;
|
|
1643
|
+
link.click();
|
|
1644
|
+
}
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
### DBML Export (dbdiagram.io interop)
|
|
1648
|
+
|
|
1649
|
+
```typescript
|
|
1650
|
+
// lib/export/dbml-export.ts
|
|
1651
|
+
import type { ERDDocument, Cardinality } from '@/types/erd';
|
|
1652
|
+
|
|
1653
|
+
export function generateDBML(doc: ERDDocument): string {
|
|
1654
|
+
const lines: string[] = [];
|
|
1655
|
+
|
|
1656
|
+
for (const table of doc.tables) {
|
|
1657
|
+
lines.push(`Table ${table.name} {`);
|
|
1658
|
+
for (const col of table.columns) {
|
|
1659
|
+
const attrs: string[] = [];
|
|
1660
|
+
if (col.isPrimaryKey) attrs.push('pk');
|
|
1661
|
+
if (!col.isNullable) attrs.push('not null');
|
|
1662
|
+
if (col.isUnique) attrs.push('unique');
|
|
1663
|
+
if (col.defaultValue) attrs.push(`default: '${col.defaultValue}'`);
|
|
1664
|
+
if (col.comment) attrs.push(`note: '${col.comment}'`);
|
|
1665
|
+
const attrStr = attrs.length > 0 ? ` [${attrs.join(', ')}]` : '';
|
|
1666
|
+
lines.push(` ${col.name} ${col.type}${attrStr}`);
|
|
1667
|
+
}
|
|
1668
|
+
lines.push('}\n');
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Relationships
|
|
1672
|
+
for (const rel of doc.relationships) {
|
|
1673
|
+
const srcTable = doc.tables.find(t => t.id === rel.sourceTableId);
|
|
1674
|
+
const tgtTable = doc.tables.find(t => t.id === rel.targetTableId);
|
|
1675
|
+
const srcCol = srcTable?.columns.find(c => c.id === rel.sourceColumnId);
|
|
1676
|
+
const tgtCol = tgtTable?.columns.find(c => c.id === rel.targetColumnId);
|
|
1677
|
+
if (srcTable && tgtTable && srcCol && tgtCol) {
|
|
1678
|
+
const symbol = cardinalityToDBML(rel.sourceCardinality, rel.targetCardinality);
|
|
1679
|
+
lines.push(`Ref: ${srcTable.name}.${srcCol.name} ${symbol} ${tgtTable.name}.${tgtCol.name}`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
return lines.join('\n');
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function cardinalityToDBML(source: Cardinality, target: Cardinality): string {
|
|
1687
|
+
// DBML uses: - (one-to-one), < (one-to-many), > (many-to-one), <> (many-to-many)
|
|
1688
|
+
if (target === 'zero-or-many' || target === 'one-or-many') return '<';
|
|
1689
|
+
if (source === 'zero-or-many' || source === 'one-or-many') return '>';
|
|
1690
|
+
return '-';
|
|
1691
|
+
}
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
---
|
|
1695
|
+
|
|
1696
|
+
## MCP Integration (ERFlow Pattern)
|
|
1697
|
+
|
|
1698
|
+
ERFlow exposes 25+ MCP tools for natural language schema editing from CLI/IDE. Implement the same pattern for the ERD editor:
|
|
1699
|
+
|
|
1700
|
+
```typescript
|
|
1701
|
+
// mcp/erd-mcp-tools.ts
|
|
1702
|
+
// Register as MCP tools via the Claude MCP protocol
|
|
1703
|
+
|
|
1704
|
+
export const erdMCPTools = {
|
|
1705
|
+
'erd.addTable': {
|
|
1706
|
+
description: 'Add a new table to the ERD',
|
|
1707
|
+
parameters: {
|
|
1708
|
+
name: { type: 'string', description: 'Table name' },
|
|
1709
|
+
columns: { type: 'array', description: 'Column definitions' },
|
|
1710
|
+
},
|
|
1711
|
+
handler: async ({ name, columns }: { name: string; columns: any[] }) => {
|
|
1712
|
+
const table = createTableFromNL(name, columns);
|
|
1713
|
+
useERDStore.getState().addTable(table);
|
|
1714
|
+
return { success: true, tableId: table.id };
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
|
|
1718
|
+
'erd.addRelationship': {
|
|
1719
|
+
description: 'Add a FK relationship between two tables',
|
|
1720
|
+
parameters: {
|
|
1721
|
+
from: { type: 'string', description: 'Source table.column' },
|
|
1722
|
+
to: { type: 'string', description: 'Target table.column' },
|
|
1723
|
+
cardinality: { type: 'string', description: 'one-to-one, one-to-many, many-to-many' },
|
|
1724
|
+
},
|
|
1725
|
+
handler: async ({ from, to, cardinality }: {
|
|
1726
|
+
from: string; to: string; cardinality: string;
|
|
1727
|
+
}) => {
|
|
1728
|
+
const [srcTable, srcCol] = resolveTableColumn(from);
|
|
1729
|
+
const [tgtTable, tgtCol] = resolveTableColumn(to);
|
|
1730
|
+
const { source, target } = mapNLCardinality(cardinality);
|
|
1731
|
+
useERDStore.getState().addRelationship({
|
|
1732
|
+
sourceHandle: `${srcTable.id}.${srcCol.id}-source`,
|
|
1733
|
+
targetHandle: `${tgtTable.id}.${tgtCol.id}-target`,
|
|
1734
|
+
});
|
|
1735
|
+
return { success: true };
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
|
|
1739
|
+
'erd.exportDDL': {
|
|
1740
|
+
description: 'Export the current ERD as SQL DDL',
|
|
1741
|
+
parameters: {
|
|
1742
|
+
dialect: { type: 'string', enum: ['postgresql', 'mysql', 'sqlite'] },
|
|
1743
|
+
},
|
|
1744
|
+
handler: async ({ dialect }: { dialect: SQLDialect }) => {
|
|
1745
|
+
const doc = useERDStore.getState().toDocument();
|
|
1746
|
+
return { ddl: generateDDL(doc, dialect) };
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
|
|
1750
|
+
'erd.importDDL': {
|
|
1751
|
+
description: 'Import SQL DDL into the ERD',
|
|
1752
|
+
parameters: {
|
|
1753
|
+
sql: { type: 'string' },
|
|
1754
|
+
dialect: { type: 'string' },
|
|
1755
|
+
},
|
|
1756
|
+
handler: async ({ sql, dialect }: { sql: string; dialect: SQLDialect }) => {
|
|
1757
|
+
await importDDL(sql, dialect);
|
|
1758
|
+
return {
|
|
1759
|
+
success: true,
|
|
1760
|
+
tableCount: useERDStore.getState().document.tables.length,
|
|
1761
|
+
};
|
|
1762
|
+
},
|
|
1763
|
+
},
|
|
1764
|
+
|
|
1765
|
+
// Additional tools following ERFlow pattern:
|
|
1766
|
+
// erd.renameTable, erd.renameColumn, erd.addColumn, erd.removeColumn,
|
|
1767
|
+
// erd.changeColumnType, erd.setNullable, erd.addIndex,
|
|
1768
|
+
// erd.generateMigration (checkpoint-based diff)
|
|
1769
|
+
};
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
---
|
|
1773
|
+
|
|
1774
|
+
## Persistence (Tauri)
|
|
1775
|
+
|
|
1776
|
+
### Desktop: File System Save/Load
|
|
1777
|
+
|
|
1778
|
+
```typescript
|
|
1779
|
+
// lib/persistence/tauri-fs.ts
|
|
1780
|
+
import { save, open } from '@tauri-apps/plugin-dialog';
|
|
1781
|
+
import { writeTextFile, readTextFile } from '@tauri-apps/plugin-fs';
|
|
1782
|
+
import { appDataDir } from '@tauri-apps/api/path';
|
|
1783
|
+
import type { ERDDocument } from '@/types/erd';
|
|
1784
|
+
import { useERDStore } from '@/store/erd-store';
|
|
1785
|
+
|
|
1786
|
+
const ERD_FILTER = {
|
|
1787
|
+
name: 'ERD Files',
|
|
1788
|
+
extensions: ['erd.json'],
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
export async function saveDocument(): Promise<void> {
|
|
1792
|
+
const doc = useERDStore.getState().toDocument();
|
|
1793
|
+
const filePath = await save({
|
|
1794
|
+
defaultPath: `${doc.name}.erd.json`,
|
|
1795
|
+
filters: [ERD_FILTER],
|
|
1796
|
+
});
|
|
1797
|
+
if (filePath) {
|
|
1798
|
+
await writeTextFile(filePath, JSON.stringify(doc, null, 2));
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export async function openDocument(): Promise<void> {
|
|
1803
|
+
const filePath = await open({
|
|
1804
|
+
filters: [ERD_FILTER],
|
|
1805
|
+
multiple: false,
|
|
1806
|
+
});
|
|
1807
|
+
if (filePath) {
|
|
1808
|
+
const content = await readTextFile(filePath as string);
|
|
1809
|
+
const doc: ERDDocument = JSON.parse(content);
|
|
1810
|
+
useERDStore.getState().loadDocument(doc);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/** Auto-save every 30 seconds to a temp file */
|
|
1815
|
+
export function startAutoSave(intervalMs = 30_000): () => void {
|
|
1816
|
+
const timer = setInterval(async () => {
|
|
1817
|
+
const doc = useERDStore.getState().toDocument();
|
|
1818
|
+
const tempPath = `${await appDataDir()}/autosave.erd.json`;
|
|
1819
|
+
await writeTextFile(tempPath, JSON.stringify(doc));
|
|
1820
|
+
}, intervalMs);
|
|
1821
|
+
return () => clearInterval(timer);
|
|
1822
|
+
}
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
### Web Fallback: Dexie.js (IndexedDB)
|
|
1826
|
+
|
|
1827
|
+
```typescript
|
|
1828
|
+
// lib/persistence/dexie-store.ts
|
|
1829
|
+
// ChartDB uses Dexie.js for zero-backend persistence in the browser
|
|
1830
|
+
import Dexie, { type Table as DexieTable } from 'dexie';
|
|
1831
|
+
import type { ERDDocument } from '@/types/erd';
|
|
1832
|
+
|
|
1833
|
+
class ERDDatabase extends Dexie {
|
|
1834
|
+
documents!: DexieTable<ERDDocument & { id: string }, string>;
|
|
1835
|
+
|
|
1836
|
+
constructor() {
|
|
1837
|
+
super('erd-editor');
|
|
1838
|
+
this.version(1).stores({
|
|
1839
|
+
documents: 'id, name, updatedAt',
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
export const db = new ERDDatabase();
|
|
1845
|
+
|
|
1846
|
+
export async function saveToIndexedDB(doc: ERDDocument): Promise<void> {
|
|
1847
|
+
await db.documents.put({ ...doc, id: doc.name });
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
export async function loadFromIndexedDB(id: string): Promise<ERDDocument | undefined> {
|
|
1851
|
+
return db.documents.get(id);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
export async function listDocuments(): Promise<ERDDocument[]> {
|
|
1855
|
+
return db.documents.orderBy('updatedAt').reverse().toArray();
|
|
1856
|
+
}
|
|
1857
|
+
```
|
|
1858
|
+
|
|
1859
|
+
---
|
|
1860
|
+
|
|
1861
|
+
## Project Structure
|
|
1862
|
+
|
|
1863
|
+
```
|
|
1864
|
+
src/
|
|
1865
|
+
types/
|
|
1866
|
+
erd.ts # ERDDocument, Table, Column, Relationship types
|
|
1867
|
+
components/
|
|
1868
|
+
ERDCanvas.tsx # Main React Flow canvas wrapper
|
|
1869
|
+
nodes/
|
|
1870
|
+
TableNode.tsx # Crow's Foot table card with per-column handles
|
|
1871
|
+
ChenEntityNode.tsx # Chen rectangle entity
|
|
1872
|
+
ChenAttributeNode.tsx # Chen oval attribute
|
|
1873
|
+
ChenRelationshipNode.tsx # Chen diamond relationship
|
|
1874
|
+
edges/
|
|
1875
|
+
CrowsFootMarkers.tsx # SVG <defs> for 4 cardinality markers
|
|
1876
|
+
CrowsFootEdge.tsx # Custom edge with markerStart/markerEnd
|
|
1877
|
+
ChenEdge.tsx # Simple line with cardinality label
|
|
1878
|
+
panels/
|
|
1879
|
+
TableEditor.tsx # Side panel for editing table/column properties
|
|
1880
|
+
ImportDialog.tsx # DDL paste / file upload dialog
|
|
1881
|
+
ExportDialog.tsx # Dialect picker + DDL preview
|
|
1882
|
+
store/
|
|
1883
|
+
erd-store.ts # Zustand + zundo (undo/redo)
|
|
1884
|
+
lib/
|
|
1885
|
+
node-types.ts # Node type registry (Crow's Foot + Chen)
|
|
1886
|
+
edge-types.ts # Edge type registry
|
|
1887
|
+
auto-layout.ts # dagre-based auto-layout
|
|
1888
|
+
parsers/
|
|
1889
|
+
types.ts # DDLParser / DDLGenerator interfaces
|
|
1890
|
+
postgres-parser.ts # PG DDL -> ERDDocument
|
|
1891
|
+
mysql-parser.ts # MySQL DDL -> ERDDocument
|
|
1892
|
+
prisma-parser.ts # Prisma schema -> ERDDocument
|
|
1893
|
+
generators/
|
|
1894
|
+
index.ts # Generator registry + generateDDL()
|
|
1895
|
+
postgres-generator.ts # ERDDocument -> PG DDL
|
|
1896
|
+
mysql-generator.ts # ERDDocument -> MySQL DDL
|
|
1897
|
+
dbml-generator.ts # ERDDocument -> DBML
|
|
1898
|
+
import/
|
|
1899
|
+
ddl-import.ts # Orchestrates parse + auto-layout + load
|
|
1900
|
+
smart-query.ts # ChartDB-style information_schema queries
|
|
1901
|
+
export/
|
|
1902
|
+
image-export.ts # toPng() / toSvg()
|
|
1903
|
+
persistence/
|
|
1904
|
+
tauri-fs.ts # Tauri file system save/load/autosave
|
|
1905
|
+
dexie-store.ts # IndexedDB fallback for web
|
|
1906
|
+
mcp/
|
|
1907
|
+
erd-mcp-tools.ts # MCP tool definitions for NL editing
|
|
1908
|
+
```
|
|
1909
|
+
|
|
1910
|
+
---
|
|
1911
|
+
|
|
1912
|
+
## Reference Implementations — Study Order
|
|
1913
|
+
|
|
1914
|
+
| Priority | Project | Why Study It | Key Files to Read |
|
|
1915
|
+
|----------|---------|-------------|-------------------|
|
|
1916
|
+
| 1 | **ChartDB** | Closest stack match (React + Vite + ReactFlow + shadcn + Dexie) | `src/pages/editor-page/`, `src/context/chartdb-context/` |
|
|
1917
|
+
| 2 | **DrawDB** | Gold-standard parser/generator architecture | `src/utils/importFrom/`, `src/utils/exportAs/` |
|
|
1918
|
+
| 3 | **React Flow DatabaseSchemaNode** | Official per-column handle pattern | `reactflow.dev/ui/components/database-schema-node` |
|
|
1919
|
+
| 4 | **NextERD** | Simplest React Flow + shadcn integration, good for learning | Full codebase (~small) |
|
|
1920
|
+
| 5 | **Liam ERD** | 100+ table performance, schema file importers | `src/` layout engine |
|
|
1921
|
+
| 6 | **dineug/erd-editor** | .erd.json format design, VS Code integration | Format spec |
|
|
1922
|
+
| 7 | **JointJS 4.2** | Virtual rendering, z-ordering for massive schemas | Docs: ERD shapes API |
|
|
1923
|
+
| 8 | **ERFlow** | MCP tool design patterns for NL schema editing | Tool definitions |
|
|
1924
|
+
|
|
1925
|
+
---
|
|
1926
|
+
|
|
1927
|
+
## When to Use
|
|
1928
|
+
|
|
1929
|
+
- Building a visual database schema editor with React
|
|
1930
|
+
- Need Crow's Foot or Chen notation rendering on a canvas
|
|
1931
|
+
- Importing existing schemas (DDL, Prisma, live database) into a visual editor
|
|
1932
|
+
- Exporting ERDs to multiple SQL dialects
|
|
1933
|
+
- Adding MCP/NL editing capabilities to a schema tool
|
|
1934
|
+
|
|
1935
|
+
## When NOT to Use
|
|
1936
|
+
|
|
1937
|
+
- **Simple static ERD diagrams** — Use Mermaid `erDiagram` syntax instead (see `database-schema-designer.md`)
|
|
1938
|
+
- **Database migration tooling only** — Use Prisma Migrate, Drizzle Kit, or Alembic directly
|
|
1939
|
+
- **UML diagrams** — Different domain; use PlantUML or Excalidraw
|
|
1940
|
+
- **Existing app with JointJS** — JointJS 4.2 has built-in ERD primitives; do not add React Flow on top
|
|
1941
|
+
|
|
1942
|
+
## Related Skills
|
|
1943
|
+
|
|
1944
|
+
- `er-diagram-components.md` — Canonical reference for all ER notation components (Chen, Crow's Foot, attribute types, cardinality rules). Read this first for theory.
|
|
1945
|
+
- `erd-creator-textbook-research.md` — Academic research findings behind these patterns
|
|
1946
|
+
- `database-schema-designer.md` — Schema design methodology (Prisma, Drizzle, RLS, seeds). Use for the data modeling side.
|
|
1947
|
+
- `reserved-word-context-aware-quoting.md` — Quoting rules for DDL generators
|
|
1948
|
+
- `regex-alternation-ordering-sql-types.md` — SQL type parsing edge cases
|
|
1949
|
+
- `postgresql-to-mysql-runtime-translation.md` — PG-to-MySQL type mapping reference
|
|
1950
|
+
|
|
1951
|
+
## References
|
|
1952
|
+
|
|
1953
|
+
- [ChartDB](https://github.com/chartdb/chartdb) — MIT, React + Vite + ReactFlow + shadcn + Dexie.js
|
|
1954
|
+
- [DrawDB](https://github.com/drawdb-io/drawdb) — AGPL-3.0, ~24,400 stars, gold-standard DDL parsers
|
|
1955
|
+
- [Liam ERD](https://github.com/liam-hq/liam) — Apache-2.0, handles 100+ tables, CLI tool
|
|
1956
|
+
- [NextERD](https://github.com/vaxad/NextERD) — Next.js + React Flow + shadcn, small readable codebase
|
|
1957
|
+
- [dineug/erd-editor](https://github.com/dineug/erd-editor) — Web Component, VS Code + IntelliJ integration
|
|
1958
|
+
- [React Flow DatabaseSchemaNode](https://reactflow.dev/ui/components/database-schema-node) — Official per-column handle pattern
|
|
1959
|
+
- [relliv/crows-foot-notations](https://github.com/relliv/crows-foot-notations) — SVG symbol reference
|
|
1960
|
+
- [node-sql-parser](https://github.com/nicereporter/node-sql-parser) — Bidirectional SQL-AST, multi-dialect
|
|
1961
|
+
- [sql-ddl-to-json-schema](https://github.com/nicereporter/node-sql-parser) — DDL-focused, nearley grammar
|
|
1962
|
+
- [ERFlow](https://github.com/ageborn-dev/erflow) — MCP-based ERD editing, 25+ tools
|
|
1963
|
+
- [JointJS 4.2](https://www.jointjs.com/) — Built-in ERD shapes + Crow's Foot + virtual rendering
|
|
1964
|
+
- [zundo](https://github.com/charkour/zundo) — Undo/redo middleware for Zustand
|
|
1965
|
+
- [@dagrejs/dagre](https://github.com/dagrejs/dagre) — Directed graph auto-layout
|