├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── demo.gif ├── lerna.json ├── package.json ├── packages ├── dbml-to-json-table-schema │ ├── .gitignore │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── constants │ │ │ └── schema.ts │ │ ├── errors │ │ │ ├── FieldNotExistsError.ts │ │ │ └── TableNotExistError.ts │ │ ├── index.ts │ │ ├── tests │ │ │ └── data.ts │ │ ├── utils │ │ │ ├── computeNameWithSchemaName.ts │ │ │ ├── createEnumsSet.ts │ │ │ ├── createRelationalTalesMap.ts │ │ │ ├── tests │ │ │ │ ├── createEnumsSet.test.ts │ │ │ │ └── createRelationalTalesMap.test.ts │ │ │ └── transfomers │ │ │ │ ├── dbmlEnumToJSONTableEnum.test.ts │ │ │ │ ├── dbmlEnumToJSONTableEnum.ts │ │ │ │ ├── dbmlFieldToJSONTableField.test.ts │ │ │ │ ├── dbmlFieldToJSONTableField.ts │ │ │ │ ├── dbmlIndexColToJSONTableIndexCol.test.ts │ │ │ │ ├── dbmlIndexColToJSONTableIndexCol.ts │ │ │ │ ├── dbmlIndexToJSONTableIndex.test.ts │ │ │ │ ├── dbmlIndexToJSONTableIndex.tsx │ │ │ │ ├── dbmlRefEndpointToJSONTableRefEndpoint.test.ts │ │ │ │ ├── dbmlRefEndpointToJSONTableRefEndpoint.ts │ │ │ │ ├── dbmlRefToJSONTableRef.test.ts │ │ │ │ ├── dbmlRefToJSONTableRef.ts │ │ │ │ ├── dbmlSchemaToJSONTableSchema.test.ts │ │ │ │ ├── dbmlSchemaToJSONTableSchema.ts │ │ │ │ ├── dbmlTableToJSONTableTable.test.ts │ │ │ │ └── dbmlTableToJSONTableTable.ts │ │ └── validators │ │ │ ├── index.ts │ │ │ └── validateRefs.ts │ └── tsconfig.json ├── dbml-vs-code-extension │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode-test.mjs │ ├── .vscodeignore │ ├── .yarnrc │ ├── CHANGELOG.md │ ├── LICENCE │ ├── README.md │ ├── assets │ │ ├── icons │ │ │ ├── open-preview-dark.svg │ │ │ ├── open-preview.svg │ │ │ ├── preview-dark.svg │ │ │ └── preview.svg │ │ └── logo.png │ ├── extension │ │ ├── constants │ │ │ └── index.ts │ │ └── index.ts │ ├── icons │ │ └── dbml-logo.svg │ ├── index.html │ ├── package.json │ ├── src │ │ └── index.tsx │ ├── tsconfig.json │ ├── vite.config.js │ ├── vsc-extension-quickstart.md │ └── yarn-error.log ├── extension-shared │ ├── extension │ │ ├── constants │ │ │ └── index.ts │ │ ├── helper │ │ │ └── extensionConfigs.ts │ │ ├── types │ │ │ ├── configKeys.ts │ │ │ ├── defaultPageConfig.ts │ │ │ ├── index.ts │ │ │ └── webviewCommand.ts │ │ └── views │ │ │ ├── helper.ts │ │ │ └── panel.ts │ ├── globals.d.ts │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── hooks │ │ │ └── schema.ts │ │ └── index.tsx │ └── tsconfig.json ├── json-table-schema-visualizer │ ├── .eslintignore │ ├── .gitignore │ ├── .storybook │ │ ├── main.js │ │ └── preview.jsx │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── components │ │ │ ├── Column │ │ │ │ ├── Column.stories.tsx │ │ │ │ ├── Column.tsx │ │ │ │ └── ColumnWrapper.tsx │ │ │ ├── DiagramViewer │ │ │ │ ├── Connections.tsx │ │ │ │ ├── DiagramViewer.stories.tsx │ │ │ │ ├── DiagramViewer.tsx │ │ │ │ ├── DiagramWrapper.stories.tsx │ │ │ │ ├── DiagramWrapper.tsx │ │ │ │ └── Tables.tsx │ │ │ ├── FieldDetails │ │ │ │ ├── EnumDetails.stories.tsx │ │ │ │ ├── EnumDetails.tsx │ │ │ │ ├── FieldDetailWrapper.stories.tsx │ │ │ │ ├── FieldDetailWrapper.tsx │ │ │ │ ├── FieldDetails.stories.tsx │ │ │ │ └── FieldDetails.tsx │ │ │ ├── Messages │ │ │ │ ├── EmptyTableMessage.stories.tsx │ │ │ │ ├── EmptyTableMessage.tsx │ │ │ │ ├── MessageWrapper.tsx │ │ │ │ ├── NoSchemaMessage.stories.tsx │ │ │ │ └── NoSchemaMessage.tsx │ │ │ ├── RelationConnection │ │ │ │ ├── ConnectionPath.tsx │ │ │ │ ├── RelationConnection.stories.tsx │ │ │ │ └── RelationConnection.tsx │ │ │ ├── Table.stories.tsx │ │ │ ├── Table.tsx │ │ │ ├── TableHeader.stories.tsx │ │ │ ├── TableHeader.tsx │ │ │ ├── TableWrapper.tsx │ │ │ ├── Toolbar │ │ │ │ ├── AutoArrage │ │ │ │ │ ├── AutoArrangeTables.stories.tsx │ │ │ │ │ └── AutoArrangeTables.tsx │ │ │ │ ├── Button.tsx │ │ │ │ ├── DetailLevelToggle │ │ │ │ │ ├── DetailLevelToggle.stories.tsx │ │ │ │ │ └── DetailLevelToggle.tsx │ │ │ │ ├── FitToView │ │ │ │ │ └── FitToView.tsx │ │ │ │ ├── ThemeToggler │ │ │ │ │ ├── ThemeToggler.stories.tsx │ │ │ │ │ └── ThemeToggler.tsx │ │ │ │ ├── Toolbar.stories.tsx │ │ │ │ └── Toolbar.tsx │ │ │ └── dumb │ │ │ │ └── KonvaText.tsx │ │ ├── constants │ │ │ ├── colors.ts │ │ │ ├── font.ts │ │ │ ├── sizing.ts │ │ │ ├── tableCoords.ts │ │ │ └── theme.ts │ │ ├── events-emitter │ │ │ └── index.ts │ │ ├── fake │ │ │ └── fakeJsonTables.ts │ │ ├── hooks │ │ │ ├── cursor.ts │ │ │ ├── enums.ts │ │ │ ├── relationConnection.ts │ │ │ ├── stage.ts │ │ │ ├── table.ts │ │ │ ├── tableColor.ts │ │ │ ├── tableDetailLevel.ts │ │ │ ├── tableWidthStore.ts │ │ │ ├── theme.ts │ │ │ └── window.ts │ │ ├── providers │ │ │ ├── EnumsProvider.tsx │ │ │ ├── MainProviders.tsx │ │ │ ├── TableDetailLevelProvider.tsx │ │ │ ├── TableDimension.tsx │ │ │ ├── TablesColorProvider.tsx │ │ │ ├── TablesInfoProvider.tsx │ │ │ ├── TablesPositionsProvider.tsx │ │ │ └── ThemeProvider.tsx │ │ ├── storages │ │ │ ├── local.ts │ │ │ └── session.ts │ │ ├── stores │ │ │ ├── PersitableStore.ts │ │ │ ├── StoreSignal.ts │ │ │ ├── detailLevelStore.ts │ │ │ ├── stagesState.ts │ │ │ ├── tableCoords.ts │ │ │ └── tableWidth.ts │ │ ├── styles │ │ │ └── index.css │ │ ├── types │ │ │ ├── dimension.ts │ │ │ ├── enums.ts │ │ │ ├── positions.ts │ │ │ ├── relation.ts │ │ │ ├── stage.ts │ │ │ ├── storage.ts │ │ │ ├── table.ts │ │ │ ├── tableColor.ts │ │ │ ├── tableDetailLevel.ts │ │ │ ├── tablesInfoProviderValue.ts │ │ │ └── theme.tsx │ │ └── utils │ │ │ ├── colors │ │ │ ├── getContrastColor.ts │ │ │ └── getTableColorFromName.ts │ │ │ ├── computeCaretPoints.ts │ │ │ ├── computeColIndexes.ts │ │ │ ├── computeColY.ts │ │ │ ├── computeConnectionHandlePositions.ts │ │ │ ├── computeConnectionPaths.ts │ │ │ ├── computeEgde │ │ │ ├── computeBezierEdge.ts │ │ │ └── computeSmoothEdge.ts │ │ │ ├── computeEnumDetailBoxMaxW.ts │ │ │ ├── computeFieldDetailBoxDimension.ts │ │ │ ├── computeTableDimension.ts │ │ │ ├── computeTextSize.ts │ │ │ ├── computeTextsMaxWidth.ts │ │ │ ├── createEnumItemText.ts │ │ │ ├── createTablesColorMap.ts │ │ │ ├── estimateSentenceLineCount.ts │ │ │ ├── eventName.ts │ │ │ ├── filterByDetailLevel.ts │ │ │ ├── getFieldType.ts │ │ │ ├── getRelationSymbol.ts │ │ │ ├── shouldHighLightCol.ts │ │ │ ├── tablePositioning │ │ │ ├── computeTablesPositions.ts │ │ │ ├── getColsNumber.ts │ │ │ └── tests │ │ │ │ └── computeTablesPositions.test.ts │ │ │ ├── tableWComputation │ │ │ ├── computeTablePreferredWidth.ts │ │ │ └── getTableLinesText.ts │ │ │ └── tests │ │ │ ├── computeCaretPoints.test.ts │ │ │ ├── computeColIndexes.test.ts │ │ │ ├── computeColY.test.ts │ │ │ ├── computeConnectionHandlePosition.test.ts │ │ │ ├── computeTableDimension.test.ts │ │ │ └── computeTextsMaxWidth.test.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.js ├── prisma-to-json-table-schema │ ├── .gitignore │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── constants │ │ │ └── scalarFieldType.ts │ │ ├── enums │ │ │ └── prismaAstNodeType.ts │ │ ├── index.ts │ │ ├── tests │ │ │ └── data.ts │ │ ├── types │ │ │ ├── intermediateFormattedNode.ts │ │ │ └── node.ts │ │ └── utils │ │ │ ├── computeKey.ts │ │ │ ├── isTypeOf.ts │ │ │ └── transformers │ │ │ ├── createRefsFromPrismaASTNodes.ts │ │ │ ├── enumNodeToJSONTableEnum.ts │ │ │ ├── getFieldConfig.ts │ │ │ ├── getFieldTypeName.ts │ │ │ ├── intermediate │ │ │ ├── createIntermediateSchema.ts │ │ │ ├── formatIntermediateField.ts │ │ │ ├── formatIntermediateTable.ts │ │ │ ├── lookForRelation.ts │ │ │ └── tests │ │ │ │ └── lookForRelation.test.ts │ │ │ ├── intermediateFieldToJSONTableField.ts │ │ │ ├── intermediateTableToJSONTableTable.ts │ │ │ ├── prismaASTToJSONTableSchema.ts │ │ │ └── tests │ │ │ └── enumNodeToJSONTableEnum.test.ts │ └── tsconfig.json ├── prisma-vs-code-extension │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode-test.mjs │ ├── .vscodeignore │ ├── .yarnrc │ ├── CHANGELOG.md │ ├── LICENCE │ ├── README.md │ ├── assets │ │ ├── icons │ │ │ ├── open-preview-dark.svg │ │ │ ├── open-preview.svg │ │ │ ├── preview-dark.svg │ │ │ └── preview.svg │ │ └── logo.png │ ├── extension │ │ ├── constants │ │ │ └── index.ts │ │ └── index.ts │ ├── index.html │ ├── package.json │ ├── src │ │ └── index.tsx │ ├── tsconfig.json │ ├── vite.config.js │ ├── vsc-extension-quickstart.md │ └── yarn-error.log └── shared │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── types │ ├── diagnostic.ts │ ├── tableSchema.ts │ └── utils.ts │ └── utils │ ├── computeRelationalFieldKey.ts │ └── tests │ └── computeRelationalFieldKey.test.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | .lintstagedrc.js 3 | *.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["standard-with-typescript", "plugin:react/recommended", "prettier"], 7 | overrides: [ 8 | { 9 | files: ["*.tsx"], 10 | rules: { 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | }, 13 | } 14 | ], 15 | parserOptions: { 16 | ecmaVersion: "latest", 17 | sourceType: "module", 18 | }, 19 | plugins: ["react"], 20 | rules: { 21 | "react/react-in-jsx-scope": "off", 22 | "import/order": [ 23 | "error", 24 | { 25 | groups: [ 26 | "builtin", 27 | "external", 28 | "internal", 29 | "parent", 30 | "sibling", 31 | "index", 32 | "object", 33 | "type", 34 | ], 35 | "newlines-between": "always", 36 | }, 37 | ], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *storybook.log 3 | .DS_Store 4 | coverage/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.(tsx|ts)": () => "yarn tsc-files --noEmit", 3 | "*.(tsx|ts|js)": ["eslint --fix", "prettier --write"], 4 | "*.(md|json)": "prettier --write", 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "ms-vscode.extension-test-runner"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Debug DBML Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}/packages/dbml-vs-code-extension" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/packages/dbml-vs-code-extension/dist/extension/*.js" 17 | ], 18 | "preLaunchTask": "npm: dev" 19 | }, 20 | { 21 | "name": "Preview DBML Extension", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}/packages/dbml-vs-code-extension" 26 | ], 27 | "outFiles": [ 28 | "${workspaceFolder}/packages/dbml-vs-code-extension/dist/extension/*.js" 29 | ], 30 | "preLaunchTask": "npm: build" 31 | }, 32 | { 33 | "name": "Preview Prisma Extension", 34 | "type": "extensionHost", 35 | "request": "launch", 36 | "args": [ 37 | "--extensionDevelopmentPath=${workspaceFolder}/packages/prisma-vs-code-extension" 38 | ], 39 | "outFiles": [ 40 | "${workspaceFolder}/packages/prisma-vs-code-extension/dist/extension/*.js" 41 | ], 42 | "preLaunchTask": "npm: build:prisma" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "problemMatcher": { 8 | "owner": "typescript", 9 | "fileLocation": "relative", 10 | "pattern": { 11 | "regexp": "^([a-zA-Z]\\:/?([\\w\\-]/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", 12 | "file": 1, 13 | "line": 3, 14 | "column": 4, 15 | "code": 5, 16 | "message": 6 17 | }, 18 | "background": { 19 | "activeOnStart": true, 20 | "beginsPattern": "^.*extension build start*$", 21 | "endsPattern": "^.*extension (build|rebuild) success.*$" 22 | } 23 | }, 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never" 27 | }, 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "options": { 33 | "cwd": "${workspaceFolder}/packages/dbml-vs-code-extension" 34 | } 35 | }, 36 | { 37 | "type": "npm", 38 | "script": "build", 39 | "group": { 40 | "kind": "build", 41 | "isDefault": true 42 | }, 43 | "problemMatcher": [], 44 | "options": { 45 | "cwd": "${workspaceFolder}/packages/dbml-vs-code-extension" 46 | } 47 | }, 48 | 49 | // prisma 50 | { 51 | "type": "npm", 52 | "script": "build:prisma", 53 | "group": { 54 | "kind": "build", 55 | "isDefault": true 56 | }, 57 | "problemMatcher": [], 58 | "options": { 59 | "cwd": "${workspaceFolder}/packages/prisma-vs-code-extension" 60 | } 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Code of conduct 4 | 5 | Please read our [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## The project structure 8 | 9 | [TODO] 10 | 11 | ## How to start the project 12 | 13 | [TODO] 14 | 15 | ## How to contribute 16 | 17 | [TODO] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 DBSchemaVisualizer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Database schema visualizer 2 | 3 | An Vscode extension to visualize the database schema in ERD from dbml or prisma file in your vscode. 4 | 5 | ## Demo 6 | 7 | ![DBML Demo](./assets/demo.gif) 8 | 9 | ## Features 10 | 11 | - Create entity relations diagrams from your DBML/Prisma code 12 | - Available in light and dark modes 13 | 14 | ## How to install and use it 15 | 16 | Follow this article: 17 | 18 | ## Downloads 19 | 20 | - [The DBML extension](https://marketplace.visualstudio.com/items?itemName=bocovo.dbml-erd-visualizer) 21 | - [The Prisma extension](https://marketplace.visualstudio.com/items?itemName=bocovo.prisma-erd-visualizer) 22 | 23 | ## Extensions 24 | 25 | - [The DBML extension](./packages/dbml-vs-code-extension/README.md) 26 | - [The Prisma extension](./packages/prisma-vs-code-extension/README.md) 27 | 28 | ## Contribute 29 | 30 | If you want to contribute to this project please read the [contribution note](./CODE_OF_CONDUCT.md) 31 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOCOVO/db-schema-visualizer/0769e64dd03bedc103fe9e5e82e3ec3eed5bd5ca/assets/demo.gif -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "0.0.0", 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "npmClient": "yarn" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db-schema-visualiser", 3 | "version": "0.0.3", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "dependencies": { 11 | "lerna": "^8.1.2" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.12", 15 | "@typescript-eslint/eslint-plugin": "^6.4.0", 16 | "eslint": "^8.0.1", 17 | "eslint-config-prettier": "^9.1.0", 18 | "eslint-config-standard-with-typescript": "^43.0.1", 19 | "eslint-plugin-import": "^2.25.2", 20 | "eslint-plugin-n": "^15.0.0", 21 | "eslint-plugin-promise": "^6.0.0", 22 | "eslint-plugin-react": "^7.34.1", 23 | "husky": "^9.0.11", 24 | "jest": "^29.7.0", 25 | "lint-staged": "^15.2.2", 26 | "prettier": "3.2.5", 27 | "ts-jest": "^29.1.2", 28 | "tsc-files": "^1.1.4", 29 | "typescript": "^5.4.3" 30 | }, 31 | "scripts": { 32 | "prepare": "husky" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/README.md: -------------------------------------------------------------------------------- 1 | # DBML Parser 2 | 3 | This module contains functions to parse the DBML code to a JSON object 4 | called JSON Table Schema 5 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest'); 2 | const { compilerOptions } = require("./tsconfig.json"); 3 | 4 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 5 | module.exports = { 6 | preset: 'ts-jest', 7 | testEnvironment: 'node', 8 | roots: ["./"], 9 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 10 | prefix: "/src", 11 | }), 12 | }; -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbml-to-json-table-schema", 3 | "version": "0.0.0", 4 | "main": "src/index.js", 5 | "types": "src/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest --collectCoverage" 9 | }, 10 | "dependencies": { 11 | "@dbml/core": "^3.4.0", 12 | "shared": "0.0.0" 13 | }, 14 | "devDependencies": {} 15 | } 16 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/constants/schema.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SCHEMA_NAME = "public"; 2 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/errors/FieldNotExistsError.ts: -------------------------------------------------------------------------------- 1 | export class FieldNotExistsError extends Error { 2 | constructor(fieldName: string, tableName: string) { 3 | super(`Field ${fieldName} does not exist in table ${tableName}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/errors/TableNotExistError.ts: -------------------------------------------------------------------------------- 1 | export class TableNotExistError extends Error { 2 | constructor(tableName: string) { 3 | super(`Table ${tableName} does not exist.`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Parser, type CompilerDiagnostic } from "@dbml/core"; 2 | import { DiagnosticError } from "shared/types/diagnostic"; 3 | 4 | import { dbmlSchemaToJSONTableSchema } from "./utils/transfomers/dbmlSchemaToJSONTableSchema"; 5 | import { validateSchema } from "./validators"; 6 | 7 | import type { JSONTableSchema } from "shared/types/tableSchema"; 8 | 9 | export const parseDBMLToJSON = (dbmlCode: string): JSONTableSchema => { 10 | try { 11 | const rawParsedSchema = Parser.parseDBMLToJSON(dbmlCode); 12 | validateSchema(rawParsedSchema); 13 | return dbmlSchemaToJSONTableSchema(rawParsedSchema); 14 | } catch (error) { 15 | if ("location" in (error as any) && "message" in (error as any)) { 16 | const _error = error as CompilerDiagnostic; 17 | 18 | const locationEnd = _error.location.end; 19 | const locationStart = _error.location.start; 20 | 21 | if (locationEnd === undefined || locationStart === undefined) { 22 | throw error; 23 | } 24 | 25 | throw new DiagnosticError( 26 | { 27 | end: { 28 | column: locationEnd.column - 1, 29 | line: locationEnd.line - 1, 30 | }, 31 | start: { 32 | column: locationStart.column - 1, 33 | line: locationStart.line - 1, 34 | }, 35 | }, 36 | _error.message, 37 | ); 38 | } 39 | throw error; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/computeNameWithSchemaName.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SCHEMA_NAME } from "../constants/schema"; 2 | 3 | import type Enum from "@dbml/core/types/model_structure/enum"; 4 | import type Table from "@dbml/core/types/model_structure/table"; 5 | 6 | export const computeNameWithSchemaName = ( 7 | objectName: string, 8 | schemaName?: string, 9 | ): string => { 10 | if ( 11 | schemaName !== undefined && 12 | schemaName !== null && 13 | schemaName !== DEFAULT_SCHEMA_NAME 14 | ) { 15 | return `${schemaName}.${objectName}`; 16 | } 17 | 18 | return objectName; 19 | }; 20 | 21 | export const getTableFullName = (table: Table): string => { 22 | // unfortunately the Table type from dbml package not define the schemaName property 23 | // while it exists 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 25 | return computeNameWithSchemaName(table.name, (table as any).schemaName); 26 | }; 27 | 28 | export const getEnumFullName = (_enum: Enum): string => { 29 | // unfortunately the Enum type from dbml package not define the schemaName property 30 | // while it exists 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 32 | return computeNameWithSchemaName(_enum.name, (_enum as any).schemaName); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/createEnumsSet.ts: -------------------------------------------------------------------------------- 1 | import { getEnumFullName } from "./computeNameWithSchemaName"; 2 | 3 | import type Enum from "@dbml/core/types/model_structure/enum"; 4 | 5 | export const createEnumsSet = (enums: Enum[]): Set => { 6 | const map = new Set(); 7 | 8 | enums.forEach((enumObj) => { 9 | map.add(getEnumFullName(enumObj)); 10 | }); 11 | 12 | return map; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/createRelationalTalesMap.ts: -------------------------------------------------------------------------------- 1 | import { computeRelationalFieldKey } from "shared/utils/computeRelationalFieldKey"; 2 | 3 | import type Ref from "@dbml/core/types/model_structure/ref"; 4 | 5 | export const createRelationalTalesMap = ( 6 | refs: Ref[], 7 | ): Map => { 8 | let map = new Map(); 9 | 10 | refs.forEach((ref) => { 11 | const [sourceEndpoint, targetEndpoint] = ref.endpoints; 12 | const sourceFieldKey = computeRelationalFieldKey( 13 | sourceEndpoint.tableName, 14 | sourceEndpoint.fieldNames[0], 15 | ); 16 | map = appendRelationalTablesMap( 17 | map, 18 | sourceFieldKey, 19 | targetEndpoint.tableName, 20 | ); 21 | 22 | const targetFieldKey = computeRelationalFieldKey( 23 | targetEndpoint.tableName, 24 | targetEndpoint.fieldNames[0], 25 | ); 26 | map = appendRelationalTablesMap( 27 | map, 28 | targetFieldKey, 29 | sourceEndpoint.tableName, 30 | ); 31 | }); 32 | 33 | return map; 34 | }; 35 | 36 | const appendRelationalTablesMap = ( 37 | map: Map, 38 | fieldKey: string, 39 | tableToAdd: string, 40 | ): Map => { 41 | const newMap = new Map(map); 42 | 43 | if (newMap.has(fieldKey)) { 44 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 45 | const tablesSet = newMap.get(fieldKey)!; 46 | tablesSet.push(tableToAdd); 47 | } else { 48 | const tablesSet = [tableToAdd]; 49 | newMap.set(fieldKey, tablesSet); 50 | } 51 | 52 | return newMap; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/tests/createEnumsSet.test.ts: -------------------------------------------------------------------------------- 1 | import { createEnumsSet } from "../createEnumsSet"; 2 | 3 | import type Enum from "@dbml/core/types/model_structure/enum"; 4 | 5 | import { dbmlTestCodeInJSONTableFormat } from "@/tests/data"; 6 | 7 | describe("create enums set", () => { 8 | test("create enums set", () => { 9 | expect(createEnumsSet(dbmlTestCodeInJSONTableFormat.enums as Enum[])).toEqual( 10 | new Set(["status"]), 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/tests/createRelationalTalesMap.test.ts: -------------------------------------------------------------------------------- 1 | import { createRelationalTalesMap } from "../createRelationalTalesMap"; 2 | 3 | import type Ref from "@dbml/core/types/model_structure/ref"; 4 | 5 | import { dbmlTestCodeInJSONTableFormat } from "@/tests/data"; 6 | 7 | 8 | describe("create relational tables map", () => { 9 | test("create relational tables map", () => { 10 | expect( 11 | createRelationalTalesMap([ 12 | ...(dbmlTestCodeInJSONTableFormat.refs as unknown as Ref[]), 13 | ...(dbmlTestCodeInJSONTableFormat.refs as unknown as Ref[]), 14 | ]), 15 | ).toEqual( 16 | new Map([ 17 | ["users.id", new Set(["follows"])], 18 | ["follows.following_user_id", new Set(["users"])], 19 | ]), 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlEnumToJSONTableEnum.test.ts: -------------------------------------------------------------------------------- 1 | import { dbmlEnumToJSONTableEnum } from "./dbmlEnumToJSONTableEnum"; 2 | 3 | import { dbmlTestCodeInJSONTableFormat, parsedDBML } from "@/tests/data"; 4 | 5 | describe("transform dbml enum to json table enum", () => { 6 | test("transform enum", () => { 7 | expect(dbmlEnumToJSONTableEnum(parsedDBML.enums[0])).toEqual( 8 | dbmlTestCodeInJSONTableFormat.enums[0], 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlEnumToJSONTableEnum.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableEnum } from "shared/types/tableSchema"; 2 | 3 | import { getEnumFullName } from "../computeNameWithSchemaName"; 4 | 5 | import type Enum from "@dbml/core/types/model_structure/enum"; 6 | 7 | export const dbmlEnumToJSONTableEnum = (_enum: Enum): JSONTableEnum => { 8 | return { 9 | name: getEnumFullName(_enum), 10 | values: _enum.values.map( 11 | ({ name, note }) => ({ 12 | name, 13 | // the note returned by the dbml parser is not string 14 | // but an object there is an typing error in their package 15 | note: (note as any)?.value, 16 | }), 17 | ), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlFieldToJSONTableField.test.ts: -------------------------------------------------------------------------------- 1 | import { createEnumsSet } from "../createEnumsSet"; 2 | import { createRelationalTalesMap } from "../createRelationalTalesMap"; 3 | 4 | import { dbmlFieldToJSONTableField } from "./dbmlFieldToJSONTableField"; 5 | 6 | import { dbmlTestCodeInJSONTableFormat, parsedDBML } from "@/tests/data"; 7 | 8 | describe("transform dbml field to json table field", () => { 9 | const enumsSet = createEnumsSet(parsedDBML.enums); 10 | const relationalFieldMap = createRelationalTalesMap(parsedDBML.refs); 11 | const table = parsedDBML.tables[0]; 12 | 13 | test("transform simple field", () => { 14 | const field = table.fields[0]; 15 | 16 | expect( 17 | dbmlFieldToJSONTableField({ 18 | field, 19 | enumsSet, 20 | ownerTable: table.name, 21 | relationalFieldMap, 22 | }), 23 | ).toEqual(dbmlTestCodeInJSONTableFormat.tables[0].fields[0]); 24 | }); 25 | 26 | test("transform relational field", () => { 27 | const field = table.fields[2]; 28 | 29 | expect( 30 | dbmlFieldToJSONTableField({ 31 | field, 32 | enumsSet, 33 | ownerTable: table.name, 34 | relationalFieldMap, 35 | }), 36 | ).toEqual(dbmlTestCodeInJSONTableFormat.tables[0].fields[2]); 37 | }); 38 | 39 | test("transform enum field", () => { 40 | const field = table.fields[4]; 41 | 42 | expect( 43 | dbmlFieldToJSONTableField({ 44 | field, 45 | enumsSet, 46 | ownerTable: table.name, 47 | relationalFieldMap, 48 | }), 49 | ).toEqual(dbmlTestCodeInJSONTableFormat.tables[0].fields[4]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlFieldToJSONTableField.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableField } from "shared/types/tableSchema"; 2 | import { computeRelationalFieldKey } from "shared/utils/computeRelationalFieldKey"; 3 | 4 | import { computeNameWithSchemaName } from "../computeNameWithSchemaName"; 5 | 6 | import type Field from "@dbml/core/types/model_structure/field"; 7 | 8 | interface DbmlToJSONTableFieldParams { 9 | field: Field; 10 | relationalFieldMap: Map; 11 | enumsSet: Set; 12 | ownerTable: string; 13 | } 14 | export const dbmlFieldToJSONTableField = ({ 15 | field: { 16 | name, 17 | pk, 18 | unique, 19 | note, 20 | dbdefault, 21 | increment, 22 | // eslint-disable-next-line @typescript-eslint/naming-convention 23 | not_null, 24 | type, 25 | }, 26 | relationalFieldMap, 27 | enumsSet: enumsMap, 28 | ownerTable, 29 | }: DbmlToJSONTableFieldParams): JSONTableField => { 30 | const fieldKey = computeRelationalFieldKey(ownerTable, name); 31 | 32 | const hasRelation = relationalFieldMap.has(fieldKey); 33 | const typeName = computeNameWithSchemaName( 34 | type.type_name as string, 35 | type.schemaName as string | undefined, 36 | ); 37 | 38 | return { 39 | name, 40 | pk, 41 | unique, 42 | // the note returned by the dbml parser is not string 43 | // but an object there is an typing error in their package 44 | note: (note as any)?.value, 45 | not_null, 46 | type: { 47 | type_name: typeName, 48 | is_enum: enumsMap.has(typeName), 49 | }, 50 | is_relation: hasRelation, 51 | relational_tables: hasRelation ? relationalFieldMap.get(fieldKey) : null, 52 | increment, 53 | dbdefault, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlIndexColToJSONTableIndexCol.test.ts: -------------------------------------------------------------------------------- 1 | import { dbmlIndexColToJSONTableIndexCol } from "./dbmlIndexColToJSONTableIndexCol"; 2 | 3 | import { parsedDBML } from "@/tests/data"; 4 | 5 | describe("transform dbml index column to json table index column", () => { 6 | test("transform index column", () => { 7 | expect( 8 | dbmlIndexColToJSONTableIndexCol( 9 | parsedDBML.tables[2].indexes[0].columns[0], 10 | ), 11 | ).toEqual({ 12 | type: "column", 13 | value: "id", 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlIndexColToJSONTableIndexCol.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableIndexColumn } from "shared/types/tableSchema"; 2 | 3 | import type IndexColumn from "@dbml/core/types/model_structure/indexColumn"; 4 | 5 | export const dbmlIndexColToJSONTableIndexCol = ({ 6 | type, 7 | value, 8 | }: IndexColumn): JSONTableIndexColumn => { 9 | return { 10 | type, 11 | value, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlIndexToJSONTableIndex.test.ts: -------------------------------------------------------------------------------- 1 | import { dbmlIndexToJSONTableIndex } from "./dbmlIndexToJSONTableIndex"; 2 | 3 | import { parsedDBML, dbmlTestCodeInJSONTableFormat } from "@/tests/data"; 4 | describe("transform dbml index to json table index", () => { 5 | test("transform index", () => { 6 | expect(dbmlIndexToJSONTableIndex(parsedDBML.tables[2].indexes[0])).toEqual( 7 | dbmlTestCodeInJSONTableFormat.tables[2].indexes[0], 8 | ); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlIndexToJSONTableIndex.tsx: -------------------------------------------------------------------------------- 1 | import { type JSONTableIndex } from "shared/types/tableSchema"; 2 | 3 | import { dbmlIndexColToJSONTableIndexCol } from "./dbmlIndexColToJSONTableIndexCol"; 4 | 5 | import type Index from "@dbml/core/types/model_structure/indexes"; 6 | 7 | export const dbmlIndexToJSONTableIndex = ({ 8 | pk, 9 | unique, 10 | type, 11 | name, 12 | note, 13 | columns, 14 | }: Index): JSONTableIndex => { 15 | return { 16 | unique, 17 | type, 18 | pk: Boolean(pk), 19 | name, 20 | // the note returned by the dbml parser is not string 21 | // but an object there is an typing error in their package 22 | note: (note as any)?.value, 23 | columns: columns.map(dbmlIndexColToJSONTableIndexCol), 24 | }; 25 | }; -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlRefEndpointToJSONTableRefEndpoint.test.ts: -------------------------------------------------------------------------------- 1 | import { dbmlRefEndpointToJSONTableRefEndpoint } from "./dbmlRefEndpointToJSONTableRefEndpoint"; 2 | 3 | import { dbmlTestCodeInJSONTableFormat, parsedDBML } from "@/tests/data"; 4 | 5 | describe('transform dbml ref endpoint to json table ref endpoint', () => { 6 | test('transform ref endpoint', () => { 7 | expect(dbmlRefEndpointToJSONTableRefEndpoint(parsedDBML.refs[0].endpoints[0])).toEqual(dbmlTestCodeInJSONTableFormat.refs[0].endpoints[0]); 8 | }); 9 | }); -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlRefEndpointToJSONTableRefEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableRef } from "shared/types/tableSchema"; 2 | 3 | import { computeNameWithSchemaName } from "../computeNameWithSchemaName"; 4 | 5 | import type Endpoint from "@dbml/core/types/model_structure/endpoint"; 6 | 7 | export const dbmlRefEndpointToJSONTableRefEndpoint = ({ 8 | tableName, 9 | fieldNames, 10 | relation, 11 | schemaName, 12 | }: Endpoint): JSONTableRef["endpoints"][number] => { 13 | return { 14 | relation, 15 | tableName: computeNameWithSchemaName(tableName, schemaName), 16 | fieldNames, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlRefToJSONTableRef.test.ts: -------------------------------------------------------------------------------- 1 | import { dbmlRefToJSONTableRef } from "./dbmlRefToJSONTableRef"; 2 | 3 | import { dbmlTestCodeInJSONTableFormat, parsedDBML } from "@/tests/data"; 4 | 5 | describe("transform dbml ref to json table ref", () => { 6 | test("transform ref", () => { 7 | expect(dbmlRefToJSONTableRef(parsedDBML.refs[0])).toEqual( 8 | dbmlTestCodeInJSONTableFormat.refs[0], 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlRefToJSONTableRef.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableRef } from "shared/types/tableSchema"; 2 | 3 | import { dbmlRefEndpointToJSONTableRefEndpoint } from "./dbmlRefEndpointToJSONTableRefEndpoint"; 4 | 5 | import type Ref from "@dbml/core/types/model_structure/ref"; 6 | 7 | 8 | export const dbmlRefToJSONTableRef = (ref: Ref): JSONTableRef => { 9 | return { 10 | name: ref.name, 11 | endpoints: ref.endpoints.map(dbmlRefEndpointToJSONTableRefEndpoint), 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlSchemaToJSONTableSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { dbmlSchemaToJSONTableSchema } from "./dbmlSchemaToJSONTableSchema"; 2 | 3 | import { dbmlTestCodeInJSONTableFormat, parsedDBML } from "@/tests/data"; 4 | 5 | describe("transform dbml schema to json table schema", () => { 6 | test("transform dbml schema to json table schema", () => { 7 | expect(dbmlSchemaToJSONTableSchema(parsedDBML)).toEqual( 8 | dbmlTestCodeInJSONTableFormat, 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlSchemaToJSONTableSchema.ts: -------------------------------------------------------------------------------- 1 | import { type RawDatabase } from "@dbml/core/types/model_structure/database"; 2 | import { type JSONTableSchema } from "shared/types/tableSchema"; 3 | 4 | import { createEnumsSet } from "../createEnumsSet"; 5 | import { createRelationalTalesMap } from "../createRelationalTalesMap"; 6 | 7 | import { dbmlEnumToJSONTableEnum } from "./dbmlEnumToJSONTableEnum"; 8 | import { dbmlRefToJSONTableRef } from "./dbmlRefToJSONTableRef"; 9 | import { dbmlTableToJSONTableTable } from "./dbmlTableToJSONTableTable"; 10 | 11 | export const dbmlSchemaToJSONTableSchema = ({ 12 | refs, 13 | enums, 14 | tables, 15 | }: RawDatabase): JSONTableSchema => { 16 | const relationalFieldMap = createRelationalTalesMap(refs); 17 | const enumsMap = createEnumsSet(enums); 18 | 19 | const jsonTableRefs = refs.map(dbmlRefToJSONTableRef); 20 | const jsonTableEnums = enums.map(dbmlEnumToJSONTableEnum); 21 | const jsonTableTables = tables.map((table) => 22 | dbmlTableToJSONTableTable(table, relationalFieldMap, enumsMap), 23 | ); 24 | 25 | return { 26 | refs: jsonTableRefs, 27 | enums: jsonTableEnums, 28 | tables: jsonTableTables, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlTableToJSONTableTable.test.ts: -------------------------------------------------------------------------------- 1 | import { createEnumsSet } from "../createEnumsSet"; 2 | import { createRelationalTalesMap } from "../createRelationalTalesMap"; 3 | 4 | import { dbmlTableToJSONTableTable } from "./dbmlTableToJSONTableTable"; 5 | 6 | import { dbmlTestCodeInJSONTableFormat, parsedDBML } from "@/tests/data"; 7 | 8 | describe("transform dbml table to json table table", () => { 9 | test("transform table", () => { 10 | const enumSet = createEnumsSet(parsedDBML.enums); 11 | const relationalFieldMap = createRelationalTalesMap(parsedDBML.refs); 12 | expect( 13 | dbmlTableToJSONTableTable( 14 | parsedDBML.tables[0], 15 | relationalFieldMap, 16 | enumSet, 17 | ), 18 | ).toEqual(dbmlTestCodeInJSONTableFormat.tables[0]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/utils/transfomers/dbmlTableToJSONTableTable.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import { getTableFullName } from "../computeNameWithSchemaName"; 4 | 5 | import { dbmlFieldToJSONTableField } from "./dbmlFieldToJSONTableField"; 6 | import { dbmlIndexToJSONTableIndex } from "./dbmlIndexToJSONTableIndex"; 7 | 8 | import type Table from "@dbml/core/types/model_structure/table"; 9 | 10 | export const dbmlTableToJSONTableTable = ( 11 | table: Table, 12 | relationalFieldMap: Map, 13 | enumsMap: Set, 14 | ): JSONTableTable => { 15 | const { name, note, headerColor, fields, indexes } = table; 16 | 17 | return { 18 | name: getTableFullName(table), 19 | headerColor, 20 | // the note returned by the dbml parser is not string 21 | // but an object there is an typing error in their package 22 | note: (note as any)?.value, 23 | fields: fields.map((field) => 24 | dbmlFieldToJSONTableField({ 25 | field, 26 | enumsSet: enumsMap, 27 | relationalFieldMap, 28 | ownerTable: name, 29 | }), 30 | ), 31 | indexes: indexes.map(dbmlIndexToJSONTableIndex), 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/validators/index.ts: -------------------------------------------------------------------------------- 1 | import { type RawDatabase } from "@dbml/core/types/model_structure/database"; 2 | 3 | import { validateRefs } from "./validateRefs"; 4 | 5 | export const validateSchema = (schema: RawDatabase): void => { 6 | validateRefs(schema.refs, schema.tables); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/src/validators/validateRefs.ts: -------------------------------------------------------------------------------- 1 | import { TableNotExistError } from "../errors/TableNotExistError"; 2 | import { FieldNotExistsError } from "../errors/FieldNotExistsError"; 3 | import { 4 | computeNameWithSchemaName, 5 | getTableFullName, 6 | } from "../utils/computeNameWithSchemaName"; 7 | 8 | import type Endpoint from "@dbml/core/types/model_structure/endpoint"; 9 | import type Ref from "@dbml/core/types/model_structure/ref"; 10 | import type Table from "@dbml/core/types/model_structure/table"; 11 | 12 | export const validateRefs = (refs: Ref[], tables: Table[]): void => { 13 | const tableMap = new Map(); 14 | tables.forEach((table) => { 15 | const tableName = getTableFullName(table); 16 | tableMap.set(tableName, table); 17 | }); 18 | 19 | refs.forEach((ref) => { 20 | ref.endpoints.forEach((endpoint) => { 21 | const relatedTableFullName = computeNameWithSchemaName( 22 | endpoint.tableName, 23 | endpoint.schemaName, 24 | ); 25 | if (tableMap.has(relatedTableFullName)) { 26 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 27 | validateEndpoint(endpoint, tableMap.get(relatedTableFullName)!); 28 | return; 29 | } 30 | 31 | throw new TableNotExistError(relatedTableFullName); 32 | }); 33 | }); 34 | }; 35 | 36 | export const validateEndpoint = (endpoint: Endpoint, table: Table): void => { 37 | const endpointFieldName = endpoint.fieldNames[0]; 38 | const colExists = table.fields.some( 39 | (field) => endpointFieldName === field.name, 40 | ); 41 | if (!colExists) { 42 | throw new FieldNotExistsError(endpointFieldName, table.name); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /packages/dbml-to-json-table-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "baseUrl": "./src", 7 | "paths": { 8 | "@/*": ["./*"] 9 | }, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "skipLibCheck": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": [ 13 | "warn", 14 | { 15 | "selector": "import", 16 | "format": [ "camelCase", "PascalCase" ] 17 | } 18 | ], 19 | "@typescript-eslint/semi": "warn", 20 | "curly": "warn", 21 | "eqeqeq": "warn", 22 | "no-throw-literal": "warn", 23 | "semi": "off" 24 | }, 25 | "ignorePatterns": [ 26 | "out", 27 | "dist", 28 | "**/*.d.ts" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | *.vsix 16 | extension/** 17 | index.html 18 | vite.config.js -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "dbml-erd-visualizer" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [0.5.0] 8 | 9 | ### Added 10 | 11 | - Display diagnostic errors directly on code editor lines instead of displaying toast messages 12 | - Showing `not_null` label for not null columns 13 | 14 | ## [0.4.0] 15 | 16 | ### Added 17 | 18 | - Improve auto layout with dagrejs 19 | 20 | ## [0.3.4] 21 | 22 | ### Added 23 | 24 | - Added DBML logo as file icon for dbml file 25 | 26 | ### Fixed 27 | 28 | - Dependence with the `vscode-dbml` VS Code plugin 29 | 30 | ## [0.3.3] 31 | 32 | ### Fixed 33 | 34 | - Prevent table names from being truncated for long table name 35 | - Typo in preview tab name 36 | 37 | ## [0.3.2] 38 | 39 | ### Fixed 40 | 41 | - Remove `languages` section from the package.json 42 | 43 | ## [0.3.1] 44 | 45 | ### Fixed 46 | 47 | - Improved multi-schema code handling 48 | 49 | ## [0.3.0] 50 | 51 | ### Added 52 | 53 | - Ability to toggle table visualization mode. Display all columns, relational columns only or table headers only by [@tv-long](https://github.com/tv-long) 54 | 55 | ## [0.2.0] 56 | 57 | ### Added 58 | 59 | - Support table header customization via table settings in the dbml code by [@tv-long](https://github.com/tv-long) 60 | 61 | ## [0.1.0] 62 | 63 | ### Added 64 | 65 | - Make the table width fit the table content 66 | - Save and restore tables positions on exiting 67 | - Save and restore stage position on exiting 68 | 69 | ## [0.0.3] 70 | 71 | ### Added 72 | 73 | - Display an `empty content message` when there is no table in the schema 74 | - Enhance message for undefined schema 75 | 76 | ### Fixed 77 | 78 | - Remove mention of Prisma from plugin description 79 | 80 | ## [0.0.2] 81 | 82 | ### Added 83 | 84 | - Create diagram from DBML code 85 | - Add light and dark theme 86 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 DBSchemaVisualizer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE 22 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/README.md: -------------------------------------------------------------------------------- 1 | # DBML ERD Visualizer 2 | 3 | Allow to visualize the database schema in ERD ( Entity Relationship Diagram ) from .dbml file in your vscode. 4 | 5 | ## Features 6 | 7 | ![Demo](https://github.com/BOCOVO/db-schema-visualizer/assets/51182814/a59fd0c0-246d-4f00-be39-9885d88b8b85) 8 | 9 | - Create Entity Relationship Diagram from your dbml file 10 | - Allow you to drag diagrams 11 | - Support both light and dark themes 12 | - Multiple display mode. Display all columns, relational columns only or table headers only 13 | 14 | ## Extension Settings 15 | 16 | The following Visual Studio Code settings are available for the extension. 17 | 18 | - `dbmlERDPreviewer.preferredTheme`: This configuration define the theme to use. There are two different theme the `light` and `dark`. The default theme is `dark`. 19 | 20 | ## Release Notes 21 | 22 | Release notes are [here](./CHANGELOG.md) 23 | 24 | ## Author 25 | 26 | [@BOCOVO](https://github.com/BOCOVO) 27 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/assets/icons/open-preview-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/assets/icons/open-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/assets/icons/preview-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/assets/icons/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOCOVO/db-schema-visualizer/0769e64dd03bedc103fe9e5e82e3ec3eed5bd5ca/packages/dbml-vs-code-extension/assets/logo.png -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/extension/constants/index.ts: -------------------------------------------------------------------------------- 1 | // In the text below, you may notice a spelling 2 | // mistake: dblm instead of dbml. 3 | // But by changing this, users will lose their table 4 | // position if the plugin version is updated. 5 | // This constant is the identifier given to the webview 6 | // displaying the diagram, and since the positions of 7 | // tables and so on are stored in the localStorage, a 8 | // change to this ID will cause this data to be lost, 9 | // as if a brand new webview were created. 10 | export const WEB_VIEW_NAME = "dblm-preview-webview"; 11 | 12 | export const WEB_VIEW_TITLE = "DBML Diagram Preview"; 13 | export const EXTENSION_CONFIG_SESSION = "dbmlERDPreviewer"; 14 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/extension/index.ts: -------------------------------------------------------------------------------- 1 | import { commands, type ExtensionContext } from "vscode"; 2 | 3 | import { parseDBMLToJSON } from "dbml-to-json-table-schema"; 4 | 5 | import { MainPanel } from "extension-shared/extension/views/panel"; 6 | import { 7 | EXTENSION_CONFIG_SESSION, 8 | WEB_VIEW_NAME, 9 | WEB_VIEW_TITLE, 10 | } from "@/extension/constants"; 11 | 12 | export function activate(context: ExtensionContext): void { 13 | // Add command to the extension context 14 | context.subscriptions.push( 15 | commands.registerCommand( 16 | "dbml-erd-visualizer.previewDiagrams", 17 | async () => { 18 | lunchExtension(context); 19 | }, 20 | ), 21 | ); 22 | } 23 | 24 | const lunchExtension = (context: ExtensionContext): void => { 25 | MainPanel.render({ 26 | context, 27 | extensionConfigSession: EXTENSION_CONFIG_SESSION, 28 | webviewConfig: { 29 | name: WEB_VIEW_NAME, 30 | title: WEB_VIEW_TITLE, 31 | }, 32 | parser: parseDBMLToJSON, 33 | fileExt: "dbml", 34 | }); 35 | }; 36 | 37 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 38 | export function deactivate() {} 39 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createExtensionApp } from "extension-shared/src/index"; 2 | 3 | createExtensionApp(); 4 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "Node16", 5 | "target": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "./", 9 | "baseUrl": "./", 10 | "strict": true, 11 | "jsx": "react-jsx", 12 | "paths": { 13 | "@/*": ["./*"] 14 | }, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": ["node_modules", "vite.config.js"], 18 | "include": ["**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/dbml-vs-code-extension/vite.config.js: -------------------------------------------------------------------------------- 1 | import vscode from "@tomjs/vite-plugin-vscode"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [tsconfigPaths() , react(), vscode()], 9 | }); 10 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const DIAGRAM_UPDATER_DEBOUNCE_TIME = 500; 2 | export const WEBVIEW_HTML_MARKER_FOR_DEFAULT_CONFIG = "// <%DEFAULT-SCRIPT%>"; 3 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/helper/extensionConfigs.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "json-table-schema-visualizer/src/types/theme"; 2 | import { workspace, type WorkspaceConfiguration } from "vscode"; 3 | 4 | import { ConfigKeys } from "../types/configKeys"; 5 | import { type DefaultPageConfig } from "../types/defaultPageConfig"; 6 | 7 | export class ExtensionConfig { 8 | private readonly config: WorkspaceConfiguration; 9 | 10 | constructor(configSession: string) { 11 | const extensionConfigs = workspace.getConfiguration(configSession); 12 | this.config = extensionConfigs; 13 | } 14 | 15 | async setTheme(theme: Theme): Promise { 16 | try { 17 | await this.config.update(ConfigKeys.preferredTheme, theme); 18 | } catch (error) { 19 | console.error("Failed to set theme preference", error); 20 | } 21 | } 22 | 23 | getPreferredTheme(): Theme { 24 | const preferredTheme = this.config.get(ConfigKeys.preferredTheme); 25 | if (Theme.light === preferredTheme) { 26 | return preferredTheme; 27 | } 28 | 29 | return Theme.dark; 30 | } 31 | 32 | getDefaultPageConfig(): DefaultPageConfig { 33 | const theme = this.getPreferredTheme(); 34 | 35 | return { theme }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/types/configKeys.ts: -------------------------------------------------------------------------------- 1 | export enum ConfigKeys { 2 | preferredTheme = "preferredTheme", 3 | } 4 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/types/defaultPageConfig.ts: -------------------------------------------------------------------------------- 1 | import { type Theme } from "json-table-schema-visualizer/src/types/theme"; 2 | 3 | export interface DefaultPageConfig { 4 | theme: Theme; 5 | } 6 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/types/index.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableSchema } from "shared/types/tableSchema"; 2 | import { type ExtensionContext } from "vscode"; 3 | 4 | export interface WebviewConfig { 5 | name: string; 6 | title: string; 7 | } 8 | 9 | export interface ExtensionRenderProps { 10 | context: ExtensionContext; 11 | extensionConfigSession: string; 12 | webviewConfig: WebviewConfig; 13 | parser: (code: string) => JSONTableSchema; 14 | fileExt: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/types/webviewCommand.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableSchema } from "shared/types/tableSchema"; 2 | 3 | export enum WebviewCommand { 4 | SET_THEME_PREFERENCES = "SET_THEME_PREFERENCES", 5 | } 6 | 7 | export interface WebviewPostMessage { 8 | command: WebviewCommand; 9 | message: string; 10 | } 11 | 12 | export interface SetSchemaCommandPayload { 13 | type: string; 14 | payload: JSONTableSchema; 15 | key: string; 16 | } 17 | -------------------------------------------------------------------------------- /packages/extension-shared/extension/views/helper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-ts-expect-error */ 2 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 3 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 4 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 5 | 6 | import { type Disposable, type ExtensionContext, type Webview } from "vscode"; 7 | import { type Theme } from "json-table-schema-visualizer/src/types/theme"; 8 | 9 | import { 10 | WebviewCommand, 11 | type WebviewPostMessage, 12 | } from "../types/webviewCommand"; 13 | import { type DefaultPageConfig } from "../types/defaultPageConfig"; 14 | import { type ExtensionConfig } from "../helper/extensionConfigs"; 15 | import { WEBVIEW_HTML_MARKER_FOR_DEFAULT_CONFIG } from "../constants"; 16 | 17 | export class WebviewHelper { 18 | public static setupHtml( 19 | webview: Webview, 20 | context: ExtensionContext, 21 | defaultConfig: DefaultPageConfig, 22 | ): string { 23 | const html: string = process.env.VITE_DEV_SERVER_URL 24 | ? /* @ts-ignore */ 25 | __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) 26 | : /* @ts-ignore */ 27 | __getWebviewHtml__(webview, context); 28 | 29 | return WebviewHelper.injectDefaultConfig(html, defaultConfig); 30 | } 31 | 32 | public static injectDefaultConfig( 33 | html: string, 34 | configs: DefaultPageConfig, 35 | ): string { 36 | return html.replace( 37 | WEBVIEW_HTML_MARKER_FOR_DEFAULT_CONFIG, 38 | ` 39 | window.EXTENSION_DEFAULT_CONFIG = ${JSON.stringify(configs)}; 40 | `, 41 | ); 42 | } 43 | 44 | public static handleWebviewMessage( 45 | command: string, 46 | message: string, 47 | extensionConfig: ExtensionConfig, 48 | ): void { 49 | switch (command) { 50 | case WebviewCommand.SET_THEME_PREFERENCES: 51 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 52 | extensionConfig.setTheme(message as Theme); 53 | break; 54 | default: 55 | } 56 | } 57 | 58 | public static setupWebviewHooks( 59 | webview: Webview, 60 | extensionConfig: ExtensionConfig, 61 | disposables: Disposable[], 62 | ): void { 63 | webview.onDidReceiveMessage( 64 | (message: WebviewPostMessage) => { 65 | const command = message.command; 66 | const textMessage = message.message; 67 | console.log("Received message", command, textMessage); 68 | WebviewHelper.handleWebviewMessage( 69 | command, 70 | textMessage, 71 | extensionConfig, 72 | ); 73 | }, 74 | undefined, 75 | disposables, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/extension-shared/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { type DefaultPageConfig } from "@/extension/types/defaultPageConfig"; 2 | 3 | export interface WebviewApi { 4 | postMessage: (message: unknown) => void; 5 | getState: () => StateType | undefined; 6 | setState: (newState: T) => T; 7 | } 8 | 9 | declare global { 10 | interface Window { 11 | EXTENSION_DEFAULT_CONFIG?: DefaultPageConfig; 12 | vsCodeWebviewAPI: WebviewApi; 13 | } 14 | } 15 | 16 | declare module NodeJS { 17 | interface Global { 18 | __getWebviewHtml__: (webview: Webview, context: ExtensionContext) => string; 19 | __getWebviewHtml__: (url: string) => string; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/extension-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension-shared", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "engines": { 7 | "vscode": "^1.87.0" 8 | }, 9 | "dependencies": { 10 | "react": "18.2.0", 11 | "react-dom": "18.2.0" 12 | }, 13 | "devDependencies": { 14 | "@tomjs/vscode-extension-webview": "^1.2.0", 15 | "@types/node": "^20.12.12", 16 | "@types/vscode": "^1.89.0", 17 | "@vscode/test-cli": "^0.0.8", 18 | "@vscode/test-electron": "^2.3.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/extension-shared/src/App.tsx: -------------------------------------------------------------------------------- 1 | import DiagramViewer from "json-table-schema-visualizer/src/components/DiagramViewer/DiagramViewer"; 2 | import { useCreateTheme } from "json-table-schema-visualizer/src/hooks/theme"; 3 | import ThemeProvider from "json-table-schema-visualizer/src/providers/ThemeProvider"; 4 | import NoSchemaMessage from "json-table-schema-visualizer/src/components/Messages/NoSchemaMessage"; 5 | import { type Theme } from "json-table-schema-visualizer/src/types/theme"; 6 | 7 | import { 8 | WebviewCommand, 9 | type WebviewPostMessage, 10 | } from "../extension/types/webviewCommand"; 11 | 12 | import { useSchema } from "./hooks/schema"; 13 | 14 | const App = () => { 15 | const { setTheme, theme, themeColors } = useCreateTheme( 16 | window.EXTENSION_DEFAULT_CONFIG?.theme, 17 | ); 18 | const { schema, key } = useSchema(); 19 | 20 | if (schema === null) { 21 | return ; 22 | } 23 | 24 | // update the preference in the extension settings 25 | const saveThemePreference = (theme: Theme) => { 26 | setTheme(theme); 27 | const updateThemeMessage: WebviewPostMessage = { 28 | command: WebviewCommand.SET_THEME_PREFERENCES, 29 | message: theme, 30 | }; 31 | 32 | if (window.vsCodeWebviewAPI === undefined) { 33 | console.error( 34 | "can't send message to extension due vsCodeWebviewAPI global variable is not defined", 35 | ); 36 | } else { 37 | window.vsCodeWebviewAPI?.postMessage(updateThemeMessage); 38 | } 39 | }; 40 | 41 | return ( 42 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /packages/extension-shared/src/hooks/schema.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { type JSONTableSchema } from "shared/types/tableSchema"; 3 | import { tableCoordsStore } from "json-table-schema-visualizer/src/stores/tableCoords"; 4 | import { stageStateStore } from "json-table-schema-visualizer/src/stores/stagesState"; 5 | import { detailLevelStore } from "json-table-schema-visualizer/src/stores/detailLevelStore"; 6 | 7 | import { type SetSchemaCommandPayload } from "../../extension/types/webviewCommand"; 8 | 9 | export const useSchema = (): { 10 | schema: JSONTableSchema | null; 11 | key: string | null; 12 | } => { 13 | const [schema, setSchema] = useState(null); 14 | const [schemaKey, setSchemaKey] = useState(null); 15 | 16 | const updater = (e: MessageEvent): void => { 17 | const message = e.data as SetSchemaCommandPayload; 18 | if ( 19 | !(message.type === "setSchema" && typeof message.payload === "object") 20 | ) { 21 | return; 22 | } 23 | 24 | if (message.key !== schemaKey) { 25 | // update stores 26 | tableCoordsStore.switchTo( 27 | message.key, 28 | message.payload.tables, 29 | message.payload.refs, 30 | ); 31 | stageStateStore.switchTo(message.key); 32 | detailLevelStore.switchTo(message.key); 33 | 34 | setSchemaKey(message.key); 35 | } 36 | 37 | setSchema(message.payload); 38 | }; 39 | 40 | useEffect(() => { 41 | window.addEventListener("message", updater); 42 | 43 | return () => { 44 | window.removeEventListener("message", updater); 45 | // save current table position 46 | tableCoordsStore.saveCurrentStore(); 47 | }; 48 | }, []); 49 | 50 | return { schema, key: schemaKey }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/extension-shared/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import "@tomjs/vscode-extension-webview/client"; 3 | import { tableCoordsStore } from "json-table-schema-visualizer/src/stores/tableCoords"; 4 | 5 | import App from "./App"; 6 | 7 | export const createExtensionApp = () => { 8 | // save current table position when exiting the page 9 | window.addEventListener("unload", () => { 10 | tableCoordsStore.saveCurrentStore(); 11 | }); 12 | 13 | const View = () => { 14 | return ; 15 | }; 16 | 17 | const appWrapper = document.getElementById("app"); 18 | 19 | if (appWrapper !== null) { 20 | const root = createRoot(appWrapper); 21 | root.render(); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/extension-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "Node16", 5 | "target": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "./", 9 | "strict": true, 10 | "jsx": "react-jsx", 11 | "skipLibCheck": true 12 | }, 13 | "exclude": ["node_modules"], 14 | "include": ["**/*.ts", "**/*.tsx"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/.eslintignore: -------------------------------------------------------------------------------- 1 | *.config.js 2 | node_modules -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/.gitignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/.storybook/main.js: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path"; 2 | 3 | /** 4 | * This function is used to resolve the absolute path of a package. 5 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 6 | */ 7 | function getAbsolutePath(value) { 8 | return dirname(require.resolve(join(value, "package.json"))); 9 | } 10 | 11 | /** @type { import('@storybook/react-vite').StorybookConfig } */ 12 | const config = { 13 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 14 | addons: [ 15 | getAbsolutePath("@storybook/addon-onboarding"), 16 | getAbsolutePath("@storybook/addon-links"), 17 | getAbsolutePath("@storybook/addon-essentials"), 18 | getAbsolutePath("@chromatic-com/storybook"), 19 | getAbsolutePath("@storybook/addon-interactions"), 20 | ], 21 | framework: { 22 | name: getAbsolutePath("@storybook/react-vite"), 23 | options: {}, 24 | }, 25 | docs: { 26 | autodocs: "tag", 27 | }, 28 | }; 29 | export default config; 30 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/.storybook/preview.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Layer, Stage } from "react-konva"; 3 | import { darkThemeConfig } from "../src/constants/theme"; 4 | import ThemeProvider from "../src/providers/ThemeProvider"; 5 | import "../src/styles/index.css"; 6 | 7 | /** @type { import('@storybook/react').Preview } */ 8 | const preview = { 9 | parameters: { 10 | controls: { 11 | matchers: { 12 | color: /(background|color)$/i, 13 | date: /Date$/i, 14 | }, 15 | }, 16 | }, 17 | decorators: [ 18 | (Story, configs) => 19 | configs.parameters.withKonvaWrapper ? ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) : ( 28 | 29 | ), 30 | ], 31 | }; 32 | 33 | export default preview; 34 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/README.md: -------------------------------------------------------------------------------- 1 | # The diagram visual component 2 | 3 | This package holds UI components for diagram. 4 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require("ts-jest"); 2 | const { compilerOptions } = require("./tsconfig.json"); 3 | 4 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 5 | module.exports = { 6 | preset: "ts-jest", 7 | testEnvironment: "node", 8 | roots: ["./"], 9 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 10 | prefix: "/src", 11 | }), 12 | }; 13 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-table-schema-visualizer", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@dagrejs/dagre": "^1.1.2", 8 | "eventemitter3": "^5.0.1", 9 | "konva": "^9.3.6", 10 | "lucide-react": "^0.365.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-konva": "^18.2.10", 14 | "shared": "0.0.0" 15 | }, 16 | "devDependencies": { 17 | "@chromatic-com/storybook": "^1.2.25", 18 | "@storybook/addon-essentials": "^8.0.4", 19 | "@storybook/addon-interactions": "^8.0.4", 20 | "@storybook/addon-links": "^8.0.4", 21 | "@storybook/addon-onboarding": "^8.0.4", 22 | "@storybook/blocks": "^8.0.4", 23 | "@storybook/react": "^8.0.4", 24 | "@storybook/react-vite": "^8.0.4", 25 | "@storybook/test": "^8.0.4", 26 | "autoprefixer": "^10.4.19", 27 | "postcss": "^8.4.38", 28 | "prop-types": "^15.8.1", 29 | "storybook": "^8.0.4", 30 | "tailwindcss": "^3.4.3", 31 | "vite": "^5.2.6" 32 | }, 33 | "scripts": { 34 | "sb": "storybook dev -p 6006", 35 | "build-storybook": "storybook build", 36 | "test": "jest --collectCoverage" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Column/Column.stories.tsx: -------------------------------------------------------------------------------- 1 | import Column from "./Column"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import TablesInfoProvider from "@/providers/TablesInfoProvider"; 6 | import { exampleData } from "@/fake/fakeJsonTables"; 7 | 8 | const meta: Meta = { 9 | component: Column, 10 | title: "components/Column", 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const ColumnStory: Story = { 18 | args: { 19 | colName: "username", 20 | type: "varchar", 21 | }, 22 | parameters: { 23 | withKonvaWrapper: true, 24 | }, 25 | render: (props) => ( 26 | 27 | 28 | 29 | ), 30 | }; 31 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Column/ColumnWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode, useState } from "react"; 2 | import { Group, Rect } from "react-konva"; 3 | 4 | import { COLUMN_HEIGHT } from "@/constants/sizing"; 5 | import { useTablesInfo, useTableWidth } from "@/hooks/table"; 6 | import { shouldHighLightCol } from "@/utils/shouldHighLightCol"; 7 | 8 | interface ColumnWrapperProps { 9 | children: (highlighted: boolean) => ReactNode; 10 | offsetY?: number; 11 | tableName: string; 12 | relationalTables?: string[] | null; 13 | highlightColor: string; 14 | } 15 | 16 | const ColumnWrapper = ({ 17 | children, 18 | offsetY, 19 | tableName, 20 | relationalTables, 21 | highlightColor, 22 | }: ColumnWrapperProps) => { 23 | const { hoveredTableName } = useTablesInfo(); 24 | const [hovered, setHovered] = useState(false); 25 | const tablePreferredWidth = useTableWidth(); 26 | 27 | const handleOnHover = () => { 28 | setHovered(true); 29 | }; 30 | 31 | const handleOnLeave = () => { 32 | setHovered(false); 33 | }; 34 | 35 | const highlighted = shouldHighLightCol( 36 | hovered, 37 | tableName, 38 | hoveredTableName, 39 | relationalTables, 40 | ); 41 | 42 | return ( 43 | 44 | 49 | {children(highlighted)} 50 | 51 | ); 52 | }; 53 | 54 | export default ColumnWrapper; 55 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/DiagramViewer/Connections.tsx: -------------------------------------------------------------------------------- 1 | import { type JSONTableRef } from "shared/types/tableSchema"; 2 | 3 | import RelationConnection from "../RelationConnection/RelationConnection"; 4 | 5 | interface RelationsConnectionsProps { 6 | refs: JSONTableRef[]; 7 | } 8 | 9 | const RelationsConnections = ({ refs }: RelationsConnectionsProps) => { 10 | return refs.map((ref) => { 11 | const source = ref.endpoints[0]; 12 | const target = ref.endpoints[1]; 13 | 14 | const key = `${source.tableName}-${source.fieldNames[0]}-${target.tableName}-${target.fieldNames[0]}`; 15 | 16 | return ; 17 | }); 18 | }; 19 | 20 | export default RelationsConnections; 21 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/DiagramViewer/DiagramViewer.stories.tsx: -------------------------------------------------------------------------------- 1 | import DiagramViewer from "./DiagramViewer"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import { createBookingsTableClone, exampleData } from "@/fake/fakeJsonTables"; 6 | import { tableCoordsStore } from "@/stores/tableCoords"; 7 | 8 | const meta: Meta = { 9 | component: DiagramViewer, 10 | title: "components/DiagramViewer", 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | const tables = [ 18 | ...exampleData.tables, 19 | createBookingsTableClone("1"), 20 | createBookingsTableClone("2"), 21 | createBookingsTableClone("3"), 22 | createBookingsTableClone("4"), 23 | createBookingsTableClone("5"), 24 | createBookingsTableClone("6"), 25 | ]; 26 | 27 | export const DiagramViewerStory: Story = { 28 | render: (props) => , 29 | args: { 30 | tables, 31 | enums: exampleData.enums, 32 | refs: exampleData.refs, 33 | }, 34 | decorators: [ 35 | (Story) => { 36 | tableCoordsStore.resetPositions(tables, exampleData.refs); 37 | 38 | return ; 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/DiagramViewer/DiagramViewer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type JSONTableEnum, 3 | type JSONTableRef, 4 | type JSONTableTable, 5 | } from "shared/types/tableSchema"; 6 | 7 | import EmptyTableMessage from "../Messages/EmptyTableMessage"; 8 | 9 | import Tables from "./Tables"; 10 | import RelationsConnections from "./Connections"; 11 | import DiagramWrapper from "./DiagramWrapper"; 12 | 13 | import TablesPositionsProvider from "@/providers/TablesPositionsProvider"; 14 | import MainProviders from "@/providers/MainProviders"; 15 | import TableLevelDetailProvider from "@/providers/TableDetailLevelProvider"; 16 | 17 | interface DiagramViewerProps { 18 | tables: JSONTableTable[]; 19 | refs: JSONTableRef[]; 20 | enums: JSONTableEnum[]; 21 | } 22 | 23 | const DiagramViewer = ({ refs, tables, enums }: DiagramViewerProps) => { 24 | if (tables.length === 0) { 25 | return ; 26 | } 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default DiagramViewer; 44 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/DiagramViewer/DiagramWrapper.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "react-konva"; 2 | 3 | import DiagramWrapper from "./DiagramWrapper"; 4 | 5 | import type { Meta, StoryObj } from "@storybook/react"; 6 | 7 | import TablesPositionsProvider from "@/providers/TablesPositionsProvider"; 8 | 9 | const meta: Meta = { 10 | component: DiagramWrapper, 11 | title: "components/DiagramWrapper", 12 | }; 13 | 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const DiagramWrapperWrapper: Story = { 19 | render: (props) => , 20 | args: { 21 | children: ( 22 | 23 | ), 24 | }, 25 | decorators: [ 26 | (Story) => ( 27 | 28 | 29 | 30 | ), 31 | ], 32 | }; 33 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/DiagramViewer/Tables.tsx: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import TableWrapper from "../TableWrapper"; 4 | 5 | interface TablesProps { 6 | tables: JSONTableTable[]; 7 | } 8 | 9 | const Tables = ({ tables }: TablesProps) => { 10 | return tables.map((table) => ); 11 | }; 12 | 13 | export default Tables; 14 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/FieldDetails/EnumDetails.stories.tsx: -------------------------------------------------------------------------------- 1 | import EnumDetails from "./EnumDetails"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import EnumsProvider from "@/providers/EnumsProvider"; 6 | import { exampleData } from "@/fake/fakeJsonTables"; 7 | 8 | const meta: Meta = { 9 | component: EnumDetails, 10 | title: "components/EnumDetails", 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const EnumDetailsStory: Story = { 18 | render: (props) => ( 19 | 20 | 21 | 22 | ), 23 | args: { 24 | enumName: "status", 25 | }, 26 | parameters: { 27 | withKonvaWrapper: true, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/FieldDetails/EnumDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Group } from "react-konva"; 2 | 3 | import KonvaText from "../dumb/KonvaText"; 4 | 5 | import { useGetEnum } from "@/hooks/enums"; 6 | import { useThemeColors } from "@/hooks/theme"; 7 | import { computeTextSize } from "@/utils/computeTextSize"; 8 | import { PADDINGS } from "@/constants/sizing"; 9 | import { createEnumItemText } from "@/utils/createEnumItemText"; 10 | 11 | interface EnumDetailsProps { 12 | enumName: string; 13 | y?: number; 14 | } 15 | 16 | const enumTextSize = computeTextSize("Enum"); 17 | 18 | const EnumDetails = ({ enumName, y }: EnumDetailsProps) => { 19 | const enumObject = useGetEnum(enumName); 20 | const themeColors = useThemeColors(); 21 | 22 | if (enumObject === undefined) { 23 | return null; 24 | } 25 | 26 | const enumNameX = enumTextSize.width + PADDINGS.md; 27 | 28 | return ( 29 | 30 | 31 | 32 | 37 | 38 | {enumObject.values.map((item, index) => ( 39 | 45 | ))} 46 | 47 | ); 48 | }; 49 | 50 | export default EnumDetails; 51 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/FieldDetails/FieldDetailWrapper.stories.tsx: -------------------------------------------------------------------------------- 1 | import FieldDetailWrapper from "./FieldDetailWrapper"; 2 | import FieldDetails from "./FieldDetails"; 3 | 4 | import type { Meta, StoryObj } from "@storybook/react"; 5 | 6 | import EnumsProvider from "@/providers/EnumsProvider"; 7 | import { exampleData } from "@/fake/fakeJsonTables"; 8 | 9 | const meta: Meta = { 10 | component: FieldDetailWrapper, 11 | title: "components/FieldDetailWrapper", 12 | }; 13 | 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const FieldDetailWrapperStory: Story = { 19 | render: (props) => , 20 | args: { 21 | children: ( 22 | 23 | 27 | 28 | ), 29 | }, 30 | parameters: { 31 | withKonvaWrapper: true, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/FieldDetails/FieldDetailWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Line, Rect } from "react-konva"; 2 | import { useState, type ReactNode } from "react"; 3 | 4 | import { COLUMN_HEIGHT, PADDINGS } from "@/constants/sizing"; 5 | import { useThemeColors } from "@/hooks/theme"; 6 | import { computeCaretPoints } from "@/utils/computeCaretPoints"; 7 | import { useTableWidth } from "@/hooks/table"; 8 | 9 | interface FieldDetailWrapperProps { 10 | children: ReactNode; 11 | } 12 | 13 | const FieldDetailWrapper = ({ children }: FieldDetailWrapperProps) => { 14 | const [isDetailVisible, setIsDetailVisible] = useState(false); 15 | const themeColors = useThemeColors(); 16 | const tablePreferredWidth = useTableWidth(); 17 | const handleOnHover = () => { 18 | setIsDetailVisible(true); 19 | }; 20 | 21 | const handleOnLeave = () => { 22 | setIsDetailVisible(false); 23 | }; 24 | 25 | const popoverX = tablePreferredWidth; 26 | 27 | return ( 28 | 36 | 37 | 38 | {isDetailVisible ? ( 39 | 44 | ) : null} 45 | 46 | {isDetailVisible ? children : null} 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default FieldDetailWrapper; 53 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/FieldDetails/FieldDetails.stories.tsx: -------------------------------------------------------------------------------- 1 | import FieldDetails from "./FieldDetails"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import { exampleData } from "@/fake/fakeJsonTables"; 6 | import MainProviders from "@/providers/MainProviders"; 7 | 8 | const meta: Meta = { 9 | component: FieldDetails, 10 | title: "components/FieldDetails", 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const FieldDetailsStory: Story = { 18 | render: (props) => ( 19 | 20 | 21 | 22 | ), 23 | args: { 24 | enumName: "status", 25 | note: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", 26 | }, 27 | parameters: { 28 | withKonvaWrapper: true, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/FieldDetails/FieldDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Rect } from "react-konva"; 2 | 3 | import KonvaText from "../dumb/KonvaText"; 4 | 5 | import EnumDetails from "./EnumDetails"; 6 | import FieldDetailWrapper from "./FieldDetailWrapper"; 7 | 8 | import { useThemeColors } from "@/hooks/theme"; 9 | import { computeFieldDetailBoxDimension } from "@/utils/computeFieldDetailBoxDimension"; 10 | import { useGetEnum } from "@/hooks/enums"; 11 | import { PADDINGS } from "@/constants/sizing"; 12 | 13 | interface FieldDetailsProps { 14 | note?: string; 15 | enumName?: string; 16 | } 17 | 18 | const FieldDetails = ({ note, enumName = "" }: FieldDetailsProps) => { 19 | const themeColors = useThemeColors(); 20 | const enumObject = useGetEnum(enumName); 21 | 22 | const contentDimension = computeFieldDetailBoxDimension(note, enumObject); 23 | return ( 24 | 25 | 26 | 32 | 33 | 34 | 40 | 41 | {enumName !== undefined ? ( 42 | 43 | ) : null} 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default FieldDetails; 51 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Messages/EmptyTableMessage.stories.tsx: -------------------------------------------------------------------------------- 1 | import { type StoryObj, type Meta } from "@storybook/react"; 2 | 3 | import EmptyTableMessage from "./EmptyTableMessage"; 4 | 5 | const meta = { 6 | title: "components/Messages/EmptyTableMessage", 7 | component: EmptyTableMessage, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const EmptyTableMessageStory: Story = { 15 | render: () => , 16 | }; 17 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Messages/EmptyTableMessage.tsx: -------------------------------------------------------------------------------- 1 | import MessageWrapper from "./MessageWrapper"; 2 | 3 | const EmptyTableMessage = () => { 4 | return ( 5 | 6 |

No table found

7 |
8 | ); 9 | }; 10 | 11 | export default EmptyTableMessage; 12 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Messages/MessageWrapper.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from "react"; 2 | 3 | const MessageWrapper = ({ children }: PropsWithChildren) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | }; 10 | 11 | export default MessageWrapper; 12 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Messages/NoSchemaMessage.stories.tsx: -------------------------------------------------------------------------------- 1 | import { type StoryObj, type Meta } from "@storybook/react"; 2 | 3 | import NoSchemaMessage from "./NoSchemaMessage"; 4 | 5 | const meta = { 6 | title: "components/Messages/NoSchemaMessage", 7 | component: NoSchemaMessage, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const NoSchemaMessageStory: Story = { 15 | render: () => , 16 | }; 17 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Messages/NoSchemaMessage.tsx: -------------------------------------------------------------------------------- 1 | import MessageWrapper from "./MessageWrapper"; 2 | 3 | const NoSchemaMessage = () => { 4 | return ( 5 | 6 |

No schema found

7 |
8 | ); 9 | }; 10 | 11 | export default NoSchemaMessage; 12 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/RelationConnection/ConnectionPath.tsx: -------------------------------------------------------------------------------- 1 | import { Path } from "react-konva"; 2 | import { useState } from "react"; 3 | 4 | import { useThemeColors } from "@/hooks/theme"; 5 | import { useTablesInfo } from "@/hooks/table"; 6 | import { useTableColor } from "@/hooks/tableColor"; 7 | 8 | interface ConnectionPathProps { 9 | path: string; 10 | sourceTableName: string; 11 | targetTableName: string; 12 | relationOwner: string; 13 | } 14 | const ConnectionPath = ({ 15 | path, 16 | sourceTableName, 17 | targetTableName, 18 | relationOwner, 19 | }: ConnectionPathProps) => { 20 | const themeColors = useThemeColors(); 21 | const { hoveredTableName } = useTablesInfo(); 22 | const sourceTableColors = useTableColor(relationOwner); 23 | const [isHovered, setIsHovered] = useState(false); 24 | 25 | const highlight = 26 | hoveredTableName === sourceTableName || 27 | hoveredTableName === targetTableName || 28 | isHovered; 29 | 30 | const strokeColor = highlight 31 | ? sourceTableColors?.regular ?? themeColors.connection.active 32 | : themeColors.connection.default; 33 | 34 | const handleOnHover = () => { 35 | setIsHovered(true); 36 | }; 37 | 38 | const handleOnBlur = () => { 39 | setIsHovered(false); 40 | }; 41 | 42 | return ( 43 | 50 | ); 51 | }; 52 | 53 | export default ConnectionPath; 54 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/RelationConnection/RelationConnection.stories.tsx: -------------------------------------------------------------------------------- 1 | import Table from "../Table"; 2 | 3 | import RelationConnection from "./RelationConnection"; 4 | 5 | import type { Meta, StoryObj } from "@storybook/react"; 6 | 7 | import { exampleData } from "@/fake/fakeJsonTables"; 8 | import MainProviders from "@/providers/MainProviders"; 9 | import TablesPositionsProvider from "@/providers/TablesPositionsProvider"; 10 | 11 | const meta: Meta = { 12 | component: RelationConnection, 13 | title: "components/RelationConnection", 14 | }; 15 | 16 | export default meta; 17 | 18 | type Story = StoryObj; 19 | 20 | export const RelationConnectionStory: Story = { 21 | render: (props) => ( 22 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 | ), 37 | args: { 38 | source: exampleData.refs[0].endpoints[0], 39 | target: exampleData.refs[0].endpoints[1], 40 | }, 41 | parameters: { 42 | withKonvaWrapper: true, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/RelationConnection/RelationConnection.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import ConnectionPath from "./ConnectionPath"; 4 | 5 | import type { RelationItem } from "@/types/relation"; 6 | 7 | import { useRelationsCoords } from "@/hooks/relationConnection"; 8 | import { computeConnectionPathWithSymbols } from "@/utils/computeConnectionPaths"; 9 | 10 | interface RelationConnectionProps { 11 | source: RelationItem; 12 | target: RelationItem; 13 | } 14 | 15 | const RelationConnection = ({ source, target }: RelationConnectionProps) => { 16 | const { sourcePosition, sourceXY, targetPosition, targetXY } = 17 | useRelationsCoords(source, target); 18 | 19 | const { x: sourceX, y: sourceY } = sourceXY; 20 | const { x: targetX, y: targetY } = targetXY; 21 | 22 | const linePath = useMemo(() => { 23 | return computeConnectionPathWithSymbols({ 24 | targetXY, 25 | sourceXY, 26 | sourcePosition, 27 | targetPosition, 28 | relationSource: source.relation, 29 | relationTarget: target.relation, 30 | }); 31 | }, [sourcePosition, targetPosition, sourceX, targetX, sourceY, targetY]); 32 | 33 | const relationOwner = 34 | source.relation === "1" ? source.tableName : target.tableName; 35 | 36 | return ( 37 | <> 38 | 44 | 45 | ); 46 | }; 47 | 48 | export default RelationConnection; 49 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Table.stories.tsx: -------------------------------------------------------------------------------- 1 | import Table from "./Table"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import { exampleData } from "@/fake/fakeJsonTables"; 6 | import MainProviders from "@/providers/MainProviders"; 7 | import TablesPositionsProvider from "@/providers/TablesPositionsProvider"; 8 | 9 | const meta: Meta = { 10 | component: Table, 11 | title: "components/Table", 12 | }; 13 | 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const TableStory: Story = { 19 | render: (props) => ( 20 | 21 | 22 |
23 | 24 | 25 | ), 26 | args: { 27 | ...exampleData.tables[0], 28 | }, 29 | parameters: { 30 | withKonvaWrapper: true, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/TableHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import TableHeader from "./TableHeader"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | const meta: Meta = { 6 | component: TableHeader, 7 | title: "components/TableHeader", 8 | }; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const TableHeaderStory: Story = { 15 | render: (props) => , 16 | args: { 17 | title: "users", 18 | }, 19 | parameters: { 20 | withKonvaWrapper: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/TableHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Rect } from "react-konva"; 2 | 3 | import KonvaText from "./dumb/KonvaText"; 4 | 5 | import { 6 | COLUMN_HEIGHT, 7 | FONT_SIZES, 8 | PADDINGS, 9 | TABLE_COLOR_HEIGHT, 10 | } from "@/constants/sizing"; 11 | import { useThemeColors } from "@/hooks/theme"; 12 | import { useTableColor } from "@/hooks/tableColor"; 13 | import { useTableWidth } from "@/hooks/table"; 14 | 15 | interface TableHeaderProps { 16 | title: string; 17 | } 18 | 19 | const TableHeader = ({ title }: TableHeaderProps) => { 20 | const themeColors = useThemeColors(); 21 | const tableColors = useTableColor(title); 22 | const tablePreferredWidth = useTableWidth(); 23 | const tableMarkerColor = tableColors?.regular ?? "red"; 24 | 25 | return ( 26 | 27 | 33 | 34 | 40 | 41 | 52 | 53 | ); 54 | }; 55 | 56 | export default TableHeader; 57 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/TableWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import Table from "./Table"; 4 | 5 | import { useGetTableMinWidth } from "@/hooks/table"; 6 | import TableDimensionProvider from "@/providers/TableDimension"; 7 | 8 | interface TableWrapperProps { 9 | table: JSONTableTable; 10 | } 11 | 12 | const TableWrapper = ({ table }: TableWrapperProps) => { 13 | const width = useGetTableMinWidth(table); 14 | 15 | return ( 16 | 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default TableWrapper; 23 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/AutoArrage/AutoArrangeTables.stories.tsx: -------------------------------------------------------------------------------- 1 | import AutoArrangeTableButton from "./AutoArrangeTables"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import TablesPositionsProvider from "@/providers/TablesPositionsProvider"; 6 | 7 | const meta: Meta = { 8 | component: AutoArrangeTableButton, 9 | title: "components/Toolbar/AutoArrangeTableButton", 10 | }; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj; 15 | 16 | export const AutoArrangeTableButtonStory: Story = { 17 | render: () => , 18 | decorators: [ 19 | (Story) => ( 20 | 21 | 22 | 23 | ), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/AutoArrage/AutoArrangeTables.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutPanelLeftIcon } from "lucide-react"; 2 | 3 | import ToolbarButton from "../Button"; 4 | 5 | import { useTablePositionContext } from "@/hooks/table"; 6 | 7 | const AutoArrangeTableButton = () => { 8 | const { resetPositions } = useTablePositionContext(); 9 | 10 | return ( 11 | 12 | 13 | 14 | Auto-arrange 15 | 16 | ); 17 | }; 18 | 19 | export default AutoArrangeTableButton; 20 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/Button.tsx: -------------------------------------------------------------------------------- 1 | import { type ButtonHTMLAttributes } from "react"; 2 | 3 | interface ToolbarButtonProps extends ButtonHTMLAttributes { 4 | onClick: () => void; 5 | title: string; 6 | } 7 | 8 | const ToolbarButton = ({ 9 | onClick, 10 | title, 11 | children, 12 | className = "", 13 | ...props 14 | }: ToolbarButtonProps) => { 15 | return ( 16 | 24 | ); 25 | }; 26 | 27 | export default ToolbarButton; 28 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/DetailLevelToggle/DetailLevelToggle.stories.tsx: -------------------------------------------------------------------------------- 1 | import DetailLevelToggle from "./DetailLevelToggle"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import TableDetailLevelProvider from "@/providers/TableDetailLevelProvider"; 6 | 7 | const meta: Meta = { 8 | component: DetailLevelToggle, 9 | title: "components/Toolbar/DetailLevelToggle", 10 | }; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj; 15 | 16 | export const DetailLevelToggleStory: Story = { 17 | render: () => , 18 | decorators: [ 19 | (Story) => ( 20 | 21 | 22 | 23 | ), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/DetailLevelToggle/DetailLevelToggle.tsx: -------------------------------------------------------------------------------- 1 | import { PanelsTopLeftIcon, PanelTopIcon, KeyRoundIcon } from "lucide-react"; 2 | import { useMemo } from "react"; 3 | 4 | import ToolbarButton from "../Button"; 5 | 6 | import { useTableDetailLevel } from "@/hooks/tableDetailLevel"; 7 | import { TableDetailLevel } from "@/types/tableDetailLevel"; 8 | 9 | interface DetailLevelToggleProps { 10 | onClick: () => void; 11 | } 12 | 13 | const FullDetailLevel = ({ onClick }: DetailLevelToggleProps) => { 14 | return ( 15 | 16 | 17 | 18 | Full Details 19 | 20 | ); 21 | }; 22 | const HeaderOnlyLevel = ({ onClick }: DetailLevelToggleProps) => { 23 | return ( 24 | 25 | 26 | 27 | Header Only 28 | 29 | ); 30 | }; 31 | const KeyOnlyLevel = ({ onClick }: DetailLevelToggleProps) => { 32 | return ( 33 | 34 | 35 | 36 | Key Only 37 | 38 | ); 39 | }; 40 | 41 | const COMPONENT_MAP = { 42 | [TableDetailLevel.FullDetails]: FullDetailLevel, 43 | [TableDetailLevel.HeaderOnly]: HeaderOnlyLevel, 44 | [TableDetailLevel.KeyOnly]: KeyOnlyLevel, 45 | }; 46 | 47 | const DetailLevelToggle = () => { 48 | const { detailLevel, next } = useTableDetailLevel(); 49 | const Component = useMemo(() => COMPONENT_MAP[detailLevel], [detailLevel]); 50 | 51 | return ; 52 | }; 53 | 54 | export default DetailLevelToggle; 55 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/FitToView/FitToView.tsx: -------------------------------------------------------------------------------- 1 | import { ExpandIcon } from "lucide-react"; 2 | 3 | import ToolbarButton from "../Button"; 4 | 5 | interface FitToViewButtonProps { 6 | onClick: () => void; 7 | } 8 | 9 | const FitToViewButton = ({ onClick } : FitToViewButtonProps) => { 10 | return ( 11 | 12 | 13 | Fit To View 14 | 15 | ); 16 | }; 17 | 18 | export default FitToViewButton; 19 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/ThemeToggler/ThemeToggler.stories.tsx: -------------------------------------------------------------------------------- 1 | import ThemeToggler from "./ThemeToggler"; 2 | 3 | import type { Meta, StoryObj } from "@storybook/react"; 4 | 5 | import { Theme } from "@/types/theme"; 6 | 7 | const meta: Meta = { 8 | component: ThemeToggler, 9 | title: "components/Toolbar/ThemeToggler", 10 | }; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj; 15 | 16 | export const ThemeTogglerStory: Story = { 17 | render: () => ( 18 |
19 | 20 |
21 | ), 22 | args: { theme: Theme.dark }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/ThemeToggler/ThemeToggler.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | 3 | import ToolbarButton from "../Button"; 4 | 5 | import { Theme } from "@/types/theme"; 6 | import { useThemeContext } from "@/hooks/theme"; 7 | 8 | const ThemeToggler = () => { 9 | const { setTheme, theme } = useThemeContext(); 10 | 11 | const handleThemeToggle = () => { 12 | setTheme(Theme.light === theme ? Theme.dark : Theme.light); 13 | }; 14 | 15 | return ( 16 | 21 |
22 | 23 | 24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default ThemeToggler; 31 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/Toolbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { type Meta, type StoryObj } from "@storybook/react"; 2 | 3 | import Toolbar from "./Toolbar"; 4 | 5 | import TablesPositionsProvider from "@/providers/TablesPositionsProvider"; 6 | 7 | const meta: Meta = { 8 | component: Toolbar, 9 | title: "components/Toolbar", 10 | }; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj; 15 | 16 | export const ToolbarStory: Story = { 17 | render: () => , 18 | decorators: [ 19 | (Story) => ( 20 |
21 | 22 | 23 | 24 |
25 | ), 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/Toolbar/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | import AutoArrangeTableButton from "./AutoArrage/AutoArrangeTables"; 4 | import ThemeToggler from "./ThemeToggler/ThemeToggler"; 5 | import DetailLevelToggle from "./DetailLevelToggle/DetailLevelToggle"; 6 | import FitToViewButton from "./FitToView/FitToView"; 7 | 8 | const Toolbar = ({ onFitToView }: { onFitToView: () => void }) => { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | Toolbar.propTypes = { 21 | onFitToView: PropTypes.func.isRequired, 22 | }; 23 | 24 | export default Toolbar; 25 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/components/dumb/KonvaText.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "react-konva"; 2 | 3 | import type Konva from "konva"; 4 | 5 | import { FONT_FAMILY } from "@/constants/font"; 6 | import { FONT_SIZES } from "@/constants/sizing"; 7 | 8 | const KonvaText = (props: Konva.TextConfig) => { 9 | return ; 10 | }; 11 | 12 | export default KonvaText; 13 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/constants/colors.ts: -------------------------------------------------------------------------------- 1 | export const tableColors = [ 2 | { 3 | regular: "#6366f1", 4 | lighter: "#e4e6ff", 5 | }, 6 | { 7 | regular: "#8b5cf6", 8 | lighter: "#f0f1ff", 9 | }, 10 | { 11 | regular: "#a855f7", 12 | lighter: "#f8f7ff", 13 | }, 14 | { 15 | regular: "#d946ef", 16 | lighter: "#fdfbff", 17 | }, 18 | { 19 | regular: "#ec4899", 20 | lighter: "#ffe6f0", 21 | }, 22 | { 23 | regular: "#f43f5e", 24 | lighter: "#ffebec", 25 | }, 26 | { 27 | regular: "#ef4444", 28 | lighter: "#ffefec", 29 | }, 30 | { 31 | regular: "#f97316", 32 | lighter: "#fff8f5", 33 | }, 34 | { 35 | regular: "#f59e0b", 36 | lighter: "#fffaf1", 37 | }, 38 | { 39 | regular: "#eab308", 40 | lighter: "#fffcea", 41 | }, 42 | { 43 | regular: "#84cc16", 44 | lighter: "#f6ffdf", 45 | }, 46 | { 47 | regular: "#22c55e", 48 | lighter: "#eaffe6", 49 | }, 50 | { 51 | regular: "#10b981", 52 | lighter: "#e1ffed", 53 | }, 54 | { 55 | regular: "#14b8a6", 56 | lighter: "#eaffe9", 57 | }, 58 | { 59 | regular: "#06b6d4", 60 | lighter: "#e7f8ff", 61 | }, 62 | { 63 | regular: "#0ea5e9", 64 | lighter: "#edf7ff", 65 | }, 66 | { 67 | regular: "#3b82f6", 68 | lighter: "#f7faff", 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/constants/font.ts: -------------------------------------------------------------------------------- 1 | export const FONT_FAMILY = "sans-serif"; 2 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/constants/sizing.ts: -------------------------------------------------------------------------------- 1 | export const TABLE_DEFAULT_MIN_WIDTH = 150; 2 | export const TABLE_COLOR_HEIGHT = 6; 3 | export const TABLE_LINE_HEIGHT = 25; 4 | export const COLUMN_HEIGHT = 30; 5 | export const TABLE_HEADER_HEIGHT = COLUMN_HEIGHT + TABLE_COLOR_HEIGHT; 6 | export const CONNECTION_STROKE = 2; 7 | export const DEFAULT_PADDING = 5; 8 | export const CROSS_CONNECTION_MIN_MARGIN = 20; 9 | export const CONNECTION_MARGIN = 40; 10 | export const COLS_OFFSET_Y_TO_COL_MIDDLE = 11 | TABLE_COLOR_HEIGHT + 12 | COLUMN_HEIGHT + 13 | COLUMN_HEIGHT / 2; /* to point to cols middle */ 14 | export const CONNECTION_HANDLE_OFFSET = 20; 15 | export const PADDINGS = { 16 | xs: 5, 17 | sm: 8, 18 | md: 10, 19 | lg: 20, 20 | }; 21 | export const TABLE_FIELD_TYPE_PADDING = PADDINGS.sm; 22 | 23 | export const FONT_SIZES = { 24 | md: 15, 25 | lg: 18, 26 | tableTitle: 18, 27 | }; 28 | 29 | export const FIELD_DETAILS_CARET = { 30 | w: 5, 31 | h: 5, 32 | }; 33 | 34 | export const FIELD_DETAILS_TOOLTIPS_W = 200; 35 | 36 | export const TABLES_GAP_X = 50; 37 | export const TABLES_GAP_Y = 50; 38 | export const DIAGRAM_PADDING = 60; 39 | export const CONNECTION_RELATION_SYMBOL_OFFSET = 8; 40 | 41 | export const STAGE_SCALE_FACTOR = 0.75; 42 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/constants/tableCoords.ts: -------------------------------------------------------------------------------- 1 | // For some reason, if the position of a table could not be restored or not computed, 2 | // this const is used to keep the same reference during rendering to avoid 3 | // unnecessary rendering created by the useTableDefaultPosition hook 4 | export const defaultTableCoord = { x: 0, y: 0 }; 5 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/constants/theme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeColors } from "@/types/theme"; 2 | 3 | export const defaultThemeConfig: ThemeColors = { 4 | text: { 5 | "900": "#636363", 6 | "700": "#9C9C9C", 7 | }, 8 | connection: { 9 | active: "#029CFD", 10 | default: "#888", 11 | }, 12 | colAccent: "aliceblue", 13 | table: { 14 | bg: "white", 15 | shadow: "black", 16 | }, 17 | tableHeader: { 18 | bg: "#F8FAFC", 19 | fg: "black", 20 | }, 21 | red: "red", 22 | green: "#00FF00", 23 | enumItem: "#ccc", 24 | white: "white", 25 | noteBg: "#000000d6", 26 | bg: "white", 27 | }; 28 | 29 | export const darkThemeConfig: ThemeColors = { 30 | text: { 31 | "900": "#E6E6E6", 32 | "700": "#B3B3B3", 33 | }, 34 | connection: { 35 | active: "#00BFFF", 36 | default: "#888888", 37 | }, 38 | colAccent: "#333333", 39 | table: { 40 | bg: "#2F2F2F", 41 | shadow: "rgba(255, 255, 255, 0.1)", 42 | }, 43 | tableHeader: { 44 | bg: "#1E1E1E", 45 | fg: "#CCCCCC", 46 | }, 47 | red: "#FF6347", 48 | green: "#32CD32", 49 | enumItem: "#999999", 50 | white: "#FFFFFF", 51 | noteBg: "#3F3F3F", 52 | bg: "#1A1A1A", 53 | }; 54 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/events-emitter/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | const eventEmitter = new EventEmitter(); 3 | 4 | export default eventEmitter; 5 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/cursor.ts: -------------------------------------------------------------------------------- 1 | interface UseCursorChangerReturnValue { 2 | onChange: () => void; 3 | onRestore: () => void; 4 | } 5 | 6 | export const useCursorChanger = ( 7 | cursor: string, 8 | ): UseCursorChangerReturnValue => { 9 | return { 10 | onChange: () => { 11 | document.body.style.cursor = cursor; 12 | }, 13 | onRestore: () => { 14 | document.body.style.cursor = "default"; 15 | }, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/enums.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { type JSONTableEnum } from "shared/types/tableSchema"; 3 | 4 | import { EnumsContext } from "@/providers/EnumsProvider"; 5 | import { type EnumsContextValue } from "@/types/enums"; 6 | 7 | export const useEnums = (): EnumsContextValue => { 8 | const value = useContext(EnumsContext); 9 | 10 | if (value == null) { 11 | throw new Error("useEnums must be used within an EnumsProvider"); 12 | } 13 | 14 | return value; 15 | }; 16 | 17 | export const useGetEnum = (enumName: string): JSONTableEnum | undefined => { 18 | const value = useEnums(); 19 | 20 | return value.enums.find((en) => en.name === enumName); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/stage.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import { useWindowSize } from "./window"; 4 | 5 | import { DIAGRAM_PADDING, STAGE_SCALE_FACTOR } from "@/constants/sizing"; 6 | import { type StageState } from "@/types/stage"; 7 | import { stageStateStore } from "@/stores/stagesState"; 8 | import { tableCoordsStore } from "@/stores/tableCoords"; 9 | 10 | export const useStageStartingState = (): StageState => { 11 | const { height: windowHeight, width: windowWidth } = useWindowSize(); 12 | 13 | const state = useMemo(() => { 14 | const savedStageState = stageStateStore.getCurrentStageState(); 15 | if (savedStageState !== null) { 16 | return savedStageState; 17 | } 18 | 19 | let maxX = 0; 20 | let maxY = 0; 21 | let minX = 0; 22 | let minY = 0; 23 | 24 | tableCoordsStore.getCurrentStore().forEach(({ x, y }) => { 25 | maxX = Math.max(maxX, x); 26 | maxY = Math.max(maxY, y); 27 | minX = Math.min(minX, x); 28 | minY = Math.min(minY, y); 29 | }); 30 | 31 | const contentW = maxX - minX; 32 | const contentH = maxY - minY; 33 | const scaleX = (windowWidth - DIAGRAM_PADDING) / contentW; 34 | const scaleY = (windowHeight - DIAGRAM_PADDING) / contentH; 35 | const scale = 36 | Math.min(scaleX, scaleY, 1 /* scale must not ne higher than 1 */) * 37 | STAGE_SCALE_FACTOR; 38 | 39 | const scaledW = contentW * scale; 40 | const scaledH = contentH * scale; 41 | 42 | const centerPositionX = -minX * scale + (windowWidth - scaledW) / 4; 43 | const centerPositionY = -minY * scale + (windowHeight - scaledH) / 4; 44 | 45 | const state = { 46 | scale, 47 | position: { x: centerPositionX, y: centerPositionY }, 48 | }; 49 | 50 | return state; 51 | }, []); 52 | 53 | return state; 54 | }; 55 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/table.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo, useSyncExternalStore } from "react"; 2 | import { type JSONTableTable } from "shared/types/tableSchema"; 3 | 4 | import type { TablesInfoProviderValue } from "@/types/tablesInfoProviderValue"; 5 | import type { XYPosition } from "@/types/positions"; 6 | 7 | import { TablesInfoContext } from "@/providers/TablesInfoProvider"; 8 | import { TablesPositionsContext } from "@/providers/TablesPositionsProvider"; 9 | import { TableDimensionContext } from "@/providers/TableDimension"; 10 | import { TABLE_DEFAULT_MIN_WIDTH } from "@/constants/sizing"; 11 | import { getTableLinesText } from "@/utils/tableWComputation/getTableLinesText"; 12 | import { computeTablePreferredWidth } from "@/utils/tableWComputation/computeTablePreferredWidth"; 13 | import { tableWidthStore } from "@/stores/tableWidth"; 14 | import { type TablesPositionsContextValue } from "@/types/dimension"; 15 | import { tableCoordsStore } from "@/stores/tableCoords"; 16 | 17 | export const useTablesInfo = (): TablesInfoProviderValue => { 18 | const tablesInfo = useContext(TablesInfoContext); 19 | 20 | if (tablesInfo == null) { 21 | throw new Error("useTablesInfo must be used within a TableInfoProvider"); 22 | } 23 | 24 | return tablesInfo; 25 | }; 26 | 27 | export const useTablePositionContext = (): TablesPositionsContextValue => { 28 | const tablesPositionsMap = useContext(TablesPositionsContext); 29 | 30 | if (tablesPositionsMap == null) { 31 | throw new Error( 32 | "useTablePosition must be used within the TablesPositionsContext", 33 | ); 34 | } 35 | 36 | return tablesPositionsMap; 37 | }; 38 | 39 | export const useTableDefaultPosition = (tableName: string): XYPosition => { 40 | const tablesPositions = useSyncExternalStore( 41 | (callback) => { 42 | return tableCoordsStore.subscribeToReset(callback); 43 | }, 44 | () => { 45 | return tableCoordsStore.getCoords(tableName); 46 | }, 47 | ); 48 | 49 | return tablesPositions; 50 | }; 51 | 52 | export const useGetTableMinWidth = (table: JSONTableTable): number => { 53 | const tableLinesTexts = getTableLinesText(table.fields); 54 | const minWidth = useMemo(() => { 55 | const minW = computeTablePreferredWidth(tableLinesTexts, table.name); 56 | tableWidthStore.setWidth(table.name, minW); 57 | return minW; 58 | }, [tableLinesTexts]); 59 | 60 | return minWidth; 61 | }; 62 | 63 | export const useTableWidth = (): number => { 64 | const contextValue = useContext(TableDimensionContext); 65 | 66 | return contextValue?.width ?? TABLE_DEFAULT_MIN_WIDTH; 67 | }; 68 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/tableColor.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import type { TableColors } from "@/types/tableColor"; 4 | 5 | import { TablesColorContext } from "@/providers/TablesColorProvider"; 6 | 7 | export const useTableColor = (table: string): TableColors | undefined => { 8 | const contextValue = useContext(TablesColorContext); 9 | 10 | if (contextValue == null) { 11 | throw new Error( 12 | "it seem you forgot to wrap your app with TablesColorContext", 13 | ); 14 | } 15 | 16 | return contextValue.tableColors.get(table); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/tableDetailLevel.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { TableDetailLevelContext } from "@/providers/TableDetailLevelProvider"; 4 | import { type TableDetailLevel } from "@/types/tableDetailLevel"; 5 | 6 | export const useTableDetailLevel = (): { 7 | detailLevel: TableDetailLevel; 8 | next: () => void; 9 | } => { 10 | const contextValue = useContext(TableDetailLevelContext); 11 | if (contextValue === undefined) { 12 | throw new Error( 13 | "it seem you forgot to wrap your app with TableDetailLevelProvider", 14 | ); 15 | } 16 | return contextValue; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/tableWidthStore.ts: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from "react"; 2 | 3 | import { tableWidthStore } from "@/stores/tableWidth"; 4 | 5 | export const useTableWidthStoredValue = (tableName: string): number => { 6 | const registerSubscriber = (callback: (w: number) => void): (() => void) => { 7 | tableWidthStore.subscribe(tableName, callback); 8 | 9 | // return function that unsubscribe 10 | return () => { 11 | tableWidthStore.unSubscribe(tableName, callback); 12 | }; 13 | }; 14 | 15 | const getCurrentValue = (): number => 16 | tableWidthStore.getWidth(tableName) ?? 0; 17 | 18 | const width = useSyncExternalStore(registerSubscriber, getCurrentValue); 19 | 20 | return width; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/theme.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | 3 | import { 4 | Theme, 5 | type ThemeColors, 6 | type ThemeProviderValue, 7 | } from "@/types/theme"; 8 | import { ThemeContext } from "@/providers/ThemeProvider"; 9 | import { darkThemeConfig, defaultThemeConfig } from "@/constants/theme"; 10 | 11 | export const useThemeContext = (): ThemeProviderValue => { 12 | const contextValue = useContext(ThemeContext); 13 | if (contextValue === undefined) { 14 | throw new Error("it seem you forgot to wrap your app with ThemeProvider"); 15 | } 16 | 17 | return contextValue; 18 | }; 19 | 20 | export const useThemeColors = (): ThemeColors => { 21 | const contextValue = useThemeContext(); 22 | 23 | return contextValue.themeColors; 24 | }; 25 | 26 | export const useCreateTheme = ( 27 | defaultTheme: Theme = Theme.dark, 28 | ): ThemeProviderValue => { 29 | const [theme, setTheme] = useState(defaultTheme); 30 | 31 | const themeColors = 32 | theme === Theme.dark ? darkThemeConfig : defaultThemeConfig; 33 | 34 | return { setTheme, theme, themeColors }; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/hooks/window.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from "react"; 2 | 3 | import { type Dimension } from "@/types/dimension"; 4 | 5 | export function useWindowSize(): Dimension { 6 | const [size, setSize] = useState({ 7 | width: window.innerWidth, 8 | height: window.innerHeight, 9 | }); 10 | 11 | useLayoutEffect(() => { 12 | const handleResize = (): void => { 13 | setSize({ 14 | width: window.innerWidth, 15 | height: window.innerHeight, 16 | }); 17 | }; 18 | 19 | handleResize(); 20 | window.addEventListener("resize", handleResize); 21 | 22 | return () => { 23 | window.removeEventListener("resize", handleResize); 24 | }; 25 | }, []); 26 | return size; 27 | } 28 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/EnumsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, type ReactNode } from "react"; 2 | import { type JSONTableEnum } from "shared/types/tableSchema"; 3 | 4 | import { type EnumsContextValue } from "@/types/enums"; 5 | 6 | export const EnumsContext = createContext(null); 7 | 8 | interface EnumsProviderProps { 9 | children: ReactNode; 10 | enums: JSONTableEnum[]; 11 | } 12 | const EnumsProvider = ({ children, enums }: EnumsProviderProps) => { 13 | return ( 14 | {children} 15 | ); 16 | }; 17 | 18 | export default EnumsProvider; 19 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/MainProviders.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type JSONTableEnum, 3 | type JSONTableTable, 4 | } from "shared/types/tableSchema"; 5 | import { type ReactNode } from "react"; 6 | 7 | import TablesInfoProvider from "./TablesInfoProvider"; 8 | import EnumsProvider from "./EnumsProvider"; 9 | import TablesColorProvider from "./TablesColorProvider"; 10 | 11 | interface MainProvidersProps { 12 | tables: JSONTableTable[]; 13 | enums: JSONTableEnum[]; 14 | children: ReactNode; 15 | } 16 | const MainProviders = ({ enums, tables, children }: MainProvidersProps) => { 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default MainProviders; 27 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/TableDetailLevelProvider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useCallback, 4 | useEffect, 5 | useState, 6 | type ReactNode, 7 | } from "react"; 8 | 9 | import { 10 | type TableDetailLevelValue, 11 | TableDetailLevel, 12 | } from "@/types/tableDetailLevel"; 13 | import { detailLevelStore } from "@/stores/detailLevelStore"; 14 | 15 | export const TableDetailLevelContext = createContext({ 16 | detailLevel: TableDetailLevel.FullDetails, 17 | next() {}, 18 | }); 19 | 20 | interface TableLevelDetailProviderProps { 21 | children: ReactNode; 22 | level?: TableDetailLevel; 23 | } 24 | 25 | const TableLevelDetailProvider = ({ 26 | children, 27 | }: TableLevelDetailProviderProps) => { 28 | const [state, setState] = useState(() => 29 | detailLevelStore.getCurrentDetailLevel(), 30 | ); 31 | useEffect(() => { 32 | if (state !== detailLevelStore.getCurrentDetailLevel()) { 33 | detailLevelStore.set(state); 34 | detailLevelStore.saveCurrentState(); 35 | } 36 | }, [state]); 37 | const next = useCallback((): void => { 38 | if (state === TableDetailLevel.FullDetails) { 39 | setState(TableDetailLevel.HeaderOnly); 40 | } else if (state === TableDetailLevel.HeaderOnly) { 41 | setState(TableDetailLevel.KeyOnly); 42 | } else { 43 | setState(TableDetailLevel.FullDetails); 44 | } 45 | }, [state, setState]); 46 | return ( 47 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | export default TableLevelDetailProvider; 54 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/TableDimension.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useMemo, type ReactNode } from "react"; 2 | 3 | import type { TableDimensionProviderValue } from "@/types/table"; 4 | 5 | export const TableDimensionContext = createContext< 6 | TableDimensionProviderValue | undefined 7 | >(undefined); 8 | 9 | interface TablesInfoProviderProps { 10 | width: number; 11 | children: ReactNode; 12 | } 13 | 14 | // only store the width of the table at this time 15 | const TableDimensionProvider = ({ 16 | children, 17 | width, 18 | }: TablesInfoProviderProps) => { 19 | const contextValue = useMemo(() => { 20 | return { width }; 21 | }, [width]); 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default TableDimensionProvider; 31 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/TablesColorProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useMemo, type ReactNode } from "react"; 2 | import { type JSONTableTable } from "shared/types/tableSchema"; 3 | 4 | import type { TableColorContextValue } from "@/types/tableColor"; 5 | 6 | import { createTablesColorMap } from "@/utils/createTablesColorMap"; 7 | 8 | export const TablesColorContext = createContext( 9 | null, 10 | ); 11 | 12 | interface TablesColorProviderProps { 13 | children: ReactNode; 14 | tables: JSONTableTable[]; 15 | } 16 | 17 | const TablesColorProvider = ({ 18 | children, 19 | tables, 20 | }: TablesColorProviderProps) => { 21 | const tableColors = useMemo(() => createTablesColorMap(tables), [tables]); 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default TablesColorProvider; 31 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/TablesInfoProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, type ReactNode } from "react"; 2 | 3 | import type { TablesInfoProviderValue } from "@/types/tablesInfoProviderValue"; 4 | import type { JSONTableTable } from "shared/types/tableSchema"; 5 | 6 | import { computeColIndexes } from "@/utils/computeColIndexes"; 7 | import { useTableDetailLevel } from "@/hooks/tableDetailLevel"; 8 | 9 | export const TablesInfoContext = createContext< 10 | TablesInfoProviderValue | undefined 11 | >(undefined); 12 | 13 | interface TablesInfoProviderProps { 14 | tables: JSONTableTable[]; 15 | children: ReactNode; 16 | } 17 | 18 | const TablesInfoProvider = ({ children, tables }: TablesInfoProviderProps) => { 19 | const [hoveredTableName, setHoveredTableName] = useState(null); 20 | const { detailLevel } = useTableDetailLevel(); 21 | const colsIndexes = computeColIndexes(tables, detailLevel); 22 | 23 | return ( 24 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | export default TablesInfoProvider; 33 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/TablesPositionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useMemo, type PropsWithChildren } from "react"; 2 | 3 | import type { JSONTableRef, JSONTableTable } from "shared/types/tableSchema"; 4 | import type { TablesPositionsContextValue } from "@/types/dimension"; 5 | 6 | import { tableCoordsStore } from "@/stores/tableCoords"; 7 | 8 | export const TablesPositionsContext = 9 | createContext(null); 10 | 11 | interface TablesPositionsProviderProps extends PropsWithChildren { 12 | tables: JSONTableTable[]; 13 | refs: JSONTableRef[]; 14 | } 15 | 16 | const TablesPositionsProvider = ({ 17 | tables, 18 | refs, 19 | children, 20 | }: TablesPositionsProviderProps) => { 21 | const resetPositions = () => { 22 | tableCoordsStore.resetPositions(tables, refs); 23 | }; 24 | 25 | const contextValue = useMemo(() => ({ resetPositions }), [resetPositions]); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | export default TablesPositionsProvider; 35 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, type PropsWithChildren } from "react"; 2 | 3 | import { 4 | type ThemeColors, 5 | type ThemeProviderValue, 6 | Theme, 7 | } from "@/types/theme"; 8 | import { darkThemeConfig } from "@/constants/theme"; 9 | 10 | export const ThemeContext = createContext({ 11 | themeColors: darkThemeConfig, 12 | theme: Theme.dark, 13 | setTheme: () => {}, 14 | }); 15 | 16 | interface ThemeProviderProps extends PropsWithChildren { 17 | theme: Theme; 18 | themeColors: ThemeColors; 19 | setTheme: (value: Theme) => void; 20 | } 21 | 22 | const ThemeProvider = ({ 23 | theme, 24 | themeColors, 25 | setTheme, 26 | children, 27 | }: ThemeProviderProps) => { 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | }; 34 | 35 | export default ThemeProvider; 36 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/storages/local.ts: -------------------------------------------------------------------------------- 1 | import Storage from "@/types/storage"; 2 | 3 | export class AppLocalStorage extends Storage { 4 | getItem(key: string): object | null { 5 | const value = localStorage.getItem(key); 6 | if (value === null) return null; 7 | 8 | return JSON.parse(value); 9 | } 10 | 11 | setItem(key: string, value: T): void { 12 | localStorage.setItem(key, JSON.stringify(value)); 13 | } 14 | 15 | removeItem(key: string): void { 16 | localStorage.removeItem(key); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/storages/session.ts: -------------------------------------------------------------------------------- 1 | import Storage from "@/types/storage"; 2 | 3 | export class AppSessionStorage extends Storage { 4 | getItem(key: string): object | null { 5 | const value = sessionStorage.getItem(key); 6 | if (value === null) return null; 7 | 8 | return JSON.parse(value); 9 | } 10 | 11 | setItem(key: string, value: T): void { 12 | sessionStorage.setItem(key, JSON.stringify(value)); 13 | } 14 | 15 | removeItem(key: string): void { 16 | sessionStorage.removeItem(key); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/stores/PersitableStore.ts: -------------------------------------------------------------------------------- 1 | import type Storage from "@/types/storage"; 2 | 3 | import { AppLocalStorage } from "@/storages/local"; 4 | 5 | export class PersistableStore { 6 | private readonly storeName: string; 7 | private readonly storage: Storage; 8 | 9 | constructor(storeName: string, storage?: Storage) { 10 | this.storeName = storeName; 11 | this.storage = storage ?? new AppLocalStorage(); 12 | } 13 | 14 | createPersistanceKey(key: string): string { 15 | return `${this.storeName}:${key}`; 16 | } 17 | 18 | persist(name: string, value: T): void { 19 | const persistanceKey = this.createPersistanceKey(name); 20 | 21 | this.storage.setItem(persistanceKey, value); 22 | } 23 | 24 | retrieve(name: string): object | null { 25 | const persistanceKey = this.createPersistanceKey(name); 26 | 27 | return this.storage.getItem(persistanceKey); 28 | } 29 | 30 | clear(name: string): void { 31 | const persistanceKey = this.createPersistanceKey(name); 32 | 33 | this.storage.removeItem(persistanceKey); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/stores/StoreSignal.ts: -------------------------------------------------------------------------------- 1 | import eventEmitter from "@/events-emitter"; 2 | 3 | export class StoreSignal { 4 | private readonly storeEventPrefix: string; 5 | 6 | constructor(storeName: string) { 7 | this.storeEventPrefix = `store:${storeName}`; 8 | } 9 | 10 | getEventName(eventKey: string): string { 11 | return `${this.storeEventPrefix}:${eventKey}`; 12 | } 13 | 14 | subscribe(eventKey: string, callback: (value: T) => void): void { 15 | const eventName = this.getEventName(eventKey); 16 | eventEmitter.on(eventName, callback); 17 | } 18 | 19 | unSubscribe(eventKey: string, callback: (value: T) => void): void { 20 | const eventName = this.getEventName(eventKey); 21 | 22 | eventEmitter.off(eventName, callback); 23 | } 24 | 25 | protected emit(eventKey: string, value: T): void { 26 | const eventName = this.getEventName(eventKey); 27 | eventEmitter.emit(eventName, value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/stores/detailLevelStore.ts: -------------------------------------------------------------------------------- 1 | import { PersistableStore } from "./PersitableStore"; 2 | 3 | import { AppLocalStorage } from "@/storages/local"; 4 | import { TableDetailLevel } from "@/types/tableDetailLevel"; 5 | 6 | class DetailLevelStore extends PersistableStore { 7 | private detailLevel: TableDetailLevel = TableDetailLevel.FullDetails; 8 | private currentStoreKey = "none"; 9 | 10 | constructor() { 11 | super("detailLevel", new AppLocalStorage()); 12 | } 13 | 14 | public getCurrentDetailLevel(): TableDetailLevel { 15 | return this.detailLevel; 16 | } 17 | 18 | public saveCurrentState(): void { 19 | if (this.detailLevel === null) { 20 | this.persist(this.currentStoreKey, TableDetailLevel.FullDetails); 21 | } else { 22 | this.persist(this.currentStoreKey, this.detailLevel); 23 | } 24 | } 25 | 26 | public switchTo(newStoreKey: string): void { 27 | this.currentStoreKey = newStoreKey; 28 | const recoveredStore = this.retrieve(this.currentStoreKey); 29 | if (recoveredStore === null) { 30 | this.detailLevel = TableDetailLevel.FullDetails; 31 | } 32 | for (const val of Object.values(TableDetailLevel)) { 33 | if (val.toString() === String(recoveredStore)) { 34 | this.detailLevel = val; 35 | } 36 | } 37 | } 38 | 39 | public set(newState: TableDetailLevel): void { 40 | this.detailLevel = newState; 41 | } 42 | } 43 | 44 | export const detailLevelStore = new DetailLevelStore(); 45 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/stores/stagesState.ts: -------------------------------------------------------------------------------- 1 | import { PersistableStore } from "./PersitableStore"; 2 | 3 | import { AppSessionStorage } from "@/storages/session"; 4 | import { type StageState } from "@/types/stage"; 5 | 6 | class StageStateStore extends PersistableStore { 7 | private stageState: StageState | null = null; 8 | private currentStoreKey = "none"; 9 | 10 | constructor() { 11 | super("stageState", new AppSessionStorage()); 12 | } 13 | 14 | public getCurrentStageState(): StageState | null { 15 | return this.stageState; 16 | } 17 | 18 | public saveCurrentState(): void { 19 | if (this.stageState === null) { 20 | return; 21 | } 22 | this.persist(this.currentStoreKey, this.stageState); 23 | } 24 | 25 | public switchTo(newStoreKey: string): void { 26 | this.saveCurrentState(); 27 | 28 | this.currentStoreKey = newStoreKey; 29 | const recoveredStore = this.retrieve(this.currentStoreKey) as StageState; 30 | if (recoveredStore === null) { 31 | this.stageState = null; 32 | } 33 | 34 | this.stageState = recoveredStore; 35 | } 36 | 37 | public set(newState: StageState): void { 38 | this.stageState = newState; 39 | } 40 | } 41 | 42 | export const stageStateStore = new StageStateStore(); 43 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/stores/tableWidth.ts: -------------------------------------------------------------------------------- 1 | import { StoreSignal } from "./StoreSignal"; 2 | 3 | class TableWidthStore extends StoreSignal { 4 | private readonly tableWidths = new Map(); 5 | 6 | constructor() { 7 | super("tableWidth"); 8 | } 9 | 10 | public getWidth(tableName: string): number | undefined { 11 | return this.tableWidths.get(tableName); 12 | } 13 | 14 | public setWidth(table: string, width: number): void { 15 | this.tableWidths.set(table, width); 16 | this.emit(table, width); 17 | } 18 | 19 | public remove(table: string): void { 20 | this.tableWidths.delete(table); 21 | } 22 | } 23 | 24 | export const tableWidthStore = new TableWidthStore(); 25 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind utilities; 3 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/dimension.ts: -------------------------------------------------------------------------------- 1 | export interface Dimension { 2 | width: number; 3 | height: number; 4 | } 5 | 6 | export interface TablesPositionsContextValue { 7 | resetPositions: () => void; 8 | } 9 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/enums.ts: -------------------------------------------------------------------------------- 1 | import type { JSONTableEnum } from "shared/types/tableSchema"; 2 | 3 | export interface EnumsContextValue { 4 | enums: JSONTableEnum[]; 5 | } 6 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/positions.ts: -------------------------------------------------------------------------------- 1 | export enum Position { 2 | Left = "left", 3 | Top = "top", 4 | Right = "right", 5 | Bottom = "bottom", 6 | } 7 | 8 | export interface XYPosition { 9 | x: number; 10 | y: number; 11 | } 12 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/relation.ts: -------------------------------------------------------------------------------- 1 | import type { JSONTableRef } from "shared/types/tableSchema"; 2 | 3 | export type RelationItem = JSONTableRef["endpoints"][number]; 4 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/stage.ts: -------------------------------------------------------------------------------- 1 | import { type XYPosition } from "./positions"; 2 | 3 | export interface StageState { 4 | position: XYPosition; 5 | scale: number; 6 | } 7 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/storage.ts: -------------------------------------------------------------------------------- 1 | abstract class Storage { 2 | abstract getItem(key: string): object | null; 3 | abstract setItem(key: string, value: T): void; 4 | abstract removeItem(key: string): void; 5 | } 6 | 7 | export default Storage; 8 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/table.ts: -------------------------------------------------------------------------------- 1 | export interface TableDimensionProviderValue { 2 | width: number; 3 | } 4 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/tableColor.ts: -------------------------------------------------------------------------------- 1 | export interface TableColors { 2 | regular: string; 3 | lighter: string; 4 | } 5 | 6 | export interface TableColorContextValue { 7 | tableColors: Map; 8 | } 9 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/tableDetailLevel.ts: -------------------------------------------------------------------------------- 1 | export enum TableDetailLevel { 2 | FullDetails = "FullDetails", 3 | KeyOnly = "KeyOnly", 4 | HeaderOnly = "HeaderOnly", 5 | } 6 | 7 | export interface TableDetailLevelValue { 8 | detailLevel: TableDetailLevel; 9 | next: () => void; 10 | } 11 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/tablesInfoProviderValue.ts: -------------------------------------------------------------------------------- 1 | export type ColsIndexesMap = Record; 2 | 3 | export interface TablesInfoProviderValue { 4 | colsIndexes: ColsIndexesMap; 5 | hoveredTableName: string | null; 6 | setHoveredTableName: (tableName: string | null) => void; 7 | } 8 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/types/theme.tsx: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | light = "light", 3 | dark = "dark", 4 | } 5 | export interface ThemeColors { 6 | text: { 7 | 900: string; 8 | 700: string; 9 | }; 10 | connection: { 11 | default: string; 12 | active: string; 13 | }; 14 | tableHeader: { 15 | bg: string; 16 | fg: string; 17 | }; 18 | colAccent: string; 19 | table: { 20 | bg: string; 21 | shadow: string; 22 | }; 23 | red: string; 24 | green: string; 25 | enumItem: string; 26 | white: string; 27 | noteBg: string; 28 | bg: string; 29 | } 30 | 31 | export interface ThemeProviderValue { 32 | themeColors: ThemeColors; 33 | theme: Theme; 34 | setTheme: (theme: Theme) => void; 35 | } 36 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/colors/getContrastColor.ts: -------------------------------------------------------------------------------- 1 | export function getContrastColor(hexColor: string) { 2 | // Convert hex color to RGB values 3 | const r = parseInt(hexColor.slice(1, 3), 16); 4 | const g = parseInt(hexColor.slice(3, 5), 16); 5 | const b = parseInt(hexColor.slice(5, 7), 16); 6 | 7 | // Lighten the RGB values by the specified amount (0-100) 8 | const lightenAmount = 0.95; 9 | const newR = Math.round(r + (255 - r) * lightenAmount); 10 | const newG = Math.round(g + (255 - g) * lightenAmount); 11 | const newB = Math.round(b + (255 - b) * lightenAmount); 12 | 13 | // Convert the new RGB values back to a hexadecimal color code 14 | const newHexColor = '#' + ((1 << 24) + (newR << 16) + (newG << 8) + newB).toString(16).slice(1); 15 | 16 | return newHexColor; 17 | } -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/colors/getTableColorFromName.ts: -------------------------------------------------------------------------------- 1 | import type { TableColors } from "@/types/tableColor"; 2 | 3 | import { tableColors } from "@/constants/colors"; 4 | 5 | export const getTableColorFromName = (str: string): TableColors => { 6 | const numColors = tableColors.length; 7 | 8 | let sum = 0; 9 | for (let i = 0; i < str.length; i++) { 10 | sum += str.charCodeAt(i); 11 | } 12 | 13 | const index = sum % numColors; 14 | 15 | return tableColors[index]; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeCaretPoints.ts: -------------------------------------------------------------------------------- 1 | import { FIELD_DETAILS_CARET } from "@/constants/sizing"; 2 | 3 | export const computeCaretPoints = ( 4 | startingX: number, 5 | colHeight: number, 6 | ): number[] => { 7 | const colHalf = colHeight / 2; 8 | const caretX2 = startingX + FIELD_DETAILS_CARET.w; 9 | return [startingX, colHalf, caretX2, colHalf - 5, caretX2, colHalf + 5]; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeColIndexes.ts: -------------------------------------------------------------------------------- 1 | import { filterByDetailLevel } from "./filterByDetailLevel"; 2 | 3 | import type { ColsIndexesMap } from "@/types//tablesInfoProviderValue"; 4 | import type { JSONTableTable } from "shared/types/tableSchema"; 5 | 6 | import { TableDetailLevel } from "@/types/tableDetailLevel"; 7 | 8 | export const computeColIndexes = ( 9 | tables: JSONTableTable[], 10 | detailLevel: TableDetailLevel, 11 | ): ColsIndexesMap => { 12 | if (detailLevel === TableDetailLevel.HeaderOnly) { 13 | return {}; 14 | } 15 | return tables.reduce((acc, table) => { 16 | const currentTableColsIndexes = filterByDetailLevel( 17 | table.fields, 18 | detailLevel, 19 | ).reduce((tableAcc, field, index) => { 20 | return { 21 | ...tableAcc, 22 | [computeColIndexesKey(table.name, field.name)]: index, 23 | }; 24 | }, {}); 25 | 26 | return { 27 | ...acc, 28 | ...currentTableColsIndexes, 29 | }; 30 | }, {}); 31 | }; 32 | 33 | export const computeColIndexesKey = ( 34 | tableName: string, 35 | attr: string, 36 | ): string => { 37 | return `${tableName}.${attr}`; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeColY.ts: -------------------------------------------------------------------------------- 1 | import { computeColIndexesKey } from "./computeColIndexes"; 2 | 3 | import type { RelationItem } from "@/types/relation"; 4 | import type { ColsIndexesMap } from "@/types/tablesInfoProviderValue"; 5 | 6 | import { COLS_OFFSET_Y_TO_COL_MIDDLE, COLUMN_HEIGHT } from "@/constants/sizing"; 7 | 8 | export const computeColY = ( 9 | colsIndexesMap: ColsIndexesMap, 10 | relation: RelationItem, 11 | ): number => { 12 | const indexKey = computeColIndexesKey( 13 | relation.tableName, 14 | relation.fieldNames[0], 15 | ); 16 | 17 | const colIndex = colsIndexesMap[indexKey] ?? 0; 18 | 19 | const colY = COLS_OFFSET_Y_TO_COL_MIDDLE + COLUMN_HEIGHT * colIndex; 20 | 21 | return colY; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeConnectionHandlePositions.ts: -------------------------------------------------------------------------------- 1 | import { Position } from "@/types/positions"; 2 | 3 | interface Params { 4 | sourceW: number; 5 | sourceX: number; 6 | targetX: number; 7 | targetW: number; 8 | } 9 | 10 | const intersectionGap = 40; 11 | 12 | export const computeConnectionHandlePos = ({ 13 | sourceX, 14 | sourceW, 15 | targetW, 16 | targetX, 17 | }: Params): [Position, Position, number, number] => { 18 | const sourceEndX = sourceX + sourceW; 19 | const targetEndX = targetX + targetW; 20 | 21 | const horizontalIntersection = 22 | Math.max(sourceEndX, targetEndX) - Math.min(sourceX, targetX); 23 | 24 | if (horizontalIntersection <= targetW + sourceW + intersectionGap) { 25 | return [Position.Left, Position.Left, sourceX, targetX]; 26 | } 27 | 28 | if (sourceEndX < targetEndX) { 29 | return [Position.Right, Position.Left, sourceEndX, targetX]; 30 | } 31 | 32 | return [Position.Left, Position.Right, sourceX, targetEndX]; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeConnectionPaths.ts: -------------------------------------------------------------------------------- 1 | import { getBezierPath } from "./computeEgde/computeBezierEdge"; 2 | import { getRelationSymbol } from "./getRelationSymbol"; 3 | 4 | import { type Position, type XYPosition } from "@/types/positions"; 5 | 6 | interface Props { 7 | sourceXY: XYPosition; 8 | sourcePosition: Position; 9 | targetXY: XYPosition; 10 | targetPosition: Position; 11 | relationSource: string; 12 | relationTarget: string; 13 | } 14 | 15 | export const computeConnectionPathWithSymbols = ({ 16 | relationSource, 17 | relationTarget, 18 | sourceXY, 19 | targetXY, 20 | sourcePosition, 21 | targetPosition, 22 | }: Props): string => { 23 | const linePath = getBezierPath({ 24 | sourcePosition, 25 | targetPosition, 26 | source: sourceXY, 27 | target: targetXY, 28 | }); 29 | 30 | const sourceSymbolPath = getRelationSymbol( 31 | relationSource, 32 | sourcePosition, 33 | sourceXY, 34 | ); 35 | const targetSymbolPath = getRelationSymbol( 36 | relationTarget, 37 | targetPosition, 38 | targetXY, 39 | ); 40 | 41 | return `${linePath} ${sourceSymbolPath} ${targetSymbolPath}`; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeEgde/computeBezierEdge.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * copied from https://github.com/xyflow/xyflow/blob/8b200b4f25c5e017a86974161b19bf75656d671b/packages/system/src/utils/edges/bezier-edge.ts 3 | * with some update 4 | */ 5 | 6 | import { compteSymbolOffset } from "../getRelationSymbol"; 7 | 8 | import { Position, type XYPosition } from "@/types/positions"; 9 | 10 | interface GetBezierPathParams { 11 | source: XYPosition; 12 | target: XYPosition; 13 | sourcePosition?: Position; 14 | targetPosition?: Position; 15 | curvature?: number; 16 | } 17 | 18 | interface GetControlWithCurvatureParams { 19 | pos: Position; 20 | x1: number; 21 | y1: number; 22 | x2: number; 23 | y2: number; 24 | c: number; 25 | } 26 | 27 | function calculateControlOffset(distance: number, curvature: number): number { 28 | if (distance >= 0) { 29 | return 0.5 * distance; 30 | } 31 | 32 | return curvature * 25 * Math.sqrt(-distance); 33 | } 34 | 35 | function getControlWithCurvature({ 36 | pos, 37 | x1, 38 | y1, 39 | x2, 40 | y2, 41 | c, 42 | }: GetControlWithCurvatureParams): [number, number] { 43 | switch (pos) { 44 | case Position.Left: 45 | return [x1 - calculateControlOffset(x1 - x2, c), y1]; 46 | case Position.Right: 47 | return [x1 + calculateControlOffset(x2 - x1, c), y1]; 48 | case Position.Top: 49 | return [x1, y1 - calculateControlOffset(y1 - y2, c)]; 50 | case Position.Bottom: 51 | return [x1, y1 + calculateControlOffset(y2 - y1, c)]; 52 | } 53 | } 54 | 55 | export function getBezierPath({ 56 | source, 57 | sourcePosition = Position.Bottom, 58 | target, 59 | targetPosition = Position.Top, 60 | curvature = 0.5, 61 | }: GetBezierPathParams): string { 62 | const [sourceControlX, sourceControlY] = getControlWithCurvature({ 63 | pos: sourcePosition, 64 | x1: source.x, 65 | y1: source.y, 66 | x2: target.x, 67 | y2: target.y, 68 | c: curvature, 69 | }); 70 | 71 | const [targetControlX, targetControlY] = getControlWithCurvature({ 72 | pos: targetPosition, 73 | x1: target.x, 74 | y1: target.y, 75 | x2: source.x, 76 | y2: source.y, 77 | c: curvature, 78 | }); 79 | 80 | const sourceOffset = compteSymbolOffset(sourcePosition, source); 81 | const targetOffset = compteSymbolOffset(targetPosition, target); 82 | 83 | const path = `M${source.x},${source.y} L${sourceOffset.x},${sourceOffset.y} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetOffset.x},${targetOffset.y} L${target.x},${target.y}`; 84 | 85 | return path; 86 | } 87 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeEnumDetailBoxMaxW.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableEnum } from "shared/types/tableSchema"; 2 | 3 | import { computeTextsMaxWidth } from "./computeTextsMaxWidth"; 4 | import { createEnumItemText } from "./createEnumItemText"; 5 | 6 | /** 7 | * compute and return the max width of the enum. 8 | * Compares the text width of the enum title with the text widths of each menu item 9 | * 10 | * @param {JSONTableEnum} enumObject - The enum object to compute the detail box width for 11 | * @return {number} The maximum width of the detail box 12 | */ 13 | export const computeEnumDetailBoxMaxW = (enumObject: JSONTableEnum): number => { 14 | const titleText = `Enum ${enumObject.name}`; 15 | const itemsTexts = enumObject.values.map((item) => 16 | createEnumItemText(item.name), 17 | ); 18 | return computeTextsMaxWidth([titleText, ...itemsTexts]); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeFieldDetailBoxDimension.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableEnum } from "shared/types/tableSchema"; 2 | 3 | import { computeEnumDetailBoxMaxW } from "./computeEnumDetailBoxMaxW"; 4 | import { 5 | computeTextSize, 6 | getLetterApproximateDimension, 7 | } from "./computeTextSize"; 8 | import { estimateSentenceLineCount } from "./estimateSentenceLineCount"; 9 | 10 | import { FIELD_DETAILS_TOOLTIPS_W, PADDINGS } from "@/constants/sizing"; 11 | 12 | export const computeFieldDetailBoxDimension = ( 13 | note?: string, 14 | enumObject?: JSONTableEnum, 15 | ): { w: number; h: number; noteH: number } => { 16 | const enumDetailMaxW = 17 | enumObject === undefined ? 0 : computeEnumDetailBoxMaxW(enumObject); 18 | const oneLineNoteW = note != null ? computeTextSize(note).width : 0; 19 | const preferredWidth = Math.max(enumDetailMaxW, oneLineNoteW) + PADDINGS.md; 20 | const finalWidth = Math.min(preferredWidth, FIELD_DETAILS_TOOLTIPS_W); 21 | 22 | const letterApproximateDim = getLetterApproximateDimension(); 23 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 24 | const noteH = note 25 | ? estimateSentenceLineCount(note, finalWidth) * letterApproximateDim.height 26 | : 0; 27 | 28 | const enumsDetailsH = 29 | enumObject === undefined 30 | ? 0 31 | : letterApproximateDim.height * 32 | (1 + enumObject.values.length); /* the 1 added is for the title line */ 33 | 34 | const enumsPanelHWithPadding = 35 | enumsDetailsH + (enumsDetailsH > 0 ? PADDINGS.md : 0); 36 | 37 | const noteHWithPadding = noteH + (noteH !== 0 ? PADDINGS.lg : PADDINGS.md); 38 | const totalH = noteHWithPadding + enumsPanelHWithPadding; 39 | 40 | return { w: finalWidth, h: totalH, noteH: noteHWithPadding }; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeTableDimension.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import { getTableLinesText } from "./tableWComputation/getTableLinesText"; 4 | import { computeTablePreferredWidth } from "./tableWComputation/computeTablePreferredWidth"; 5 | 6 | import { COLUMN_HEIGHT, TABLE_HEADER_HEIGHT } from "@/constants/sizing"; 7 | import { type Dimension } from "@/types/dimension"; 8 | 9 | export const computeTableDimension = (table: JSONTableTable): Dimension => { 10 | const tableTexts = getTableLinesText(table.fields); 11 | const width = computeTablePreferredWidth(tableTexts, table.name); 12 | const height = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * table.fields.length; 13 | 14 | return { width, height }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeTextSize.ts: -------------------------------------------------------------------------------- 1 | import { Text, type TextConfig } from "konva/lib/shapes/Text"; 2 | 3 | import { FONT_FAMILY } from "@/constants/font"; 4 | import { FONT_SIZES } from "@/constants/sizing"; 5 | import { type Dimension } from "@/types/dimension"; 6 | 7 | const textNode = new Text({ 8 | fontFamily: FONT_FAMILY, 9 | fontSize: FONT_SIZES.md, 10 | }); 11 | 12 | export const computeTextSize = ( 13 | text: string, 14 | config?: TextConfig, 15 | ): Dimension => { 16 | if (config != null) { 17 | const clone = textNode.clone(config); 18 | return clone.measureSize(text); 19 | } 20 | 21 | return textNode.measureSize(text); 22 | }; 23 | 24 | export const getLetterApproximateDimension = ( 25 | config?: TextConfig, 26 | ): Dimension => { 27 | return computeTextSize("a", config); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/computeTextsMaxWidth.ts: -------------------------------------------------------------------------------- 1 | import { computeTextSize } from "./computeTextSize"; 2 | 3 | export const computeTextsMaxWidth = (text: string[]): number => { 4 | return Math.max(...text.map((t) => computeTextSize(t).width)); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/createEnumItemText.ts: -------------------------------------------------------------------------------- 1 | export const createEnumItemText = (item: string): string => { 2 | return ` - ${item}`; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/createTablesColorMap.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import { getTableColorFromName } from "./colors/getTableColorFromName"; 4 | import { getContrastColor } from "./colors/getContrastColor"; 5 | 6 | import { type TableColors } from "@/types/tableColor"; 7 | 8 | export const createTablesColorMap = ( 9 | tables: JSONTableTable[], 10 | ): Map => { 11 | const tableColors = new Map(); 12 | tables.forEach((table) => { 13 | const tableColor = !!table.headerColor ? { regular: table.headerColor, lighter: getContrastColor(table.headerColor) } : getTableColorFromName(table.name); 14 | 15 | tableColors.set(table.name, tableColor); 16 | }); 17 | 18 | return tableColors; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/estimateSentenceLineCount.ts: -------------------------------------------------------------------------------- 1 | import { computeTextSize } from "./computeTextSize"; 2 | 3 | export const estimateSentenceLineCount = ( 4 | sentence: string, 5 | containerW: number, 6 | ): number => { 7 | let numberOfLine = 1; 8 | let accumulatedWidth = 0; 9 | 10 | sentence.split(" ").forEach((word) => { 11 | const wordW = computeTextSize(`${word}-`).width; 12 | accumulatedWidth += wordW; 13 | if (accumulatedWidth > containerW) { 14 | const approximateLineForTheWord = Math.ceil(wordW / containerW); 15 | numberOfLine += approximateLineForTheWord; 16 | accumulatedWidth = 0; 17 | } 18 | }); 19 | 20 | return numberOfLine; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/eventName.ts: -------------------------------------------------------------------------------- 1 | export const computeTableDragEventName = (tableName: string): string => { 2 | return `on:table:drag:${tableName}`; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/filterByDetailLevel.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableField } from "shared/types/tableSchema"; 2 | 3 | import { TableDetailLevel } from "@/types/tableDetailLevel"; 4 | 5 | export function filterByDetailLevel( 6 | fields: JSONTableField[], 7 | detailLevel: TableDetailLevel, 8 | ): JSONTableField[] { 9 | function isFullDetail(): boolean { 10 | return detailLevel === TableDetailLevel.FullDetails; 11 | } 12 | function isKeyOnly(): boolean { 13 | return detailLevel === TableDetailLevel.KeyOnly; 14 | } 15 | function hasRelations(field: JSONTableField): boolean { 16 | if (!Array.isArray(field.relational_tables)) { 17 | return false; 18 | } 19 | return field.relational_tables.length > 0; 20 | } 21 | return fields.filter( 22 | (f) => isFullDetail() || (isKeyOnly() && hasRelations(f)), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/getFieldType.ts: -------------------------------------------------------------------------------- 1 | import type { JSONTableField } from "shared/types/tableSchema"; 2 | 3 | const computeFieldDisplayTypeName = (field: JSONTableField): string => { 4 | if (field.not_null === true) { 5 | return `${field.type.type_name} (!)`; 6 | } 7 | return field.type.type_name; 8 | }; 9 | 10 | export default computeFieldDisplayTypeName; 11 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/getRelationSymbol.ts: -------------------------------------------------------------------------------- 1 | import { CONNECTION_RELATION_SYMBOL_OFFSET } from "@/constants/sizing"; 2 | import { Position, type XYPosition } from "@/types/positions"; 3 | const computeOneTypeRelationSymbol = ( 4 | { x, y }: XYPosition, 5 | position: Position, 6 | ): string => { 7 | const halfHeight = 3; 8 | 9 | const symbolOffset = compteSymbolOffset(position, { x, y }); 10 | const path = `M${symbolOffset.x},${y - halfHeight} L${symbolOffset.x},${y + halfHeight}`; 11 | 12 | return path; 13 | }; 14 | 15 | export const compteSymbolOffset = ( 16 | position: Position, 17 | startPoint: XYPosition, 18 | ): XYPosition => { 19 | const x = 20 | position === Position.Left 21 | ? startPoint.x - CONNECTION_RELATION_SYMBOL_OFFSET 22 | : startPoint.x + CONNECTION_RELATION_SYMBOL_OFFSET; 23 | return { x, y: startPoint.y }; 24 | }; 25 | 26 | const computeMultipleTypeRelationSymbol = ( 27 | { x, y }: XYPosition, 28 | position: Position, 29 | ): string => { 30 | const halfHeight = 5; 31 | const symbolOffset = compteSymbolOffset(position, { x, y }); 32 | 33 | return `M${x},${y - halfHeight} L${symbolOffset.x},${symbolOffset.y} L${x},${y + halfHeight}`; 34 | }; 35 | 36 | export const getRelationSymbol = ( 37 | relation: string, 38 | position: Position, 39 | coord: XYPosition, 40 | ): string => { 41 | if (relation === "1") { 42 | return computeOneTypeRelationSymbol(coord, position); 43 | } 44 | 45 | return computeMultipleTypeRelationSymbol(coord, position); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/shouldHighLightCol.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 2 | export const shouldHighLightCol = ( 3 | hovered: boolean, 4 | tableName: string | null, 5 | hoveredTable: string | null, 6 | relationalTables?: string[] | null, 7 | ): boolean => { 8 | if (hovered) { 9 | return true; 10 | } 11 | 12 | if (hoveredTable === tableName && !!relationalTables) { 13 | return true; 14 | } 15 | 16 | if (!!hoveredTable && relationalTables?.includes(hoveredTable)) { 17 | return true; 18 | } 19 | 20 | return false; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tablePositioning/computeTablesPositions.ts: -------------------------------------------------------------------------------- 1 | import dagre from "@dagrejs/dagre"; 2 | 3 | import { computeTableDimension } from "../computeTableDimension"; 4 | 5 | import type { JSONTableRef, JSONTableTable } from "shared/types/tableSchema"; 6 | 7 | import { TABLES_GAP_X, TABLES_GAP_Y } from "@/constants/sizing"; 8 | import { type XYPosition } from "@/types/positions"; 9 | 10 | const computeTablesPositions = ( 11 | tables: JSONTableTable[], 12 | refs: JSONTableRef[], 13 | ): Map => { 14 | const tablesPositions = new Map(); 15 | 16 | const graph = new dagre.graphlib.Graph(); 17 | graph.setGraph({ 18 | nodesep: TABLES_GAP_X * 3, 19 | ranksep: TABLES_GAP_Y * 3, 20 | rankdir: "LR", 21 | }); 22 | graph.setDefaultEdgeLabel(function () { 23 | return {}; 24 | }); 25 | 26 | tables.forEach((table) => { 27 | const { height, width } = computeTableDimension(table); 28 | graph.setNode(table.name, { width, height }); 29 | }); 30 | 31 | refs.forEach((ref) => { 32 | graph.setEdge(ref.endpoints[0].tableName, ref.endpoints[1].tableName); 33 | }); 34 | 35 | dagre.layout(graph); 36 | 37 | graph.nodes().forEach((node) => { 38 | const { x, y } = graph.node(node); 39 | tablesPositions.set(node, { x, y }); 40 | }); 41 | return tablesPositions; 42 | }; 43 | 44 | export default computeTablesPositions; 45 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tablePositioning/getColsNumber.ts: -------------------------------------------------------------------------------- 1 | export const getColsNumber = (tableCount: number): number => { 2 | // display tables in 3 columns if the number of tables is less than 6 3 | // and 4 columns if the number of tables is greater than 6 4 | const MINIMAL_COLS_COUNT = 3; 5 | const MAXIMAL_COLS_COUNT = 4; 6 | 7 | return tableCount > 6 ? MAXIMAL_COLS_COUNT : MINIMAL_COLS_COUNT; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tablePositioning/tests/computeTablesPositions.test.ts: -------------------------------------------------------------------------------- 1 | import computeTablesPositions from "../computeTablesPositions"; 2 | import { getColsNumber } from "../getColsNumber"; 3 | 4 | import { 5 | COLUMN_HEIGHT, 6 | TABLE_HEADER_HEIGHT, 7 | TABLE_DEFAULT_MIN_WIDTH, 8 | TABLES_GAP_X, 9 | TABLES_GAP_Y, 10 | } from "@/constants/sizing"; 11 | import { createBookingsTableClone, exampleData } from "@/fake/fakeJsonTables"; 12 | 13 | jest.mock("../getColsNumber", () => ({ 14 | getColsNumber: jest.fn(), 15 | })); 16 | 17 | const TABLE_WIDTH_WITH_GAP = TABLE_DEFAULT_MIN_WIDTH + TABLES_GAP_X; 18 | 19 | describe("compute tables positions", () => { 20 | test("less than 6 tables positions", () => { 21 | (getColsNumber as jest.Mock).mockReturnValue(3); 22 | 23 | const tablesPositions = computeTablesPositions([ 24 | ...exampleData.tables, 25 | createBookingsTableClone("1"), 26 | ]); 27 | 28 | expect(tablesPositions).toEqual( 29 | new Map([ 30 | ["follows", [0, 0]], 31 | ["users", [TABLE_WIDTH_WITH_GAP, 0]], 32 | ["bookings", [TABLE_WIDTH_WITH_GAP * 2, 0]], 33 | [ 34 | "bookings_1", 35 | [0, TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * 5 + TABLES_GAP_Y], 36 | ], 37 | ]), 38 | ); 39 | }); 40 | 41 | test("more than 6 tables positions", () => { 42 | (getColsNumber as jest.Mock).mockReturnValue(4); 43 | 44 | const tablesPositions = computeTablesPositions([ 45 | ...exampleData.tables, 46 | createBookingsTableClone("1"), 47 | ]); 48 | 49 | expect(tablesPositions).toEqual( 50 | new Map([ 51 | ["follows", [0, 0]], 52 | ["users", [TABLE_WIDTH_WITH_GAP, 0]], 53 | ["bookings", [TABLE_WIDTH_WITH_GAP * 2, 0]], 54 | ["bookings_1", [TABLE_WIDTH_WITH_GAP * 3, 0]], 55 | ]), 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tableWComputation/computeTablePreferredWidth.ts: -------------------------------------------------------------------------------- 1 | import { computeTextSize } from "../computeTextSize"; 2 | import { computeTextsMaxWidth } from "../computeTextsMaxWidth"; 3 | 4 | import { 5 | FONT_SIZES, 6 | TABLE_DEFAULT_MIN_WIDTH, 7 | TABLE_FIELD_TYPE_PADDING, 8 | } from "@/constants/sizing"; 9 | 10 | export const computeTablePreferredWidth = ( 11 | tableTexts: string[], 12 | tableName: string, 13 | ): number => { 14 | const minColsW = computeTextsMaxWidth(tableTexts); 15 | const { width: tableNameW } = computeTextSize(tableName, { 16 | fontSize: FONT_SIZES.tableTitle, 17 | }); 18 | 19 | const maxOnColsAndTableName = Math.max(minColsW, tableNameW); 20 | 21 | return Math.max( 22 | maxOnColsAndTableName + TABLE_FIELD_TYPE_PADDING * 2, 23 | TABLE_DEFAULT_MIN_WIDTH, 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tableWComputation/getTableLinesText.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import computeFieldDisplayTypeName from "../getFieldType"; 4 | 5 | export const getTableLinesText = ( 6 | fields: JSONTableTable["fields"], 7 | ): string[] => { 8 | const stringColsNames = fields.map( 9 | (field) => `${field.name} ${computeFieldDisplayTypeName(field)}`, 10 | ); 11 | 12 | return stringColsNames; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tests/computeCaretPoints.test.ts: -------------------------------------------------------------------------------- 1 | import { computeCaretPoints } from "../computeCaretPoints"; 2 | 3 | describe("compute field detail popover caret points", () => { 4 | test("compute field detail popover caret points", () => { 5 | expect(computeCaretPoints(0, 10)).toEqual([0, 5, 5, 0, 5, 10]); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tests/computeColIndexes.test.ts: -------------------------------------------------------------------------------- 1 | import { computeColIndexes } from "../computeColIndexes"; 2 | 3 | import { exampleData } from "@/fake/fakeJsonTables"; 4 | 5 | describe("compute cols index map", () => { 6 | test("compute cols index map", () => { 7 | expect(computeColIndexes(exampleData.tables)).toEqual({ 8 | "users.id": 0, 9 | "users.email": 1, 10 | "bookings.booking_date": 2, 11 | "bookings.country": 1, 12 | "bookings.id": 0, 13 | "follows.created_at": 3, 14 | "follows.following_user_id": 2, 15 | "follows.id": 0, 16 | "follows.status": 4, 17 | "follows.view": 1, 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tests/computeColY.test.ts: -------------------------------------------------------------------------------- 1 | import { computeColY } from "../computeColY"; 2 | 3 | import { COLS_OFFSET_Y_TO_COL_MIDDLE } from "@/constants/sizing"; 4 | describe("computing col y", () => { 5 | test("computing col y", () => { 6 | expect( 7 | computeColY( 8 | { "users.email": 0 }, 9 | { fieldNames: ["email"], relation: "1", tableName: "users" }, 10 | ), 11 | ).toBe(COLS_OFFSET_Y_TO_COL_MIDDLE); 12 | }); 13 | 14 | test("computing col y while index is missing in map", () => { 15 | expect( 16 | computeColY( 17 | { "users.email": 0 }, 18 | { fieldNames: ["not-email"], relation: "1", tableName: "users" }, 19 | ), 20 | ).toBe(COLS_OFFSET_Y_TO_COL_MIDDLE); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tests/computeConnectionHandlePosition.test.ts: -------------------------------------------------------------------------------- 1 | import { computeConnectionHandlePos } from "../computeConnectionHandlePositions"; 2 | 3 | import { Position } from "@/types/positions"; 4 | 5 | describe("compute connection handle positions", () => { 6 | test("return same position", () => { 7 | const input = { 8 | sourceW: 300, 9 | sourceX: 0, 10 | targetW: 300, 11 | targetX: 300, 12 | }; 13 | expect(computeConnectionHandlePos(input)).toEqual([ 14 | Position.Left, 15 | Position.Left, 16 | input.sourceX, 17 | input.targetX, 18 | ]); 19 | }); 20 | 21 | test("source at right", () => { 22 | const input = { 23 | sourceW: 300, 24 | sourceX: 0, 25 | targetW: 300, 26 | targetX: 500, 27 | }; 28 | expect(computeConnectionHandlePos(input)).toEqual([ 29 | Position.Right, 30 | Position.Left, 31 | input.sourceX + input.sourceW, 32 | input.targetX, 33 | ]); 34 | }); 35 | 36 | test("source at left", () => { 37 | const input = { 38 | sourceW: 300, 39 | sourceX: 500, 40 | targetW: 300, 41 | targetX: 0, 42 | }; 43 | expect(computeConnectionHandlePos(input)).toEqual([ 44 | Position.Left, 45 | Position.Right, 46 | input.sourceX, 47 | input.targetW + input.targetX, 48 | ]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tests/computeTableDimension.test.ts: -------------------------------------------------------------------------------- 1 | import { computeTableDimension } from "../computeTableDimension"; 2 | 3 | import { exampleData } from "@/fake/fakeJsonTables"; 4 | import { 5 | TABLE_HEADER_HEIGHT, 6 | TABLE_DEFAULT_MIN_WIDTH, 7 | } from "@/constants/sizing"; 8 | 9 | describe("compute table dimension", () => { 10 | test("compute table dimension", () => { 11 | expect(computeTableDimension(exampleData.tables[0])).toEqual({ 12 | width: TABLE_DEFAULT_MIN_WIDTH, 13 | height: TABLE_HEADER_HEIGHT * 5, 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/src/utils/tests/computeTextsMaxWidth.test.ts: -------------------------------------------------------------------------------- 1 | import { computeTextSize } from "../computeTextSize"; 2 | import { computeTextsMaxWidth } from "../computeTextsMaxWidth"; 3 | 4 | describe("get the more longer text width", () => { 5 | test("get the more longer text width", () => { 6 | expect(computeTextsMaxWidth(["simple", "more longer"])).toBe( 7 | computeTextSize("more longer"), 8 | ); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | darkMode: "selector", 9 | }; 10 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "baseUrl": "./src", 7 | "paths": { 8 | "@/*": ["./*"] 9 | }, 10 | "allowJs": true, 11 | "checkJs": true 12 | }, 13 | "exclude": ["node_modules", "vite.config.js"], 14 | "include": ["**/*.ts", "**/*.tsx"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/json-table-schema-visualizer/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: { 8 | "@": fileURLToPath(new URL("./src", import.meta.url)), 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require("ts-jest"); 2 | const { compilerOptions } = require("./tsconfig.json"); 3 | 4 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 5 | module.exports = { 6 | preset: "ts-jest", 7 | testEnvironment: "node", 8 | roots: ["./"], 9 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 10 | prefix: "/src", 11 | }), 12 | }; 13 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-to-json-table-schema", 3 | "version": "0.0.1", 4 | "description": "Transform prisma code to JSON table schema", 5 | "main": "src/index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@mrleebo/prisma-ast": "^0.12.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/constants/scalarFieldType.ts: -------------------------------------------------------------------------------- 1 | export const scalarFieldType = new Set([ 2 | "String", 3 | "Boolean", 4 | "Int", 5 | "BigInt", 6 | "Float", 7 | "Decimal", 8 | "DateTime", 9 | "Json", 10 | "Bytes", 11 | "Unsupported", 12 | ]); 13 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/enums/prismaAstNodeType.ts: -------------------------------------------------------------------------------- 1 | export enum PrismaAstNodeType { 2 | schema = "schema", 3 | model = "model", 4 | type = "type", 5 | view = "view", 6 | datasource = "datasource", 7 | generator = "generator", 8 | enum = "enum", 9 | comment = "comment", 10 | break = "break", 11 | assignment = "assignment", 12 | enumerator = "enumerator", 13 | attribute = "attribute", 14 | field = "field", 15 | attributeArgument = "attributeArgument", 16 | keyValue = "keyValue", 17 | array = "array", 18 | function = "function", 19 | } 20 | 21 | export enum PrismaFieldAttributeType { 22 | default = "default", 23 | id = "id", 24 | unique = "unique", 25 | relation = "relation", 26 | } 27 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type CstNodeLocation, getSchema } from "@mrleebo/prisma-ast"; 2 | import { type JSONTableSchema } from "shared/types/tableSchema"; 3 | import { DiagnosticError } from "shared/types/diagnostic"; 4 | 5 | import { prismaASTToJSONTableSchema } from "./utils/transformers/prismaASTToJSONTableSchema"; 6 | 7 | export const parsePrismaToJSON = (prismaCode: string): JSONTableSchema => { 8 | try { 9 | const rawParsedSchema = getSchema(prismaCode); 10 | return prismaASTToJSONTableSchema(rawParsedSchema); 11 | } catch (error) { 12 | if ("token" in (error as any)) { 13 | const token = (error as any).token as CstNodeLocation; 14 | 15 | const endColumn = token.endColumn; 16 | const endLine = token.endLine; 17 | const startColumn = token.startColumn; 18 | const startLine = token.startLine; 19 | 20 | if ( 21 | endColumn === undefined || 22 | startColumn === undefined || 23 | endLine === undefined || 24 | startLine === undefined 25 | ) { 26 | throw error; 27 | } 28 | 29 | const locationEnd = { column: endColumn, line: endLine }; 30 | const locationStart = { 31 | column: startColumn, 32 | line: startLine, 33 | }; 34 | 35 | throw new DiagnosticError( 36 | { 37 | end: { 38 | column: locationEnd.column - 1, 39 | line: locationEnd.line - 1, 40 | }, 41 | start: { 42 | column: locationStart.column - 1, 43 | line: locationStart.line - 1, 44 | }, 45 | }, 46 | String((error as Error).message), 47 | ); 48 | } 49 | 50 | throw error; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/types/intermediateFormattedNode.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | JSONTableField, 3 | JSONTableSchema, 4 | JSONTableTable, 5 | } from "shared/types/tableSchema"; 6 | 7 | export type RelationType = "one" | "many"; 8 | export type RelationMap = Map; 9 | export type RawRelation = RawRelationInfo[]; 10 | 11 | /** 12 | * @property {string} key - a combination of the table and field concerned. 13 | * @property {string[]} value - list of tables with which it has a relationship 14 | */ 15 | export type FieldRelationsMap = Map; 16 | 17 | export interface RawRelationInfo { 18 | table: string; 19 | field: string; 20 | name?: string; 21 | referenceTable: string; 22 | referenceField: string; 23 | } 24 | 25 | // exclude the default `type` because before the first grouping, enums 26 | // are enums not yet known 27 | export interface IntermediateField 28 | extends Omit { 29 | type: { type_name: string; many?: boolean }; 30 | } 31 | 32 | export interface FieldConfig 33 | extends Omit {} 34 | 35 | export interface IntermediateTable extends Omit { 36 | fields: IntermediateField[]; 37 | } 38 | 39 | export interface IntermediateSchema 40 | extends Omit { 41 | tables: IntermediateTable[]; 42 | enumsNames: Set; 43 | rawRelations: RawRelation; 44 | inverseRelationMap: Map; 45 | tablesNames: Set; 46 | } 47 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/types/node.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaAstNodeType } from "../enums/prismaAstNodeType"; 2 | 3 | export interface AstNode { 4 | type: PrismaAstNodeType | string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/computeKey.ts: -------------------------------------------------------------------------------- 1 | export const computeKey = (...tags: string[]): string => tags.join("."); 2 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/isTypeOf.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PrismaAstNodeType, 3 | PrismaFieldAttributeType, 4 | } from "../enums/prismaAstNodeType"; 5 | 6 | import type { 7 | Model, 8 | Enumerator, 9 | Field, 10 | KeyValue, 11 | Value, 12 | RelationArray, 13 | Enum, 14 | Func, 15 | Type, 16 | Attribute, 17 | } from "@mrleebo/prisma-ast"; 18 | import type { AstNode } from "../types/node"; 19 | 20 | export const isEnumNode = (node: AstNode): node is Enum => { 21 | return node.type === PrismaAstNodeType.enum; 22 | }; 23 | 24 | export const isEnumeratorNode = (node: AstNode): node is Enumerator => { 25 | return node.type === PrismaAstNodeType.enumerator; 26 | }; 27 | 28 | export const isModelNode = (node: AstNode): node is Model => { 29 | return node.type === PrismaAstNodeType.model; 30 | }; 31 | 32 | export const isField = (node: AstNode): node is Field => { 33 | return node.type === PrismaAstNodeType.field; 34 | }; 35 | 36 | export const isRelationFieldAttr = ( 37 | node: Model["properties"][number], 38 | ): node is Field => { 39 | return node.type === PrismaAstNodeType.attribute && node.name === "relation"; 40 | }; 41 | 42 | export const isKeyValue = (node: AstNode | Value): node is KeyValue => { 43 | return ( 44 | typeof node === "object" && 45 | !Array.isArray(node) && 46 | node.type === PrismaAstNodeType.keyValue 47 | ); 48 | }; 49 | 50 | export const isRelationNode = (node: AstNode): node is Attribute => { 51 | return ( 52 | node.type === PrismaAstNodeType.attribute && 53 | (node as Attribute).name === PrismaFieldAttributeType.relation 54 | ); 55 | }; 56 | 57 | export const isRelationArray = ( 58 | node: AstNode | Value, 59 | ): node is RelationArray => { 60 | return ( 61 | typeof node === "object" && 62 | !Array.isArray(node) && 63 | node.type === PrismaAstNodeType.array && 64 | Array.isArray((node as RelationArray).args) 65 | ); 66 | }; 67 | 68 | export const isDefaultFieldValueNode = ( 69 | node: NonNullable[number], 70 | ): boolean => { 71 | return node.type === PrismaAstNodeType.attribute && node.name === "default"; 72 | }; 73 | 74 | export const isFunNodeType = (node: AstNode | Value[]): node is Func => { 75 | return !Array.isArray(node) && node.type === PrismaAstNodeType.function; 76 | }; 77 | 78 | export const isTypeNode = (node: AstNode): node is Type => { 79 | return node.type === PrismaAstNodeType.type; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/createRefsFromPrismaASTNodes.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableRef } from "shared/types/tableSchema"; 2 | 3 | import { computeKey } from "../computeKey"; 4 | 5 | import type { 6 | FieldRelationsMap, 7 | IntermediateSchema, 8 | } from "../../types/intermediateFormattedNode"; 9 | 10 | export const createRefsAndFieldRelationsArray = ( 11 | rawRelations: IntermediateSchema["rawRelations"], 12 | inverseRelationMap: IntermediateSchema["inverseRelationMap"], 13 | tablesNames: IntermediateSchema["tablesNames"], 14 | ): [JSONTableRef[], FieldRelationsMap] => { 15 | const refs: JSONTableRef[] = []; 16 | const allFieldRelations: FieldRelationsMap = new Map(); 17 | 18 | const appendFieldRelation = ( 19 | fieldKey: string, 20 | relationName: string, 21 | ): void => { 22 | if (allFieldRelations.has(fieldKey)) { 23 | allFieldRelations.get(fieldKey)?.push(relationName); 24 | } else { 25 | allFieldRelations.set(fieldKey, [relationName]); 26 | } 27 | }; 28 | 29 | rawRelations.forEach((relation) => { 30 | // check if invest relationship exists 31 | if ( 32 | // check all table exists 33 | !tablesNames.has(relation.table) && 34 | !tablesNames.has(relation.referenceTable) 35 | ) 36 | return; 37 | 38 | const id = computeKey( 39 | relation.referenceTable, 40 | relation.table, 41 | ...(relation.name !== undefined ? [relation.name] : []), 42 | ); 43 | const relationType = inverseRelationMap.get(id); 44 | if (relationType === undefined) return; 45 | 46 | const ref: JSONTableRef = { 47 | name: relation.name, 48 | endpoints: [ 49 | { 50 | relation: "1", 51 | tableName: relation.referenceTable, 52 | fieldNames: [relation.referenceField], 53 | }, 54 | { 55 | relation: relationType === "many" ? "*" : "1", 56 | fieldNames: [relation.field], 57 | tableName: relation.table, 58 | }, 59 | ], 60 | }; 61 | 62 | refs.push(ref); 63 | 64 | appendFieldRelation( 65 | computeKey(relation.referenceTable, relation.referenceField), 66 | relation.table, 67 | ); 68 | 69 | appendFieldRelation( 70 | computeKey(relation.table, relation.field), 71 | relation.referenceTable, 72 | ); 73 | }); 74 | 75 | return [refs, allFieldRelations]; 76 | }; 77 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/enumNodeToJSONTableEnum.ts: -------------------------------------------------------------------------------- 1 | import { isEnumeratorNode } from "../isTypeOf"; 2 | 3 | import type { Enum } from "@mrleebo/prisma-ast"; 4 | import type { JSONTableEnum } from "shared/types/tableSchema"; 5 | 6 | export const enumNodeToJSONTableEnum = (node: Enum): JSONTableEnum => { 7 | const values: JSONTableEnum["values"] = []; 8 | for (const enumerator of node.enumerators) { 9 | if (isEnumeratorNode(enumerator)) { 10 | values.push({ 11 | name: enumerator.name, 12 | note: enumerator.comment, 13 | }); 14 | } 15 | } 16 | 17 | const _enum: JSONTableEnum = { 18 | values, 19 | name: node.name, 20 | }; 21 | 22 | return _enum; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/getFieldConfig.ts: -------------------------------------------------------------------------------- 1 | import { isFunNodeType, isKeyValue } from "../isTypeOf"; 2 | import { PrismaFieldAttributeType } from "../../enums/prismaAstNodeType"; 3 | 4 | import type { Field } from "@mrleebo/prisma-ast"; 5 | import type { FieldConfig } from "@/types/intermediateFormattedNode"; 6 | 7 | // Browse column attributes to identify its configurations 8 | export const getFieldConfig = ( 9 | fieldProps: Field["attributes"], 10 | ): FieldConfig => { 11 | if (fieldProps === undefined) return {}; 12 | 13 | let defaultValue: string | number | boolean | null = null; 14 | let isPrimary = false; 15 | let incrementable = false; 16 | let isUniqueField = false; 17 | 18 | for (const prop of fieldProps) { 19 | switch (prop.name) { 20 | // 21 | case PrismaFieldAttributeType.default: 22 | if (prop.args === undefined) { 23 | break; 24 | } 25 | 26 | for (const defaultPropArg of prop.args) { 27 | // handle each case according to the value type 28 | 29 | // the arg is a scalar type 30 | if (typeof defaultPropArg.value !== "object") { 31 | defaultValue = defaultPropArg.value; 32 | break; 33 | } 34 | 35 | // the arg is KeyValue type 36 | if (isKeyValue(defaultPropArg.value)) { 37 | // only handle the case where the value is a primitive value 38 | if (typeof defaultPropArg.value.value !== "object") { 39 | defaultValue = defaultPropArg.value.value; 40 | } 41 | 42 | break; 43 | } 44 | 45 | // the arg is a function type 46 | if (isFunNodeType(defaultPropArg.value)) { 47 | defaultValue = defaultPropArg.value.name; 48 | // check if it is auto-incrementable 49 | if (defaultPropArg.value.name === "autoincrement") { 50 | incrementable = true; 51 | } 52 | break; 53 | } 54 | } 55 | break; 56 | 57 | case PrismaFieldAttributeType.id: 58 | isPrimary = true; 59 | break; 60 | 61 | case PrismaFieldAttributeType.unique: 62 | isUniqueField = true; 63 | break; 64 | default: 65 | break; 66 | } 67 | } 68 | 69 | return { 70 | dbdefault: defaultValue, 71 | increment: incrementable, 72 | pk: isPrimary, 73 | unique: isUniqueField, 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/getFieldTypeName.ts: -------------------------------------------------------------------------------- 1 | import type { Field } from "@mrleebo/prisma-ast"; 2 | 3 | export const getFieldTypeName = (fieldType: Field["fieldType"]): string => { 4 | return typeof fieldType === "string" ? fieldType : fieldType.name; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/intermediate/createIntermediateSchema.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableEnum } from "shared/types/tableSchema"; 2 | 3 | import { isEnumNode, isModelNode, isTypeNode } from "../../isTypeOf"; 4 | import { enumNodeToJSONTableEnum } from "../enumNodeToJSONTableEnum"; 5 | 6 | import { formatIntermediateTable } from "./formatIntermediateTable"; 7 | 8 | import type { Schema } from "@mrleebo/prisma-ast"; 9 | 10 | import { 11 | type RawRelationInfo, 12 | type IntermediateSchema, 13 | type IntermediateTable, 14 | type RelationType, 15 | } from "@/types/intermediateFormattedNode"; 16 | 17 | export const createIntermediateSchema = ( 18 | nodes: Schema["list"], 19 | ): IntermediateSchema => { 20 | const enums: JSONTableEnum[] = []; 21 | const tables: IntermediateTable[] = []; 22 | const enumsNames = new Set(); 23 | const types = new Set(); 24 | const rawRelations: RawRelationInfo[] = []; 25 | const inverseRelationMap = new Map(); 26 | const tablesNames = new Set(); 27 | 28 | const registerInverseRelation = (name: string, type: RelationType): void => { 29 | inverseRelationMap.set(name, type); 30 | }; 31 | 32 | const registerRawRelation = (info: RawRelationInfo): void => { 33 | rawRelations.push(info); 34 | }; 35 | 36 | for (const node of nodes) { 37 | if (isEnumNode(node)) { 38 | const _enum = enumNodeToJSONTableEnum(node); 39 | enumsNames.add(_enum.name); 40 | enums.push(_enum); 41 | } 42 | 43 | if (isModelNode(node)) { 44 | tablesNames.add(node.name); 45 | tables.push( 46 | formatIntermediateTable( 47 | node, 48 | registerRawRelation, 49 | registerInverseRelation, 50 | ), 51 | ); 52 | } 53 | 54 | if (isTypeNode(node)) { 55 | types.add(node.name); 56 | } 57 | } 58 | 59 | return { 60 | enums, 61 | tables, 62 | enumsNames, 63 | rawRelations, 64 | inverseRelationMap, 65 | tablesNames, 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/intermediate/formatIntermediateField.ts: -------------------------------------------------------------------------------- 1 | import { getFieldTypeName } from "../getFieldTypeName"; 2 | import { getFieldConfig } from "../getFieldConfig"; 3 | 4 | import type { IntermediateField } from "@/types/intermediateFormattedNode"; 5 | import type { Field } from "@mrleebo/prisma-ast"; 6 | 7 | export const formatIntermediateTableField = ( 8 | node: Field, 9 | ): IntermediateField => { 10 | const fieldConfig = getFieldConfig(node.attributes); 11 | 12 | return { 13 | name: node.name, 14 | type: { 15 | type_name: getFieldTypeName(node.fieldType), 16 | many: node.array, 17 | }, 18 | not_null: node.optional === undefined ? true : !node.optional, 19 | ...fieldConfig, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/intermediate/formatIntermediateTable.ts: -------------------------------------------------------------------------------- 1 | import { type Model } from "@mrleebo/prisma-ast"; 2 | 3 | import { isField } from "../../isTypeOf"; 4 | 5 | import { formatIntermediateTableField } from "./formatIntermediateField"; 6 | import { lookForRelation } from "./lookForRelation"; 7 | 8 | import { 9 | type IntermediateTable, 10 | type RawRelationInfo, 11 | type RelationType, 12 | } from "@/types/intermediateFormattedNode"; 13 | 14 | export const formatIntermediateTable = ( 15 | node: Model, 16 | registerRawRelation: (info: RawRelationInfo) => void, 17 | registerInverseRelation: (name: string, info: RelationType) => void, 18 | ): IntermediateTable => { 19 | const fields: IntermediateTable["fields"] = []; 20 | 21 | for (const mayAField of node.properties) { 22 | if (!isField(mayAField)) continue; 23 | 24 | const field = formatIntermediateTableField(mayAField); 25 | lookForRelation( 26 | mayAField, 27 | node.name, 28 | registerRawRelation, 29 | registerInverseRelation, 30 | ); 31 | 32 | fields.push(field); 33 | } 34 | 35 | return { 36 | fields, 37 | name: node.name, 38 | indexes: [], // TODO : also format indexes, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/intermediateFieldToJSONTableField.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableField } from "shared/types/tableSchema"; 2 | 3 | import { computeKey } from "../computeKey"; 4 | 5 | import type { 6 | FieldRelationsMap, 7 | IntermediateField, 8 | } from "@/types/intermediateFormattedNode"; 9 | 10 | export const intermediateFieldToJSONTableField = ( 11 | tableName: string, 12 | intermediateField: IntermediateField, 13 | enumsSet: Set, 14 | fieldRelationTable: FieldRelationsMap, 15 | ): JSONTableField => { 16 | const isEnum = enumsSet.has(intermediateField.type.type_name); 17 | 18 | const keyInFieldRelationTable = computeKey(tableName, intermediateField.name); 19 | const relationship = fieldRelationTable.get(keyInFieldRelationTable); 20 | 21 | const field: JSONTableField = { 22 | ...intermediateField, 23 | type: { 24 | type_name: 25 | intermediateField.type.many === true 26 | ? `${intermediateField.type.type_name} [ ]` 27 | : intermediateField.type.type_name, 28 | is_enum: isEnum, 29 | }, 30 | is_relation: relationship !== undefined && relationship.length > 0, 31 | relational_tables: relationship, 32 | }; 33 | 34 | return field; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/intermediateTableToJSONTableTable.ts: -------------------------------------------------------------------------------- 1 | import { type JSONTableTable } from "shared/types/tableSchema"; 2 | 3 | import { intermediateFieldToJSONTableField } from "./intermediateFieldToJSONTableField"; 4 | 5 | import { 6 | type FieldRelationsMap, 7 | type IntermediateTable, 8 | } from "@/types/intermediateFormattedNode"; 9 | 10 | export const intermediateTableToJSONTableTable = ( 11 | { fields: intermediateFields, name, indexes }: IntermediateTable, 12 | enumsSet: Set, 13 | fieldRelationTable: FieldRelationsMap, 14 | tablesSet: Set, 15 | ): JSONTableTable => { 16 | const fields: JSONTableTable["fields"] = []; 17 | 18 | for (let index = 0; index < intermediateFields.length; index++) { 19 | const intermediateField = intermediateFields[index]; 20 | 21 | /* 22 | VirtualReferenceField is field that not a column in the database 23 | but just a reference field for Prisma to allow navigation between 24 | relationships. 25 | */ 26 | const isVirtualReferenceField = tablesSet.has( 27 | intermediateField.type.type_name, 28 | ); 29 | if (isVirtualReferenceField) { 30 | continue; 31 | } 32 | 33 | const field = intermediateFieldToJSONTableField( 34 | name, 35 | intermediateField, 36 | enumsSet, 37 | fieldRelationTable, 38 | ); 39 | fields.push(field); 40 | } 41 | 42 | return { name, fields, indexes }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/prismaASTToJSONTableSchema.ts: -------------------------------------------------------------------------------- 1 | import { type Schema } from "@mrleebo/prisma-ast"; 2 | import { type JSONTableSchema } from "shared/types/tableSchema"; 3 | 4 | import { createIntermediateSchema } from "./intermediate/createIntermediateSchema"; 5 | import { createRefsAndFieldRelationsArray } from "./createRefsFromPrismaASTNodes"; 6 | import { intermediateTableToJSONTableTable } from "./intermediateTableToJSONTableTable"; 7 | 8 | export const prismaASTToJSONTableSchema = ( 9 | prismaAST: Schema, 10 | ): JSONTableSchema => { 11 | const { 12 | enums, 13 | tables: intermediateTables, 14 | enumsNames, 15 | rawRelations, 16 | inverseRelationMap, 17 | tablesNames, 18 | } = createIntermediateSchema(prismaAST.list); 19 | 20 | const [refs, fieldRelationsArray] = createRefsAndFieldRelationsArray( 21 | rawRelations, 22 | inverseRelationMap, 23 | tablesNames, 24 | ); 25 | 26 | const tables = intermediateTables.map((intermediateTable) => { 27 | return intermediateTableToJSONTableTable( 28 | intermediateTable, 29 | enumsNames, 30 | fieldRelationsArray, 31 | tablesNames, 32 | ); 33 | }); 34 | 35 | return { tables, enums, refs }; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/src/utils/transformers/tests/enumNodeToJSONTableEnum.test.ts: -------------------------------------------------------------------------------- 1 | import { enumNodeToJSONTableEnum } from "../enumNodeToJSONTableEnum"; 2 | 3 | import { colorEnum } from "@/tests/data"; 4 | 5 | describe("transform dbml enum to json table enum", () => { 6 | test("transform enum", () => { 7 | expect(enumNodeToJSONTableEnum(colorEnum)).toMatchObject({ 8 | name: colorEnum.name, 9 | values: [ 10 | { 11 | name: "Red", 12 | }, 13 | ], 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/prisma-to-json-table-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "baseUrl": "./src", 7 | "paths": { 8 | "@/*": ["./*"] 9 | }, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "skipLibCheck": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": [ 11 | "warn", 12 | { 13 | "selector": "import", 14 | "format": ["camelCase", "PascalCase"] 15 | } 16 | ], 17 | "@typescript-eslint/semi": "warn", 18 | "curly": "warn", 19 | "eqeqeq": "warn", 20 | "no-throw-literal": "warn", 21 | "semi": "off" 22 | }, 23 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | *.vsix 16 | extension/** 17 | index.html 18 | vite.config.js -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "prisma-erd-visualizer" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [0.3.0] 8 | 9 | ### Added 10 | 11 | - Showing diagnostics error directly on the code editor lines instead showing toast messages 12 | - Showing `not_null` label for not null columns 13 | 14 | ## [0.2.0] 15 | 16 | ### Added 17 | 18 | - Improve auto layout with dagrejs 19 | 20 | ## [0.1.1] 21 | 22 | ### Fixed 23 | 24 | - Prevent table names from being truncated for long table name 25 | 26 | ## [0.1.0] 27 | 28 | ### Fixed 29 | 30 | - Improve description 31 | 32 | ## [0.0.1] 33 | 34 | ### Added 35 | 36 | - Create diagram from Prisma code 37 | - Support light and dark theme 38 | - Save and restore tables positions on exiting 39 | - Save and restore stage position on exiting 40 | - Ability to toggle table visualization mode. Display all columns, relational columns only or table headers only by [@tv-long](https://github.com/tv-long) 41 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 DBSchemaVisualizer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE 22 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/README.md: -------------------------------------------------------------------------------- 1 | # Prisma ERD Visualizer 2 | 3 | Allow to visualize the database schema in ERD ( Entity Relationship Diagram ) from .prisma file in your vscode. 4 | 5 | ## Features 6 | 7 | ![Demo](https://github.com/user-attachments/assets/12fbebff-afc7-4ef3-97b8-5ee844c93d3c) 8 | 9 | - Create Entity Relationship Diagram from your prisma file 10 | - Allow you to drag diagrams 11 | - Support both light and dark themes 12 | 13 | ## Tutorial 14 | 15 | This [tutorial](https://juste.bocovo.me/visualize-the-entity-relationship-diagram-from-prisma-code-in-the-vscode-editor) shows how to use it. 16 | 17 | ## Extension Settings 18 | 19 | The following Visual Studio Code settings are available for the extension. 20 | 21 | - `prismaERDPreviewer.preferredTheme`: This configuration define the theme to use. There are two different theme the `light` and `dark`. The default theme is `dark`. 22 | 23 | ## Release Notes 24 | 25 | Release notes are [here](./CHANGELOG.md) 26 | 27 | ## Author 28 | 29 | [@BOCOVO](https://github.com/BOCOVO) 30 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/assets/icons/open-preview-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/assets/icons/open-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/assets/icons/preview-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/assets/icons/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOCOVO/db-schema-visualizer/0769e64dd03bedc103fe9e5e82e3ec3eed5bd5ca/packages/prisma-vs-code-extension/assets/logo.png -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/extension/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const WEB_VIEW_NAME = "prisma-preview-webview"; 2 | export const WEB_VIEW_TITLE = "Prisma Diagram Preview"; 3 | export const EXTENSION_CONFIG_SESSION = "prismaERDPreviewer"; 4 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/extension/index.ts: -------------------------------------------------------------------------------- 1 | import { commands, type ExtensionContext } from "vscode"; 2 | 3 | import { parsePrismaToJSON } from "prisma-to-json-table-schema"; 4 | 5 | import { MainPanel } from "extension-shared/extension/views/panel"; 6 | import { 7 | EXTENSION_CONFIG_SESSION, 8 | WEB_VIEW_NAME, 9 | WEB_VIEW_TITLE, 10 | } from "@/extension/constants"; 11 | 12 | export function activate(context: ExtensionContext): void { 13 | // Add command to the extension context 14 | context.subscriptions.push( 15 | commands.registerCommand( 16 | "prisma-erd-visualizer.previewDiagrams", 17 | async () => { 18 | lunchExtension(context); 19 | }, 20 | ), 21 | ); 22 | } 23 | 24 | const lunchExtension = (context: ExtensionContext): void => { 25 | MainPanel.render({ 26 | context, 27 | extensionConfigSession: EXTENSION_CONFIG_SESSION, 28 | webviewConfig: { 29 | name: WEB_VIEW_NAME, 30 | title: WEB_VIEW_TITLE, 31 | }, 32 | parser: parsePrismaToJSON, 33 | fileExt: "prisma", 34 | }); 35 | }; 36 | 37 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 38 | export function deactivate() {} 39 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createExtensionApp } from "extension-shared/src/index"; 2 | 3 | createExtensionApp(); 4 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "Node16", 5 | "target": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "./", 9 | "baseUrl": "./", 10 | "strict": true, 11 | "jsx": "react-jsx", 12 | "paths": { 13 | "@/*": ["./*"] 14 | }, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": ["node_modules", "vite.config.js"], 18 | "include": ["**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/prisma-vs-code-extension/vite.config.js: -------------------------------------------------------------------------------- 1 | import vscode from "@tomjs/vite-plugin-vscode"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [tsconfigPaths(), react(), vscode({})], 9 | }); 10 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared types and utilities 2 | 3 | This packages holds commons types and utilities shared by the other packages 4 | -------------------------------------------------------------------------------- /packages/shared/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | }; 6 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@dbml/core": "^3.4.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/shared/types/diagnostic.ts: -------------------------------------------------------------------------------- 1 | interface Range { 2 | line: number; 3 | column: number; 4 | } 5 | 6 | interface DiagnosticLocation { 7 | start: Range; 8 | end: Range; 9 | } 10 | 11 | export class DiagnosticError extends Error { 12 | constructor( 13 | public readonly location: DiagnosticLocation, 14 | public readonly message: string, 15 | ) { 16 | super(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/shared/types/tableSchema.ts: -------------------------------------------------------------------------------- 1 | import type Endpoint from "@dbml/core/types/model_structure/endpoint"; 2 | import type Enum from "@dbml/core/types/model_structure/enum"; 3 | import type Field from "@dbml/core/types/model_structure/field"; 4 | import type IndexColumn from "@dbml/core/types/model_structure/indexColumn"; 5 | import type Index from "@dbml/core/types/model_structure/indexes"; 6 | import type Table from "@dbml/core/types/model_structure/table"; 7 | import type { PartialRequired } from "./utils"; 8 | 9 | export interface JSONTableSchema { 10 | refs: JSONTableRef[]; 11 | enums: JSONTableEnum[]; 12 | tables: JSONTableTable[]; 13 | } 14 | 15 | export interface JSONTableEnum 16 | extends PartialRequired, "name"> { 17 | values: Array<{ name: string; note?: string }>; 18 | } 19 | 20 | export interface JSONTableRef { 21 | name?: string | null; 22 | endpoints: Array>; 23 | } 24 | 25 | export interface JSONTableField 26 | extends PartialRequired< 27 | Pick< 28 | Field, 29 | "name" | "pk" | "unique" | "note" | "increment" | "not_null" | "dbdefault" 30 | >, 31 | "name" 32 | > { 33 | type: { type_name: string; is_enum: boolean }; 34 | is_relation: boolean; 35 | relational_tables?: string[] | null; 36 | } 37 | 38 | export interface JSONTableIndexColumn 39 | extends Pick {} 40 | 41 | export interface JSONTableIndex 42 | extends Partial> { 43 | // the parser return `pk` as boolean not string 44 | pk?: boolean; 45 | columns: JSONTableIndexColumn[]; 46 | } 47 | 48 | export interface JSONTableTable 49 | extends PartialRequired< 50 | Pick, 51 | "name" 52 | > { 53 | fields: JSONTableField[]; 54 | indexes: JSONTableIndex[]; 55 | } 56 | -------------------------------------------------------------------------------- /packages/shared/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type PartialRequired = Required> & Partial>; 2 | -------------------------------------------------------------------------------- /packages/shared/utils/computeRelationalFieldKey.ts: -------------------------------------------------------------------------------- 1 | export const computeRelationalFieldKey = ( 2 | tableName: string, 3 | fieldName: string, 4 | ): string => { 5 | return `${tableName}.${fieldName}`; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/shared/utils/tests/computeRelationalFieldKey.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOCOVO/db-schema-visualizer/0769e64dd03bedc103fe9e5e82e3ec3eed5bd5ca/packages/shared/utils/tests/computeRelationalFieldKey.test.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "strictFunctionTypes": true, 10 | "strictBindCallApply": true, 11 | "strictPropertyInitialization": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "skipLibCheck": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "exclude": ["node_modules"], 20 | "include": ["**/*.ts", "**/*.tsx"] 21 | } 22 | --------------------------------------------------------------------------------