diff --git a/.env b/.env
new file mode 100644
index 000000000..4a46383ec
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+VITE_IS_CHARTDB_IO=false
+VITE_APP_URL=http://localhost:5173
+VITE_HOST_URL=http://localhost:5173
+VITE_HIDE_CHARTDB_CLOUD=true
+VITE_DISABLE_ANALYTICS=true
diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..2f6f792f9
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+# Vite environment variables (sample)
+VITE_OPENAI_API_KEY=
+VITE_OPENAI_API_ENDPOINT=https://api.openai.com/v1
+VITE_LLM_MODEL_NAME=gpt-4o-mini
+VITE_IS_CHARTDB_IO=false
+VITE_APP_URL=http://localhost:5173
+VITE_HOST_URL=http://localhost:5173
+VITE_HIDE_CHARTDB_CLOUD=true
+VITE_DISABLE_ANALYTICS=true
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 000000000..9d46490da
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,51 @@
+name: custom docker build
+
+on:
+ push:
+ branches:
+ - dev
+ - 'feat-*'
+ tags:
+ - docker-run-*
+ workflow_dispatch:
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: derdeno/chartdb
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Log in to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Build and push (dev)
+ if: github.ref == 'refs/heads/dev'
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.run_number }}
+
+ - name: Build and push (feat-* branches)
+ if: startsWith(github.ref, 'refs/heads/feat-')
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index b6bf41a10..000000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: CI
-
-on:
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Use Node.js 22.x
- uses: actions/setup-node@v4
- with:
- node-version: 22.x
-
- - name: Install dependencies
- run: npm ci
-
- - name: Lint
- run: npm run lint
-
- - name: Build
- run: npm run build
-
- - name: Run tests
- run: npm run test:ci
\ No newline at end of file
diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml
deleted file mode 100644
index 0147e36a3..000000000
--- a/.github/workflows/cla.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: "CLA Assistant"
-on:
- issue_comment:
- types: [created]
- pull_request_target:
- types: [opened,closed,synchronize]
-
-permissions:
- actions: write
- contents: read
- pull-requests: write
- statuses: write
-
-
-jobs:
- CLAAssistant:
- runs-on: ubuntu-latest
- steps:
- - name: "CLA Assistant"
- if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
- # Beta Release
- uses: contributor-assistant/github-action@v2.6.1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- PERSONAL_ACCESS_TOKEN: ${{ secrets.CHARTDB_CLA_SIGNATURES_PAT }}
- with:
- remote-organization-name: 'chartdb'
- remote-repository-name: 'cla-signatures'
- path-to-signatures: 'signatures/version1/cla.json'
- path-to-document: 'https://github.com/chartdb/chartdb/blob/main/CLA.md'
- # branch should not be protected
- branch: 'main'
- allowlist:
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
deleted file mode 100644
index 21c32c9e0..000000000
--- a/.github/workflows/publish.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-name: Publish
-
-on:
- push:
- tags:
- - 'v*'
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository }}
-
-jobs:
- build-and-publish:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- id-token: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Use Node.js 22.x
- uses: actions/setup-node@v4
- with:
- node-version: 22.x
-
- - name: Log in to the Container registry
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Install dependencies
- run: npm ci
-
- - name: Lint
- run: npm run lint
-
- - name: Build project
- run: npm run build
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@v4
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- tags: |
- type=semver,pattern={{version}}
-
- - name: Build and push multi-arch Docker image
- uses: docker/build-push-action@v6
- with:
- context: .
- push: true
- platforms: linux/amd64,linux/arm64
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
deleted file mode 100644
index 188984160..000000000
--- a/.github/workflows/release.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: Release
-
-on:
- push:
- branches:
- - main
-
-permissions:
- contents: write
- pull-requests: write
-
-jobs:
- release:
- runs-on: ubuntu-latest
- steps:
- - name: release-please
- id: release
- uses: googleapis/release-please-action@v4
- with:
- release-type: node
- token: ${{ secrets.CHARTDB_OSS_RELEASE }}
diff --git a/.gitignore b/.gitignore
index 9ed38eadc..0bbc92721 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,5 @@ dist-ssr
*.sln
*.sw?
-.env
-stats/
\ No newline at end of file
+stats/
+data/
diff --git a/.nvmrc b/.nvmrc
index 5fadc1f63..18c92ea98 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v22.18.0
\ No newline at end of file
+v24
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
index 56849bf56..33e51a94a 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -2,5 +2,6 @@
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
- "singleQuote": true
+ "singleQuote": true,
+ "endOfLine": "lf"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index f0e409ada..000000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,544 +0,0 @@
-# Changelog
-
-## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27)
-
-
-### Bug Fixes
-
-* add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0))
-* **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671))
-* handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872))
-
-## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26)
-
-
-### Features
-
-* add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103))
-* **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4))
-* include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992))
-* **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006))
-* support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c))
-
-
-### Bug Fixes
-
-* area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026))
-* **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037))
-* **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f))
-* **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de))
-* DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3))
-* filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02))
-* **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d))
-* **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22))
-* for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2))
-* improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1))
-* initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9))
-* **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195))
-* merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015))
-* move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51))
-* open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e))
-* preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771))
-* prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b))
-* remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c))
-* reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20))
-* **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268))
-* set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925))
-* show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb))
-* **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4))
-* **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca))
-* **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a))
-* update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4))
-
-## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
-
-
-### Features
-
-* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
-* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
-* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
-* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
-* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
-* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
-* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
-* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
-* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
-* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
-
-
-### Bug Fixes
-
-* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
-* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
-* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
-* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
-* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
-* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
-* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
-* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
-* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
-* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
-* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
-* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
-* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
-* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
-* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
-* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
-* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
-* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
-* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
-* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
-* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
-* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
-* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
-* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
-* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
-* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
-* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
-* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
-* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
-* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
-* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
-* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
-* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
-* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
-* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
-* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
-* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
-* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
-* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
-* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
-* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
-* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
-* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
-
-## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
-
-
-### Bug Fixes
-
-* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
-
-## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
-
-
-### Bug Fixes
-
-* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
-* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
-* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
-* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
-* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
-* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
-* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
-* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
-* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
-* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
-
-## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
-
-
-### Features
-
-* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
-* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
-* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
-
-
-### Bug Fixes
-
-* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
-* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
-* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
-* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
-* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
-* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
-* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
-* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
-* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
-* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
-
-## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
-
-
-### Features
-
-* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
-* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
-* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
-
-
-### Bug Fixes
-
-* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
-* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
-* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
-* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
-* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
-* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
-* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
-* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
-* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
-* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
-* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
-* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
-* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
-* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
-* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
-* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
-* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
-* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
-* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
-* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
-* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
-* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
-* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
-* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
-* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
-* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
-
-## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)
-
-
-### Features
-
-* add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b))
-* **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be))
-
-
-### Bug Fixes
-
-* **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d))
-* **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854))
-* **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a))
-* **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7))
-* **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f))
-* remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875))
-* **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791))
-
-## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
-
-
-### Features
-
-* **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37))
-
-
-### Bug Fixes
-
-* **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de))
-* **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35))
-* **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1))
-* **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c))
-* **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36))
-* **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1))
-
-## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
-
-
-### Features
-
-* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
-* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
-* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
-
-
-### Bug Fixes
-
-* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
-* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
-* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
-* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
-
-## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
-
-
-### Bug Fixes
-
-* **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76))
-* components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb))
-* **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6))
-* **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1))
-* **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511))
-* **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e))
-* **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1))
-* **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23))
-* open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496))
-* **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f))
-* **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b))
-* **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896))
-* **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3))
-
-## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13)
-
-
-### Features
-
-* **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331))
-* **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1))
-
-
-### Bug Fixes
-
-* **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3))
-* **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e))
-* **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8))
-* **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4))
-* **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41))
-* **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba))
-* **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267))
-* **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25))
-* **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67))
-* **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae))
-* **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5))
-* **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3))
-
-## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)
-
-
-### Features
-
-* **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4))
-* **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429))
-
-
-### Bug Fixes
-
-* **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c))
-* **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da))
-* **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf))
-* **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a))
-* **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09))
-* **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785))
-* **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082))
-* **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9))
-* **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852))
-* **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb))
-* **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c))
-* **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b))
-
-## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)
-
-
-### Bug Fixes
-
-* change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f))
-* **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106))
-* **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a))
-* **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953))
-* **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506))
-* **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd))
-
-## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02)
-
-
-### Features
-
-* **view-menu:** add toggle for mini map visibility ([#496](https://github.com/chartdb/chartdb/issues/496)) ([#505](https://github.com/chartdb/chartdb/issues/505)) ([8abf2a7](https://github.com/chartdb/chartdb/commit/8abf2a7bfcc36d39e60ac133b0e5e569de1bbc72))
-
-
-### Bug Fixes
-
-* add loadDiagramFromData logic to chartdb provider ([#513](https://github.com/chartdb/chartdb/issues/513)) ([ee659ea](https://github.com/chartdb/chartdb/commit/ee659eaa038a94ee13801801e84152df4d79683d))
-* **dependency:** upgrade react query to v7 - clean console warnings ([#504](https://github.com/chartdb/chartdb/issues/504)) ([7c5db08](https://github.com/chartdb/chartdb/commit/7c5db0848e49dfdb7e7120f77003d1e37f8d71b0))
-* **i18n:** translation/Arabic ([#509](https://github.com/chartdb/chartdb/issues/509)) ([4b43f72](https://github.com/chartdb/chartdb/commit/4b43f720e90e49d5461e68d188e3865000f52497))
-
-## [1.5.1](https://github.com/chartdb/chartdb/compare/v1.5.0...v1.5.1) (2024-12-15)
-
-
-### Bug Fixes
-
-* **export:** fix SQL server field.nullable type to boolean ([#486](https://github.com/chartdb/chartdb/issues/486)) ([a151f56](https://github.com/chartdb/chartdb/commit/a151f56b5d950e0b5cc54363684ada95889024b3))
-* **readme:** Update README.md - add CockroachDB ([#482](https://github.com/chartdb/chartdb/issues/482)) ([2b6b733](https://github.com/chartdb/chartdb/commit/2b6b73326155f18d6d56779c0657a3506e2d2cde))
-
-## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11)
-
-
-### Features
-
-* **CockroachDB:** Add CockroachDB support ([#472](https://github.com/chartdb/chartdb/issues/472)) ([5409288](https://github.com/chartdb/chartdb/commit/54092883883b135f6ace51d86754b1df76603d30))
-* **i18n:** translate share and dialog sections in Indonesian locale files ([#468](https://github.com/chartdb/chartdb/issues/468)) ([3574cec](https://github.com/chartdb/chartdb/commit/3574cecc7c73dcab404b82115d20e1ad0cd26b37))
-
-
-### Bug Fixes
-
-* **core:** fix update diagram id ([#477](https://github.com/chartdb/chartdb/issues/477)) ([348f805](https://github.com/chartdb/chartdb/commit/348f80568e0f686ee478147fdc43a5d43b5c1ebb))
-* **dialogs:** fix footer position on dialogs ([#470](https://github.com/chartdb/chartdb/issues/470)) ([2309306](https://github.com/chartdb/chartdb/commit/2309306ef590783b00a2489209092107dd9a3788))
-* **sql-server import:** nullable should be boolean instead of string ([#480](https://github.com/chartdb/chartdb/issues/480)) ([635fb53](https://github.com/chartdb/chartdb/commit/635fb53c9f7ebd1e5ef4d9274af041edc08f04c3))
-
-## [1.4.0](https://github.com/chartdb/chartdb/compare/v1.3.1...v1.4.0) (2024-12-02)
-
-
-### Features
-
-* **add templates:** add six more templates ([#452](https://github.com/chartdb/chartdb/issues/452)) ([be1b109](https://github.com/chartdb/chartdb/commit/be1b109f23e62df4cc63fa8914c2754f7809cc08))
-* **add templates:** add six more templates (django-axes, laravel-activitylog, octobox, pay-rails, pixelfed, polr) ([#460](https://github.com/chartdb/chartdb/issues/460)) ([03772f6](https://github.com/chartdb/chartdb/commit/03772f6b4f99f9c4350356aa0f2a4666f4f1794d))
-* **add templates:** add six more templates (reversion, screeenly, staytus, deployer, devise, talk) ([#457](https://github.com/chartdb/chartdb/issues/457)) ([ddeef3b](https://github.com/chartdb/chartdb/commit/ddeef3b134efa893e1c1e15e2f87c27157200e2d))
-* **clickhouse:** add ClickHouse support ([#463](https://github.com/chartdb/chartdb/issues/463)) ([807cd22](https://github.com/chartdb/chartdb/commit/807cd22e0c739f339fa07fe1d2f043c5411ae41f))
-* **i18n:** Added bangla translations ([#432](https://github.com/chartdb/chartdb/issues/432)) ([885eb71](https://github.com/chartdb/chartdb/commit/885eb719de577c2652fbed1ed287f38fcc98c148))
-* **side-panel:** Add functionality of order tables by drag & drop ([#425](https://github.com/chartdb/chartdb/issues/425)) ([a0e966b](https://github.com/chartdb/chartdb/commit/a0e966b64f8070d4595d47b2fb39e8bbf427b794))
-
-
-### Bug Fixes
-
-* **clipboard:** defensive for navigator clipboard ([#462](https://github.com/chartdb/chartdb/issues/462)) ([5fc10a7](https://github.com/chartdb/chartdb/commit/5fc10a7e649fc5877bb297b519b1b6a8b81f1323))
-* **import-database:** update database type after importing into an existing generic diagra ([#456](https://github.com/chartdb/chartdb/issues/456)) ([a8fe491](https://github.com/chartdb/chartdb/commit/a8fe491c1b5a30d9f4144cefa9111dd3dfd5df1a))
-* **Last Saved:** Translate the "last saved" relative date message ([#400](https://github.com/chartdb/chartdb/issues/400)) ([d45677e](https://github.com/chartdb/chartdb/commit/d45677e92d72efc6cea8f865ce46f0be6ec9961f))
-* **mariadb-types:** Add uuid data type ([#459](https://github.com/chartdb/chartdb/issues/459)) ([94656ec](https://github.com/chartdb/chartdb/commit/94656ec7a5435c2da262fb3bc6a6d381d554b0c1))
-* window type ([#454](https://github.com/chartdb/chartdb/issues/454)) ([9c7d03c](https://github.com/chartdb/chartdb/commit/9c7d03c285ff6f818eef3199c9b7a530d03a1fec))
-
-## [1.3.1](https://github.com/chartdb/chartdb/compare/v1.3.0...v1.3.1) (2024-11-26)
-
-
-### Bug Fixes
-
-* **docker:** make OPENAI_API_KEY optional in docker run ([#448](https://github.com/chartdb/chartdb/issues/448)) ([4bb4766](https://github.com/chartdb/chartdb/commit/4bb4766e1ac8d69e138668eb8a46de5affe62ceb))
-
-## [1.3.0](https://github.com/chartdb/chartdb/compare/v1.2.0...v1.3.0) (2024-11-25)
-
-
-### Features
-
-* **side panel:** collapsible side panel on desktop view + keyboard shortcut ([#439](https://github.com/chartdb/chartdb/issues/439)) ([70f545f](https://github.com/chartdb/chartdb/commit/70f545f78bab9c510a6e5936fa5b259b806b6c69))
-
-
-### Bug Fixes
-
-* **dialogs:** fix height of dialogs for small screens ([#440](https://github.com/chartdb/chartdb/issues/440)) ([667685e](https://github.com/chartdb/chartdb/commit/667685ed0f6a8cc61ae86b3ba60e052fbe6a9e1a))
-* **drawer:** set fix min size ([#429](https://github.com/chartdb/chartdb/issues/429)) ([c5e0ea6](https://github.com/chartdb/chartdb/commit/c5e0ea6fa4017666ff3bc1e3071c487df48afd3d))
-* **export-sql:** add unique to export script ([#422](https://github.com/chartdb/chartdb/issues/422)) ([b75c6fe](https://github.com/chartdb/chartdb/commit/b75c6fe4e78f3e2058be680f2fa0442db3b4a6bd))
-* fix layout warnings ([#434](https://github.com/chartdb/chartdb/issues/434)) ([94ec43b](https://github.com/chartdb/chartdb/commit/94ec43b60845bb8c3592ce1b1450ca0171a53f99))
-* **i18n:** add bahasa indonesia translation ([#331](https://github.com/chartdb/chartdb/issues/331)) ([ab07da0](https://github.com/chartdb/chartdb/commit/ab07da0b031f0d4050ff6b44ddcb94cb6c0010b6))
-* **i18n:** add missing type to vi.ts ([#444](https://github.com/chartdb/chartdb/issues/444)) ([e77ee60](https://github.com/chartdb/chartdb/commit/e77ee60a5b47e0854d11b0ee2f16d6956737d0ff))
-* **i18n:** Add Telugu Language ([#352](https://github.com/chartdb/chartdb/issues/352)) ([8749591](https://github.com/chartdb/chartdb/commit/8749591be036e131de4bfeed1e6eece8d62980dd))
-* **i18n:** add Turkish translations ([#315](https://github.com/chartdb/chartdb/issues/315)) ([d9fcbee](https://github.com/chartdb/chartdb/commit/d9fcbeec726b7bde9f7d202bf09dc6b617e3ad80))
-* **i18n:** add Vietnamese translations ([#435](https://github.com/chartdb/chartdb/issues/435)) ([6c65c2e](https://github.com/chartdb/chartdb/commit/6c65c2e9cce600b9778b84ce5b5f1625dc6f1a58))
-* **i18n:** Translating to Gujarati language ([#433](https://github.com/chartdb/chartdb/issues/433)) ([2940431](https://github.com/chartdb/chartdb/commit/2940431efa1a6aa54d80c61d5e05f0ad47cd67ba))
-* **i18n:** Translation of the export error into Russian ([#418](https://github.com/chartdb/chartdb/issues/418)) ([7c3c628](https://github.com/chartdb/chartdb/commit/7c3c62860efc98d3aabf2132a79ac945ffc8315a))
-* **i18n:** update korean for 1.2.0 ([#419](https://github.com/chartdb/chartdb/issues/419)) ([8397bef](https://github.com/chartdb/chartdb/commit/8397bef3924610d94661aae99c55ba4fa376a186))
-* **import script:** remove double quotes ([#442](https://github.com/chartdb/chartdb/issues/442)) ([fb702c8](https://github.com/chartdb/chartdb/commit/fb702c87ce5254bf6e0209c692305f5086956090))
-* **share:** fix export to handle broken indexes & relationships ([#416](https://github.com/chartdb/chartdb/issues/416)) ([4be3592](https://github.com/chartdb/chartdb/commit/4be3592cf4d160be83ddf1db01ffe9afdef119fa))
-* **templates:** add Five more templates (bouncer, cabot, feedbin, Pythonic, flarum, freescout) ([#441](https://github.com/chartdb/chartdb/issues/441)) ([eaa0678](https://github.com/chartdb/chartdb/commit/eaa067814fd96fcc1ee10488ee747a71a8e8ec7a))
-
-## [1.2.0](https://github.com/chartdb/chartdb/compare/v1.1.0...v1.2.0) (2024-11-17)
-
-
-### Features
-
-* **duplicate table:** duplicate table from the canvas and sidebar ([#404](https://github.com/chartdb/chartdb/issues/404)) ([44cf5ca](https://github.com/chartdb/chartdb/commit/44cf5ca264f52851f2dffb51a752a52b6fa7ec8d))
-
-
-### Bug Fixes
-
-* **AI exports:** add cahching layer to SQL exports ([#390](https://github.com/chartdb/chartdb/issues/390)) ([e5dbbf2](https://github.com/chartdb/chartdb/commit/e5dbbf2eaab6d80a531d451211b6f5a415bc7ce3))
-* **canvas:** fix auto zoom on diagram load ([#395](https://github.com/chartdb/chartdb/issues/395)) ([492c932](https://github.com/chartdb/chartdb/commit/492c9324d27b561470c4967ce2e99f82eec467d8))
-* **dockerfile:** support SPA refresh to resolve nginx return 404 ([#384](https://github.com/chartdb/chartdb/issues/384)) ([eaf75ce](https://github.com/chartdb/chartdb/commit/eaf75cedb0e024236c7684bb533856d7f80074da))
-* **docs:** update license reference ([#403](https://github.com/chartdb/chartdb/issues/403)) ([44d10c2](https://github.com/chartdb/chartdb/commit/44d10c23907165288951a9d2ec3165ad23f81c61))
-* **export image:** Add support for displaying cardinality relationships + background ([#407](https://github.com/chartdb/chartdb/issues/407)) ([68474e7](https://github.com/chartdb/chartdb/commit/68474e75d56ed4b4b445cc9b7f59cca96a4ca5db))
-* **i18n:** add Nepali translations ([#406](https://github.com/chartdb/chartdb/issues/406)) ([e1e55c4](https://github.com/chartdb/chartdb/commit/e1e55c4b2ac7755b0810dc1f21da44903fe68a54))
-* **i18n:** change language keeps selected language also after refreshing the page ([#409](https://github.com/chartdb/chartdb/issues/409)) ([f35f62f](https://github.com/chartdb/chartdb/commit/f35f62fdf38ca84065f171a31b80aa8123b1d8b9))
-* **i18n:** Create Translations in Marathi language ([#266](https://github.com/chartdb/chartdb/issues/266)) ([c6f7ff7](https://github.com/chartdb/chartdb/commit/c6f7ff70f841efb9cf1766338f409fe0ea7bb998))
-* **i18n:** fix language nav: close when lang selected, hide tooltip when lang selected ([#411](https://github.com/chartdb/chartdb/issues/411)) ([02aaabd](https://github.com/chartdb/chartdb/commit/02aaabdc4e9b1570d81ff03fe1e6da0307f22999))
-* **templates:** add five more templates (Sylius, Monica, Attendize, SaaS Pegasus & BookStack) ([#408](https://github.com/chartdb/chartdb/issues/408)) ([0f67394](https://github.com/chartdb/chartdb/commit/0f673947af469e86f70737427ac8fb3c2420d1a2))
-* **templates:** add six more templates (ticketit, snipe-it, refinerycms, comfortable-mexican-sofa, buddypress, lobsters) ([#402](https://github.com/chartdb/chartdb/issues/402)) ([07d3745](https://github.com/chartdb/chartdb/commit/07d374574775d132e1cba0908c47dcbbd6cd2c3f))
-* **templates:** fix cloned indexes from a template ([#398](https://github.com/chartdb/chartdb/issues/398)) ([9f8500f](https://github.com/chartdb/chartdb/commit/9f8500fc7e36e6a819ecb9029f263d80eac88279))
-* **templates:** fix tags urls ([#405](https://github.com/chartdb/chartdb/issues/405)) ([fe8b9f9](https://github.com/chartdb/chartdb/commit/fe8b9f9e91481d8a3272113b6f4be4da8d61ad04))
-* **templates:** tag urls lowercase to support browsers ([#397](https://github.com/chartdb/chartdb/issues/397)) ([959e540](https://github.com/chartdb/chartdb/commit/959e5402b8c112fae6243ce9283947057506c128))
-
-## [1.1.0](https://github.com/chartdb/chartdb/compare/v1.0.1...v1.1.0) (2024-11-13)
-
-
-### Features
-
-* **add templates:** add five more templates (laravel, django, twitter… ([#371](https://github.com/chartdb/chartdb/issues/371)) ([20b3396](https://github.com/chartdb/chartdb/commit/20b3396ec2afff09ca8bcdd91f5c6284c93cd959))
-* **canvas:** Added Snap to grid functionality. Toggle/hold shift to enable snap to grid. ([#373](https://github.com/chartdb/chartdb/issues/373)) ([6c7eb46](https://github.com/chartdb/chartdb/commit/6c7eb4609d8466278de30317665929ec529c1f94))
-* **share:** add sharing capabilities to import and export diagrams ([#365](https://github.com/chartdb/chartdb/issues/365)) ([94a5d84](https://github.com/chartdb/chartdb/commit/94a5d84fae819b0de6c1e471d1aad16dc8f39dd6))
-
-
-### Bug Fixes
-
-* **bundle:** fix bundle size ([#382](https://github.com/chartdb/chartdb/issues/382)) ([4ca1832](https://github.com/chartdb/chartdb/commit/4ca18327324106950f0d1af851b9b74379b67b7b))
-* **dockerfile:** support openai key in docker build ([#366](https://github.com/chartdb/chartdb/issues/366)) ([545e857](https://github.com/chartdb/chartdb/commit/545e8578c9e8aa71696f6aa8bec81cacaa602c2d))
-* **i18n:** add korean ([#362](https://github.com/chartdb/chartdb/issues/362)) ([b305be8](https://github.com/chartdb/chartdb/commit/b305be82aee00994ef576ca6fd62d72dd491f771))
-* **i18n:** Add simplified chinese ([#385](https://github.com/chartdb/chartdb/issues/385)) ([9f28933](https://github.com/chartdb/chartdb/commit/9f2893319a1a2aed9a7c03d15e25a17ab37c2465))
-* **i18n:** Added Russian language ([#376](https://github.com/chartdb/chartdb/issues/376)) ([2c69b08](https://github.com/chartdb/chartdb/commit/2c69b08eaea6b86ce0c1ddb18a23e22629198bf5))
-* **i18n:** added traditional Chinese language translation ([#356](https://github.com/chartdb/chartdb/issues/356)) ([123f40f](https://github.com/chartdb/chartdb/commit/123f40f39e703ad612635964af530ac72c387d3c))
-* **i18n:** Fixed part of RU lang introduced in [#365](https://github.com/chartdb/chartdb/issues/365) feat(share) ([#380](https://github.com/chartdb/chartdb/issues/380)) ([5508c1e](https://github.com/chartdb/chartdb/commit/5508c1e084e0ee24d1a54f721f760b9fc14df107))
-* **i18n:** french translation update - share menu ([#391](https://github.com/chartdb/chartdb/issues/391)) ([e3129ce](https://github.com/chartdb/chartdb/commit/e3129cec744d18f09953544d9e74cd5adc4e8afb))
-* **import json:** for Check Script Result, default with quotes ([#358](https://github.com/chartdb/chartdb/issues/358)) ([1430d2c](https://github.com/chartdb/chartdb/commit/1430d2c2365b7b74e36b8ff9d32a163d7437448a))
-* improve title name edit interaction ([#367](https://github.com/chartdb/chartdb/issues/367)) ([84e7591](https://github.com/chartdb/chartdb/commit/84e7591d0586b9a457f31737c6e363ef41574142))
-* **share:** add loader to the export ([#381](https://github.com/chartdb/chartdb/issues/381)) ([3609bfe](https://github.com/chartdb/chartdb/commit/3609bfea4d4c78b03711ff8d721b4e67bf82185a))
-* **sql export:** make loading for export interactive ([#388](https://github.com/chartdb/chartdb/issues/388)) ([125a39f](https://github.com/chartdb/chartdb/commit/125a39fb5be803f0e6db0b68fb5bc8e290fa8dae))
-* **templates:** change the template url to be database instead of db ([#374](https://github.com/chartdb/chartdb/issues/374)) ([f1d073d](https://github.com/chartdb/chartdb/commit/f1d073d05383955da6f60a9a66ed2be879b103e4))
-* **templates:** fix issue with double-clone on localhost ([#394](https://github.com/chartdb/chartdb/issues/394)) ([78c427f](https://github.com/chartdb/chartdb/commit/78c427f38e5c64fc340d13ceb2153c2b85db437e))
-
-## [1.0.1](https://github.com/chartdb/chartdb/compare/v1.0.0...v1.0.1) (2024-11-06)
-
-
-### Bug Fixes
-
-* **offline:** add support when running on isolated network ([#359](https://github.com/chartdb/chartdb/issues/359)) ([aa884b4](https://github.com/chartdb/chartdb/commit/aa884b49ce16d70f67881bdc940993c1fe901796))
-* open default diagram after deleting current diagram ([#350](https://github.com/chartdb/chartdb/issues/350)) ([87a40cf](https://github.com/chartdb/chartdb/commit/87a40cff615b04b678642ba2d6e097c38b26d239))
-* **select-box:** allow using tab & space to show choices ([#336](https://github.com/chartdb/chartdb/issues/336)) ([93f623a](https://github.com/chartdb/chartdb/commit/93f623a13a61e9143638fbe7e8346f07e37a26b2))
-* **smart query:** import postgres FKs ([#357](https://github.com/chartdb/chartdb/issues/357)) ([acb736e](https://github.com/chartdb/chartdb/commit/acb736e44fd50d29a85b4eff42e20780aef710ed))
-* **templates:** add two more templates (Airbnb, Wordpress) ([#317](https://github.com/chartdb/chartdb/issues/317)) ([ebce882](https://github.com/chartdb/chartdb/commit/ebce8827eab049eefa0eebcb0ec2540698bc0e15))
-* **templates:** align database icon ([#351](https://github.com/chartdb/chartdb/issues/351)) ([efaddee](https://github.com/chartdb/chartdb/commit/efaddeebb4f24235d82f4e2bf7423fbf48b97187))
-* **template:** separator in case of empty url ([#355](https://github.com/chartdb/chartdb/issues/355)) ([180886c](https://github.com/chartdb/chartdb/commit/180886c5882f2329c797fc284b255012d21f5b5c))
-* **templates:** fetch templates data from router ([#321](https://github.com/chartdb/chartdb/issues/321)) ([d8a20eb](https://github.com/chartdb/chartdb/commit/d8a20ebbd9118989690a40fcd3aa59fb156b446f))
-
-## 1.0.0 (2024-11-04)
-
-
-### Features
-
-* ability to change zoom or pan on scroll in the canvas component ([ac208c4](https://github.com/chartdb/chartdb/commit/ac208c47dc307fd0dee5a987bb6ccde8d0599db7))
-* add import logic based on the JSON input ([01f4e4b](https://github.com/chartdb/chartdb/commit/01f4e4bc6167c61e9c6b669a10a3f9c84ebc1774))
-* add import logic based on the JSON input ([939ac22](https://github.com/chartdb/chartdb/commit/939ac2295f676796b46417433b5ec7625be29839))
-* add release ([ac37475](https://github.com/chartdb/chartdb/commit/ac37475f370fb5e11271059aaf25ee98501d4523))
-* add release ([80491ae](https://github.com/chartdb/chartdb/commit/80491aea4f9be7b72ced96245607a87b678ead6e))
-* Added darkmode support, user and system preferences ([d63700f](https://github.com/chartdb/chartdb/commit/d63700fcfbfc4c65d4a17e93a4b5c48b0c65d9e4))
-* added japanese language translation ([#235](https://github.com/chartdb/chartdb/issues/235)) ([588543f](https://github.com/chartdb/chartdb/commit/588543f324bfbec41f1ee67da856b47cc26b1ac2))
-* change the menu to activate/deactive zoom on scroll ([a69b241](https://github.com/chartdb/chartdb/commit/a69b241d74f830ebb8f894935c154a53bba93da6))
-* disable darkmode toggle until colors are fixed, remove class from create ([e2029da](https://github.com/chartdb/chartdb/commit/e2029da189b2feee772e7d9793ce01e59365f2ca))
-* **fetcher:** add pg magic sql ([f74f208](https://github.com/chartdb/chartdb/commit/f74f208a860bf821fd9ace92ffbd91276ef6175c))
-* improve dockerfile ([48a0f4f](https://github.com/chartdb/chartdb/commit/48a0f4f240f9fb603a66454e5deb4e7708c6a15d))
-
-
-### Bug Fixes
-
-* :bug: pk_column changed to column in sqlite ([f85a2d0](https://github.com/chartdb/chartdb/commit/f85a2d086d70e9aa5c63f52a297f290cb2590967))
-* add contents permissions ([b896134](https://github.com/chartdb/chartdb/commit/b896134cae940197e6995191dd09124af30ad1a3))
-* add permissions ([16a6166](https://github.com/chartdb/chartdb/commit/16a6166b4ad35e879c73ac19a52f8678aad183a8))
-* add reference_schema to support again import with FKs ([ce8ef57](https://github.com/chartdb/chartdb/commit/ce8ef57304ab73912275bfbd60e1fee6fe4b104d))
-* add reference_schema to support again import with FKs all dbs ([4d34ade](https://github.com/chartdb/chartdb/commit/4d34ade63deb6f4469970ed4fb1f0e4045aa451a))
-* add reference_schema to the postgres import script ([48414da](https://github.com/chartdb/chartdb/commit/48414dac83e99d47f2bc195e003689f01602904f))
-* add support to MySQL versions below 8.0 ([f2f74ad](https://github.com/chartdb/chartdb/commit/f2f74ad412dfec3a5795182709a949224d37a759))
-* autofocus on mobile ([d5cb3e5](https://github.com/chartdb/chartdb/commit/d5cb3e5648203a1552d76818e73c7382b6234f3d))
-* change to on push ([866f8f5](https://github.com/chartdb/chartdb/commit/866f8f5ff1aec15a190cad8958d134e9a4ce2a43))
-* change to on push ([60f0317](https://github.com/chartdb/chartdb/commit/60f0317ce60c3a8c9dede120e6cfcbbaf4c55174))
-* change to on push ([07da6d0](https://github.com/chartdb/chartdb/commit/07da6d05cf9faa809bb9d0f8cd02751f9fb137dc))
-* change workflow name ([3770703](https://github.com/chartdb/chartdb/commit/377070391d5573ccaf81ce7bf508bd79393a3d1a))
-* change workflow name ([3b4f256](https://github.com/chartdb/chartdb/commit/3b4f2565989247abf88dabd178ad48e188268e33))
-* ci ([be04ac2](https://github.com/chartdb/chartdb/commit/be04ac2ff2b2ef17b066bd3a1228408effaa90c4))
-* docker login ([de8ca35](https://github.com/chartdb/chartdb/commit/de8ca3580bcfd15ea741a518e78d5e778a8a4ed5))
-* **i18n:** add missing German translations ([#311](https://github.com/chartdb/chartdb/issues/311)) ([b2c2045](https://github.com/chartdb/chartdb/commit/b2c20459d55c087f906305707290ac4cfc52055b))
-* permissions ([f654418](https://github.com/chartdb/chartdb/commit/f6544186d04bdb54a8afb5489ca62391f8996b1f))
-* permissions issue ([e51f4d3](https://github.com/chartdb/chartdb/commit/e51f4d3c1c5471e314c17dc90566e0c8f6e889b9))
-* remove cache ([9877dd3](https://github.com/chartdb/chartdb/commit/9877dd3c5a57bfb3e8d3f7efa8e373de3071d217))
-* remove cache ([be62368](https://github.com/chartdb/chartdb/commit/be6236877e6005bc326c78fd78529b25e6bec6cb))
-* remove effective scroll action ([1c6786b](https://github.com/chartdb/chartdb/commit/1c6786bff44b1be65af814873e40749e37353fa4))
-* remove multi platform build ([90fe199](https://github.com/chartdb/chartdb/commit/90fe199b09dd1e46b4b1b29ed9765879bf23c08b))
-* remove multi platform build ([21e1a22](https://github.com/chartdb/chartdb/commit/21e1a223bf4fd7d8198ef838801a6c068a26a5ed))
-* restrict relationship handle on views ([a1734eb](https://github.com/chartdb/chartdb/commit/a1734eb376db2642405dc46a4beede8c3f9f79de))
-* rounded table node ([21b3c91](https://github.com/chartdb/chartdb/commit/21b3c91d267f0c7f3c9de741365abc23712890a3))
-* small update on mobile, add the word Saved ([0a11b6f](https://github.com/chartdb/chartdb/commit/0a11b6f88345126180031ab7359eb941c997c83b))
-* support multi schemas when using import script in postgres ([1eff951](https://github.com/chartdb/chartdb/commit/1eff9513eff7c2e52f4752e59ad5afaed52d62eb))
-* tag ([2bc8255](https://github.com/chartdb/chartdb/commit/2bc8255c58e0fbec32aeac13e9621e7db690ac7b))
-* tag ([00d1792](https://github.com/chartdb/chartdb/commit/00d1792c733335e1c7e82e62cca0e3d3da827a68))
-* to support all postgres schemas and not only public ([f203813](https://github.com/chartdb/chartdb/commit/f203813f689e10c5096cdd1a2f4e6b1991c02f33))
-* update import queries and fix bug for MySQL & MariaDB ([89e0cdd](https://github.com/chartdb/chartdb/commit/89e0cddd42431ece364301bfb700a140c2df8368))
-* uses docker/build-push-action@v5 ([4799d41](https://github.com/chartdb/chartdb/commit/4799d41cd131b8672635aeb71b19a6153b46f4c5))
-* uses docker/build-push-action@v5 ([7358c9c](https://github.com/chartdb/chartdb/commit/7358c9c98971896274ffef245ab030897cefea93))
-* when import MySQL database via smart query fix PKs import ([dac6059](https://github.com/chartdb/chartdb/commit/dac6059853833d865e0b8a86423b5dac7572e55f))
-* zoom in/out on scroll instead of panning ([6a0bc30](https://github.com/chartdb/chartdb/commit/6a0bc30cdbfebed7c12d8ceeba39058d55c170fb))
diff --git a/CLA.md b/CLA.md
deleted file mode 100644
index b8c21dd35..000000000
--- a/CLA.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# ChartDB Contributors License Agreement
-
-This Contributors License Agreement ("CLA") is entered into between the Contributor, and ChartDB, Inc. ("ChartDB"), collectively referred to as the "Parties."
-
-## Background:
-
-ChartDB is an open-source project aimed at providing an open-source database diagramming and visualization tool for all parties.This CLA governs the rights and contributions made by the Contributor to the ChartDB project.
-
-## Agreement:
-
-**Contributor Grant of License:**
-
-By submitting code, documentation, or any other materials (collectively, "Contributions") to the ChartDB project, the Contributor grants ChartDB a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the ChartDB project.
-
-**Representation of Ownership and Right to Contribute:**
-
-The Contributor represents that they have the legal right to grant the license stated in Section 1, and that the Contributions do not infringe upon the intellectual property rights of any third party. The Contributor also represents that they have the authority to submit the Contributions on their own behalf or, if applicable, on behalf of their employer or any other entity.
-
-**Patent Grant:**
-
-If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant ChartDB a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the ChartDB project.
-
-**No Implied Warranties or Support:**
-
-The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. ChartDB shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
-
-**Retention of Contributor Rights:**
-
-The Contributor retains all right, title, and interest in and to their Contributions. This CLA does not restrict the Contributor from using their own Contributions for any other purpose.
-
-**Governing Law:**
-
-This CLA shall be governed by and construed in accordance with the laws of Delaware (DE), without regard to its conflict of laws principles.
-
-**Entire Agreement:**
-
-This CLA constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior and contemporaneous understandings, agreements, representations, and warranties.
-
-**Acceptance:**
-
-By submitting Contributions to the ChartDB project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
-
-**Effective Date:**
-
-This CLA is effective as of the date of the first Contribution made by the Contributor to the ChartDB project.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 9e484015b..000000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, religion, or sexual identity
-and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
- and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the
- overall community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or
- advances of any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email
- address, without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official e-mail address,
-posting via an official social media account, or acting as an appointed
-representative at an online or offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-support@chartdb.io.
-All complaints will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining
-the consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing
-clarity around the nature of the violation and an explanation of why the
-behavior was inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series
-of actions.
-
-**Consequence**: A warning with consequences for continued behavior. No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or
-permanent ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
-private interaction with the people involved, including unsolicited interaction
-with those enforcing the Code of Conduct, is allowed during this period.
-Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior, harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within
-the community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.0, available at
-https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
-
-Community Impact Guidelines were inspired by [Mozilla's code of conduct
-enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index cc4bafedf..000000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# **Contributing to ChartDB**
-
-Thank you for your interest in contributing to ChartDB! We want to make your experience as easy as possible.
-
-## How to Contribute
-
-### Submitting Pull Requests
-
-To submit a pull request:
-
-1. Fork the repository, create a branch from `main`, and focus on a single change.
-2. Write clear, concise commit messages and ensure your code follows the project's guidelines.
-3. Open a pull request and clearly state what issue it addresses.
-4. If needed, provide a brief explanation of your solution.
-5. Submit your pull request for review.
-
-### Reporting Bugs
-
-If you find a bug, check [GitHub issues](https://github.com/chartdb/chartdb/issues) to see if it’s already reported. If not, feel free to [report it](https://github.com/chartdb/chartdb/issues/new?labels=bug).
-
-For questions about using ChartDB, reach out to us via Email (support@chartdb.io) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
-
-### Creating a Branch
-
-To get started:
-
-1. Fork [the repository](https://github.com/chartdb/chartdb/fork).
-2. Create a branch from `main`.
-3. If you’re new to GitHub pull requests, check out [this video series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
-
-### License
-
-By contributing, you agree that your work will be licensed under ChartDB's [license](https://github.com/chartdb/chartdb/blob/main/LICENSE).
-
-## Questions?
-
-Feel free to ask in `#contributing` on [Discord](https://discord.gg/QeFwyWSKwC) if you have questions about our process, how to proceed, etc.
-or [Email](support@chartdb.io)
-
----
-
-Thank you! 💙
diff --git a/Dockerfile b/Dockerfile
index 233209e6b..74afd5e66 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:22-alpine AS builder
+FROM node:24-alpine AS builder
ARG VITE_OPENAI_API_KEY
ARG VITE_OPENAI_API_ENDPOINT
@@ -22,13 +22,17 @@ RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
RUN npm run build
-FROM nginx:stable-alpine AS production
+FROM node:24-alpine AS production
-COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
-COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
-COPY entrypoint.sh /entrypoint.sh
-RUN chmod +x /entrypoint.sh
+WORKDIR /usr/src/app
+
+ENV NODE_ENV=production
+ENV PORT=80
+ENV CHARTDB_DATA_DIR=/data
+
+COPY --from=builder /usr/src/app/dist ./dist
+COPY --from=builder /usr/src/app/server ./server
EXPOSE 80
-ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
+CMD ["node", "server/index.js"]
diff --git a/README.md b/README.md
index e25d7ec60..f9b1d3c72 100644
--- a/README.md
+++ b/README.md
@@ -169,3 +169,15 @@ Thank you for helping us make ChartDB better for everyone :heart:.
## License
ChartDB is licensed under the [GNU Affero General Public License v3.0](LICENSE)
+
+
+
+## Custom Fork
+This fork introduces a docker volume bind for storing and loading persistent diagrams from a volume.
+It also introduces many customization functions like custom logo, name and colors.
+
+Other features:
+- clean mode support with the url param clean=true
+- single table embedding
+- hiding social links
+- better table color picker
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
index f38a583ce..59ecdc8b6 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -72,6 +72,15 @@ export default [
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
+ 'linebreak-style': ['error', 'unix'],
+ },
+ },
+ {
+ files: ['server/**/*.js'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
},
},
];
diff --git a/index.html b/index.html
index 000318e23..0fb45b3fa 100644
--- a/index.html
+++ b/index.html
@@ -6,7 +6,6 @@
ChartDB - Create & Visualize Database Schema Diagrams
-
=18"
},
"peerDependencies": {
- "zod": "^3.0.0"
+ "zod": "^3.25.76 || ^4.1.8"
}
},
- "node_modules/@ai-sdk/provider": {
- "version": "0.0.21",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.21.tgz",
- "integrity": "sha512-9j95uaPRxwYkzQdkl4XO/MmWWW5c5vcVSXtqvALpD9SMB9fzH46dO3UN4VbOJR2J3Z84CZAqgZu5tNlkptT9qQ==",
+ "node_modules/@ai-sdk/openai": {
+ "version": "2.0.88",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.88.tgz",
+ "integrity": "sha512-LlOf83haeZIiRUH1Zw1oEmqUfw5y54227CvndFoBpIkMJwQDGAB3VARUeOJ6iwAWDJjXSz06GdnEnhRU67Yatw==",
"license": "Apache-2.0",
"dependencies": {
- "json-schema": "0.4.0"
+ "@ai-sdk/provider": "2.0.0",
+ "@ai-sdk/provider-utils": "3.0.19"
},
"engines": {
"node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
}
},
- "node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.15",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.15.tgz",
- "integrity": "sha512-icZqf2kpV8XdSViei4pX9ylYcVn+pk9AnVquJJGjGQGnwZ/5OgShqnFcLYrMjQfQcSVkz0PxdQVsIhZHzlT9Og==",
+ "node_modules/@ai-sdk/provider": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz",
+ "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==",
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider": "0.0.21",
- "eventsource-parser": "1.1.2",
- "nanoid": "3.3.6",
- "secure-json-parse": "2.7.0"
+ "json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/@ai-sdk/provider-utils/node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/@ai-sdk/react": {
- "version": "0.0.70",
- "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.70.tgz",
- "integrity": "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==",
+ "node_modules/@ai-sdk/provider-utils": {
+ "version": "3.0.19",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz",
+ "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==",
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider-utils": "1.0.22",
- "@ai-sdk/ui-utils": "0.0.50",
- "swr": "^2.2.5",
- "throttleit": "2.1.0"
+ "@ai-sdk/provider": "2.0.0",
+ "@standard-schema/spec": "^1.0.0",
+ "eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
- "react": "^18 || ^19 || ^19.0.0-rc",
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
+ "zod": "^3.25.76 || ^4.1.8"
}
},
- "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider": {
- "version": "0.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
- "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
- "license": "Apache-2.0",
- "dependencies": {
- "json-schema": "^0.4.0"
- },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
- "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
- "license": "Apache-2.0",
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "eventsource-parser": "^1.1.2",
- "nanoid": "^3.3.7",
- "secure-json-parse": "^2.7.0"
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
},
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/react/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
"engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/solid": {
- "version": "0.0.54",
- "resolved": "https://registry.npmjs.org/@ai-sdk/solid/-/solid-0.0.54.tgz",
- "integrity": "sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@ai-sdk/provider-utils": "1.0.22",
- "@ai-sdk/ui-utils": "0.0.50"
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
},
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "solid-js": "^1.7.7"
+ "node": ">=6.9.0"
},
- "peerDependenciesMeta": {
- "solid-js": {
- "optional": true
- }
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
}
},
- "node_modules/@ai-sdk/solid/node_modules/@ai-sdk/provider": {
- "version": "0.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
- "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
- "license": "Apache-2.0",
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "json-schema": "^0.4.0"
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
},
"engines": {
- "node": ">=18"
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/solid/node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
- "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
- "license": "Apache-2.0",
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "eventsource-parser": "^1.1.2",
- "nanoid": "^3.3.7",
- "secure-json-parse": "^2.7.0"
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
},
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/solid/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
"bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "semver": "bin/semver.js"
}
},
- "node_modules/@ai-sdk/svelte": {
- "version": "0.0.57",
- "resolved": "https://registry.npmjs.org/@ai-sdk/svelte/-/svelte-0.0.57.tgz",
- "integrity": "sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@ai-sdk/provider-utils": "1.0.22",
- "@ai-sdk/ui-utils": "0.0.50",
- "sswr": "^2.1.0"
- },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
- },
- "peerDependenciesMeta": {
- "svelte": {
- "optional": true
- }
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/svelte/node_modules/@ai-sdk/provider": {
- "version": "0.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
- "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
- "license": "Apache-2.0",
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "json-schema": "^0.4.0"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
- "node": ">=18"
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/svelte/node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
- "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
- "license": "Apache-2.0",
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "eventsource-parser": "^1.1.2",
- "nanoid": "^3.3.7",
- "secure-json-parse": "^2.7.0"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
},
"engines": {
- "node": ">=18"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
+ "@babel/core": "^7.0.0"
}
},
- "node_modules/@ai-sdk/svelte/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
"engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/ui-utils": {
- "version": "0.0.50",
- "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-0.0.50.tgz",
- "integrity": "sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "@ai-sdk/provider-utils": "1.0.22",
- "json-schema": "^0.4.0",
- "secure-json-parse": "^2.7.0",
- "zod-to-json-schema": "^3.23.3"
- },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/ui-utils/node_modules/@ai-sdk/provider": {
- "version": "0.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
- "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
- "license": "Apache-2.0",
- "dependencies": {
- "json-schema": "^0.4.0"
- },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/ui-utils/node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
- "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
- "license": "Apache-2.0",
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "eventsource-parser": "^1.1.2",
- "nanoid": "^3.3.7",
- "secure-json-parse": "^2.7.0"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
},
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
+ "node": ">=6.9.0"
}
},
- "node_modules/@ai-sdk/ui-utils/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
"bin": {
- "nanoid": "bin/nanoid.cjs"
+ "parser": "bin/babel-parser.js"
},
"engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "node": ">=6.0.0"
}
},
- "node_modules/@ai-sdk/vue": {
- "version": "0.0.59",
- "resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
- "integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
- "license": "Apache-2.0",
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@ai-sdk/provider-utils": "1.0.22",
- "@ai-sdk/ui-utils": "0.0.50",
- "swrv": "^1.0.4"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "vue": "^3.3.4"
- },
- "peerDependenciesMeta": {
- "vue": {
- "optional": true
- }
- }
- },
- "node_modules/@ai-sdk/vue/node_modules/@ai-sdk/provider": {
- "version": "0.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
- "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
- "license": "Apache-2.0",
- "dependencies": {
- "json-schema": "^0.4.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@ai-sdk/vue/node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
- "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "eventsource-parser": "^1.1.2",
- "nanoid": "^3.3.7",
- "secure-json-parse": "^2.7.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/@ai-sdk/vue/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/@alloc/quick-lru": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
- "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
- "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
- "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.5",
- "@babel/helper-compilation-targets": "^7.26.5",
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.7",
- "@babel/parser": "^7.26.7",
- "@babel/template": "^7.25.9",
- "@babel/traverse": "^7.26.7",
- "@babel/types": "^7.26.7",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
- "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.26.5",
- "@babel/types": "^7.26.5",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
- "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.26.5",
- "@babel/helper-validator-option": "^7.25.9",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
- "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
- "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
- "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.26.7"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
- "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
- "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -827,81 +443,69 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
- "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
- "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.5",
- "@babel/parser": "^7.26.7",
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.7",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
- "node_modules/@babel/traverse/node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@babel/types": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
- "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@dbml/core": {
- "version": "3.13.9",
- "resolved": "https://registry.npmjs.org/@dbml/core/-/core-3.13.9.tgz",
- "integrity": "sha512-JgJ470yuTZU7tP64ZL5FpEh7zSXjSoKzkARmin8iVVhdsNM8Nq4e+FFhG6J6acPtGHtoLahOs9LqrC17B9MqYg==",
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/@dbml/core/-/core-3.14.1.tgz",
+ "integrity": "sha512-X7qQo+ILK8NZ7S6LGmAExm28kX+YkfhDIiO4kZE/4Gm5pwzBOCZM4XVYWCRO1PACTLvvvAjNNMsZjJ2DhlYWxg==",
"license": "Apache-2.0",
"dependencies": {
- "@dbml/parse": "^3.13.9",
+ "@dbml/parse": "^3.14.1",
"antlr4": "^4.13.1",
"lodash": "^4.17.15",
"parsimmon": "^1.13.0",
@@ -911,10 +515,22 @@
"node": ">=16"
}
},
+ "node_modules/@dbml/core/node_modules/@dbml/parse": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/@dbml/parse/-/parse-3.14.1.tgz",
+ "integrity": "sha512-I906RBlLF04IWQlMUw10EIKimafZ4STb86BC8hGnezb+wJqtAtVLAX8hfbzMDnGpjthm4jg0oddxTLFTZrk80g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash-es": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@dbml/parse": {
- "version": "3.13.9",
- "resolved": "https://registry.npmjs.org/@dbml/parse/-/parse-3.13.9.tgz",
- "integrity": "sha512-JMfOxWquXMZpF/MTLy2xWLImx3z9D0t67T7x/BT892WvmhM+9cnJHFA2URT1NXu9jdajbTTFuoWSyzdsfNpaRw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@dbml/parse/-/parse-5.3.0.tgz",
+ "integrity": "sha512-A9/kAdb/fdZvHC45L9vDO4k8mFXpLi9ZaFxrEjbZ5sxR/Y/cWRNBiWeiiwakvCbZoQCPbT9sWqfIbdqGy2sFzQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash-es": "^4.17.21"
@@ -979,9 +595,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
@@ -992,13 +608,13 @@
"aix"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
@@ -1009,13 +625,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
@@ -1026,13 +642,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
@@ -1043,13 +659,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
@@ -1060,13 +676,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
@@ -1077,13 +693,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
@@ -1094,13 +710,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
@@ -1111,13 +727,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
@@ -1128,13 +744,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
@@ -1145,13 +761,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
@@ -1162,13 +778,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
@@ -1179,13 +795,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
@@ -1196,13 +812,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
@@ -1213,13 +829,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
@@ -1230,13 +846,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
@@ -1247,13 +863,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
@@ -1264,13 +880,30 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
@@ -1281,13 +914,30 @@
"netbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
@@ -1298,13 +948,30 @@
"openbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
@@ -1315,13 +982,13 @@
"sunos"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
@@ -1332,13 +999,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
@@ -1349,13 +1016,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
@@ -1366,13 +1033,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
- "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1389,9 +1056,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1399,16 +1066,19 @@
}
},
"node_modules/@eslint/compat": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.6.tgz",
- "integrity": "sha512-k7HNCqApoDHM6XzT30zGoETj+D+uUcZUb+IVAJmar3u6bvHf7hhHJcWx09QHj4/a2qrKZMWU0E16tvkiAdv06Q==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
+ "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"peerDependencies": {
- "eslint": "^9.10.0"
+ "eslint": "^8.40 || 9"
},
"peerDependenciesMeta": {
"eslint": {
@@ -1417,13 +1087,13 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
- "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/object-schema": "^2.1.6",
+ "@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@@ -1431,10 +1101,23 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@eslint/core": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
- "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1445,9 +1128,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
- "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1457,7 +1140,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
+ "js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
@@ -1482,19 +1165,22 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.19.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
- "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
}
},
"node_modules/@eslint/object-schema": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1502,13 +1188,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
- "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.10.0",
+ "@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
@@ -1516,31 +1202,31 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "1.6.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
- "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
- "@floating-ui/utils": "^0.2.9"
+ "@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
- "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
- "@floating-ui/core": "^1.6.0",
- "@floating-ui/utils": "^0.2.9"
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react-dom": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
- "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
- "@floating-ui/dom": "^1.0.0"
+ "@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -1548,9 +1234,9 @@
}
},
"node_modules/@floating-ui/utils": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
- "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@humanfs/core": {
@@ -1564,33 +1250,19 @@
}
},
"node_modules/@humanfs/node": {
- "version": "0.16.6",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
- "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.3.0"
+ "@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
- "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1606,9 +1278,9 @@
}
},
"node_modules/@humanwhocodes/retry": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
- "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1619,35 +1291,25 @@
"url": "https://github.com/sponsors/nzakas"
}
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "license": "ISC",
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
"dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -1659,25 +1321,16 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1685,29 +1338,26 @@
}
},
"node_modules/@monaco-editor/loader": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
- "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
- },
- "peerDependencies": {
- "monaco-editor": ">= 0.21.0 < 1"
}
},
"node_modules/@monaco-editor/react": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
- "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"license": "MIT",
"dependencies": {
- "@monaco-editor/loader": "^1.4.0"
+ "@monaco-editor/loader": "^1.5.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
@@ -1754,27 +1404,17 @@
"node": ">=8.0.0"
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/@pkgr/core": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
- "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
+ "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
- "url": "https://opencollective.com/unts"
+ "url": "https://opencollective.com/pkgr"
}
},
"node_modules/@polka/url": {
@@ -1791,26 +1431,26 @@
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
- "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz",
- "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==",
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collapsible": "1.1.2",
- "@radix-ui/react-collection": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -1828,17 +1468,17 @@
}
},
"node_modules/@radix-ui/react-alert-dialog": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.5.tgz",
- "integrity": "sha512-1Y2sI17QzSZP58RjGtrklfSGIf3AF7U/HkD3aAcAnhOUJrm7+7GG1wRDFaUlSe0nW5B/t4mYd/+7RNbP2Wexug==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+ "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dialog": "1.1.5",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-slot": "1.1.1"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -1855,67 +1495,58 @@
}
}
},
- "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
- "integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
+ "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.4",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-slot": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.2"
+ "@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
+ "@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-arrow": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
- "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.0.1"
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -1932,16 +1563,28 @@
}
}
},
- "node_modules/@radix-ui/react-avatar": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz",
- "integrity": "sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==",
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
@@ -1959,19 +1602,19 @@
}
},
"node_modules/@radix-ui/react-checkbox": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz",
- "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-previous": "1.1.0",
- "@radix-ui/react-use-size": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -1989,19 +1632,19 @@
}
},
"node_modules/@radix-ui/react-collapsible": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
- "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2019,15 +1662,15 @@
}
},
"node_modules/@radix-ui/react-collection": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
- "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-slot": "1.1.1"
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2045,12 +1688,12 @@
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
+ "@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2063,9 +1706,9 @@
}
},
"node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
- "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -2078,9 +1721,9 @@
}
},
"node_modules/@radix-ui/react-context": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
- "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -2093,17 +1736,17 @@
}
},
"node_modules/@radix-ui/react-context-menu": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.5.tgz",
- "integrity": "sha512-MY5PFCwo/ICaaQtpQBQ0g19AyjzI0mhz+a2GUWA2pJf4XFkvglAdcgDV2Iqm+lLbXn8hb+6rbLgcmRtc6ImPvg==",
+ "version": "2.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
+ "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-menu": "2.1.5",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2121,20 +1764,20 @@
}
},
"node_modules/@radix-ui/react-dialog": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
- "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.10",
- "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
@@ -2156,17 +1799,14 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
- "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2177,10 +1817,10 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
- "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -2192,13 +1832,13 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
- "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
@@ -2219,30 +1859,19 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
- "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
- "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
"license": "MIT",
"dependencies": {
+ "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
"@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1"
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2259,14 +1888,11 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
- "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2277,14 +1903,15 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
- "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-layout-effect": "1.1.1"
+ "@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2301,14 +1928,21 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
- "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
+ "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
"license": "MIT",
"dependencies": {
+ "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2325,34 +1959,23 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "node_modules/@radix-ui/react-icons": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+ "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
"peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
+ "node_modules/@radix-ui/react-id": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
- "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2363,63 +1986,100 @@
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
- "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
+ "@radix-ui/react-primitive": "2.1.4"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
- "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.1"
+ "@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-direction": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
- "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2430,17 +2090,22 @@
}
}
},
- "node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz",
- "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==",
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
+ "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-escape-keydown": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2457,19 +2122,27 @@
}
}
},
- "node_modules/@radix-ui/react-dropdown-menu": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.5.tgz",
- "integrity": "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ==",
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-menu": "2.1.5",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2486,11 +2159,14 @@
}
}
},
- "node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
- "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2501,15 +2177,22 @@
}
}
},
- "node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
- "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0"
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2526,21 +2209,14 @@
}
}
},
- "node_modules/@radix-ui/react-hover-card": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.5.tgz",
- "integrity": "sha512-0jPlX3ZrUIhtMAY0m1SBn1koI4Yqsizq2UwdUiQF1GseSZLZBPa6b8tNS+m32K94Yb4wxtWFSQs85wujQvwahg==",
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.4",
- "@radix-ui/react-popper": "1.2.1",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2557,40 +2233,14 @@
}
}
},
- "node_modules/@radix-ui/react-icons": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
- "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
- }
- },
- "node_modules/@radix-ui/react-id": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
- "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-label": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
- "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.0.1"
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2607,30 +2257,13 @@
}
}
},
- "node_modules/@radix-ui/react-menu": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.5.tgz",
- "integrity": "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg==",
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-dismissable-layer": "1.1.4",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.1",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-roving-focus": "1.1.1",
- "@radix-ui/react-slot": "1.1.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.2"
+ "@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2647,13 +2280,13 @@
}
}
},
- "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
+ "@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2665,22 +2298,21 @@
}
}
},
- "node_modules/@radix-ui/react-menubar": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.5.tgz",
- "integrity": "sha512-Kzbpcf2bxUmI/G+949+LvSvGkyzIaY7ctb8loydt6YpJR8pQF+j4QbVhYvjs7qxaWK0DEJL3XbP2p46YPRkS3A==",
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-menu": "2.1.5",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-roving-focus": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2697,27 +2329,21 @@
}
}
},
- "node_modules/@radix-ui/react-popover": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.5.tgz",
- "integrity": "sha512-YXkTAftOIW2Bt3qKH8vYr6n9gCkVrvyvfiTObVjoHVTHnNj26rmvO87IKa3VgtgCjb8FAQ6qOjNViwl+9iIzlg==",
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz",
+ "integrity": "sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/number": "1.1.0",
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.4",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.1",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-slot": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.2"
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
@@ -2734,14 +2360,17 @@
}
}
},
- "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
+ "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
- },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2752,69 +2381,43 @@
}
}
},
- "node_modules/@radix-ui/react-popper": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
- "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"license": "MIT",
- "dependencies": {
- "@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-use-rect": "1.1.0",
- "@radix-ui/react-use-size": "1.1.0",
- "@radix-ui/rect": "1.1.0"
- },
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-portal": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
- "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-presence": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
- "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
+ "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
@@ -2832,13 +2435,13 @@
}
}
},
- "node_modules/@radix-ui/react-primitive": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
- "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.1.1"
+ "@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
@@ -2855,13 +2458,13 @@
}
}
},
- "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
+ "@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
@@ -2873,52 +2476,63 @@
}
}
},
- "node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
- "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0"
- },
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
"optional": true
}
}
},
- "node_modules/@radix-ui/react-scroll-area": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz",
- "integrity": "sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==",
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/number": "1.1.0",
- "@radix-ui/primitive": "1.1.0",
- "@radix-ui/react-compose-refs": "1.1.0",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-presence": "1.1.1",
- "@radix-ui/react-primitive": "2.0.0",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2935,17 +2549,20 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
- "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
- "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2956,14 +2573,13 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
- "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@radix-ui/react-primitive": "2.1.4"
},
"peerDependencies": {
"@types/react": "*",
@@ -2980,13 +2596,13 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
- "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.1.0"
+ "@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
@@ -3003,13 +2619,13 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
- "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.0"
+ "@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -3021,33 +2637,20 @@
}
}
},
- "node_modules/@radix-ui/react-select": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.5.tgz",
- "integrity": "sha512-eVV7N8jBXAXnyrc+PsOF89O9AfVgGnbLxUtBb0clJ8y8ENMWLARGMI/1/SBRLz7u4HqxLgN71BJ17eono3wcjA==",
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/number": "1.1.0",
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-dismissable-layer": "1.1.4",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.1",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-slot": "1.1.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-use-previous": "1.1.0",
- "@radix-ui/react-visually-hidden": "1.1.1",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.2"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -3064,31 +2667,78 @@
}
}
},
- "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-separator": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
- "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
+ "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -3105,13 +2755,24 @@
}
}
},
- "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.2.3"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3128,7 +2789,7 @@
}
}
},
- "node_modules/@radix-ui/react-slot": {
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
@@ -3146,10 +2807,10 @@
}
}
},
- "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -3161,191 +2822,83 @@
}
}
},
- "node_modules/@radix-ui/react-tabs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
- "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-roving-focus": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-toast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.5.tgz",
- "integrity": "sha512-ZzUsAaOx8NdXZZKcFNDhbSlbsCUy8qQWmzTdgrlrhhZAOx2ofLtKrBDW9fkqhFvXgmtv560Uj16pkLkqML7SHA==",
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.4",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-visually-hidden": "1.1.1"
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-toggle": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz",
- "integrity": "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==",
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-toggle-group": {
+ "node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.1.tgz",
- "integrity": "sha512-OgDLZEA30Ylyz8YSXvnGqIHtERqnUt1KUYTKdw/y8u7Ci6zGiJfXc02jahmcSNK3YcErqioj/9flWC9S1ihfwg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-roving-focus": "1.1.1",
- "@radix-ui/react-toggle": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-tooltip": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
- "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.10",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-popper": "1.2.7",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.4",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-visually-hidden": "1.2.3"
+ "@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
- "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
- "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
+ "use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -3357,10 +2910,10 @@
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
- "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -3372,37 +2925,28 @@
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
- "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-escape-keydown": "1.1.1"
+ "@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
- },
- "@types/react-dom": {
- "optional": true
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": {
+ "node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
- "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
@@ -3417,22 +2961,13 @@
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
- "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
- "@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-use-rect": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1",
- "@radix-ui/rect": "1.1.1"
+ "@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3449,193 +2984,399 @@
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
- "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
- "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
+ "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
+ "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
- "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
+ "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
- "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
+ "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
- "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
+ "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
+ "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
- "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
+ "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
+ "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
+ "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
+ "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
+ "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
+ "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
+ "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
+ "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
+ "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
+ "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
+ "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
+ "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
+ "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
+ "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
+ "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
+ "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
"dependencies": {
- "@radix-ui/rect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
- "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
- "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz",
+ "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
},
"peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -3646,1922 +3387,1928 @@
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
- "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-use-callback-ref": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
- "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
}
},
- "node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
- "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
}
},
- "node_modules/@radix-ui/react-use-effect-event": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
- "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "@babel/types": "^7.0.0"
}
},
- "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
}
},
- "node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
- "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "@types/d3-color": "*"
}
},
- "node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
- "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "dependencies": {
+ "@types/d3-selection": "*"
}
},
- "node_modules/@radix-ui/react-use-previous": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
- "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
}
},
- "node_modules/@radix-ui/react-use-rect": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
- "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/rect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "@types/ms": "*"
}
},
- "node_modules/@radix-ui/react-use-size": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
- "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "@types/estree": "*"
}
},
- "node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
- "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.0.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "@types/unist": "*"
}
},
- "node_modules/@radix-ui/rect": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
- "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
+ "node_modules/@types/js-cookie": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
+ "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"license": "MIT"
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.0.tgz",
- "integrity": "sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
+ "license": "MIT"
},
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.0.tgz",
- "integrity": "sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
+ "dependencies": {
+ "@types/unist": "*"
+ }
},
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.0.tgz",
- "integrity": "sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
+ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.0.tgz",
- "integrity": "sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/@types/pegjs": {
+ "version": "0.10.6",
+ "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz",
+ "integrity": "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.27",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
+ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
},
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.0.tgz",
- "integrity": "sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
"license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
},
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.0.tgz",
- "integrity": "sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
+ "optional": true
},
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.0.tgz",
- "integrity": "sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-mimetype": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
+ "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "license": "MIT"
},
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.0.tgz",
- "integrity": "sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
+ "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.50.0",
+ "@typescript-eslint/type-utils": "8.50.0",
+ "@typescript-eslint/utils": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.50.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.0.tgz",
- "integrity": "sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.0.tgz",
- "integrity": "sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA==",
- "cpu": [
- "arm64"
- ],
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
+ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.50.0",
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.0.tgz",
- "integrity": "sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ==",
- "cpu": [
- "loong64"
- ],
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
+ "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.50.0",
+ "@typescript-eslint/types": "^8.50.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.0.tgz",
- "integrity": "sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A==",
- "cpu": [
- "ppc64"
- ],
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
+ "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
},
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.0.tgz",
- "integrity": "sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA==",
- "cpu": [
- "riscv64"
- ],
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
+ "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.0.tgz",
- "integrity": "sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg==",
- "cpu": [
- "s390x"
- ],
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
+ "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0",
+ "@typescript-eslint/utils": "8.50.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.0.tgz",
- "integrity": "sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
+ "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.0.tgz",
- "integrity": "sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
+ "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.50.0",
+ "@typescript-eslint/tsconfig-utils": "8.50.0",
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0",
+ "debug": "^4.3.4",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.0.tgz",
- "integrity": "sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
},
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.0.tgz",
- "integrity": "sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
},
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.0.tgz",
- "integrity": "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
+ "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.50.0",
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
},
- "node_modules/@testing-library/dom": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
- "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
+ "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@babel/code-frame": "^7.10.4",
- "@babel/runtime": "^7.12.5",
- "@types/aria-query": "^5.0.1",
- "aria-query": "5.3.0",
- "chalk": "^4.1.0",
- "dom-accessibility-api": "^0.5.9",
- "lz-string": "^1.5.0",
- "pretty-format": "^27.0.2"
+ "@typescript-eslint/types": "8.50.0",
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
- "node": ">=18"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@testing-library/dom/node_modules/aria-query": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
- "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
- "dependencies": {
- "dequal": "^2.0.3"
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/@testing-library/jest-dom": {
- "version": "6.6.3",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
- "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
+ "node_modules/@uidotdev/usehooks": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
+ "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@vercel/oidc": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
+ "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@adobe/css-tools": "^4.4.0",
- "aria-query": "^5.0.0",
- "chalk": "^3.0.0",
- "css.escape": "^1.5.1",
- "dom-accessibility-api": "^0.6.3",
- "lodash": "^4.17.21",
- "redent": "^3.0.0"
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
},
"engines": {
- "node": ">=14",
- "npm": ">=6",
- "yarn": ">=1"
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
- "node_modules/@testing-library/jest-dom/node_modules/chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
},
- "engines": {
- "node": ">=8"
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
- "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@testing-library/react": {
- "version": "16.3.0",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
- "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.12.5"
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
},
- "engines": {
- "node": ">=18"
+ "funding": {
+ "url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@testing-library/dom": "^10.0.0",
- "@types/react": "^18.0.0 || ^19.0.0",
- "@types/react-dom": "^18.0.0 || ^19.0.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
- "@types/react": {
+ "msw": {
"optional": true
},
- "@types/react-dom": {
+ "vite": {
"optional": true
}
}
},
- "node_modules/@testing-library/user-event": {
- "version": "14.6.1",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
- "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=12",
- "npm": ">=6"
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
},
- "peerDependencies": {
- "@testing-library/dom": ">=7.21.4"
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/aria-query": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/babel__generator": {
- "version": "7.6.8",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
- "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.0.0"
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/babel__traverse": {
- "version": "7.20.6",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
- "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
+ "node_modules/@vitest/ui": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz",
+ "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.20.7"
+ "@vitest/utils": "3.2.4",
+ "fflate": "^0.8.2",
+ "flatted": "^3.3.3",
+ "pathe": "^2.0.3",
+ "sirv": "^3.0.1",
+ "tinyglobby": "^0.2.14",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "3.2.4"
}
},
- "node_modules/@types/chai": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
- "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/deep-eql": "*"
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "node_modules/@xobotyi/scrollbar-width": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
+ "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
"license": "MIT"
},
- "node_modules/@types/d3-color": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
- "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
- "license": "MIT"
+ "node_modules/@xyflow/react": {
+ "version": "12.10.0",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz",
+ "integrity": "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.74",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
},
- "node_modules/@types/d3-drag": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
- "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "node_modules/@xyflow/system": {
+ "version": "0.0.74",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz",
+ "integrity": "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==",
"license": "MIT",
"dependencies": {
- "@types/d3-selection": "*"
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
}
},
- "node_modules/@types/d3-interpolate": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
- "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ahooks": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.9.6.tgz",
+ "integrity": "sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ==",
"license": "MIT",
"dependencies": {
- "@types/d3-color": "*"
+ "@babel/runtime": "^7.21.0",
+ "@types/js-cookie": "^3.0.6",
+ "dayjs": "^1.9.1",
+ "intersection-observer": "^0.12.0",
+ "js-cookie": "^3.0.5",
+ "lodash": "^4.17.21",
+ "react-fast-compare": "^3.2.2",
+ "resize-observer-polyfill": "^1.5.1",
+ "screenfull": "^5.0.0",
+ "tslib": "^2.4.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
- "node_modules/@types/d3-selection": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
- "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
- "license": "MIT"
+ "node_modules/ai": {
+ "version": "5.0.114",
+ "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.114.tgz",
+ "integrity": "sha512-q/lxcJA6avYn/TXTaE41VX6p9lN245mDU9bIGuPpfk6WxDMvmMoUKUIS0/aXAPYN3UmkUn/r9rvq/8C98RoCWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/gateway": "2.0.21",
+ "@ai-sdk/provider": "2.0.0",
+ "@ai-sdk/provider-utils": "3.0.19",
+ "@opentelemetry/api": "1.9.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
},
- "node_modules/@types/d3-transition": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
- "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/d3-selection": "*"
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/@types/d3-zoom": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
- "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/d3-interpolate": "*",
- "@types/d3-selection": "*"
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
+ "node_modules/antlr4": {
+ "version": "4.13.2",
+ "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz",
+ "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=16"
+ }
},
- "node_modules/@types/diff-match-patch": {
- "version": "1.0.36",
- "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
- "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"license": "MIT"
},
- "node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
- "license": "MIT"
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
},
- "node_modules/@types/js-cookie": {
- "version": "2.2.7",
- "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
- "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"license": "MIT"
},
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
- "license": "MIT"
+ "license": "Python-2.0"
},
- "node_modules/@types/node": {
- "version": "22.13.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz",
- "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==",
- "dev": true,
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
- "undici-types": "~6.20.0"
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/@types/pegjs": {
- "version": "0.10.6",
- "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz",
- "integrity": "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==",
- "license": "MIT"
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.14",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
- "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "18.3.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
- "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
- "devOptional": true,
- "license": "MIT",
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.0.2"
+ "dequal": "^2.0.3"
}
},
- "node_modules/@types/react-dom": {
- "version": "18.3.5",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
- "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
- "devOptional": true,
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "@types/react": "^18.0.0"
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@types/whatwg-mimetype": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
- "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz",
- "integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==",
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.22.0",
- "@typescript-eslint/type-utils": "8.22.0",
- "@typescript-eslint/utils": "8.22.0",
- "@typescript-eslint/visitor-keys": "8.22.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.0.0"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz",
- "integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==",
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.22.0",
- "@typescript-eslint/types": "8.22.0",
- "@typescript-eslint/typescript-estree": "8.22.0",
- "@typescript-eslint/visitor-keys": "8.22.0",
- "debug": "^4.3.4"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz",
- "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==",
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.22.0",
- "@typescript-eslint/visitor-keys": "8.22.0"
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz",
- "integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==",
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.22.0",
- "@typescript-eslint/utils": "8.22.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.0.0"
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@typescript-eslint/types": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz",
- "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==",
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz",
- "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==",
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.22.0",
- "@typescript-eslint/visitor-keys": "8.22.0",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^2.0.0"
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <5.8.0"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "node_modules/assertion-error": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
"dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
+ "license": "MIT",
"engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">= 0.4"
}
},
- "node_modules/@typescript-eslint/utils": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz",
- "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==",
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
"dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.22.0",
- "@typescript-eslint/types": "8.22.0",
- "@typescript-eslint/typescript-estree": "8.22.0"
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
},
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "engines": {
+ "node": "^10 || ^12 || >=14"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "postcss": "^8.1.0"
}
},
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.22.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz",
- "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==",
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.22.0",
- "eslint-visitor-keys": "^4.2.0"
+ "possible-typed-array-names": "^1.0.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "node_modules/axe-core": {
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
+ "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
"dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@uidotdev/usehooks": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
- "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
- "license": "MIT",
+ "license": "MPL-2.0",
"engines": {
- "node": ">=16"
- },
- "peerDependencies": {
- "react": ">=18.0.0",
- "react-dom": ">=18.0.0"
+ "node": ">=4"
}
},
- "node_modules/@vitejs/plugin-react": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
- "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.26.0",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
- },
+ "license": "Apache-2.0",
"engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ "node": ">= 0.4"
}
},
- "node_modules/@vitest/expect": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
- "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
- "dev": true,
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
"license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "tinyrainbow": "^2.0.0"
- },
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/@vitest/mocker": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
- "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "3.2.4",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
+ "license": "MIT"
},
- "node_modules/@vitest/mocker/node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz",
+ "integrity": "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
}
},
- "node_modules/@vitest/pretty-format": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
- "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
- "dev": true,
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"license": "MIT",
- "dependencies": {
- "tinyrainbow": "^2.0.0"
+ "engines": {
+ "node": ">=8"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@vitest/runner": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
- "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "3.2.4",
- "pathe": "^2.0.3",
- "strip-literal": "^3.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
}
},
- "node_modules/@vitest/snapshot": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
- "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
- "dev": true,
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3"
+ "fill-range": "^7.1.1"
},
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/@vitest/spy": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
- "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "tinyspy": "^4.0.3"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/@vitest/ui": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz",
- "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@vitest/utils": "3.2.4",
- "fflate": "^0.8.2",
- "flatted": "^3.3.3",
- "pathe": "^2.0.3",
- "sirv": "^3.0.1",
- "tinyglobby": "^0.2.14",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "vitest": "3.2.4"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/@vitest/utils": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
- "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "loupe": "^3.1.4",
- "tinyrainbow": "^2.0.0"
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vue/compiler-core": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
- "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/parser": "^7.25.3",
- "@vue/shared": "3.5.13",
- "entities": "^4.5.0",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.0"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@vue/compiler-dom": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
- "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@vue/compiler-core": "3.5.13",
- "@vue/shared": "3.5.13"
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/@vue/compiler-sfc": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
- "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@babel/parser": "^7.25.3",
- "@vue/compiler-core": "3.5.13",
- "@vue/compiler-dom": "3.5.13",
- "@vue/compiler-ssr": "3.5.13",
- "@vue/shared": "3.5.13",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.30.11",
- "postcss": "^8.4.48",
- "source-map-js": "^1.2.0"
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@vue/compiler-ssr": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
- "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
"license": "MIT",
- "peer": true,
- "dependencies": {
- "@vue/compiler-dom": "3.5.13",
- "@vue/shared": "3.5.13"
+ "engines": {
+ "node": ">=6"
}
},
- "node_modules/@vue/reactivity": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
- "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"license": "MIT",
- "peer": true,
- "dependencies": {
- "@vue/shared": "3.5.13"
+ "engines": {
+ "node": ">= 6"
}
},
- "node_modules/@vue/runtime-core": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
- "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@vue/reactivity": "3.5.13",
- "@vue/shared": "3.5.13"
- }
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001760",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
+ "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
},
- "node_modules/@vue/runtime-dom": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
- "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"license": "MIT",
- "peer": true,
- "dependencies": {
- "@vue/reactivity": "3.5.13",
- "@vue/runtime-core": "3.5.13",
- "@vue/shared": "3.5.13",
- "csstype": "^3.1.3"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/@vue/server-renderer": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
- "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@vue/compiler-ssr": "3.5.13",
- "@vue/shared": "3.5.13"
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
},
- "peerDependencies": {
- "vue": "3.5.13"
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/@vue/shared": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
- "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@xobotyi/scrollbar-width": {
- "version": "1.9.5",
- "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
- "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
- "license": "MIT"
- },
- "node_modules/@xyflow/react": {
- "version": "12.8.2",
- "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
- "integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@xyflow/system": "0.0.66",
- "classcat": "^5.0.3",
- "zustand": "^4.4.0"
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
},
- "peerDependencies": {
- "react": ">=17",
- "react-dom": ">=17"
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/@xyflow/system": {
- "version": "0.0.66",
- "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
- "integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
"license": "MIT",
- "dependencies": {
- "@types/d3-drag": "^3.0.7",
- "@types/d3-interpolate": "^3.0.4",
- "@types/d3-selection": "^3.0.10",
- "@types/d3-transition": "^3.0.8",
- "@types/d3-zoom": "^3.0.8",
- "d3-drag": "^3.0.0",
- "d3-interpolate": "^3.0.1",
- "d3-selection": "^3.0.0",
- "d3-zoom": "^3.0.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/acorn-typescript": {
- "version": "1.4.13",
- "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
- "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
"license": "MIT",
- "peer": true,
- "peerDependencies": {
- "acorn": ">=8.9.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/ahooks": {
- "version": "3.8.4",
- "resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.8.4.tgz",
- "integrity": "sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==",
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.21.0",
- "dayjs": "^1.9.1",
- "intersection-observer": "^0.12.0",
- "js-cookie": "^3.0.5",
- "lodash": "^4.17.21",
- "react-fast-compare": "^3.2.2",
- "resize-observer-polyfill": "^1.5.1",
- "screenfull": "^5.0.0",
- "tslib": "^2.4.1"
- },
"engines": {
- "node": ">=8.0.0"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ "node": ">= 16"
}
},
- "node_modules/ai": {
- "version": "3.4.33",
- "resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
- "integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
- "license": "Apache-2.0",
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
"dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "@ai-sdk/provider-utils": "1.0.22",
- "@ai-sdk/react": "0.0.70",
- "@ai-sdk/solid": "0.0.54",
- "@ai-sdk/svelte": "0.0.57",
- "@ai-sdk/ui-utils": "0.0.50",
- "@ai-sdk/vue": "0.0.59",
- "@opentelemetry/api": "1.9.0",
- "eventsource-parser": "1.1.2",
- "json-schema": "^0.4.0",
- "jsondiffpatch": "0.6.0",
- "secure-json-parse": "^2.7.0",
- "zod-to-json-schema": "^3.23.3"
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
},
"engines": {
- "node": ">=18"
+ "node": ">= 8.10.0"
},
- "peerDependencies": {
- "openai": "^4.42.0",
- "react": "^18 || ^19 || ^19.0.0-rc",
- "sswr": "^2.1.0",
- "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
- "zod": "^3.0.0"
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
},
- "peerDependenciesMeta": {
- "openai": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "sswr": {
- "optional": true
- },
- "svelte": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
}
},
- "node_modules/ai/node_modules/@ai-sdk/provider": {
- "version": "0.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
- "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
- "license": "Apache-2.0",
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
"dependencies": {
- "json-schema": "^0.4.0"
+ "is-glob": "^4.0.1"
},
"engines": {
- "node": ">=18"
+ "node": ">= 6"
}
},
- "node_modules/ai/node_modules/@ai-sdk/provider-utils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
- "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider": "0.0.26",
- "eventsource-parser": "^1.1.2",
- "nanoid": "^3.3.7",
- "secure-json-parse": "^2.7.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "zod": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/ai/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
+ "clsx": "^2.1.1"
},
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "funding": {
+ "url": "https://polar.sh/cva"
}
},
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
- "license": "MIT",
+ "license": "ISC",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
},
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "node": ">=6"
}
},
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
"license": "MIT",
"dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
- "node_modules/antlr4": {
- "version": "4.13.2",
- "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz",
- "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==",
- "license": "BSD-3-Clause",
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
"engines": {
- "node": ">=16"
+ "node": ">=7.0.0"
}
},
- "node_modules/any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
"engines": {
- "node": ">= 8"
+ "node": ">= 6"
}
},
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
- "license": "Python-2.0"
+ "license": "MIT"
},
- "node_modules/aria-hidden": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
- "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
"engines": {
- "node": ">=10"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/aria-query": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
- "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">= 0.4"
+ "node_modules/copy-to-clipboard": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+ "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+ "license": "MIT",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
}
},
- "node_modules/array-buffer-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
- "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "is-array-buffer": "^3.0.5"
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">= 8"
}
},
- "node_modules/array-includes": {
- "version": "3.1.8",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
- "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
- "dev": true,
+ "node_modules/css-in-js-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
+ "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "is-string": "^1.0.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "hyphenate-style-name": "^1.0.3"
}
},
- "node_modules/array.prototype.findlast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
- "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
- "dev": true,
+ "node_modules/css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
+ "license": "BSD"
+ },
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=8.0.0"
}
},
- "node_modules/array.prototype.flat": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
- "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
+ "bin": {
+ "cssesc": "bin/cssesc"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=4"
}
},
- "node_modules/array.prototype.flatmap": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
- "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=12"
}
},
- "node_modules/array.prototype.tosorted": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
- "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
"dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.3",
- "es-errors": "^1.3.0",
- "es-shim-unscopables": "^1.0.2"
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
}
},
- "node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
- "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
"dependencies": {
- "array-buffer-byte-length": "^1.0.1",
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "is-array-buffer": "^3.0.4"
+ "d3-color": "1 - 3"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=12"
}
},
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
"engines": {
"node": ">=12"
}
},
- "node_modules/ast-types-flow": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
- "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/async-function": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
- "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
}
},
- "node_modules/autoprefixer": {
- "version": "10.4.20",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
- "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/autoprefixer"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
"dependencies": {
- "browserslist": "^4.23.3",
- "caniuse-lite": "^1.0.30001646",
- "fraction.js": "^4.3.7",
- "normalize-range": "^0.1.2",
- "picocolors": "^1.0.1",
- "postcss-value-parser": "^4.2.0"
- },
- "bin": {
- "autoprefixer": "bin/autoprefixer"
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
},
"engines": {
- "node": "^10 || ^12 || >=14"
+ "node": ">=12"
},
"peerDependencies": {
- "postcss": "^8.1.0"
+ "d3-selection": "2 - 3"
}
},
- "node_modules/available-typed-arrays": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
- "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
"dependencies": {
- "possible-typed-array-names": "^1.0.0"
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=12"
}
},
- "node_modules/axe-core": {
- "version": "4.10.2",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz",
- "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==",
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true,
- "license": "MPL-2.0",
- "engines": {
- "node": ">=4"
- }
+ "license": "BSD-2-Clause"
},
- "node_modules/axobject-query": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
- "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
- "license": "Apache-2.0",
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/balanced-match": {
+ "node_modules/data-view-byte-length": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "license": "MIT"
- },
- "node_modules/big-integer": {
- "version": "1.6.52",
- "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
- "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
- "license": "Unlicense",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/inspect-js"
}
},
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "node_modules/dayjs": {
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
- "fill-range": "^7.1.1"
+ "ms": "^2.1.3"
},
"engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
}
- ],
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
- },
- "bin": {
- "browserslist": "cli.js"
+ "character-entities": "^2.0.0"
},
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=6"
}
},
- "node_modules/call-bind": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
- "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.2"
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -5570,29 +5317,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
- "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=8"
}
},
- "node_modules/call-bound": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
- "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "get-intrinsic": "^1.2.6"
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
@@ -5601,800 +5345,943 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
- "node_modules/camelcase-css": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
- "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"license": "MIT",
- "engines": {
- "node": ">= 6"
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/caniuse-lite": {
- "version": "1.0.30001726",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
- "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
+ "node_modules/dexie": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz",
+ "integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==",
+ "license": "Apache-2.0"
},
- "node_modules/chai": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
- "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
- "license": "MIT",
+ "license": "Apache-2.0",
"dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
+ "esutils": "^2.0.2"
},
"engines": {
- "node": ">=12"
+ "node": ">=0.10.0"
}
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/dompurify": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
},
"engines": {
- "node": ">=10"
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
},
"funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/check-error": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
- "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
- "dev": true,
+ "node_modules/error-stack-parser": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
+ "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"license": "MIT",
- "engines": {
- "node": ">= 16"
+ "dependencies": {
+ "stackframe": "^1.3.4"
}
},
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "node_modules/es-abstract": {
+ "version": "1.24.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
+ "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
},
"engines": {
- "node": ">= 8.10.0"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/chokidar/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">= 6"
- }
- },
- "node_modules/class-variance-authority": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
- "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
- "license": "Apache-2.0",
- "dependencies": {
- "clsx": "^2.1.1"
- },
- "funding": {
- "url": "https://polar.sh/cva"
+ "node": ">= 0.4"
}
},
- "node_modules/classcat": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
- "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
- "license": "MIT"
- },
- "node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.1",
- "wrap-ansi": "^7.0.0"
- },
+ "license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">= 0.4"
}
},
- "node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
+ "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.1",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.1.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.5",
+ "safe-array-concat": "^1.1.3"
+ },
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
}
},
- "node_modules/cliui/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true,
"license": "MIT"
},
- "node_modules/cliui/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
+ "es-errors": "^1.3.0"
},
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
}
},
- "node_modules/cliui/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^5.0.1"
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
},
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
}
},
- "node_modules/cliui/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
+ "hasown": "^2.0.2"
},
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">= 0.4"
}
},
- "node_modules/cmdk": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz",
- "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==",
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-dialog": "^1.1.2",
- "@radix-ui/react-id": "^1.1.0",
- "@radix-ui/react-primitive": "^2.0.0",
- "use-sync-external-store": "^1.2.2"
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
},
- "peerDependencies": {
- "react": "^18 || ^19 || ^19.0.0-rc",
- "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
"license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
+ "bin": {
+ "esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=7.0.0"
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
}
},
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
- "node_modules/commander": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
- "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">= 6"
+ "node": ">=6"
}
},
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
- "node": ">=18"
- }
- },
- "node_modules/copy-to-clipboard": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
- "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
- "license": "MIT",
- "dependencies": {
- "toggle-selection": "^1.0.6"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
},
"engines": {
- "node": ">= 8"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
}
},
- "node_modules/css-in-js-utils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
- "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz",
+ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "hyphenate-style-name": "^1.0.3"
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
}
},
- "node_modules/css-mediaquery": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
- "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
- "license": "BSD"
- },
- "node_modules/css-tree": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
- "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "node_modules/eslint-plugin-css-modules": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-css-modules/-/eslint-plugin-css-modules-2.12.0.tgz",
+ "integrity": "sha512-ruFBdad69ABrbCDCh5mXj7UzNmrvytfzPACjyvZWIAjFZAG8BXpYSbqmE8gU5wF+pIzV3jU2CWhLvfekXT/IgQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "mdn-data": "2.0.14",
- "source-map": "^0.6.1"
+ "gonzales-pe": "^4.3.0",
+ "lodash": "^4.17.2"
},
"engines": {
- "node": ">=8.0.0"
+ "node": ">=4.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=2.0.0"
}
},
- "node_modules/css.escape": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
- "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"license": "MIT",
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
- },
- "node_modules/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-dispatch": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
- "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-drag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
- "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
- "license": "ISC",
"dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-selection": "3"
+ "aria-query": "^5.3.2",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.1"
},
"engines": {
- "node": ">=12"
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
}
},
- "node_modules/d3-ease": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
- "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
- "license": "BSD-3-Clause",
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "dev": true,
+ "license": "Apache-2.0",
"engines": {
- "node": ">=12"
+ "node": ">= 0.4"
}
},
- "node_modules/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
- "license": "ISC",
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
+ "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "d3-color": "1 - 3"
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.11.7"
},
"engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-selection": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
- "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-timer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
- "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
}
},
- "node_modules/d3-transition": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
- "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
- "license": "ISC",
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "d3-color": "1 - 3",
- "d3-dispatch": "1 - 3",
- "d3-ease": "1 - 3",
- "d3-interpolate": "1 - 3",
- "d3-timer": "1 - 3"
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=4"
},
"peerDependencies": {
- "d3-selection": "2 - 3"
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
- "node_modules/d3-zoom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
- "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
- "license": "ISC",
- "dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-drag": "2 - 3",
- "d3-interpolate": "1 - 3",
- "d3-selection": "2 - 3",
- "d3-transition": "2 - 3"
- },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
- "node_modules/damerau-levenshtein": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
- "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
"dev": true,
- "license": "BSD-2-Clause"
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
},
- "node_modules/data-view-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
- "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-tailwindcss": {
+ "version": "3.18.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.18.2.tgz",
+ "integrity": "sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.2"
+ "fast-glob": "^3.2.5",
+ "postcss": "^8.4.4"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=18.12.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "tailwindcss": "^3.4.0"
}
},
- "node_modules/data-view-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
- "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.2"
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/inspect-js"
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/data-view-byte-offset": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
- "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.1"
- },
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 0.4"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/dayjs": {
- "version": "1.11.13",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
- "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
- "license": "MIT"
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
},
- "node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"dependencies": {
- "ms": "^2.1.3"
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
- "node": ">=6.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
- "license": "MIT",
+ "license": "Apache-2.0",
"engines": {
- "node": ">=6"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
- "license": "MIT"
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
},
- "node_modules/define-data-property": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
+ "estraverse": "^5.2.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=4.0"
}
},
- "node_modules/define-lazy-prop": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
- "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=8"
+ "node": ">=4.0"
}
},
- "node_modules/define-properties": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "define-data-property": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "@types/estree": "^1.0.0"
}
},
- "node_modules/dequal": {
+ "node_modules/esutils": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">=18.0.0"
}
},
- "node_modules/detect-node-es": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
- "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
- "node_modules/dexie": {
- "version": "4.0.11",
- "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz",
- "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
- "license": "Apache-2.0"
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
},
- "node_modules/didyoumean": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
"license": "Apache-2.0"
},
- "node_modules/diff-match-patch": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
- "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
- "license": "Apache-2.0"
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
},
- "node_modules/dlv": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-shallow-equal": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
+ "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
+ },
+ "node_modules/fastest-stable-stringify": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
+ "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==",
"license": "MIT"
},
- "node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "license": "Apache-2.0",
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "license": "ISC",
"dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
+ "reusify": "^1.0.4"
}
},
- "node_modules/dom-accessibility-api": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
+ "flat-cache": "^4.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=16.0.0"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "license": "MIT"
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
- "node_modules/electron-to-chromium": {
- "version": "1.5.90",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz",
- "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==",
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
- "license": "ISC"
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "license": "MIT"
- },
- "node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "license": "BSD-2-Clause",
- "peer": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
"engines": {
- "node": ">=0.12"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/error-stack-parser": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
- "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "stackframe": "^1.3.4"
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
}
},
- "node_modules/es-abstract": {
- "version": "1.23.9",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
- "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "array-buffer-byte-length": "^1.0.2",
- "arraybuffer.prototype.slice": "^1.0.4",
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "data-view-buffer": "^1.0.2",
- "data-view-byte-length": "^1.0.2",
- "data-view-byte-offset": "^1.0.1",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-set-tostringtag": "^2.1.0",
- "es-to-primitive": "^1.3.0",
- "function.prototype.name": "^1.1.8",
- "get-intrinsic": "^1.2.7",
- "get-proto": "^1.0.0",
- "get-symbol-description": "^1.1.0",
- "globalthis": "^1.0.4",
- "gopd": "^1.2.0",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "internal-slot": "^1.1.0",
- "is-array-buffer": "^3.0.5",
- "is-callable": "^1.2.7",
- "is-data-view": "^1.0.2",
- "is-regex": "^1.2.1",
- "is-shared-array-buffer": "^1.0.4",
- "is-string": "^1.1.1",
- "is-typed-array": "^1.1.15",
- "is-weakref": "^1.1.0",
- "math-intrinsics": "^1.1.0",
- "object-inspect": "^1.13.3",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.7",
- "own-keys": "^1.0.1",
- "regexp.prototype.flags": "^1.5.3",
- "safe-array-concat": "^1.1.3",
- "safe-push-apply": "^1.0.0",
- "safe-regex-test": "^1.1.0",
- "set-proto": "^1.0.0",
- "string.prototype.trim": "^1.2.10",
- "string.prototype.trimend": "^1.0.9",
- "string.prototype.trimstart": "^1.0.8",
- "typed-array-buffer": "^1.0.3",
- "typed-array-byte-length": "^1.0.3",
- "typed-array-byte-offset": "^1.0.4",
- "typed-array-length": "^1.0.7",
- "unbox-primitive": "^1.1.0",
- "which-typed-array": "^1.1.18"
+ "is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
@@ -6403,110 +6290,148 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 0.4"
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
}
},
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
+ "node_modules/framer-motion": {
+ "version": "12.23.26",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
+ "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.23",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
"engines": {
- "node": ">= 0.4"
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/es-iterator-helpers": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
- "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
- "es-abstract": "^1.23.6",
- "es-errors": "^1.3.0",
- "es-set-tostringtag": "^2.0.3",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.6",
- "globalthis": "^1.0.4",
- "gopd": "^1.2.0",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.2.0",
- "has-symbols": "^1.1.0",
- "internal-slot": "^1.1.0",
- "iterator.prototype": "^1.1.4",
- "safe-array-concat": "^1.1.3"
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
"engines": {
"node": ">= 0.4"
}
},
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=6.9.0"
}
},
- "node_modules/es-shim-unscopables": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
- "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.0"
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
}
},
- "node_modules/es-to-primitive": {
+ "node_modules/get-intrinsic": {
"version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
- "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-callable": "^1.2.7",
- "is-date-object": "^1.0.5",
- "is-symbol": "^1.0.4"
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -6515,618 +6440,593 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
- "dev": true,
- "hasInstallScript": true,
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
"engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
+ "node": ">=6"
}
},
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
"engines": {
- "node": ">=6"
+ "node": ">= 0.4"
}
},
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
"engines": {
- "node": ">=10"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint": {
- "version": "9.19.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
- "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.19.0",
- "@eslint/core": "^0.10.0",
- "@eslint/eslintrc": "^3.2.0",
- "@eslint/js": "9.19.0",
- "@eslint/plugin-kit": "^0.2.5",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.1",
- "@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.2.0",
- "eslint-visitor-keys": "^4.2.0",
- "espree": "^10.3.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
+ "is-glob": "^4.0.3"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
+ "node": ">=10.13.0"
}
},
- "node_modules/eslint-config-prettier": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
- "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"license": "MIT",
- "bin": {
- "eslint-config-prettier": "bin/cli.js"
+ "engines": {
+ "node": ">=18"
},
- "peerDependencies": {
- "eslint": ">=7.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint-plugin-css-modules": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-css-modules/-/eslint-plugin-css-modules-2.12.0.tgz",
- "integrity": "sha512-ruFBdad69ABrbCDCh5mXj7UzNmrvytfzPACjyvZWIAjFZAG8BXpYSbqmE8gU5wF+pIzV3jU2CWhLvfekXT/IgQ==",
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "gonzales-pe": "^4.3.0",
- "lodash": "^4.17.2"
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
},
"engines": {
- "node": ">=4.0.0"
+ "node": ">= 0.4"
},
- "peerDependencies": {
- "eslint": ">=2.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-jsx-a11y": {
- "version": "6.10.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
- "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+ "node_modules/gonzales-pe": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
+ "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "aria-query": "^5.3.2",
- "array-includes": "^3.1.8",
- "array.prototype.flatmap": "^1.3.2",
- "ast-types-flow": "^0.0.8",
- "axe-core": "^4.10.0",
- "axobject-query": "^4.1.0",
- "damerau-levenshtein": "^1.0.8",
- "emoji-regex": "^9.2.2",
- "hasown": "^2.0.2",
- "jsx-ast-utils": "^3.3.5",
- "language-tags": "^1.0.9",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.8",
- "safe-regex-test": "^1.0.3",
- "string.prototype.includes": "^2.0.1"
+ "minimist": "^1.2.5"
},
- "engines": {
- "node": ">=4.0"
+ "bin": {
+ "gonzales": "bin/gonzales.js"
},
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ "engines": {
+ "node": ">=0.6.0"
}
},
- "node_modules/eslint-plugin-prettier": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
- "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "prettier-linter-helpers": "^1.0.0",
- "synckit": "^0.9.1"
- },
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://opencollective.com/eslint-plugin-prettier"
- },
- "peerDependencies": {
- "@types/eslint": ">=8.0.0",
- "eslint": ">=8.0.0",
- "eslint-config-prettier": "*",
- "prettier": ">=3.0.0"
- },
- "peerDependenciesMeta": {
- "@types/eslint": {
- "optional": true
- },
- "eslint-config-prettier": {
- "optional": true
- }
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-react": {
- "version": "7.37.4",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",
- "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==",
+ "node_modules/happy-dom": {
+ "version": "20.0.11",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.0.11.tgz",
+ "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "array-includes": "^3.1.8",
- "array.prototype.findlast": "^1.2.5",
- "array.prototype.flatmap": "^1.3.3",
- "array.prototype.tosorted": "^1.1.4",
- "doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.2.1",
- "estraverse": "^5.3.0",
- "hasown": "^2.0.2",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.8",
- "object.fromentries": "^2.0.8",
- "object.values": "^1.2.1",
- "prop-types": "^15.8.1",
- "resolve": "^2.0.0-next.5",
- "semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.12",
- "string.prototype.repeat": "^1.0.0"
+ "@types/node": "^20.0.0",
+ "@types/whatwg-mimetype": "^3.0.2",
+ "whatwg-mimetype": "^3.0.0"
},
"engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/happy-dom/node_modules/@types/node": {
+ "version": "20.19.27",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
+ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
}
},
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
- "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
+ "node_modules/happy-dom/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">= 0.4"
},
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.18",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz",
- "integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==",
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "eslint": ">=8.40"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-tailwindcss": {
- "version": "3.18.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.18.0.tgz",
- "integrity": "sha512-PQDU4ZMzFH0eb2DrfHPpbgo87Zgg2EXSMOj1NSfzdZm+aJzpuwGerfowMIaVehSREEa0idbf/eoNYAOHSJoDAQ==",
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fast-glob": "^3.2.5",
- "postcss": "^8.4.4"
+ "dunder-proto": "^1.0.0"
},
"engines": {
- "node": ">=18.12.0"
+ "node": ">= 0.4"
},
- "peerDependencies": {
- "tailwindcss": "^3.4.0"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-scope": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
- "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
+ "license": "MIT",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint/node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
- "dev": true,
- "license": "Apache-2.0",
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-from-parse5": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
+ "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "property-information": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-location": "^5.0.0",
+ "web-namespaces": "^2.0.0"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/esm-env": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
- "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
- "peer": true
- },
- "node_modules/espree": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
- "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
- "dev": true,
- "license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^8.14.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.0"
+ "@types/hast": "^3.0.0"
},
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-raw": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
+ "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "hast-util-to-parse5": "^8.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "parse5": "^7.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/espree/node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
- "dev": true,
- "license": "BSD-3-Clause",
+ "node_modules/hast-util-to-parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
+ "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
+ "license": "MIT",
"dependencies": {
- "estraverse": "^5.1.0"
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
},
- "engines": {
- "node": ">=0.10"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/esrap": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.3.tgz",
- "integrity": "sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==",
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
- "peer": true,
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.15"
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
+ "node_modules/hastscript": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
+ "license": "MIT",
"dependencies": {
- "estraverse": "^5.2.0"
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0"
},
- "engines": {
- "node": ">=4.0"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
}
},
- "node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "node_modules/html-to-image": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
+ "license": "MIT"
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
"license": "MIT",
- "peer": true
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
},
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/husky": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
+ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
"engines": {
- "node": ">=0.10.0"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
}
},
- "node_modules/eventsource-parser": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
- "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/i18next": {
+ "version": "23.16.8",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
+ "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
+ "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"license": "MIT",
- "engines": {
- "node": ">=14.18"
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
}
},
- "node_modules/expect-type": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
- "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
"engines": {
- "node": ">=12.0.0"
+ "node": ">= 4"
}
},
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "license": "MIT"
- },
- "node_modules/fast-diff": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
- "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/fast-glob": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
- "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"license": "MIT",
"dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.8"
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
},
"engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
+ "node": ">=6"
},
- "engines": {
- "node": ">= 6"
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
},
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/fast-shallow-equal": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
- "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
},
- "node_modules/fastest-stable-stringify": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
- "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==",
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
"license": "MIT"
},
- "node_modules/fastq": {
- "version": "1.19.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
- "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
- "license": "ISC",
+ "node_modules/inline-style-prefixer": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
+ "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
+ "license": "MIT",
"dependencies": {
- "reusify": "^1.0.4"
+ "css-in-js-utils": "^3.1.0"
}
},
- "node_modules/fflate": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
- "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "flat-cache": "^4.0.0"
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">= 0.4"
}
},
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "node_modules/intersection-observer": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
+ "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
+ "deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
+ "license": "Apache-2.0"
+ },
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
+ "loose-envify": "^1.0.0"
}
},
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"license": "MIT",
"dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
},
- "engines": {
- "node": ">=16"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/for-each": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz",
- "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==",
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-callable": "^1.2.7"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
},
"engines": {
"node": ">= 0.4"
@@ -7135,99 +7035,63 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/foreground-child": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
- "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
- "license": "ISC",
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^4.0.1"
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
},
"engines": {
- "node": ">=14"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/fraction.js": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
- "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": "*"
- },
- "funding": {
- "type": "patreon",
- "url": "https://github.com/sponsors/rawify"
- }
- },
- "node_modules/framer-motion": {
- "version": "12.23.6",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.6.tgz",
- "integrity": "sha512-dsJ389QImVE3lQvM8Mnk99/j8tiZDM/7706PCqvkQ8sSCnpmWxsgX+g0lj7r5OBVL0U36pIecCTBoIWcM2RuKw==",
- "license": "MIT",
"dependencies": {
- "motion-dom": "^12.23.6",
- "motion-utils": "^12.23.6",
- "tslib": "^2.4.0"
- },
- "peerDependencies": {
- "@emotion/is-prop-valid": "*",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
+ "has-bigints": "^1.0.2"
},
- "peerDependenciesMeta": {
- "@emotion/is-prop-valid": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
"engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/function.prototype.name": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
- "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
"call-bound": "^1.0.3",
- "define-properties": "^1.2.1",
- "functions-have-names": "^1.2.3",
- "hasown": "^2.0.2",
- "is-callable": "^1.2.7"
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7236,53 +7100,61 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/functions-have-names": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
- "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
"engines": {
- "node": ">=6.9.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
"engines": {
- "node": "6.* || 8.* || >= 10.*"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-intrinsic": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
- "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.0",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7291,39 +7163,49 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-nonce": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
- "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"license": "MIT",
- "engines": {
- "node": ">=6"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
+ "bin": {
+ "is-docker": "cli.js"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/get-symbol-description": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
- "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6"
+ "call-bound": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -7332,84 +7214,102 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "license": "ISC",
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "license": "ISC",
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
"dependencies": {
- "is-glob": "^4.0.3"
+ "is-extglob": "^2.1.1"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=0.10.0"
}
},
- "node_modules/glob/node_modules/brace-expansion": {
+ "node_modules/is-hexadecimal": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
"license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/glob/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/globals": {
- "version": "15.14.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
- "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/globalthis": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
- "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "define-properties": "^1.2.1",
- "gopd": "^1.0.1"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7418,26 +7318,41 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/gonzales-pe": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
- "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==",
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "minimist": "^1.2.5"
- },
- "bin": {
- "gonzales": "bin/gonzales.js"
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
},
"engines": {
- "node": ">=0.6.0"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7447,51 +7362,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/happy-dom": {
- "version": "18.0.1",
- "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz",
- "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==",
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/node": "^20.0.0",
- "@types/whatwg-mimetype": "^3.0.2",
- "whatwg-mimetype": "^3.0.0"
+ "call-bound": "^1.0.3"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/happy-dom/node_modules/@types/node": {
- "version": "20.19.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.4.tgz",
- "integrity": "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==",
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~6.21.0"
- }
- },
- "node_modules/happy-dom/node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/has-bigints": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
- "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
- "dev": true,
- "license": "MIT",
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -7499,37 +7395,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-property-descriptors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-define-property": "^1.0.0"
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-proto": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
- "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "dunder-proto": "^1.0.0"
+ "which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
@@ -7538,10 +7429,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7551,14 +7442,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
"dev": true,
"license": "MIT",
"dependencies": {
- "has-symbols": "^1.0.3"
+ "call-bound": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -7567,983 +7458,1240 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "function-bind": "^1.1.2"
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
},
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/html-parse-stringify": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
- "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "void-elements": "3.1.0"
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/html-to-image": {
- "version": "1.11.11",
- "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
- "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==",
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/husky": {
- "version": "9.1.7",
- "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
- "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
"dev": true,
"license": "MIT",
- "bin": {
- "husky": "bin.js"
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
},
"engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/typicode"
+ "node": ">= 0.4"
}
},
- "node_modules/hyphenate-style-name": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
- "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
- "license": "BSD-3-Clause"
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
},
- "node_modules/i18next": {
- "version": "23.16.8",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
- "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://locize.com"
- },
- {
- "type": "individual",
- "url": "https://locize.com/i18next.html"
- },
- {
- "type": "individual",
- "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
- }
- ],
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.23.2"
+ "engines": {
+ "node": ">=14"
}
},
- "node_modules/i18next-browser-languagedetector": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz",
- "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==",
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.2"
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
+ "license": "MIT"
},
- "node_modules/import-fresh": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
- "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
+ "bin": {
+ "json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
"engines": {
- "node": ">=0.8.19"
+ "node": ">=4.0"
}
},
- "node_modules/indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/inline-style-prefixer": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
- "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
- "license": "MIT",
"dependencies": {
- "css-in-js-utils": "^3.1.0"
+ "json-buffer": "3.0.1"
}
},
- "node_modules/internal-slot": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
- "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
- "hasown": "^2.0.2",
- "side-channel": "^1.1.0"
+ "language-subtag-registry": "^0.3.20"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=0.10"
}
},
- "node_modules/intersection-observer": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
- "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
- "license": "Apache-2.0"
- },
- "node_modules/invariant": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
- "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.0.0"
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
}
},
- "node_modules/is-array-buffer": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
- "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
- "dev": true,
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "get-intrinsic": "^1.2.6"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=14"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/antonk52"
}
},
- "node_modules/is-async-function": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
- "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "async-function": "^1.0.0",
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.1",
- "has-tostringtag": "^1.0.2",
- "safe-regex-test": "^1.1.0"
+ "p-locate": "^5.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-bigint": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
- "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.22",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
+ "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
"license": "MIT",
- "dependencies": {
- "has-bigints": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
- "binary-extensions": "^2.0.0"
+ "js-tokens": "^3.0.0 || ^4.0.0"
},
- "engines": {
- "node": ">=8"
+ "bin": {
+ "loose-envify": "cli.js"
}
},
- "node_modules/is-boolean-object": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz",
- "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==",
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true,
- "license": "MIT",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "call-bound": "^1.0.2",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "yallist": "^3.0.2"
}
},
- "node_modules/is-callable": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
- "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "node_modules/lucide-react": {
+ "version": "0.525.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
+ "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
}
},
- "node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "hasown": "^2.0.2"
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">= 18"
+ }
+ },
+ "node_modules/matchmediaquery": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz",
+ "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-mediaquery": "^0.1.2"
}
},
- "node_modules/is-data-view": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
- "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "get-intrinsic": "^1.2.6",
- "is-typed-array": "^1.1.13"
- },
"engines": {
"node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-date-object": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
- "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
- "dev": true,
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-docker": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
- "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
- "dev": true,
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
- "bin": {
- "is-docker": "cli.js"
- },
"engines": {
- "node": ">=8"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-finalizationregistry": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
- "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
- "dev": true,
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
"license": "MIT",
- "engines": {
- "node": ">=8"
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-generator-function": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
- "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
- "dev": true,
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.0",
- "has-tostringtag": "^1.0.2",
- "safe-regex-test": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
"license": "MIT",
"dependencies": {
- "is-extglob": "^2.1.1"
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
},
- "engines": {
- "node": ">=0.10.0"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-map": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
- "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
- "dev": true,
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
"license": "MIT",
- "engines": {
- "node": ">=0.12.0"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-number-object": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
- "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
- "dev": true,
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-reference": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
- "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
- "@types/estree": "^1.0.6"
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-regex": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
- "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
- "dev": true,
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2",
- "gopd": "^1.2.0",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-set": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
- "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
- "dev": true,
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-shared-array-buffer": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
- "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
- "dev": true,
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-string": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
- "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
- "dev": true,
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-symbol": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
- "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
- "dev": true,
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2",
- "has-symbols": "^1.1.0",
- "safe-regex-test": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-typed-array": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
- "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
- "dev": true,
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
"license": "MIT",
"dependencies": {
- "which-typed-array": "^1.1.16"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/mdast": "^4.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-weakmap": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
- "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
- "dev": true,
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"license": "MIT",
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">= 8"
}
},
- "node_modules/is-weakref": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz",
- "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==",
- "dev": true,
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-weakset": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
- "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
- "dev": true,
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "get-intrinsic": "^1.2.6"
- },
- "engines": {
- "node": ">= 0.4"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
- "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
- "dev": true,
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
"license": "MIT",
"dependencies": {
- "is-docker": "^2.0.0"
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
},
- "engines": {
- "node": ">=8"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/iterator.prototype": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
- "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
- "dev": true,
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
"license": "MIT",
"dependencies": {
- "define-data-property": "^1.1.4",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.6",
- "get-proto": "^1.0.0",
- "has-symbols": "^1.1.0",
- "set-function-name": "^2.0.2"
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
},
- "engines": {
- "node": ">= 0.4"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "license": "BlueOak-1.0.0",
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
"dependencies": {
- "@isaacs/cliui": "^8.0.2"
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/jiti": {
- "version": "1.21.7",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
- "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
- "license": "MIT",
- "bin": {
- "jiti": "bin/jiti.js"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/js-cookie": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
- "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
"license": "MIT",
- "engines": {
- "node": ">=14"
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
"license": "MIT",
"dependencies": {
- "argparse": "^2.0.1"
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
},
- "bin": {
- "js-yaml": "bin/js-yaml.js"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-schema": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
- "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
- "license": "(AFL-2.1 OR BSD-3-Clause)"
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/jsondiffpatch": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
- "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@types/diff-match-patch": "^1.0.36",
- "chalk": "^5.3.0",
- "diff-match-patch": "^1.0.5"
- },
- "bin": {
- "jsondiffpatch": "bin/jsondiffpatch.js"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/jsondiffpatch/node_modules/chalk": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
- "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/jsx-ast-utils": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
- "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
- "dev": true,
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
- "object.assign": "^4.1.4",
- "object.values": "^1.1.6"
- },
- "engines": {
- "node": ">=4.0"
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "json-buffer": "3.0.1"
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/language-subtag-registry": {
- "version": "0.3.23",
- "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
- "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
- "dev": true,
- "license": "CC0-1.0"
- },
- "node_modules/language-tags": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
- "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
- "dev": true,
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "language-subtag-registry": "^0.3.20"
- },
- "engines": {
- "node": ">=0.10"
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antonk52"
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "license": "MIT"
- },
- "node_modules/locate-character": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
- "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "license": "MIT"
- },
- "node_modules/lodash-es": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
- "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
- "license": "MIT"
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/loupe": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
- "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==",
- "dev": true,
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT"
},
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lucide-react": {
- "version": "0.525.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
- "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
- "node_modules/lz-string": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
- "dev": true,
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "peer": true,
- "bin": {
- "lz-string": "bin/bin.js"
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/magic-string": {
- "version": "0.30.17",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
- "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0"
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/matchmediaquery": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz",
- "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==",
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "css-mediaquery": "^0.1.2"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/mdn-data": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
- "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
- "license": "CC0-1.0"
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
@@ -8614,28 +8762,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
"node_modules/monaco-editor": {
- "version": "0.52.2",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
- "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
- "license": "MIT"
+ "version": "0.55.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
+ "license": "MIT",
+ "dependencies": {
+ "dompurify": "3.2.7",
+ "marked": "14.0.0"
+ }
},
"node_modules/motion": {
- "version": "12.23.6",
- "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.6.tgz",
- "integrity": "sha512-6U55IW5i6Vut2ryKEhrZKg55490k9d6qdGXZoNSf98oQgDj5D7bqTnVJotQ6UW3AS6QfbW6KSLa7/e1gy+a07g==",
+ "version": "12.23.26",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz",
+ "integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==",
"license": "MIT",
"dependencies": {
- "framer-motion": "^12.23.6",
+ "framer-motion": "^12.23.26",
"tslib": "^2.4.0"
},
"peerDependencies": {
@@ -8656,9 +8799,9 @@
}
},
"node_modules/motion-dom": {
- "version": "12.23.6",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.6.tgz",
- "integrity": "sha512-G2w6Nw7ZOVSzcQmsdLc0doMe64O/Sbuc2bVAbgMz6oP/6/pRStKRiVRV4bQfHp5AHYAKEGhEdVHTM+R3FDgi5w==",
+ "version": "12.23.23",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
+ "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
@@ -8684,7 +8827,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -8719,9 +8861,9 @@
}
},
"node_modules/nanoid": {
- "version": "5.0.9",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
- "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
+ "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
@@ -8744,16 +8886,16 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/node-sql-parser": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-5.3.6.tgz",
- "integrity": "sha512-TXak4rIvmpL7Ap1BJQGfAGewoELQGZ/Rf9YYt9rKz73ykDp3HNnXT08jDFC/8FjqiPoRxcPNVecSwTmjz7LrFA==",
+ "version": "5.3.13",
+ "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-5.3.13.tgz",
+ "integrity": "sha512-heyWv3lLjKHpcBDMUSR+R0DohRYZTYq+Ro3hJ4m9Ia8ccdKbL5UijIaWr2L4co+bmmFuvBVZ4v23QW2PqvBFAA==",
"license": "Apache-2.0",
"dependencies": {
"@types/pegjs": "^0.10.0",
@@ -8772,16 +8914,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
- "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -8801,9 +8933,9 @@
}
},
"node_modules/object-inspect": {
- "version": "1.13.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
- "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8845,15 +8977,16 @@
}
},
"node_modules/object.entries": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
- "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
"define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
+ "es-object-atoms": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
@@ -8983,12 +9116,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "license": "BlueOak-1.0.0"
- },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -9002,6 +9129,43 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/parsimmon": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
@@ -9022,6 +9186,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -9033,28 +9198,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
- "node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "license": "ISC"
- },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -9100,9 +9243,9 @@
}
},
"node_modules/pirates": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
- "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -9118,9 +9261,9 @@
}
},
"node_modules/possible-typed-array-names": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
- "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9128,9 +9271,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.1",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
- "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -9147,7 +9290,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -9173,12 +9316,12 @@
}
},
"node_modules/postcss-import/node_modules/resolve": {
- "version": "1.22.10",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
- "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
- "is-core-module": "^2.16.0",
+ "is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -9193,9 +9336,19 @@
}
},
"node_modules/postcss-js": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
- "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
@@ -9203,18 +9356,14 @@
"engines": {
"node": "^12 || ^14 || >= 16"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
- "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"funding": [
{
"type": "opencollective",
@@ -9227,21 +9376,28 @@
],
"license": "MIT",
"dependencies": {
- "lilconfig": "^3.0.0",
- "yaml": "^2.3.4"
+ "lilconfig": "^3.1.1"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 18"
},
"peerDependencies": {
+ "jiti": ">=1.21.0",
"postcss": ">=8.0.9",
- "ts-node": ">=9.0.0"
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
"postcss": {
"optional": true
},
- "ts-node": {
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
"optional": true
}
}
@@ -9291,9 +9447,9 @@
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@@ -9319,9 +9475,9 @@
}
},
"node_modules/prettier": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
- "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
+ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -9363,17 +9519,6 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
- "node_modules/pretty-format/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
@@ -9388,14 +9533,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/pretty-format/node_modules/react-is": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -9407,6 +9544,22 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -9483,9 +9636,9 @@
}
},
"node_modules/react-hotkeys-hook": {
- "version": "4.6.1",
- "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.1.tgz",
- "integrity": "sha512-XlZpbKUj9tkfgPgT9gA+1p7Ey6vFIZHttUjPqpTdyT5nqQ8mHL7elxvSbaC+dpSiHUSmr21Ya1mDxBZG3aje4Q==",
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.2.tgz",
+ "integrity": "sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.1",
@@ -9493,17 +9646,18 @@
}
},
"node_modules/react-i18next": {
- "version": "15.4.0",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz",
- "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==",
+ "version": "15.7.4",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz",
+ "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.25.0",
+ "@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
- "i18next": ">= 23.2.3",
- "react": ">= 16.8.0"
+ "i18next": ">= 23.4.0",
+ "react": ">= 16.8.0",
+ "typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
@@ -9511,19 +9665,51 @@
},
"react-native": {
"optional": true
+ },
+ "typescript": {
+ "optional": true
}
}
},
"node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
},
"node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9531,9 +9717,9 @@
}
},
"node_modules/react-remove-scroll": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
- "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
@@ -9578,9 +9764,9 @@
}
},
"node_modules/react-resizable-panels": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz",
- "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==",
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz",
+ "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==",
"license": "MIT",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
@@ -9588,9 +9774,9 @@
}
},
"node_modules/react-responsive": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.0.tgz",
- "integrity": "sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.1.tgz",
+ "integrity": "sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==",
"license": "MIT",
"dependencies": {
"hyphenate-style-name": "^1.0.0",
@@ -9606,15 +9792,13 @@
}
},
"node_modules/react-router": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz",
- "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==",
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
+ "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
"license": "MIT",
"dependencies": {
- "@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
- "set-cookie-parser": "^2.6.0",
- "turbo-stream": "2.4.0"
+ "set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
@@ -9630,12 +9814,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.5.tgz",
- "integrity": "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ==",
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
+ "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
"license": "MIT",
"dependencies": {
- "react-router": "7.1.5"
+ "react-router": "7.10.1"
},
"engines": {
"node": ">=20.0.0"
@@ -9702,6 +9886,12 @@
"react-dom": "*"
}
},
+ "node_modules/react-use/node_modules/@types/js-cookie": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
+ "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
+ "license": "MIT"
+ },
"node_modules/react-use/node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
@@ -9766,12 +9956,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "license": "MIT"
- },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -9793,6 +9977,87 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/rehype-raw": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
+ "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-raw": "^9.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -9838,9 +10103,9 @@
}
},
"node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@@ -9848,13 +10113,13 @@
}
},
"node_modules/rollup": {
- "version": "4.34.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.0.tgz",
- "integrity": "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
+ "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.6"
+ "@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -9864,36 +10129,39 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.34.0",
- "@rollup/rollup-android-arm64": "4.34.0",
- "@rollup/rollup-darwin-arm64": "4.34.0",
- "@rollup/rollup-darwin-x64": "4.34.0",
- "@rollup/rollup-freebsd-arm64": "4.34.0",
- "@rollup/rollup-freebsd-x64": "4.34.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.34.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.34.0",
- "@rollup/rollup-linux-arm64-gnu": "4.34.0",
- "@rollup/rollup-linux-arm64-musl": "4.34.0",
- "@rollup/rollup-linux-loongarch64-gnu": "4.34.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.34.0",
- "@rollup/rollup-linux-s390x-gnu": "4.34.0",
- "@rollup/rollup-linux-x64-gnu": "4.34.0",
- "@rollup/rollup-linux-x64-musl": "4.34.0",
- "@rollup/rollup-win32-arm64-msvc": "4.34.0",
- "@rollup/rollup-win32-ia32-msvc": "4.34.0",
- "@rollup/rollup-win32-x64-msvc": "4.34.0",
+ "@rollup/rollup-android-arm-eabi": "4.53.5",
+ "@rollup/rollup-android-arm64": "4.53.5",
+ "@rollup/rollup-darwin-arm64": "4.53.5",
+ "@rollup/rollup-darwin-x64": "4.53.5",
+ "@rollup/rollup-freebsd-arm64": "4.53.5",
+ "@rollup/rollup-freebsd-x64": "4.53.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.5",
+ "@rollup/rollup-linux-arm64-musl": "4.53.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.5",
+ "@rollup/rollup-linux-x64-gnu": "4.53.5",
+ "@rollup/rollup-linux-x64-musl": "4.53.5",
+ "@rollup/rollup-openharmony-arm64": "4.53.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.5",
+ "@rollup/rollup-win32-x64-gnu": "4.53.5",
+ "@rollup/rollup-win32-x64-msvc": "4.53.5",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-visualizer": {
- "version": "5.14.0",
- "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
- "integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.5.tgz",
+ "integrity": "sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "open": "^8.4.0",
+ "open": "^8.0.0",
"picomatch": "^4.0.2",
"source-map": "^0.7.4",
"yargs": "^17.5.1"
@@ -9905,7 +10173,7 @@
"node": ">=18"
},
"peerDependencies": {
- "rolldown": "1.x",
+ "rolldown": "1.x || ^1.0.0-beta",
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
@@ -9918,9 +10186,9 @@
}
},
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9931,13 +10199,13 @@
}
},
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
- "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
- "node": ">= 8"
+ "node": ">= 12"
}
},
"node_modules/rtl-css-js": {
@@ -10048,16 +10316,10 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/secure-json-parse": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
- "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
- "license": "BSD-3-Clause"
- },
"node_modules/semver": {
- "version": "7.7.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
- "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@@ -10068,9 +10330,9 @@
}
},
"node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/set-function-length": {
@@ -10147,6 +10409,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -10159,6 +10422,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -10247,22 +10511,10 @@
"dev": true,
"license": "ISC"
},
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/sirv": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
- "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10292,16 +10544,14 @@
"node": ">=0.10.0"
}
},
- "node_modules/sswr": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
- "integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
- "dependencies": {
- "swrev": "^4.0.0"
- },
- "peerDependencies": {
- "svelte": "^4.0.0 || ^5.0.0-next.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stack-generator": {
@@ -10363,34 +10613,31 @@
"license": "MIT"
},
"node_modules/std-env": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
- "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"dev": true,
"license": "MIT"
},
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
},
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">= 0.4"
}
},
- "node_modules/string-width-cjs": {
- "name": "string-width",
+ "node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -10401,33 +10648,13 @@
"node": ">=8"
}
},
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -10541,26 +10768,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
},
"funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
+ "node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -10569,15 +10795,6 @@
"node": ">=8"
}
},
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -10605,9 +10822,9 @@
}
},
"node_modules/strip-literal": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
- "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10624,24 +10841,42 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
"node_modules/stylis": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz",
- "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==",
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/sucrase": {
- "version": "3.35.0",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
- "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
- "glob": "^10.3.10",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
"ts-interface-checker": "^0.1.9"
},
"bin": {
@@ -10677,75 +10912,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/svelte": {
- "version": "5.19.6",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.6.tgz",
- "integrity": "sha512-6ydekB3qyqUal+UhfMjmVOjRGtxysR8vuiMhi2nwuBtPJWnctVlsGspjVFB05qmR+TXI1emuqtZt81c0XiFleA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@ampproject/remapping": "^2.3.0",
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@types/estree": "^1.0.5",
- "acorn": "^8.12.1",
- "acorn-typescript": "^1.4.13",
- "aria-query": "^5.3.1",
- "axobject-query": "^4.1.0",
- "clsx": "^2.1.1",
- "esm-env": "^1.2.1",
- "esrap": "^1.4.3",
- "is-reference": "^3.0.3",
- "locate-character": "^3.0.0",
- "magic-string": "^0.30.11",
- "zimmerframe": "^1.1.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/swr": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz",
- "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==",
- "license": "MIT",
- "dependencies": {
- "dequal": "^2.0.3",
- "use-sync-external-store": "^1.4.0"
- },
- "peerDependencies": {
- "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/swrev": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz",
- "integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==",
- "license": "MIT"
- },
- "node_modules/swrv": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.1.0.tgz",
- "integrity": "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==",
- "license": "Apache-2.0",
- "peerDependencies": {
- "vue": ">=3.2.26 < 4"
- }
- },
"node_modules/synckit": {
- "version": "0.9.2",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
- "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
+ "version": "0.11.11",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
+ "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@pkgr/core": "^0.1.0",
- "tslib": "^2.6.2"
+ "@pkgr/core": "^0.2.9"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
- "url": "https://opencollective.com/unts"
+ "url": "https://opencollective.com/synckit"
}
},
"node_modules/tailwind-merge": {
@@ -10759,9 +10939,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.17",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
- "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -10772,7 +10952,7 @@
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
- "jiti": "^1.21.6",
+ "jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
@@ -10781,7 +10961,7 @@
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.2",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
@@ -10805,12 +10985,12 @@
}
},
"node_modules/tailwindcss/node_modules/resolve": {
- "version": "1.22.10",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
- "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
- "is-core-module": "^2.16.0",
+ "is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -10854,28 +11034,16 @@
"node": ">=10"
}
},
- "node_modules/throttleit": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
- "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/timeago-react": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/timeago-react/-/timeago-react-3.0.6.tgz",
- "integrity": "sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==",
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/timeago-react/-/timeago-react-3.0.7.tgz",
+ "integrity": "sha512-5LSQuq+mzfEpMHkJQgMWnOs27dGh25aUQhffQpAW6q431vVVmHP194KYsM4bhzli0XfohUgN8+r3Pq6GVprN4A==",
"license": "MIT",
"dependencies": {
"timeago.js": "^4.0.0"
},
"peerDependencies": {
- "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/timeago.js": {
@@ -10899,14 +11067,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
- "version": "0.2.14",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
- "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
- "dev": true,
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"dependencies": {
- "fdir": "^6.4.4",
- "picomatch": "^4.0.2"
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
@@ -10916,11 +11083,13 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
- "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
- "dev": true,
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -10931,10 +11100,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
- "dev": true,
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -10964,9 +11132,9 @@
}
},
"node_modules/tinyspy": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
- "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -10997,14 +11165,34 @@
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=6"
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-api-utils": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
- "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11032,12 +11220,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/turbo-stream": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
- "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
- "license": "ISC"
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -11130,9 +11312,9 @@
}
},
"node_modules/typescript": {
- "version": "5.7.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@@ -11163,12 +11345,99 @@
}
},
"node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/unplugin": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
@@ -11207,9 +11476,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
- "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
@@ -11291,9 +11560,9 @@
}
},
"node_modules/use-sync-external-store": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
- "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -11318,22 +11587,67 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-location": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
+ "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vite": {
- "version": "5.4.14",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
- "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
+ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -11342,19 +11656,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
"lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
+ "jiti": {
+ "optional": true
+ },
"less": {
"optional": true
},
@@ -11375,6 +11695,12 @@
},
"terser": {
"optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
}
}
},
@@ -11401,6 +11727,37 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/vitest": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
@@ -11475,9 +11832,9 @@
}
},
"node_modules/vitest/node_modules/picomatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11496,32 +11853,20 @@
"node": ">=0.10.0"
}
},
- "node_modules/vue": {
- "version": "3.5.13",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
- "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
"license": "MIT",
- "peer": true,
- "dependencies": {
- "@vue/compiler-dom": "3.5.13",
- "@vue/compiler-sfc": "3.5.13",
- "@vue/runtime-dom": "3.5.13",
- "@vue/server-renderer": "3.5.13",
- "@vue/shared": "3.5.13"
- },
- "peerDependencies": {
- "typescript": "*"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/webpack-sources": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
- "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
+ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11549,6 +11894,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -11628,16 +11974,17 @@
}
},
"node_modules/which-typed-array": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz",
- "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==",
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "for-each": "^0.3.3",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
@@ -11676,27 +12023,10 @@
}
},
"node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -11710,59 +12040,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11780,18 +12057,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yaml": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
- "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -11821,51 +12086,6 @@
"node": ">=12"
}
},
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -11879,35 +12099,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/zimmerframe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
- "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
- "license": "MIT",
- "peer": true
- },
"node_modules/zod": {
- "version": "3.24.1",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
- "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
- "node_modules/zod-to-json-schema": {
- "version": "3.24.1",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
- "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
- "license": "ISC",
- "peerDependencies": {
- "zod": "^3.24.1"
- }
- },
"node_modules/zustand": {
- "version": "4.5.6",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
- "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
@@ -11931,6 +12135,16 @@
"optional": true
}
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
}
diff --git a/package.json b/package.json
index d3fb7a771..c89615690 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
- "version": "1.15.1",
+ "version": "1.19.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,10 +16,11 @@
"test:coverage": "vitest --coverage"
},
"dependencies": {
- "@ai-sdk/openai": "^0.0.51",
- "@dbml/core": "^3.13.9",
+ "@ai-sdk/openai": "^2.0.72",
+ "@dbml/core": "^3.14.1",
+ "@dbml/parse": "^5.3.0",
"@dnd-kit/sortable": "^8.0.0",
- "@monaco-editor/react": "^4.6.0",
+ "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
@@ -45,7 +46,7 @@
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
- "ai": "^3.3.14",
+ "ai": "^5.0.101",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -55,7 +56,7 @@
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.525.0",
- "monaco-editor": "^0.52.0",
+ "monaco-editor": "^0.55.1",
"motion": "^12.23.6",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",
@@ -64,10 +65,13 @@
"react-helmet-async": "^2.0.5",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^15.0.1",
+ "react-markdown": "^10.1.0",
"react-resizable-panels": "^2.0.22",
"react-responsive": "^10.0.0",
"react-router-dom": "^7.1.1",
"react-use": "^17.5.1",
+ "rehype-raw": "^7.0.0",
+ "remark-gfm": "^4.0.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"timeago-react": "^3.0.6",
@@ -81,12 +85,12 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
- "@types/node": "^22.1.0",
+ "@types/node": "^24.10.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
- "@vitejs/plugin-react": "^4.3.1",
+ "@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.16.0",
@@ -99,15 +103,15 @@
"eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-tailwindcss": "^3.17.4",
"globals": "^15.13.0",
- "happy-dom": "^18.0.1",
+ "happy-dom": "^20.0.10",
"husky": "^9.1.5",
"postcss": "^8.4.40",
"prettier": "^3.3.3",
- "rollup-plugin-visualizer": "^5.12.0",
+ "rollup-plugin-visualizer": "^6.0.5",
"tailwindcss": "^3.4.7",
"typescript": "^5.2.2",
"unplugin-inject-preload": "^3.0.0",
- "vite": "^5.3.4",
+ "vite": "^7.2.7",
"vitest": "^3.2.4"
}
}
diff --git a/public/robots.txt b/public/robots.txt
index 4464e427e..6ffbc308f 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,4 +1,3 @@
User-agent: *
Disallow: /
-Sitemap: https://app.chartdb.io/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
deleted file mode 100644
index 4f1d8495c..000000000
--- a/public/sitemap.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
- https://app.chartdb.io/
- weekly
- 1.0
-
-
- https://app.chartdb.io/examples
- monthly
- 0.8
-
-
\ No newline at end of file
diff --git a/server/api.d.ts b/server/api.d.ts
new file mode 100644
index 000000000..c4021b353
--- /dev/null
+++ b/server/api.d.ts
@@ -0,0 +1,11 @@
+import type { IncomingMessage, ServerResponse } from 'node:http';
+
+export type ApiHandler = (
+ req: IncomingMessage,
+ res: ServerResponse,
+ next?: (err?: unknown) => void
+) => Promise;
+
+export const resolveDataDir: () => string;
+
+export const createApiHandler: (options?: { dataDir?: string }) => ApiHandler;
diff --git a/server/api.js b/server/api.js
new file mode 100644
index 000000000..80df56f2e
--- /dev/null
+++ b/server/api.js
@@ -0,0 +1,371 @@
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+
+const jsonContentType = { 'Content-Type': 'application/json; charset=utf-8' };
+const defaultConfig = { defaultDiagramId: '', hideSocialLinks: false };
+const assetContentTypes = {
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.svg': 'image/svg+xml',
+ '.webp': 'image/webp',
+ '.gif': 'image/gif',
+ '.ico': 'image/x-icon',
+};
+const includeKeys = [
+ 'tables',
+ 'relationships',
+ 'dependencies',
+ 'areas',
+ 'customTypes',
+ 'notes',
+];
+
+export const resolveDataDir = () =>
+ process.env.CHARTDB_DATA_DIR ?? path.join(process.cwd(), 'data');
+
+const ensureDir = async (dir) => {
+ await fs.mkdir(dir, { recursive: true });
+};
+
+const safeJsonParse = (text) => {
+ if (!text) return undefined;
+ try {
+ return JSON.parse(text);
+ } catch {
+ return undefined;
+ }
+};
+
+const readJsonFile = async (filePath) => {
+ const text = await fs.readFile(filePath, 'utf8');
+ return safeJsonParse(text);
+};
+
+const writeJsonAtomic = async (filePath, data) => {
+ const dir = path.dirname(filePath);
+ await ensureDir(dir);
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random()
+ .toString(16)
+ .slice(2)}`;
+ const json = JSON.stringify(data);
+ const handle = await fs.open(tempPath, 'w');
+ try {
+ await handle.writeFile(json, 'utf8');
+ await handle.sync();
+ } finally {
+ await handle.close();
+ }
+ await fs.rename(tempPath, filePath);
+};
+
+const readRequestBody = async (req) => {
+ const chunks = [];
+ for await (const chunk of req) {
+ chunks.push(chunk);
+ }
+ if (chunks.length === 0) {
+ return '';
+ }
+ return Buffer.concat(chunks).toString('utf8');
+};
+
+const sendJson = (res, statusCode, data) => {
+ res.writeHead(statusCode, jsonContentType);
+ res.end(JSON.stringify(data));
+};
+
+const sendEmpty = (res, statusCode) => {
+ res.statusCode = statusCode;
+ res.end();
+};
+
+const sendFile = (res, data, contentType) => {
+ res.writeHead(200, { 'Content-Type': contentType });
+ res.end(data);
+};
+
+const resolveSafePath = (baseDir, targetPath) => {
+ if (!targetPath) return null;
+ const base = path.resolve(baseDir);
+ const resolved = path.resolve(baseDir, targetPath);
+ if (resolved === base) return resolved;
+ if (!resolved.startsWith(base + path.sep)) {
+ return null;
+ }
+ return resolved;
+};
+
+const isSafeId = (value) => {
+ if (!value) return false;
+ if (value.includes('..')) return false;
+ return path.basename(value) === value;
+};
+
+const buildIncludeSet = (url) => {
+ const include = url.searchParams.get('include');
+ if (!include) return new Set();
+ return new Set(
+ include
+ .split(',')
+ .map((value) => value.trim())
+ .filter(Boolean)
+ );
+};
+
+const stripDiagram = (diagram, includeSet) => {
+ if (includeSet.size === 0) {
+ const trimmed = { ...diagram };
+ for (const key of includeKeys) {
+ delete trimmed[key];
+ }
+ return trimmed;
+ }
+
+ const trimmed = { ...diagram };
+ for (const key of includeKeys) {
+ if (!includeSet.has(key)) {
+ delete trimmed[key];
+ }
+ }
+ return trimmed;
+};
+
+export const createApiHandler = ({ dataDir = resolveDataDir() } = {}) => {
+ const diagramsDir = path.join(dataDir, 'diagrams');
+ const filtersDir = path.join(dataDir, 'diagram-filters');
+ const configPath = path.join(dataDir, 'config.json');
+
+ return async (req, res, next) => {
+ const url = new URL(req.url ?? '/', 'http://localhost');
+
+ if (!url.pathname.startsWith('/api/')) {
+ if (next) {
+ next();
+ }
+ return false;
+ }
+
+ try {
+ if (url.pathname === '/api/health' && req.method === 'GET') {
+ sendJson(res, 200, { ok: true });
+ return true;
+ }
+
+ if (url.pathname === '/api/config') {
+ if (req.method === 'GET') {
+ try {
+ const config = await readJsonFile(configPath);
+ sendJson(res, 200, config ?? defaultConfig);
+ } catch (error) {
+ if (error?.code === 'ENOENT') {
+ sendJson(res, 200, defaultConfig);
+ } else {
+ throw error;
+ }
+ }
+ return true;
+ }
+
+ if (req.method === 'PUT') {
+ const body = await readRequestBody(req);
+ const data = safeJsonParse(body);
+ if (!data || typeof data !== 'object') {
+ sendJson(res, 400, { error: 'Invalid config JSON.' });
+ return true;
+ }
+ await writeJsonAtomic(configPath, data);
+ sendEmpty(res, 204);
+ return true;
+ }
+ }
+
+ if (url.pathname.startsWith('/api/config/assets/')) {
+ if (req.method === 'GET') {
+ const assetPath = decodeURIComponent(
+ url.pathname.replace('/api/config/assets/', '')
+ );
+ const resolvedPath = resolveSafePath(dataDir, assetPath);
+ if (!resolvedPath) {
+ sendEmpty(res, 404);
+ return true;
+ }
+
+ try {
+ const data = await fs.readFile(resolvedPath);
+ const ext = path.extname(resolvedPath).toLowerCase();
+ const contentType =
+ assetContentTypes[ext] ??
+ 'application/octet-stream';
+ sendFile(res, data, contentType);
+ } catch (error) {
+ if (error?.code === 'ENOENT') {
+ sendEmpty(res, 404);
+ } else {
+ throw error;
+ }
+ }
+ return true;
+ }
+ }
+
+ if (url.pathname.startsWith('/api/diagram-filters/')) {
+ const diagramId = decodeURIComponent(
+ url.pathname.replace('/api/diagram-filters/', '')
+ );
+
+ if (!isSafeId(diagramId)) {
+ sendJson(res, 400, { error: 'Invalid diagram id.' });
+ return true;
+ }
+
+ const filterPath = path.join(filtersDir, `${diagramId}.json`);
+
+ if (req.method === 'GET') {
+ try {
+ const filter = await readJsonFile(filterPath);
+ sendJson(res, 200, filter ?? {});
+ } catch (error) {
+ if (error?.code === 'ENOENT') {
+ sendEmpty(res, 404);
+ } else {
+ throw error;
+ }
+ }
+ return true;
+ }
+
+ if (req.method === 'PUT') {
+ const body = await readRequestBody(req);
+ const data = safeJsonParse(body);
+ if (!data || typeof data !== 'object') {
+ sendJson(res, 400, { error: 'Invalid filter JSON.' });
+ return true;
+ }
+ await writeJsonAtomic(filterPath, data);
+ sendEmpty(res, 204);
+ return true;
+ }
+
+ if (req.method === 'DELETE') {
+ try {
+ await fs.unlink(filterPath);
+ } catch (error) {
+ if (error?.code !== 'ENOENT') {
+ throw error;
+ }
+ }
+ sendEmpty(res, 204);
+ return true;
+ }
+ }
+
+ if (url.pathname === '/api/diagrams' && req.method === 'GET') {
+ const includeSet = buildIncludeSet(url);
+ await ensureDir(diagramsDir);
+ const files = await fs.readdir(diagramsDir);
+ const diagrams = [];
+
+ for (const file of files) {
+ if (!file.endsWith('.json')) continue;
+ const diagramPath = path.join(diagramsDir, file);
+ try {
+ const diagram = await readJsonFile(diagramPath);
+ if (diagram) {
+ diagrams.push(stripDiagram(diagram, includeSet));
+ }
+ } catch (error) {
+ if (error?.code !== 'ENOENT') {
+ throw error;
+ }
+ }
+ }
+
+ sendJson(res, 200, diagrams);
+ return true;
+ }
+
+ if (url.pathname === '/api/diagrams' && req.method === 'POST') {
+ const body = await readRequestBody(req);
+ const data = safeJsonParse(body);
+ if (!data || typeof data !== 'object') {
+ sendJson(res, 400, { error: 'Invalid diagram JSON.' });
+ return true;
+ }
+ const diagramId = data.id;
+ if (!isSafeId(diagramId)) {
+ sendJson(res, 400, { error: 'Invalid diagram id.' });
+ return true;
+ }
+ const diagramPath = path.join(diagramsDir, `${diagramId}.json`);
+ await writeJsonAtomic(diagramPath, data);
+ sendEmpty(res, 204);
+ return true;
+ }
+
+ if (url.pathname.startsWith('/api/diagrams/')) {
+ const diagramId = decodeURIComponent(
+ url.pathname.replace('/api/diagrams/', '')
+ );
+ if (!isSafeId(diagramId)) {
+ sendJson(res, 400, { error: 'Invalid diagram id.' });
+ return true;
+ }
+ const diagramPath = path.join(diagramsDir, `${diagramId}.json`);
+
+ if (req.method === 'GET') {
+ try {
+ const includeSet = buildIncludeSet(url);
+ const diagram = await readJsonFile(diagramPath);
+ if (!diagram) {
+ sendEmpty(res, 404);
+ return true;
+ }
+ sendJson(res, 200, stripDiagram(diagram, includeSet));
+ } catch (error) {
+ if (error?.code === 'ENOENT') {
+ sendEmpty(res, 404);
+ } else {
+ throw error;
+ }
+ }
+ return true;
+ }
+
+ if (req.method === 'PUT') {
+ const body = await readRequestBody(req);
+ const data = safeJsonParse(body);
+ if (!data || typeof data !== 'object') {
+ sendJson(res, 400, { error: 'Invalid diagram JSON.' });
+ return true;
+ }
+ if (data.id && data.id !== diagramId) {
+ sendJson(res, 400, { error: 'Diagram id mismatch.' });
+ return true;
+ }
+ await writeJsonAtomic(diagramPath, data);
+ sendEmpty(res, 204);
+ return true;
+ }
+
+ if (req.method === 'DELETE') {
+ try {
+ await fs.unlink(diagramPath);
+ } catch (error) {
+ if (error?.code !== 'ENOENT') {
+ throw error;
+ }
+ }
+ sendEmpty(res, 204);
+ return true;
+ }
+ }
+
+ sendJson(res, 404, { error: 'Not found.' });
+ return true;
+ } catch {
+ sendJson(res, 500, { error: 'Internal server error.' });
+ return true;
+ }
+ };
+};
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 000000000..c333a6331
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,90 @@
+import http from 'node:http';
+import path from 'node:path';
+import { promises as fs } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { createApiHandler, resolveDataDir } from './api.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const port = Number.parseInt(process.env.PORT ?? '80', 10);
+const dataDir = resolveDataDir();
+const staticDir =
+ process.env.CHARTDB_STATIC_DIR ?? path.resolve(__dirname, '..', 'dist');
+
+const apiHandler = createApiHandler({ dataDir });
+
+const mimeTypes = new Map([
+ ['.html', 'text/html; charset=utf-8'],
+ ['.js', 'text/javascript; charset=utf-8'],
+ ['.css', 'text/css; charset=utf-8'],
+ ['.json', 'application/json; charset=utf-8'],
+ ['.svg', 'image/svg+xml'],
+ ['.png', 'image/png'],
+ ['.jpg', 'image/jpeg'],
+ ['.jpeg', 'image/jpeg'],
+ ['.webp', 'image/webp'],
+ ['.ico', 'image/x-icon'],
+ ['.woff', 'font/woff'],
+ ['.woff2', 'font/woff2'],
+ ['.ttf', 'font/ttf'],
+ ['.map', 'application/json; charset=utf-8'],
+]);
+
+const sendFile = async (res, filePath) => {
+ const ext = path.extname(filePath);
+ const contentType = mimeTypes.get(ext) ?? 'application/octet-stream';
+ const data = await fs.readFile(filePath);
+ res.writeHead(200, { 'Content-Type': contentType });
+ res.end(data);
+};
+
+const resolveStaticPath = (urlPath) => {
+ const decoded = decodeURIComponent(urlPath.split('?')[0]);
+ const safePath = path.normalize(decoded).replace(/^([/\\])+/, '');
+ return path.join(staticDir, safePath);
+};
+
+const serveStatic = async (req, res) => {
+ const urlPath = req.url ?? '/';
+ const filePath = resolveStaticPath(
+ urlPath === '/' ? '/index.html' : urlPath
+ );
+
+ try {
+ const stat = await fs.stat(filePath);
+ if (stat.isDirectory()) {
+ const indexPath = path.join(filePath, 'index.html');
+ await sendFile(res, indexPath);
+ return;
+ }
+ await sendFile(res, filePath);
+ } catch (error) {
+ if (error?.code === 'ENOENT') {
+ const fallbackPath = path.join(staticDir, 'index.html');
+ try {
+ await sendFile(res, fallbackPath);
+ return;
+ } catch {
+ res.statusCode = 404;
+ res.end('Not Found');
+ return;
+ }
+ }
+
+ res.statusCode = 500;
+ res.end('Internal Server Error');
+ }
+};
+
+const server = http.createServer(async (req, res) => {
+ const handled = await apiHandler(req, res);
+ if (handled) {
+ return;
+ }
+ await serveStatic(req, res);
+});
+
+server.listen(port, () => {
+ console.log(`ChartDB server listening on ${port}`);
+ console.log(`Data directory: ${dataDir}`);
+ console.log(`Static directory: ${staticDir}`);
+});
diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx
index b34a5e728..52c320bdd 100644
--- a/src/components/badge/badge.tsx
+++ b/src/components/badge/badge.tsx
@@ -5,7 +5,8 @@ import { cn } from '@/lib/utils';
import { badgeVariants } from './badge-variants';
export interface BadgeProps
- extends React.HTMLAttributes,
+ extends
+ React.HTMLAttributes,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
diff --git a/src/components/button/button-with-alternatives.tsx b/src/components/button/button-with-alternatives.tsx
index ac1da6db0..8d42a7c08 100644
--- a/src/components/button/button-with-alternatives.tsx
+++ b/src/components/button/button-with-alternatives.tsx
@@ -11,18 +11,27 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/tooltip/tooltip';
+
+export interface ButtonAlternative {
+ label: string;
+ onClick: () => void;
+ disabled?: boolean;
+ icon?: React.ReactNode;
+ className?: string;
+ tooltip?: string;
+}
export interface ButtonWithAlternativesProps
- extends React.ButtonHTMLAttributes,
+ extends
+ React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
- alternatives: Array<{
- label: string;
- onClick: () => void;
- disabled?: boolean;
- icon?: React.ReactNode;
- className?: string;
- }>;
+ alternatives: Array;
dropdownTriggerClassName?: string;
chevronDownIconClassName?: string;
}
@@ -87,19 +96,36 @@ const ButtonWithAlternatives = React.forwardRef<
- {alternatives.map((alternative, index) => (
-
-
- {alternative.label}
- {alternative.icon}
-
-
- ))}
+ {alternatives.map((alternative, index) => {
+ const menuItem = (
+
+
+ {alternative.label}
+ {alternative.icon}
+
+
+ );
+
+ if (alternative.tooltip) {
+ return (
+
+
+ {menuItem}
+
+
+ {alternative.tooltip}
+
+
+ );
+ }
+
+ return menuItem;
+ })}
) : null}
diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx
index e83057dbf..8c832ed5e 100644
--- a/src/components/button/button.tsx
+++ b/src/components/button/button.tsx
@@ -6,7 +6,8 @@ import { cn } from '@/lib/utils';
import { buttonVariants } from './button-variants';
export interface ButtonProps
- extends React.ButtonHTMLAttributes,
+ extends
+ React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
diff --git a/src/components/code-snippet/code-snippet.tsx b/src/components/code-snippet/code-snippet.tsx
index e3389ca7c..0f6ba494f 100644
--- a/src/components/code-snippet/code-snippet.tsx
+++ b/src/components/code-snippet/code-snippet.tsx
@@ -38,7 +38,7 @@ export interface CodeSnippetProps {
className?: string;
code: string;
codeToCopy?: string;
- language?: 'sql' | 'shell';
+ language?: 'sql' | 'shell' | 'dbml';
loading?: boolean;
autoScroll?: boolean;
isComplete?: boolean;
@@ -203,6 +203,7 @@ export const CodeSnippet: React.FC = React.memo(
theme={effectiveTheme}
{...editorProps}
options={{
+ editContext: false,
readOnly: true,
automaticLayout: true,
scrollBeyondLastLine: false,
diff --git a/src/components/code-snippet/dbml/dbml-completion-provider.ts b/src/components/code-snippet/dbml/dbml-completion-provider.ts
new file mode 100644
index 000000000..828b49e84
--- /dev/null
+++ b/src/components/code-snippet/dbml/dbml-completion-provider.ts
@@ -0,0 +1,64 @@
+import type { Monaco } from '@monaco-editor/react';
+import type { IDisposable } from 'monaco-editor';
+import { Compiler, services } from '@dbml/parse';
+
+/**
+ * Creates and manages a DBML completion provider using @dbml/parse.
+ *
+ * The provider maintains a Compiler instance that needs to be kept in sync
+ * with the editor content to provide context-aware completions.
+ */
+export interface DBMLCompletionManager {
+ /** Disposable to clean up the completion provider registration */
+ dispose: () => void;
+ /** Update the compiler with new DBML content */
+ updateSource: (content: string) => void;
+}
+
+// Trigger characters for DBML completions
+const TRIGGER_CHARACTERS = [' ', '[', ':', '.', '>', '<', '-'];
+
+/**
+ * Registers a DBML completion provider with Monaco editor.
+ *
+ * Uses @dbml/parse's built-in DBMLCompletionItemProvider which provides:
+ * - Context-aware keyword suggestions (Table, Ref, Enum, etc.)
+ * - Symbol suggestions based on parsed DBML (table names, column names)
+ * - Field setting suggestions (pk, not null, unique, etc.)
+ * - Ref operator suggestions (>, <, -, <>)
+ *
+ * @param monaco - Monaco editor instance
+ * @param initialContent - Initial DBML content to parse
+ * @returns Manager object with dispose and updateSource methods
+ */
+export function registerDBMLCompletionProvider(
+ monaco: Monaco,
+ initialContent: string = ''
+): DBMLCompletionManager {
+ const compiler = new Compiler();
+
+ // Initialize with content if provided
+ if (initialContent) {
+ compiler.setSource(initialContent);
+ }
+
+ // Create the completion provider from @dbml/parse
+ const completionProvider = new services.DBMLCompletionItemProvider(
+ compiler,
+ TRIGGER_CHARACTERS
+ );
+
+ // Register with Monaco
+ const disposable: IDisposable =
+ monaco.languages.registerCompletionItemProvider(
+ 'dbml',
+ completionProvider
+ );
+
+ return {
+ dispose: () => disposable.dispose(),
+ updateSource: (content: string) => {
+ compiler.setSource(content);
+ },
+ };
+}
diff --git a/src/components/code-snippet/languages/dbml-language.ts b/src/components/code-snippet/languages/dbml-language.ts
index 71a77a7b8..8bb11391a 100644
--- a/src/components/code-snippet/languages/dbml-language.ts
+++ b/src/components/code-snippet/languages/dbml-language.ts
@@ -9,12 +9,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
base: 'vs-dark',
inherit: true,
rules: [
+ { token: 'comment', foreground: '6A9955' }, // Comments
{ token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
{ token: 'string', foreground: 'CE9178' }, // Strings
{ token: 'annotation', foreground: '9CDCFE' }, // [annotations]
{ token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
{ token: 'operator', foreground: 'D4D4D4' }, // Operators
- { token: 'datatype', foreground: '4EC9B0' }, // Data types
+ { token: 'type', foreground: '4EC9B0' }, // Data types
+ { token: 'identifier', foreground: '9CDCFE' }, // Field names
],
colors: {},
});
@@ -23,12 +25,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
base: 'vs',
inherit: true,
rules: [
+ { token: 'comment', foreground: '008000' }, // Comments
{ token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
{ token: 'string', foreground: 'A31515' }, // Strings
{ token: 'annotation', foreground: '001080' }, // [annotations]
{ token: 'delimiter', foreground: '000000' }, // Braces {}
{ token: 'operator', foreground: '000000' }, // Operators
{ token: 'type', foreground: '267F99' }, // Data types
+ { token: 'identifier', foreground: '001080' }, // Field names
],
colors: {},
});
@@ -36,24 +40,88 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
const dataTypesNames = dataTypes.map((dt) => dt.name);
const datatypePattern = dataTypesNames.join('|');
+ // Language configuration for auto-closing brackets, comments, etc.
+ monaco.languages.setLanguageConfiguration('dbml', {
+ brackets: [
+ ['{', '}'],
+ ['[', ']'],
+ ['(', ')'],
+ ],
+ autoClosingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"', notIn: ['string'] },
+ { open: "'", close: "'", notIn: ['string'] },
+ { open: '`', close: '`', notIn: ['string'] },
+ ],
+ surroundingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ { open: '`', close: '`' },
+ ],
+ comments: {
+ lineComment: '//',
+ },
+ });
+
monaco.languages.setMonarchTokensProvider('dbml', {
- keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
+ keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum', 'enum'],
datatypes: dataTypesNames,
+ operators: ['>', '<', '-'],
+
tokenizer: {
root: [
+ // Comments
+ [/\/\/.*$/, 'comment'],
+
+ // Keywords - case insensitive
[
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
'keyword',
],
+
+ // Annotations in brackets
[/\[.*?\]/, 'annotation'],
+
+ // Strings
[/'''/, 'string', '@tripleQuoteString'],
- [/".*?"/, 'string'],
- [/'.*?'/, 'string'],
+ [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-terminated string
+ [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-terminated string
+ [/"/, 'string', '@string_double'],
+ [/'/, 'string', '@string_single'],
[/`.*?`/, 'string'],
- [/[{}]/, 'delimiter'],
- [/[<>]/, 'operator'],
- [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
+
+ // Delimiters and operators
+ [/[{}()]/, 'delimiter'],
+ [/[<>-]/, 'operator'],
+ [/:/, 'delimiter'],
+
+ // Data types
+ [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'],
+
+ // Numbers
+ [/\d+/, 'number'],
+
+ // Identifiers
+ [/[a-zA-Z_]\w*/, 'identifier'],
],
+
+ string_double: [
+ [/[^\\"]+/, 'string'],
+ [/\\./, 'string.escape'],
+ [/"/, 'string', '@pop'],
+ ],
+
+ string_single: [
+ [/[^\\']+/, 'string'],
+ [/\\./, 'string.escape'],
+ [/'/, 'string', '@pop'],
+ ],
+
tripleQuoteString: [
[/[^']+/, 'string'],
[/'''/, 'string', '@pop'],
diff --git a/src/components/color-picker/color-picker.tsx b/src/components/color-picker/color-picker.tsx
index bc02462b9..6a8452cc8 100644
--- a/src/components/color-picker/color-picker.tsx
+++ b/src/components/color-picker/color-picker.tsx
@@ -1,22 +1,176 @@
-import React from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/popover/popover';
-import { colorOptions } from '@/lib/colors';
+import { Input } from '@/components/input/input';
import { cn } from '@/lib/utils';
export interface ColorPickerProps {
color: string;
onChange: (color: string) => void;
disabled?: boolean;
+ popoverOnMouseDown?: (e: React.MouseEvent) => void;
+ popoverOnClick?: (e: React.MouseEvent) => void;
}
export const ColorPicker = React.forwardRef<
React.ElementRef,
ColorPickerProps
->(({ color, onChange, disabled }, ref) => {
+>(({ color, onChange, disabled, popoverOnMouseDown, popoverOnClick }, ref) => {
+ const [recentColors, setRecentColors] = useState([]);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+ try {
+ const stored = window.localStorage.getItem('chartdb.recentColors');
+ if (!stored) return;
+ const parsed = JSON.parse(stored) as string[];
+ if (Array.isArray(parsed)) {
+ setRecentColors(
+ parsed.filter(
+ (item) =>
+ typeof item === 'string' && item.startsWith('#')
+ )
+ );
+ }
+ } catch {
+ // Ignore malformed localStorage content.
+ }
+ }, []);
+
+ const resolvedRgb = useMemo(() => {
+ if (!color) return null;
+ const trimmed = color.trim();
+ if (!trimmed) return null;
+
+ const hexMatch = trimmed.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
+ if (hexMatch) {
+ const raw = hexMatch[1];
+ const expanded =
+ raw.length === 3
+ ? raw
+ .split('')
+ .map((char) => char + char)
+ .join('')
+ : raw;
+ const r = parseInt(expanded.slice(0, 2), 16);
+ const g = parseInt(expanded.slice(2, 4), 16);
+ const b = parseInt(expanded.slice(4, 6), 16);
+ if ([r, g, b].some((channel) => Number.isNaN(channel))) return null;
+ return { r, g, b };
+ }
+
+ if (typeof document === 'undefined') return null;
+ const probe = document.createElement('span');
+ probe.style.position = 'absolute';
+ probe.style.left = '-9999px';
+ probe.style.color = trimmed;
+ if (!probe.style.color) return null;
+ const container = document.body ?? document.documentElement;
+ container.appendChild(probe);
+ const computed = getComputedStyle(probe).color;
+ probe.remove();
+ const parts = computed.match(/[\d.]+/g);
+ if (!parts || parts.length < 3) return null;
+ const [r, g, b] = parts.slice(0, 3).map((part) => Number(part));
+ if ([r, g, b].some((channel) => Number.isNaN(channel))) return null;
+ return { r, g, b };
+ }, [color]);
+
+ const resolvedHex = useMemo(() => {
+ if (!resolvedRgb) return '';
+ return `#${[resolvedRgb.r, resolvedRgb.g, resolvedRgb.b]
+ .map((channel) => channel.toString(16).padStart(2, '0'))
+ .join('')}`;
+ }, [resolvedRgb]);
+
+ const [hexValue, setHexValue] = useState(resolvedHex);
+ const [rgbValues, setRgbValues] = useState(() => ({
+ r: resolvedRgb ? String(resolvedRgb.r) : '',
+ g: resolvedRgb ? String(resolvedRgb.g) : '',
+ b: resolvedRgb ? String(resolvedRgb.b) : '',
+ }));
+
+ useEffect(() => {
+ if (!resolvedHex) return;
+ setHexValue(resolvedHex);
+ setRgbValues({
+ r: String(resolvedRgb?.r ?? ''),
+ g: String(resolvedRgb?.g ?? ''),
+ b: String(resolvedRgb?.b ?? ''),
+ });
+ }, [resolvedHex, resolvedRgb?.r, resolvedRgb?.g, resolvedRgb?.b]);
+
+ const normalizeHex = (value: string) => {
+ const trimmed = value.trim();
+ const match = trimmed.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
+ if (!match) return null;
+ const raw = match[1].toLowerCase();
+ const expanded =
+ raw.length === 3
+ ? raw
+ .split('')
+ .map((char) => char + char)
+ .join('')
+ : raw;
+ return `#${expanded}`;
+ };
+
+ const clampChannel = (value: string) => {
+ if (value === '') return null;
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed)) return null;
+ const rounded = Math.round(parsed);
+ if (rounded < 0 || rounded > 255) return null;
+ return rounded;
+ };
+
+ const applyColor = (nextColor: string) => {
+ setRecentColors((prev) => {
+ const next = [
+ nextColor,
+ ...prev.filter(
+ (item) => item.toLowerCase() !== nextColor.toLowerCase()
+ ),
+ ].slice(0, 8);
+ if (typeof window !== 'undefined') {
+ window.localStorage.setItem(
+ 'chartdb.recentColors',
+ JSON.stringify(next)
+ );
+ }
+ return next;
+ });
+ onChange(nextColor);
+ };
+
+ const handleHexChange = (value: string) => {
+ setHexValue(value);
+ const normalized = normalizeHex(value);
+ if (normalized) {
+ applyColor(normalized);
+ }
+ };
+
+ const handleRgbChange = (channel: 'r' | 'g' | 'b', value: string) => {
+ setRgbValues((prev) => {
+ const next = { ...prev, [channel]: value };
+ const r = clampChannel(next.r);
+ const g = clampChannel(next.g);
+ const b = clampChannel(next.b);
+ if (r !== null && g !== null && b !== null) {
+ applyColor(
+ `#${[r, g, b]
+ .map((item) => item.toString(16).padStart(2, '0'))
+ .join('')}`
+ );
+ }
+ return next;
+ });
+ };
+
return (
-
-
- {colorOptions.map((option) => (
-
onChange(option)}
+
+
+
+
+ {recentColors.length > 0 && (
+
+
+ Recent
+
+
+ {recentColors.map((option) => (
+ applyColor(option)}
+ />
+ ))}
+
+
+ )}
diff --git a/src/components/diagram-icon/diagram-icon.tsx b/src/components/diagram-icon/diagram-icon.tsx
index f9c1c44e4..ce1e57f29 100644
--- a/src/components/diagram-icon/diagram-icon.tsx
+++ b/src/components/diagram-icon/diagram-icon.tsx
@@ -12,8 +12,7 @@ import {
import type { DatabaseType } from '@/lib/domain/database-type';
import { cn } from '@/lib/utils';
-export interface DiagramIconProps
- extends React.ComponentPropsWithoutRef<'div'> {
+export interface DiagramIconProps extends React.ComponentPropsWithoutRef<'div'> {
databaseType: DatabaseType;
databaseEdition?: DatabaseEdition;
imgClassName?: string;
diff --git a/src/components/empty-state/empty-state.tsx b/src/components/empty-state/empty-state.tsx
index 1f44db035..5e08f57be 100644
--- a/src/components/empty-state/empty-state.tsx
+++ b/src/components/empty-state/empty-state.tsx
@@ -1,9 +1,32 @@
-import React, { forwardRef } from 'react';
+import React, { forwardRef, useMemo } from 'react';
import EmptyStateImage from '@/assets/empty_state.png';
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
-import { Label } from '@/components/label/label';
import { cn } from '@/lib/utils';
import { useTheme } from '@/hooks/use-theme';
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '../empty/empty';
+import { Button } from '../button/button';
+
+export interface EmptyStateActionButton {
+ label: string;
+ onClick?: () => void;
+ icon?: React.ReactNode;
+ disabled?: boolean;
+}
+
+export interface EmptyStateFooterAction {
+ label: string;
+ href?: string;
+ onClick?: () => void;
+ icon?: React.ReactNode;
+ disabled?: boolean;
+}
export interface EmptyStateProps {
title: string;
@@ -11,6 +34,9 @@ export interface EmptyStateProps {
imageClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
+ primaryAction?: EmptyStateActionButton;
+ secondaryAction?: EmptyStateActionButton;
+ footerAction?: EmptyStateFooterAction;
}
export const EmptyState = forwardRef<
@@ -25,11 +51,29 @@ export const EmptyState = forwardRef<
titleClassName,
descriptionClassName,
imageClassName,
+ primaryAction,
+ secondaryAction,
+ footerAction,
},
ref
) => {
const { effectiveTheme } = useTheme();
+ // Determine if we have any actions to show
+ const hasActions = useMemo(
+ () => !!(primaryAction || secondaryAction),
+ [primaryAction, secondaryAction]
+ );
+ const hasFooterAction = useMemo(() => !!footerAction, [footerAction]);
+
+ const emptyStateImage = useMemo(
+ () =>
+ effectiveTheme === 'dark'
+ ? EmptyStateImageDark
+ : EmptyStateImage,
+ [effectiveTheme]
+ );
+
return (
-
-
- {title}
-
-
+
+
+ {/* */}
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {/* Action buttons section */}
+ {hasActions && (
+
+
+ {primaryAction && (
+
+ {primaryAction.label}
+ {primaryAction.icon}
+
+ )}
+ {secondaryAction && (
+
+ {secondaryAction.label}
+ {secondaryAction.icon}
+
+ )}
+
+
)}
- >
- {description}
-
+
+ {/* Footer action link */}
+ {hasFooterAction && footerAction && (
+
+ {footerAction.href ? (
+
+ {footerAction.label}
+ {footerAction.icon}
+
+ ) : (
+
+ {footerAction.label}
+ {footerAction.icon}
+
+ )}
+
+ )}
+
+ {/* Render empty content if no actions */}
+ {!hasActions && !hasFooterAction &&
}
+
);
}
diff --git a/src/components/empty/empty.tsx b/src/components/empty/empty.tsx
new file mode 100644
index 000000000..6a0583228
--- /dev/null
+++ b/src/components/empty/empty.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils/index';
+
+function Empty({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+const emptyMediaVariants = cva(
+ 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
+ {
+ variants: {
+ variant: {
+ default: 'bg-transparent',
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+function EmptyMedia({
+ className,
+ variant = 'default',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps
) {
+ return (
+
+ );
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
+ return (
+ a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
+ className
+ )}
+ {...props}
+ />
+ );
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+export {
+ Empty,
+ EmptyHeader,
+ EmptyTitle,
+ EmptyDescription,
+ EmptyContent,
+ EmptyMedia,
+};
diff --git a/src/components/select-box/select-box.tsx b/src/components/select-box/select-box.tsx
index 642fd959b..a3c845f96 100644
--- a/src/components/select-box/select-box.tsx
+++ b/src/components/select-box/select-box.tsx
@@ -27,6 +27,7 @@ export interface SelectBoxOption {
regex?: string;
extractRegex?: RegExp;
group?: string;
+ icon?: React.ReactNode;
}
export interface SelectBoxProps {
@@ -54,6 +55,11 @@ export interface SelectBoxProps {
onOpenChange?: (open: boolean) => void;
popoverClassName?: string;
readonly?: boolean;
+ footerButtons?: React.ReactNode;
+ commandOnMouseDown?: (e: React.MouseEvent) => void;
+ commandOnClick?: (e: React.MouseEvent) => void;
+ onSearchChange?: (search: string) => void;
+ modal?: boolean;
}
export const SelectBox = React.forwardRef
(
@@ -80,6 +86,11 @@ export const SelectBox = React.forwardRef(
onOpenChange: setOpen,
popoverClassName,
readonly,
+ footerButtons,
+ commandOnMouseDown,
+ commandOnClick,
+ onSearchChange,
+ modal = true,
},
ref
) => {
@@ -233,13 +244,19 @@ export const SelectBox = React.forwardRef(
handleSelect(
option.value,
matches?.map((match) => match?.toString())
)
}
+ onMouseDown={commandOnMouseDown}
+ onClick={commandOnClick}
>
{multiple && (
(
)}
+ {option.icon ? (
+
+ {option.icon}
+
+ ) : null}
{isRegexMatch ? searchTerm : option.label}
{!isRegexMatch && optionSuffix
@@ -280,11 +302,19 @@ export const SelectBox = React.forwardRef(
);
},
- [value, multiple, searchTerm, handleSelect, optionSuffix]
+ [
+ value,
+ multiple,
+ searchTerm,
+ handleSelect,
+ optionSuffix,
+ commandOnClick,
+ commandOnMouseDown,
+ ]
);
return (
-
+
(
popoverClassName
)}
align="center"
+ onMouseDown={(e) => e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
>
{
+ const searchLower = search.toLowerCase();
+
if (
keywords?.length &&
- keywords.some((keyword) =>
- new RegExp(keyword).test(search)
+ keywords.some(
+ (keyword) =>
+ keyword
+ .toLowerCase()
+ .includes(searchLower) ||
+ new RegExp(keyword).test(search)
)
) {
return 1;
}
- return value
- .toLowerCase()
- .includes(search.toLowerCase())
+ return value.toLowerCase().includes(searchLower)
? 1
: 0;
}}
@@ -380,7 +416,10 @@ export const SelectBox = React.forwardRef(
setSearchTerm(e)}
+ onValueChange={(e) => {
+ setSearchTerm(e);
+ onSearchChange?.(e);
+ }}
ref={ref}
placeholder={inputPlaceholder ?? 'Search...'}
className="h-9"
@@ -447,6 +486,9 @@ export const SelectBox = React.forwardRef(
+ {footerButtons ? (
+
{footerButtons}
+ ) : null}
);
diff --git a/src/components/sheet/sheet.tsx b/src/components/sheet/sheet.tsx
index c1fc1bb9b..d8b1c9f80 100644
--- a/src/components/sheet/sheet.tsx
+++ b/src/components/sheet/sheet.tsx
@@ -45,7 +45,8 @@ const sheetVariants = cva(
);
interface SheetContentProps
- extends React.ComponentPropsWithoutRef
,
+ extends
+ React.ComponentPropsWithoutRef,
VariantProps {}
const SheetContent = React.forwardRef<
diff --git a/src/components/spinner/spinner.tsx b/src/components/spinner/spinner.tsx
index 67d27b77a..2f89512bc 100644
--- a/src/components/spinner/spinner.tsx
+++ b/src/components/spinner/spinner.tsx
@@ -30,7 +30,8 @@ const loaderVariants = cva('animate-spin text-primary', {
});
interface SpinnerContentProps
- extends VariantProps,
+ extends
+ VariantProps,
VariantProps {
className?: string;
children?: React.ReactNode;
diff --git a/src/components/textarea/textarea.tsx b/src/components/textarea/textarea.tsx
index 8477d1d38..f16275b9e 100644
--- a/src/components/textarea/textarea.tsx
+++ b/src/components/textarea/textarea.tsx
@@ -2,8 +2,7 @@ import React from 'react';
import { cn } from '@/lib/utils';
-export interface TextareaProps
- extends React.TextareaHTMLAttributes {}
+export interface TextareaProps extends React.TextareaHTMLAttributes {}
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
diff --git a/src/components/tree-view/tree-item-skeleton.tsx b/src/components/tree-view/tree-item-skeleton.tsx
index 1baac118d..bf0a1541f 100644
--- a/src/components/tree-view/tree-item-skeleton.tsx
+++ b/src/components/tree-view/tree-item-skeleton.tsx
@@ -2,8 +2,7 @@ import React from 'react';
import { Skeleton } from '../skeleton/skeleton';
import { cn } from '@/lib/utils';
-export interface TreeItemSkeletonProps
- extends React.HTMLAttributes {}
+export interface TreeItemSkeletonProps extends React.HTMLAttributes {}
export const TreeItemSkeleton: React.FC = ({
className,
diff --git a/src/components/tree-view/tree-view.tsx b/src/components/tree-view/tree-view.tsx
index 9424be2d5..e396963f4 100644
--- a/src/components/tree-view/tree-view.tsx
+++ b/src/components/tree-view/tree-view.tsx
@@ -42,6 +42,7 @@ interface TreeViewProps<
renderHoverComponent?: (node: TreeNode) => ReactNode;
renderActionsComponent?: (node: TreeNode) => ReactNode;
loadingNodeIds?: string[];
+ disableCache?: boolean;
}
export function TreeView<
@@ -62,12 +63,14 @@ export function TreeView<
renderHoverComponent,
renderActionsComponent,
loadingNodeIds,
+ disableCache = false,
}: TreeViewProps) {
const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
useTree({
fetchChildren,
expanded: expandedProp,
setExpanded: setExpandedProp,
+ disableCache,
});
const [selectedIdInternal, setSelectedIdInternal] = React.useState<
string | undefined
@@ -145,6 +148,7 @@ export function TreeView<
renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds}
+ disableCache={disableCache}
/>
))}
@@ -179,6 +183,7 @@ interface TreeNodeProps<
renderHoverComponent?: (node: TreeNode) => ReactNode;
renderActionsComponent?: (node: TreeNode) => ReactNode;
loadingNodeIds?: string[];
+ disableCache?: boolean;
}
function TreeNode>({
@@ -201,11 +206,16 @@ function TreeNode>({
renderHoverComponent,
renderActionsComponent,
loadingNodeIds,
+ disableCache = false,
}: TreeNodeProps) {
const [isHovered, setIsHovered] = useState(false);
const isExpanded = expanded[node.id];
const isLoading = loading[node.id];
- const children = loadedChildren[node.id] || node.children;
+ // If cache is disabled, always use fresh node.children
+ // Otherwise, use cached loadedChildren if available (for async fetched data)
+ const children = disableCache
+ ? node.children
+ : node.children || loadedChildren[node.id];
const isSelected = selectedId === node.id;
const IconComponent =
@@ -344,13 +354,27 @@ function TreeNode>({
- {node.empty ? '' : node.name}
+
+ {node.empty ? '' : node.name}
+
+ {node.suffix && (
+
+ {node.suffix}
+
+ )}
{renderActionsComponent && renderActionsComponent(node)}
{isHovered && renderHoverComponent
@@ -423,6 +447,7 @@ function TreeNode>({
renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds}
+ disableCache={disableCache}
/>
))}
{isLoading ? (
diff --git a/src/components/tree-view/tree.ts b/src/components/tree-view/tree.ts
index 71287f3f5..740781904 100644
--- a/src/components/tree-view/tree.ts
+++ b/src/components/tree-view/tree.ts
@@ -12,6 +12,7 @@ export interface TreeNode<
icon?: LucideIcon;
iconProps?: React.ComponentProps;
labelProps?: React.ComponentProps<'span'>;
+ suffix?: string;
type: Type;
unselectable?: boolean;
tooltip?: string;
diff --git a/src/components/tree-view/use-tree.ts b/src/components/tree-view/use-tree.ts
index 5a3c94761..19e34da95 100644
--- a/src/components/tree-view/use-tree.ts
+++ b/src/components/tree-view/use-tree.ts
@@ -28,10 +28,12 @@ export function useTree<
fetchChildren,
expanded: expandedProp,
setExpanded: setExpandedProp,
+ disableCache = false,
}: {
fetchChildren?: FetchChildrenFunction;
expanded?: ExpandedState;
setExpanded?: Dispatch>;
+ disableCache?: boolean;
}) {
const [expandedInternal, setExpandedInternal] = useState({});
@@ -89,8 +91,8 @@ export function useTree<
// Get any previously fetched children
const previouslyFetchedChildren = loadedChildren[nodeId] || [];
- // If we have static children, merge them with any previously fetched children
- if (staticChildren?.length) {
+ // Only cache if caching is enabled
+ if (!disableCache && staticChildren?.length) {
const mergedChildren = mergeChildren(
staticChildren,
previouslyFetchedChildren
@@ -110,8 +112,8 @@ export function useTree<
// Set expanded state immediately to show static/previously fetched children
setExpanded((prev) => ({ ...prev, [nodeId]: true }));
- // If we haven't loaded dynamic children yet
- if (!previouslyFetchedChildren.length) {
+ // If we haven't loaded dynamic children yet and cache is enabled
+ if (!disableCache && !previouslyFetchedChildren.length) {
setLoading((prev) => ({ ...prev, [nodeId]: true }));
try {
const fetchedChildren = await fetchChildren?.(
@@ -140,7 +142,14 @@ export function useTree<
}
}
},
- [expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
+ [
+ expanded,
+ loadedChildren,
+ fetchChildren,
+ mergeChildren,
+ setExpanded,
+ disableCache,
+ ]
);
return {
diff --git a/src/context/canvas-context/canvas-context.tsx b/src/context/canvas-context/canvas-context.tsx
index 3f4f86615..900c76a28 100644
--- a/src/context/canvas-context/canvas-context.tsx
+++ b/src/context/canvas-context/canvas-context.tsx
@@ -2,6 +2,24 @@ import { createContext } from 'react';
import { emptyFn } from '@/lib/utils';
import type { Graph } from '@/lib/graph';
import { createGraph } from '@/lib/graph';
+import { EventEmitter } from 'ahooks/lib/useEventEmitter';
+
+export type CanvasEventType = 'pan_click';
+
+export type CanvasEventBase = {
+ action: T;
+ data: D;
+};
+
+export type PanClickEvent = CanvasEventBase<
+ 'pan_click',
+ {
+ x: number;
+ y: number;
+ }
+>;
+
+export type CanvasEvent = PanClickEvent;
export interface CanvasContext {
reorderTables: (options?: { updateHistory?: boolean }) => void;
@@ -14,6 +32,51 @@ export interface CanvasContext {
overlapGraph: Graph;
setShowFilter: React.Dispatch>;
showFilter: boolean;
+ editTableModeTable: {
+ tableId: string;
+ fieldId?: string;
+ } | null;
+ setEditTableModeTable: React.Dispatch<
+ React.SetStateAction<{
+ tableId: string;
+ fieldId?: string;
+ } | null>
+ >;
+ openRelationshipPopover: (params: {
+ relationshipId: string;
+ position: { x: number; y: number };
+ }) => void;
+ closeRelationshipPopover: () => void;
+ editRelationshipPopover: {
+ relationshipId: string;
+ position: { x: number; y: number };
+ } | null;
+ tempFloatingEdge: {
+ sourceNodeId: string;
+ targetNodeId?: string;
+ } | null;
+ setTempFloatingEdge: React.Dispatch<
+ React.SetStateAction<{
+ sourceNodeId: string;
+ targetNodeId?: string;
+ } | null>
+ >;
+ startFloatingEdgeCreation: ({
+ sourceNodeId,
+ }: {
+ sourceNodeId: string;
+ }) => void;
+ endFloatingEdgeCreation: () => void;
+ hoveringTableId: string | null;
+ setHoveringTableId: React.Dispatch>;
+ showCreateRelationshipNode: (params: {
+ sourceTableId: string;
+ targetTableId: string;
+ x: number;
+ y: number;
+ }) => void;
+ hideCreateRelationshipNode: () => void;
+ events: EventEmitter;
}
export const canvasContext = createContext({
@@ -23,4 +86,18 @@ export const canvasContext = createContext({
overlapGraph: createGraph(),
setShowFilter: emptyFn,
showFilter: false,
+ editTableModeTable: null,
+ setEditTableModeTable: emptyFn,
+ openRelationshipPopover: emptyFn,
+ closeRelationshipPopover: emptyFn,
+ editRelationshipPopover: null,
+ tempFloatingEdge: null,
+ setTempFloatingEdge: emptyFn,
+ startFloatingEdgeCreation: emptyFn,
+ endFloatingEdgeCreation: emptyFn,
+ hoveringTableId: null,
+ setHoveringTableId: emptyFn,
+ showCreateRelationshipNode: emptyFn,
+ hideCreateRelationshipNode: emptyFn,
+ events: new EventEmitter(),
});
diff --git a/src/context/canvas-context/canvas-provider.tsx b/src/context/canvas-context/canvas-provider.tsx
index 8f166d27c..21f1bf9cf 100644
--- a/src/context/canvas-context/canvas-provider.tsx
+++ b/src/context/canvas-context/canvas-provider.tsx
@@ -5,6 +5,7 @@ import React, {
useEffect,
useRef,
} from 'react';
+import type { CanvasContext, CanvasEvent } from './canvas-context';
import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb';
import { adjustTablePositions } from '@/lib/domain/db-table';
@@ -15,6 +16,12 @@ import { createGraph } from '@/lib/graph';
import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
+import {
+ CREATE_RELATIONSHIP_NODE_ID,
+ type CreateRelationshipNodeType,
+} from '@/pages/editor-page/canvas/create-relationship-node/create-relationship-node';
+import { useEventEmitter } from 'ahooks';
+import { useLocalConfig } from '@/hooks/use-local-config';
interface CanvasProviderProps {
children: ReactNode;
@@ -29,12 +36,34 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
areas,
diagramId,
} = useChartDB();
- const { filter, loading: filterLoading } = useDiagramFilter();
- const { fitView } = useReactFlow();
+ const {
+ filter,
+ loading: filterLoading,
+ hasActiveFilter,
+ } = useDiagramFilter();
+ const { showDBViews } = useLocalConfig();
+ const { fitView, screenToFlowPosition, setNodes } = useReactFlow();
const [overlapGraph, setOverlapGraph] =
useState>(createGraph());
+ const [editTableModeTable, setEditTableModeTable] = useState<{
+ tableId: string;
+ fieldId?: string;
+ } | null>(null);
+
+ const [editRelationshipPopover, setEditRelationshipPopover] = useState<{
+ relationshipId: string;
+ position: { x: number; y: number };
+ } | null>(null);
+
+ const events = useEventEmitter();
const [showFilter, setShowFilter] = useState(false);
+
+ const [tempFloatingEdge, setTempFloatingEdge] =
+ useState(null);
+
+ const [hoveringTableId, setHoveringTableId] = useState(null);
+
const diagramIdActiveFilterRef = useRef();
useEffect(() => {
@@ -48,8 +77,11 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
diagramIdActiveFilterRef.current = diagramId;
- setShowFilter(true);
- }, [filterLoading, diagramId]);
+ // Only show filter if there's an active filter
+ if (hasActiveFilter) {
+ setShowFilter(true);
+ }
+ }, [filterLoading, diagramId, hasActiveFilter]);
const reorderTables = useCallback(
(
@@ -59,17 +91,18 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
) => {
const newTables = adjustTablePositions({
relationships,
- tables: tables.filter((table) =>
- filterTable({
- table: {
- id: table.id,
- schema: table.schema,
- },
- filter,
- options: {
- defaultSchema: defaultSchemas[databaseType],
- },
- })
+ tables: tables.filter(
+ (table) =>
+ filterTable({
+ table: {
+ id: table.id,
+ schema: table.schema,
+ },
+ filter,
+ options: {
+ defaultSchema: defaultSchemas[databaseType],
+ },
+ }) && (showDBViews ? true : !table.isView)
),
areas,
mode: 'all',
@@ -115,9 +148,80 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
fitView,
databaseType,
areas,
+ showDBViews,
]
);
+ const startFloatingEdgeCreation: CanvasContext['startFloatingEdgeCreation'] =
+ useCallback(({ sourceNodeId }) => {
+ setShowFilter(false);
+ setTempFloatingEdge({
+ sourceNodeId,
+ });
+ }, []);
+
+ const endFloatingEdgeCreation: CanvasContext['endFloatingEdgeCreation'] =
+ useCallback(() => {
+ setTempFloatingEdge(null);
+ }, []);
+
+ const hideCreateRelationshipNode: CanvasContext['hideCreateRelationshipNode'] =
+ useCallback(() => {
+ setNodes((nds) =>
+ nds.filter((n) => n.id !== CREATE_RELATIONSHIP_NODE_ID)
+ );
+ endFloatingEdgeCreation();
+ }, [setNodes, endFloatingEdgeCreation]);
+
+ const openRelationshipPopover: CanvasContext['openRelationshipPopover'] =
+ useCallback(({ relationshipId, position }) => {
+ setEditRelationshipPopover({ relationshipId, position });
+ }, []);
+
+ const closeRelationshipPopover: CanvasContext['closeRelationshipPopover'] =
+ useCallback(() => {
+ setEditRelationshipPopover(null);
+ }, []);
+
+ const showCreateRelationshipNode: CanvasContext['showCreateRelationshipNode'] =
+ useCallback(
+ ({ sourceTableId, targetTableId, x, y }) => {
+ setTempFloatingEdge((edge) =>
+ edge
+ ? {
+ ...edge,
+ targetNodeId: targetTableId,
+ }
+ : null
+ );
+ const cursorPos = screenToFlowPosition({
+ x,
+ y,
+ });
+
+ const newNode: CreateRelationshipNodeType = {
+ id: CREATE_RELATIONSHIP_NODE_ID,
+ type: 'create-relationship',
+ position: cursorPos,
+ data: {
+ sourceTableId,
+ targetTableId,
+ },
+ draggable: true,
+ selectable: false,
+ zIndex: 1000,
+ };
+
+ setNodes((nds) => {
+ const nodesWithoutOldCreateRelationshipNode = nds.filter(
+ (n) => n.id !== CREATE_RELATIONSHIP_NODE_ID
+ );
+ return [...nodesWithoutOldCreateRelationshipNode, newNode];
+ });
+ },
+ [screenToFlowPosition, setNodes]
+ );
+
return (
{
overlapGraph,
setShowFilter,
showFilter,
+ editTableModeTable,
+ setEditTableModeTable,
+ openRelationshipPopover,
+ closeRelationshipPopover,
+ editRelationshipPopover,
+ tempFloatingEdge: tempFloatingEdge,
+ setTempFloatingEdge: setTempFloatingEdge,
+ startFloatingEdgeCreation: startFloatingEdgeCreation,
+ endFloatingEdgeCreation: endFloatingEdgeCreation,
+ hoveringTableId,
+ setHoveringTableId,
+ showCreateRelationshipNode,
+ hideCreateRelationshipNode,
+ events,
}}
>
{children}
diff --git a/src/context/chartdb-context/chartdb-context.tsx b/src/context/chartdb-context/chartdb-context.tsx
index 31cc68b1e..c94a030f9 100644
--- a/src/context/chartdb-context/chartdb-context.tsx
+++ b/src/context/chartdb-context/chartdb-context.tsx
@@ -4,6 +4,7 @@ import { emptyFn } from '@/lib/utils';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
+import type { DBCheckConstraint } from '@/lib/domain/db-check-constraint';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { Diagram } from '@/lib/domain/diagram';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
@@ -12,6 +13,7 @@ import type { DBDependency } from '@/lib/domain/db-dependency';
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
+import type { Note } from '@/lib/domain/note';
export type ChartDBEventType =
| 'add_tables'
@@ -74,6 +76,7 @@ export interface ChartDBContext {
dependencies: DBDependency[];
areas: Area[];
customTypes: DBCustomType[];
+ notes: Note[];
currentDiagram: Diagram;
events: EventEmitter;
readonly?: boolean;
@@ -172,6 +175,25 @@ export interface ChartDBContext {
options?: { updateHistory: boolean }
) => Promise;
+ // Check constraint operations
+ createCheckConstraint: (tableId: string) => Promise;
+ addCheckConstraint: (
+ tableId: string,
+ constraint: DBCheckConstraint,
+ options?: { updateHistory: boolean }
+ ) => Promise;
+ removeCheckConstraint: (
+ tableId: string,
+ constraintId: string,
+ options?: { updateHistory: boolean }
+ ) => Promise;
+ updateCheckConstraint: (
+ tableId: string,
+ constraintId: string,
+ constraint: Partial,
+ options?: { updateHistory: boolean }
+ ) => Promise;
+
// Relationship operations
createRelationship: (params: {
sourceTableId: string;
@@ -255,6 +277,31 @@ export interface ChartDBContext {
options?: { updateHistory: boolean }
) => Promise;
+ // Note operations
+ createNote: (attributes?: Partial>) => Promise;
+ addNote: (
+ note: Note,
+ options?: { updateHistory: boolean }
+ ) => Promise;
+ addNotes: (
+ notes: Note[],
+ options?: { updateHistory: boolean }
+ ) => Promise;
+ getNote: (id: string) => Note | null;
+ removeNote: (
+ id: string,
+ options?: { updateHistory: boolean }
+ ) => Promise;
+ removeNotes: (
+ ids: string[],
+ options?: { updateHistory: boolean }
+ ) => Promise;
+ updateNote: (
+ id: string,
+ note: Partial,
+ options?: { updateHistory: boolean }
+ ) => Promise;
+
// Custom type operations
createCustomType: (
attributes?: Partial>
@@ -292,6 +339,7 @@ export const chartDBContext = createContext({
dependencies: [],
areas: [],
customTypes: [],
+ notes: [],
schemas: [],
highlightCustomTypeId: emptyFn,
currentDiagram: {
@@ -341,6 +389,12 @@ export const chartDBContext = createContext({
removeIndex: emptyFn,
updateIndex: emptyFn,
+ // Check constraint operations
+ createCheckConstraint: emptyFn,
+ addCheckConstraint: emptyFn,
+ removeCheckConstraint: emptyFn,
+ updateCheckConstraint: emptyFn,
+
// Relationship operations
createRelationship: emptyFn,
addRelationship: emptyFn,
@@ -368,6 +422,15 @@ export const chartDBContext = createContext({
removeAreas: emptyFn,
updateArea: emptyFn,
+ // Note operations
+ createNote: emptyFn,
+ addNote: emptyFn,
+ addNotes: emptyFn,
+ getNote: emptyFn,
+ removeNote: emptyFn,
+ removeNotes: emptyFn,
+ updateNote: emptyFn,
+
// Custom type operations
createCustomType: emptyFn,
addCustomType: emptyFn,
diff --git a/src/context/chartdb-context/chartdb-provider.tsx b/src/context/chartdb-context/chartdb-provider.tsx
index b712cf8c9..44d05bb05 100644
--- a/src/context/chartdb-context/chartdb-provider.tsx
+++ b/src/context/chartdb-context/chartdb-provider.tsx
@@ -10,6 +10,7 @@ import {
getTableIndexesWithPrimaryKey,
type DBIndex,
} from '@/lib/domain/db-index';
+import type { DBCheckConstraint } from '@/lib/domain/db-check-constraint';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
@@ -24,6 +25,7 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
+import type { Note } from '@/lib/domain/note';
import { storageInitialValue } from '../storage-context/storage-context';
import { useDiff } from '../diff-context/use-diff';
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
@@ -31,6 +33,7 @@ import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
+import { getDefaultPrimaryKeyType } from '@/lib/data/data-types/data-types';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -67,6 +70,7 @@ export const ChartDBProvider: React.FC<
const [customTypes, setCustomTypes] = useState(
diagram?.customTypes ?? []
);
+ const [notes, setNotes] = useState(diagram?.notes ?? []);
const { events: diffEvents } = useDiff();
@@ -74,10 +78,11 @@ export const ChartDBProvider: React.FC<
useState();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
- const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
+ const { tablesToAdd, fieldsToAdd, relationshipsToAdd, areasToAdd } =
+ event.data;
setTables((tables) =>
- [...tables, ...(tablesAdded ?? [])].map((table) => {
- const fields = fieldsAdded.get(table.id);
+ [...tables, ...(tablesToAdd ?? [])].map((table) => {
+ const fields = fieldsToAdd.get(table.id);
return fields
? { ...table, fields: [...table.fields, ...fields] }
: table;
@@ -85,8 +90,9 @@ export const ChartDBProvider: React.FC<
);
setRelationships((relationships) => [
...relationships,
- ...(relationshipsAdded ?? []),
+ ...(relationshipsToAdd ?? []),
]);
+ setAreas((areas) => [...areas, ...(areasToAdd ?? [])]);
}, []);
diffEvents.useSubscription(diffCalculatedHandler);
@@ -147,6 +153,7 @@ export const ChartDBProvider: React.FC<
dependencies,
areas,
customTypes,
+ notes,
}),
[
diagramId,
@@ -158,6 +165,7 @@ export const ChartDBProvider: React.FC<
dependencies,
areas,
customTypes,
+ notes,
diagramCreatedAt,
diagramUpdatedAt,
]
@@ -171,6 +179,7 @@ export const ChartDBProvider: React.FC<
setDependencies([]);
setAreas([]);
setCustomTypes([]);
+ setNotes([]);
setDiagramUpdatedAt(updatedAt);
resetRedoStack();
@@ -183,6 +192,7 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
db.deleteDiagramCustomTypes(diagramId),
+ db.deleteDiagramNotes(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -197,6 +207,7 @@ export const ChartDBProvider: React.FC<
setDependencies([]);
setAreas([]);
setCustomTypes([]);
+ setNotes([]);
resetRedoStack();
resetUndoStack();
@@ -207,6 +218,7 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
db.deleteDiagramCustomTypes(diagramId),
+ db.deleteDiagramNotes(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -325,19 +337,20 @@ export const ChartDBProvider: React.FC<
const createTable: ChartDBContext['createTable'] = useCallback(
async (attributes) => {
+ const isView = attributes?.isView ?? false;
+ const count = isView
+ ? tables.filter((t) => t.isView).length + 1
+ : tables.filter((t) => !t.isView).length + 1;
const table: DBTable = {
id: generateId(),
- name: `table_${tables.length + 1}`,
+ name: isView ? `view_${count}` : `table_${count}`,
x: 0,
y: 0,
fields: [
{
id: generateId(),
name: 'id',
- type:
- databaseType === DatabaseType.SQLITE
- ? { id: 'integer', name: 'integer' }
- : { id: 'bigint', name: 'bigint' },
+ type: getDefaultPrimaryKeyType(databaseType),
unique: true,
nullable: false,
primaryKey: true,
@@ -350,6 +363,7 @@ export const ChartDBProvider: React.FC<
isView: false,
order: tables.length,
...attributes,
+ schema: attributes?.schema ?? defaultSchemas[databaseType],
};
table.indexes = getTableIndexesWithPrimaryKey({
@@ -856,10 +870,7 @@ export const ChartDBProvider: React.FC<
const field: DBField = {
id: generateId(),
name: `field_${(table?.fields?.length ?? 0) + 1}`,
- type:
- databaseType === DatabaseType.SQLITE
- ? { id: 'integer', name: 'integer' }
- : { id: 'bigint', name: 'bigint' },
+ type: getDefaultPrimaryKeyType(databaseType),
unique: false,
nullable: true,
primaryKey: false,
@@ -1053,6 +1064,212 @@ export const ChartDBProvider: React.FC<
[db, diagramId, setTables, addUndoAction, resetRedoStack, getIndex]
);
+ const addCheckConstraint: ChartDBContext['addCheckConstraint'] =
+ useCallback(
+ async (
+ tableId: string,
+ constraint: DBCheckConstraint,
+ options = { updateHistory: true }
+ ) => {
+ setTables((tables) =>
+ tables.map((t) =>
+ t.id === tableId
+ ? {
+ ...t,
+ checkConstraints: [
+ ...(t.checkConstraints ?? []),
+ constraint,
+ ],
+ }
+ : t
+ )
+ );
+
+ const dbTable = await db.getTable({ diagramId, id: tableId });
+ if (!dbTable) {
+ return;
+ }
+
+ const updatedAt = new Date();
+ setDiagramUpdatedAt(updatedAt);
+ await Promise.all([
+ db.updateDiagram({
+ id: diagramId,
+ attributes: { updatedAt },
+ }),
+ db.updateTable({
+ id: tableId,
+ attributes: {
+ ...dbTable,
+ checkConstraints: [
+ ...(dbTable.checkConstraints ?? []),
+ constraint,
+ ],
+ },
+ }),
+ ]);
+
+ if (options.updateHistory) {
+ addUndoAction({
+ action: 'addCheckConstraint',
+ redoData: { tableId, constraint },
+ undoData: { tableId, constraintId: constraint.id },
+ });
+ resetRedoStack();
+ }
+ },
+ [db, diagramId, setTables, addUndoAction, resetRedoStack]
+ );
+
+ const createCheckConstraint: ChartDBContext['createCheckConstraint'] =
+ useCallback(
+ async (tableId: string) => {
+ const constraint: DBCheckConstraint = {
+ id: generateId(),
+ expression: '',
+ createdAt: Date.now(),
+ };
+
+ await addCheckConstraint(tableId, constraint);
+
+ return constraint;
+ },
+ [addCheckConstraint]
+ );
+
+ const removeCheckConstraint: ChartDBContext['removeCheckConstraint'] =
+ useCallback(
+ async (
+ tableId: string,
+ constraintId: string,
+ options = { updateHistory: true }
+ ) => {
+ const table = getTable(tableId);
+ const prevConstraint = table?.checkConstraints?.find(
+ (c) => c.id === constraintId
+ );
+
+ setTables((tables) =>
+ tables.map((t) =>
+ t.id === tableId
+ ? {
+ ...t,
+ checkConstraints: (
+ t.checkConstraints ?? []
+ ).filter((c) => c.id !== constraintId),
+ }
+ : t
+ )
+ );
+
+ const dbTable = await db.getTable({ diagramId, id: tableId });
+ if (!dbTable) {
+ return;
+ }
+
+ const updatedAt = new Date();
+ setDiagramUpdatedAt(updatedAt);
+ await Promise.all([
+ db.updateDiagram({
+ id: diagramId,
+ attributes: { updatedAt },
+ }),
+ db.updateTable({
+ id: tableId,
+ attributes: {
+ ...dbTable,
+ checkConstraints: (
+ dbTable.checkConstraints ?? []
+ ).filter((c) => c.id !== constraintId),
+ },
+ }),
+ ]);
+
+ if (!!prevConstraint && options.updateHistory) {
+ addUndoAction({
+ action: 'removeCheckConstraint',
+ redoData: { tableId, constraintId },
+ undoData: { tableId, constraint: prevConstraint },
+ });
+ resetRedoStack();
+ }
+ },
+ [db, diagramId, setTables, addUndoAction, resetRedoStack, getTable]
+ );
+
+ const updateCheckConstraint: ChartDBContext['updateCheckConstraint'] =
+ useCallback(
+ async (
+ tableId: string,
+ constraintId: string,
+ constraint: Partial,
+ options = { updateHistory: true }
+ ) => {
+ const table = getTable(tableId);
+ const prevConstraint = table?.checkConstraints?.find(
+ (c) => c.id === constraintId
+ );
+
+ setTables((tables) =>
+ tables.map((t) =>
+ t.id === tableId
+ ? {
+ ...t,
+ checkConstraints: (
+ t.checkConstraints ?? []
+ ).map((c) =>
+ c.id === constraintId
+ ? { ...c, ...constraint }
+ : c
+ ),
+ }
+ : t
+ )
+ );
+
+ const dbTable = await db.getTable({ diagramId, id: tableId });
+ if (!dbTable) {
+ return;
+ }
+
+ const updatedAt = new Date();
+ setDiagramUpdatedAt(updatedAt);
+ await Promise.all([
+ db.updateDiagram({
+ id: diagramId,
+ attributes: { updatedAt },
+ }),
+ db.updateTable({
+ id: tableId,
+ attributes: {
+ ...dbTable,
+ checkConstraints: (
+ dbTable.checkConstraints ?? []
+ ).map((c) =>
+ c.id === constraintId
+ ? { ...c, ...constraint }
+ : c
+ ),
+ },
+ }),
+ ]);
+
+ if (!!prevConstraint && options.updateHistory) {
+ addUndoAction({
+ action: 'updateCheckConstraint',
+ redoData: { tableId, constraintId, constraint },
+ undoData: {
+ tableId,
+ constraintId,
+ constraint: prevConstraint,
+ },
+ });
+ resetRedoStack();
+ }
+ },
+ [db, diagramId, setTables, addUndoAction, resetRedoStack, getTable]
+ );
+
const addRelationships: ChartDBContext['addRelationships'] = useCallback(
async (
relationships: DBRelationship[],
@@ -1527,6 +1744,130 @@ export const ChartDBProvider: React.FC<
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
);
+ // Note operations
+ const addNotes: ChartDBContext['addNotes'] = useCallback(
+ async (notes: Note[], options = { updateHistory: true }) => {
+ setNotes((currentNotes) => [...currentNotes, ...notes]);
+
+ const updatedAt = new Date();
+ setDiagramUpdatedAt(updatedAt);
+
+ await Promise.all([
+ ...notes.map((note) => db.addNote({ diagramId, note })),
+ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
+ ]);
+
+ if (options.updateHistory) {
+ addUndoAction({
+ action: 'addNotes',
+ redoData: { notes },
+ undoData: { noteIds: notes.map((n) => n.id) },
+ });
+ resetRedoStack();
+ }
+ },
+ [db, diagramId, setNotes, addUndoAction, resetRedoStack]
+ );
+
+ const addNote: ChartDBContext['addNote'] = useCallback(
+ async (note: Note, options = { updateHistory: true }) => {
+ return addNotes([note], options);
+ },
+ [addNotes]
+ );
+
+ const createNote: ChartDBContext['createNote'] = useCallback(
+ async (attributes) => {
+ const note: Note = {
+ id: generateId(),
+ content: '',
+ x: 0,
+ y: 0,
+ width: 200,
+ height: 150,
+ color: '#ffe374', // Default warm yellow
+ ...attributes,
+ };
+
+ await addNote(note);
+
+ return note;
+ },
+ [addNote]
+ );
+
+ const getNote: ChartDBContext['getNote'] = useCallback(
+ (id: string) => notes.find((note) => note.id === id) ?? null,
+ [notes]
+ );
+
+ const removeNotes: ChartDBContext['removeNotes'] = useCallback(
+ async (ids: string[], options = { updateHistory: true }) => {
+ const prevNotes = [
+ ...notes.filter((note) => ids.includes(note.id)),
+ ];
+
+ setNotes((notes) => notes.filter((note) => !ids.includes(note.id)));
+
+ const updatedAt = new Date();
+ setDiagramUpdatedAt(updatedAt);
+
+ await Promise.all([
+ ...ids.map((id) => db.deleteNote({ diagramId, id })),
+ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
+ ]);
+
+ if (prevNotes.length > 0 && options.updateHistory) {
+ addUndoAction({
+ action: 'removeNotes',
+ redoData: { noteIds: ids },
+ undoData: { notes: prevNotes },
+ });
+ resetRedoStack();
+ }
+ },
+ [db, diagramId, setNotes, notes, addUndoAction, resetRedoStack]
+ );
+
+ const removeNote: ChartDBContext['removeNote'] = useCallback(
+ async (id: string, options = { updateHistory: true }) => {
+ return removeNotes([id], options);
+ },
+ [removeNotes]
+ );
+
+ const updateNote: ChartDBContext['updateNote'] = useCallback(
+ async (
+ id: string,
+ note: Partial,
+ options = { updateHistory: true }
+ ) => {
+ const prevNote = getNote(id);
+
+ setNotes((notes) =>
+ notes.map((n) => (n.id === id ? { ...n, ...note } : n))
+ );
+
+ const updatedAt = new Date();
+ setDiagramUpdatedAt(updatedAt);
+
+ await Promise.all([
+ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
+ db.updateNote({ id, attributes: note }),
+ ]);
+
+ if (!!prevNote && options.updateHistory) {
+ addUndoAction({
+ action: 'updateNote',
+ redoData: { noteId: id, note },
+ undoData: { noteId: id, note: prevNote },
+ });
+ resetRedoStack();
+ }
+ },
+ [db, diagramId, setNotes, getNote, addUndoAction, resetRedoStack]
+ );
+
const highlightCustomTypeId = useCallback(
(id?: string) => setHighlightedCustomTypeId(id),
[setHighlightedCustomTypeId]
@@ -1553,6 +1894,7 @@ export const ChartDBProvider: React.FC<
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
setHighlightedCustomTypeId(undefined);
+ setNotes(diagram.notes ?? []);
events.emit({ action: 'load_diagram', data: { diagram } });
@@ -1573,6 +1915,7 @@ export const ChartDBProvider: React.FC<
setDiagramUpdatedAt,
setHighlightedCustomTypeId,
events,
+ setNotes,
resetRedoStack,
resetUndoStack,
]
@@ -1596,6 +1939,7 @@ export const ChartDBProvider: React.FC<
includeDependencies: true,
includeAreas: true,
includeCustomTypes: true,
+ includeNotes: true,
});
if (diagram) {
@@ -1761,6 +2105,7 @@ export const ChartDBProvider: React.FC<
relationships,
dependencies,
areas,
+ notes,
currentDiagram,
schemas,
events,
@@ -1793,6 +2138,10 @@ export const ChartDBProvider: React.FC<
getField,
getIndex,
updateIndex,
+ createCheckConstraint,
+ addCheckConstraint,
+ removeCheckConstraint,
+ updateCheckConstraint,
addRelationship,
addRelationships,
createRelationship,
@@ -1824,6 +2173,13 @@ export const ChartDBProvider: React.FC<
updateCustomType,
highlightCustomTypeId,
highlightedCustomType,
+ createNote,
+ addNote,
+ addNotes,
+ getNote,
+ removeNote,
+ removeNotes,
+ updateNote,
}}
>
{children}
diff --git a/src/context/config-context/config-provider.tsx b/src/context/config-context/config-provider.tsx
index 5a9e26cd2..c07de383a 100644
--- a/src/context/config-context/config-provider.tsx
+++ b/src/context/config-context/config-provider.tsx
@@ -4,6 +4,86 @@ import { ConfigContext } from './config-context';
import { useStorage } from '@/hooks/use-storage';
import type { ChartDBConfig } from '@/lib/domain/config';
+const defaultPrimaryForegroundLight = '210 40% 98%';
+const defaultPrimaryForegroundDark = '222.2 47.4% 11.2%';
+
+const parseRgb = (value: string) => {
+ const parts = value.match(/[\d.]+/g);
+ if (!parts || parts.length < 3) return null;
+ const [r, g, b] = parts.slice(0, 3).map((part) => Number(part));
+ if ([r, g, b].some((channel) => Number.isNaN(channel))) {
+ return null;
+ }
+ return { r, g, b };
+};
+
+const rgbToHsl = ({ r, g, b }: { r: number; g: number; b: number }) => {
+ const rNorm = r / 255;
+ const gNorm = g / 255;
+ const bNorm = b / 255;
+ const max = Math.max(rNorm, gNorm, bNorm);
+ const min = Math.min(rNorm, gNorm, bNorm);
+ const delta = max - min;
+ let h = 0;
+
+ if (delta !== 0) {
+ if (max === rNorm) {
+ h = ((gNorm - bNorm) / delta) % 6;
+ } else if (max === gNorm) {
+ h = (bNorm - rNorm) / delta + 2;
+ } else {
+ h = (rNorm - gNorm) / delta + 4;
+ }
+ h *= 60;
+ if (h < 0) h += 360;
+ }
+
+ const l = (max + min) / 2;
+ const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+
+ return { h, s: s * 100, l: l * 100 };
+};
+
+const getLuminance = ({ r, g, b }: { r: number; g: number; b: number }) => {
+ const toLinear = (value: number) => {
+ const normalized = value / 255;
+ return normalized <= 0.03928
+ ? normalized / 12.92
+ : Math.pow((normalized + 0.055) / 1.055, 2.4);
+ };
+ const rLinear = toLinear(r);
+ const gLinear = toLinear(g);
+ const bLinear = toLinear(b);
+ return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
+};
+
+const resolvePrimaryColor = (value?: string | null) => {
+ if (!value) return null;
+ if (typeof document === 'undefined') return null;
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+
+ const probe = document.createElement('span');
+ probe.style.position = 'absolute';
+ probe.style.left = '-9999px';
+ probe.style.color = trimmed;
+ if (!probe.style.color) return null;
+
+ const container = document.body ?? document.documentElement;
+ container.appendChild(probe);
+ const computed = getComputedStyle(probe).color;
+ probe.remove();
+
+ const rgb = parseRgb(computed);
+ if (!rgb) return null;
+
+ const { h, s, l } = rgbToHsl(rgb);
+ return {
+ hsl: `${h.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(1)}%`,
+ luminance: getLuminance(rgb),
+ };
+};
+
export const ConfigProvider: React.FC = ({
children,
}) => {
@@ -19,13 +99,36 @@ export const ConfigProvider: React.FC = ({
loadConfig();
}, [getConfig]);
+ useEffect(() => {
+ if (typeof document === 'undefined') return;
+ const root = document.documentElement;
+ const resolved = resolvePrimaryColor(config?.primaryColor);
+
+ if (!resolved) {
+ root.style.removeProperty('--primary');
+ root.style.removeProperty('--primary-foreground');
+ return;
+ }
+
+ root.style.setProperty('--primary', resolved.hsl);
+ root.style.setProperty(
+ '--primary-foreground',
+ resolved.luminance > 0.6
+ ? defaultPrimaryForegroundDark
+ : defaultPrimaryForegroundLight
+ );
+ }, [config?.primaryColor]);
+
const updateConfig: ConfigContext['updateConfig'] = async ({
config,
updateFn,
}) => {
const promise = new Promise((resolve) => {
setConfig((prevConfig) => {
- let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
+ let baseConfig: ChartDBConfig = {
+ defaultDiagramId: '',
+ hideSocialLinks: false,
+ };
if (prevConfig) {
baseConfig = prevConfig;
}
diff --git a/src/context/dialog-context/dialog-context.tsx b/src/context/dialog-context/dialog-context.tsx
index a574e4c7f..0992b21ad 100644
--- a/src/context/dialog-context/dialog-context.tsx
+++ b/src/context/dialog-context/dialog-context.tsx
@@ -7,7 +7,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
-import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
@@ -67,12 +66,6 @@ export interface DialogContext {
params: Omit
) => void;
closeImportDiagramDialog: () => void;
-
- // Import DBML dialog
- openImportDBMLDialog: (
- params?: Omit
- ) => void;
- closeImportDBMLDialog: () => void;
}
export const dialogContext = createContext({
@@ -96,6 +89,4 @@ export const dialogContext = createContext({
closeExportDiagramDialog: emptyFn,
openImportDiagramDialog: emptyFn,
closeImportDiagramDialog: emptyFn,
- openImportDBMLDialog: emptyFn,
- closeImportDBMLDialog: emptyFn,
});
diff --git a/src/context/dialog-context/dialog-provider.tsx b/src/context/dialog-context/dialog-provider.tsx
index 35a834c5f..4399a1c6e 100644
--- a/src/context/dialog-context/dialog-provider.tsx
+++ b/src/context/dialog-context/dialog-provider.tsx
@@ -20,8 +20,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
-import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
-import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
export const DialogProvider: React.FC = ({
children,
@@ -103,8 +101,12 @@ export const DialogProvider: React.FC = ({
});
const openImportDatabaseDialogHandler: DialogContext['openImportDatabaseDialog'] =
useCallback(
- ({ databaseType }) => {
- setImportDatabaseDialogParams({ databaseType });
+ ({ databaseType, importMethods, initialImportMethod }) => {
+ setImportDatabaseDialogParams({
+ databaseType,
+ importMethods,
+ initialImportMethod,
+ });
setOpenImportDatabaseDialog(true);
},
[setOpenImportDatabaseDialog]
@@ -132,11 +134,6 @@ export const DialogProvider: React.FC = ({
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
useState(false);
- // Import DBML dialog
- const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
- const [importDBMLDialogParams, setImportDBMLDialogParams] =
- useState>();
-
return (
= ({
closeCreateRelationshipDialog: () =>
setOpenCreateRelationshipDialog(false),
openImportDatabaseDialog: openImportDatabaseDialogHandler,
- closeImportDatabaseDialog: () =>
- setOpenImportDatabaseDialog(false),
+ closeImportDatabaseDialog: () => {
+ setOpenImportDatabaseDialog(false);
+ },
openTableSchemaDialog: openTableSchemaDialogHandler,
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
openStarUsDialog: () => setOpenStarUsDialog(true),
@@ -165,11 +163,6 @@ export const DialogProvider: React.FC = ({
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
closeImportDiagramDialog: () =>
setOpenImportDiagramDialog(false),
- openImportDBMLDialog: (params) => {
- setImportDBMLDialogParams(params);
- setOpenImportDBMLDialog(true);
- },
- closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
}}
>
{children}
@@ -204,10 +197,6 @@ export const DialogProvider: React.FC = ({
/>
-
);
};
diff --git a/src/context/diff-context/diff-context.tsx b/src/context/diff-context/diff-context.tsx
index 491fa045a..62c831bc0 100644
--- a/src/context/diff-context/diff-context.tsx
+++ b/src/context/diff-context/diff-context.tsx
@@ -5,6 +5,7 @@ import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import type { DBRelationship } from '@/lib/domain/db-relationship';
+import type { Area } from '@/lib/domain/area';
import type { DiffMap } from '@/lib/domain/diff/diff';
export type DiffEventType = 'diff_calculated';
@@ -15,9 +16,10 @@ export type DiffEventBase = {
};
export type DiffCalculatedData = {
- tablesAdded: DBTable[];
- fieldsAdded: Map;
- relationshipsAdded: DBRelationship[];
+ tablesToAdd: DBTable[];
+ fieldsToAdd: Map;
+ relationshipsToAdd: DBRelationship[];
+ areasToAdd: Area[];
};
export type DiffCalculatedEvent = DiffEventBase<
@@ -33,6 +35,7 @@ export interface DiffContext {
diffMap: DiffMap;
hasDiff: boolean;
isSummaryOnly: boolean;
+ relationshipIdMap: Map;
calculateDiff: ({
diagram,
@@ -44,15 +47,21 @@ export interface DiffContext {
options?: {
summaryOnly?: boolean;
};
- }) => void;
+ }) => { foundDiff: boolean };
resetDiff: () => void;
// table diff
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
- getTableNewName: ({ tableId }: { tableId: string }) => string | null;
- getTableNewColor: ({ tableId }: { tableId: string }) => string | null;
+ getTableNewName: ({ tableId }: { tableId: string }) => {
+ old: string;
+ new: string;
+ } | null;
+ getTableNewColor: ({ tableId }: { tableId: string }) => {
+ old: string;
+ new: string;
+ } | null;
// field diff
checkIfFieldHasChange: ({
@@ -64,19 +73,53 @@ export interface DiffContext {
}) => boolean;
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
- getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
- getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
- getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
- getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
+ getFieldNewName: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: string; new: string } | null;
+ getFieldNewType: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: DataType; new: DataType } | null;
+ getFieldNewPrimaryKey: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: boolean; new: boolean } | null;
+ getFieldNewNullable: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: boolean; new: boolean } | null;
getFieldNewCharacterMaximumLength: ({
fieldId,
}: {
fieldId: string;
- }) => string | null;
- getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
- getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
+ }) => { old: string; new: string } | null;
+ getFieldNewScale: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: number; new: number } | null;
+ getFieldNewPrecision: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: number; new: number } | null;
+ getFieldNewIsArray: ({
+ fieldId,
+ }: {
+ fieldId: string;
+ }) => { old: boolean; new: boolean } | null;
// relationship diff
+ checkIfRelationshipHasChange: ({
+ relationshipId,
+ }: {
+ relationshipId: string;
+ }) => boolean;
checkIfNewRelationship: ({
relationshipId,
}: {
@@ -87,6 +130,15 @@ export interface DiffContext {
}: {
relationshipId: string;
}) => boolean;
+ getRelationshipNewName: ({
+ relationshipId,
+ }: {
+ relationshipId: string;
+ }) => { old: string; new: string } | null;
+
+ // area diff
+ checkIfNewArea: ({ areaId }: { areaId: string }) => boolean;
+ checkIfAreaRemoved: ({ areaId }: { areaId: string }) => boolean;
events: EventEmitter;
}
diff --git a/src/context/diff-context/diff-provider.tsx b/src/context/diff-context/diff-provider.tsx
index b11e8bc17..dd596155c 100644
--- a/src/context/diff-context/diff-provider.tsx
+++ b/src/context/diff-context/diff-provider.tsx
@@ -15,6 +15,7 @@ import { useEventEmitter } from 'ahooks';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import type { DBRelationship } from '@/lib/domain/db-relationship';
+import type { Area } from '@/lib/domain/area';
import type { ChartDBDiff, DiffMap } from '@/lib/domain/diff/diff';
export const DiffProvider: React.FC = ({
@@ -32,11 +33,17 @@ export const DiffProvider: React.FC = ({
const [fieldsChanged, setFieldsChanged] = React.useState<
Map
>(new Map());
+ const [relationshipsChanged, setRelationshipsChanged] = React.useState<
+ Map
+ >(new Map());
+ const [relationshipIdMap, setRelationshipIdMap] = React.useState<
+ Map
+ >(new Map());
const [isSummaryOnly, setIsSummaryOnly] = React.useState(false);
const events = useEventEmitter();
- const generateNewFieldsMap = useCallback(
+ const generateFieldsToAddMap = useCallback(
({
diffMap,
newDiagram,
@@ -66,7 +73,7 @@ export const DiffProvider: React.FC = ({
[]
);
- const findNewRelationships = useCallback(
+ const findRelationshipsToAdd = useCallback(
({
diffMap,
newDiagram,
@@ -92,6 +99,32 @@ export const DiffProvider: React.FC = ({
[]
);
+ const findAreasToAdd = useCallback(
+ ({
+ diffMap,
+ newDiagram,
+ }: {
+ diffMap: DiffMap;
+ newDiagram: Diagram;
+ }) => {
+ const areas: Area[] = [];
+ diffMap.forEach((diff) => {
+ if (diff.object === 'area' && diff.type === 'added') {
+ const area = newDiagram?.areas?.find(
+ (a) => a.id === diff.areaAdded.id
+ );
+
+ if (area) {
+ areas.push(area);
+ }
+ }
+ });
+
+ return areas;
+ },
+ []
+ );
+
const generateDiffCalculatedData = useCallback(
({
newDiagram,
@@ -101,7 +134,7 @@ export const DiffProvider: React.FC = ({
diffMap: DiffMap;
}): DiffCalculatedData => {
return {
- tablesAdded:
+ tablesToAdd:
newDiagram?.tables?.filter((table) => {
const tableKey = getDiffMapKey({
diffObject: 'table',
@@ -114,17 +147,21 @@ export const DiffProvider: React.FC = ({
);
}) ?? [],
- fieldsAdded: generateNewFieldsMap({
+ fieldsToAdd: generateFieldsToAddMap({
diffMap: diffMap,
newDiagram: newDiagram,
}),
- relationshipsAdded: findNewRelationships({
+ relationshipsToAdd: findRelationshipsToAdd({
+ diffMap: diffMap,
+ newDiagram: newDiagram,
+ }),
+ areasToAdd: findAreasToAdd({
diffMap: diffMap,
newDiagram: newDiagram,
}),
};
},
- [findNewRelationships, generateNewFieldsMap]
+ [findRelationshipsToAdd, generateFieldsToAddMap, findAreasToAdd]
);
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
@@ -133,11 +170,15 @@ export const DiffProvider: React.FC = ({
diffMap: newDiffs,
changedTables: newChangedTables,
changedFields: newChangedFields,
+ changedRelationships: newChangedRelationships,
+ relationshipIdMap: newRelationshipIdMap,
} = generateDiff({ diagram, newDiagram: newDiagramArg });
setDiffMap(newDiffs);
setTablesChanged(newChangedTables);
setFieldsChanged(newChangedFields);
+ setRelationshipsChanged(newChangedRelationships);
+ setRelationshipIdMap(newRelationshipIdMap);
setNewDiagram(newDiagramArg);
setOriginalDiagram(diagram);
setIsSummaryOnly(options?.summaryOnly ?? false);
@@ -149,6 +190,8 @@ export const DiffProvider: React.FC = ({
newDiagram: newDiagramArg,
}),
});
+
+ return { foundDiff: !!newDiffs.size };
},
[setDiffMap, events, generateDiffCalculatedData]
);
@@ -165,7 +208,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(tableNameKey);
if (diff?.type === 'changed') {
- return diff.newValue as string;
+ return {
+ new: diff.newValue as string,
+ old: diff.oldValue as string,
+ };
}
}
@@ -186,7 +232,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(tableColorKey);
if (diff?.type === 'changed') {
- return diff.newValue as string;
+ return {
+ new: diff.newValue as string,
+ old: diff.oldValue as string,
+ };
}
}
return null;
@@ -277,7 +326,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as string;
+ return {
+ old: diff.oldValue as string,
+ new: diff.newValue as string,
+ };
}
}
@@ -298,7 +350,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as DataType;
+ return {
+ old: diff.oldValue as DataType,
+ new: diff.newValue as DataType,
+ };
}
}
@@ -321,7 +376,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as boolean;
+ return {
+ old: diff.oldValue as boolean,
+ new: diff.newValue as boolean,
+ };
}
}
@@ -342,7 +400,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as boolean;
+ return {
+ old: diff.oldValue as boolean,
+ new: diff.newValue as boolean,
+ };
}
}
@@ -365,7 +426,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as string;
+ return {
+ old: diff.oldValue as string,
+ new: diff.newValue as string,
+ };
}
}
@@ -386,7 +450,10 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as number;
+ return {
+ old: diff.oldValue as number,
+ new: diff.newValue as number,
+ };
}
}
@@ -409,7 +476,34 @@ export const DiffProvider: React.FC = ({
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
- return diff.newValue as number;
+ return {
+ old: diff.oldValue as number,
+ new: diff.newValue as number,
+ };
+ }
+ }
+
+ return null;
+ },
+ [diffMap]
+ );
+
+ const getFieldNewIsArray = useCallback(
+ ({ fieldId }) => {
+ const fieldKey = getDiffMapKey({
+ diffObject: 'field',
+ objectId: fieldId,
+ attribute: 'isArray',
+ });
+
+ if (diffMap.has(fieldKey)) {
+ const diff = diffMap.get(fieldKey);
+
+ if (diff?.type === 'changed') {
+ return {
+ old: diff.oldValue as boolean,
+ new: diff.newValue as boolean,
+ };
}
}
@@ -418,6 +512,62 @@ export const DiffProvider: React.FC = ({
[diffMap]
);
+ const checkIfRelationshipHasChange = useCallback<
+ DiffContext['checkIfRelationshipHasChange']
+ >(
+ ({ relationshipId }) =>
+ relationshipsChanged.get(relationshipId) ?? false,
+ [relationshipsChanged]
+ );
+
+ const getRelationshipNewName = useCallback<
+ DiffContext['getRelationshipNewName']
+ >(
+ ({ relationshipId }) => {
+ // Try with the given ID first
+ const relationshipNameKey = getDiffMapKey({
+ diffObject: 'relationship',
+ objectId: relationshipId,
+ attribute: 'name',
+ });
+
+ if (diffMap.has(relationshipNameKey)) {
+ const diff = diffMap.get(relationshipNameKey);
+
+ if (diff?.type === 'changed') {
+ return {
+ new: diff.newValue as string,
+ old: diff.oldValue as string,
+ };
+ }
+ }
+
+ // If not found, try with the mapped ID (old <-> new mapping)
+ const mappedId = relationshipIdMap.get(relationshipId);
+ if (mappedId) {
+ const mappedKey = getDiffMapKey({
+ diffObject: 'relationship',
+ objectId: mappedId,
+ attribute: 'name',
+ });
+
+ if (diffMap.has(mappedKey)) {
+ const diff = diffMap.get(mappedKey);
+
+ if (diff?.type === 'changed') {
+ return {
+ new: diff.newValue as string,
+ old: diff.oldValue as string,
+ };
+ }
+ }
+ }
+
+ return null;
+ },
+ [diffMap, relationshipIdMap]
+ );
+
const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship']
>(
@@ -452,10 +602,40 @@ export const DiffProvider: React.FC = ({
[diffMap]
);
+ const checkIfNewArea = useCallback(
+ ({ areaId }) => {
+ const areaKey = getDiffMapKey({
+ diffObject: 'area',
+ objectId: areaId,
+ });
+
+ return (
+ diffMap.has(areaKey) && diffMap.get(areaKey)?.type === 'added'
+ );
+ },
+ [diffMap]
+ );
+
+ const checkIfAreaRemoved = useCallback(
+ ({ areaId }) => {
+ const areaKey = getDiffMapKey({
+ diffObject: 'area',
+ objectId: areaId,
+ });
+
+ return (
+ diffMap.has(areaKey) && diffMap.get(areaKey)?.type === 'removed'
+ );
+ },
+ [diffMap]
+ );
+
const resetDiff = useCallback(() => {
setDiffMap(new Map());
setTablesChanged(new Map());
setFieldsChanged(new Map());
+ setRelationshipsChanged(new Map());
+ setRelationshipIdMap(new Map());
setNewDiagram(null);
setOriginalDiagram(null);
setIsSummaryOnly(false);
@@ -491,10 +671,18 @@ export const DiffProvider: React.FC = ({
getFieldNewCharacterMaximumLength,
getFieldNewScale,
getFieldNewPrecision,
+ getFieldNewIsArray,
// relationship diff
+ checkIfRelationshipHasChange,
checkIfNewRelationship,
checkIfRelationshipRemoved,
+ getRelationshipNewName,
+ relationshipIdMap,
+
+ // area diff
+ checkIfNewArea,
+ checkIfAreaRemoved,
events,
}}
diff --git a/src/context/export-image-context/export-image-provider.tsx b/src/context/export-image-context/export-image-provider.tsx
index cec6a3319..4c76ac56d 100644
--- a/src/context/export-image-context/export-image-provider.tsx
+++ b/src/context/export-image-context/export-image-provider.tsx
@@ -119,10 +119,51 @@ export const ExportImageProvider: React.FC = ({
'defs'
);
+ // Inline styles for marker elements before copying since skipFonts: true prevents CSS processing
+ const markerCircles = document.querySelectorAll(
+ '.marker-definitions marker circle'
+ ) as NodeListOf;
+ const markerTexts = document.querySelectorAll(
+ '.marker-definitions marker text'
+ ) as NodeListOf;
+
+ const originalMarkerStyles: {
+ element: SVGElement;
+ fill: string;
+ stroke: string;
+ }[] = [];
+
+ markerCircles.forEach((circle) => {
+ const computedStyle = window.getComputedStyle(circle);
+ originalMarkerStyles.push({
+ element: circle,
+ fill: circle.style.fill,
+ stroke: circle.style.stroke,
+ });
+ circle.style.fill = computedStyle.fill;
+ circle.style.stroke = computedStyle.stroke;
+ });
+
+ markerTexts.forEach((text) => {
+ const computedStyle = window.getComputedStyle(text);
+ originalMarkerStyles.push({
+ element: text,
+ fill: text.style.fill,
+ stroke: text.style.stroke,
+ });
+ text.style.fill = computedStyle.fill;
+ });
+
if (markerDefs) {
defs.innerHTML = markerDefs.innerHTML;
}
+ // Restore original marker styles
+ originalMarkerStyles.forEach(({ element, fill, stroke }) => {
+ element.style.fill = fill;
+ element.style.stroke = stroke;
+ });
+
if (includePatternBG) {
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
@@ -185,6 +226,27 @@ export const ExportImageProvider: React.FC = ({
viewportElement.firstChild
);
+ // Inline stroke styles for edge paths since skipFonts: true prevents CSS processing
+ const edgePaths = viewportElement.querySelectorAll(
+ '.react-flow__edge-path'
+ ) as NodeListOf;
+ const originalStyles: {
+ element: SVGPathElement;
+ stroke: string;
+ strokeWidth: string;
+ }[] = [];
+
+ edgePaths.forEach((path) => {
+ const computedStyle = window.getComputedStyle(path);
+ originalStyles.push({
+ element: path,
+ stroke: path.style.stroke,
+ strokeWidth: path.style.strokeWidth,
+ });
+ path.style.stroke = computedStyle.stroke;
+ path.style.strokeWidth = computedStyle.strokeWidth;
+ });
+
try {
// Handle SVG export differently
if (type === 'svg') {
@@ -291,6 +353,13 @@ export const ExportImageProvider: React.FC = ({
};
});
} finally {
+ // Restore original styles
+ originalStyles.forEach(
+ ({ element, stroke, strokeWidth }) => {
+ element.style.stroke = stroke;
+ element.style.strokeWidth = strokeWidth;
+ }
+ );
viewportElement.removeChild(tempSvg);
hideLoader();
}
diff --git a/src/context/history-context/history-provider.tsx b/src/context/history-context/history-provider.tsx
index f15633043..b156dd19f 100644
--- a/src/context/history-context/history-provider.tsx
+++ b/src/context/history-context/history-provider.tsx
@@ -32,6 +32,9 @@ export const HistoryProvider: React.FC = ({
addIndex,
removeIndex,
updateIndex,
+ addCheckConstraint,
+ removeCheckConstraint,
+ updateCheckConstraint,
removeRelationships,
addAreas,
removeAreas,
@@ -39,6 +42,9 @@ export const HistoryProvider: React.FC = ({
addCustomTypes,
removeCustomTypes,
updateCustomType,
+ addNotes,
+ removeNotes,
+ updateNote,
} = useChartDB();
const redoActionHandlers = useMemo(
@@ -113,6 +119,30 @@ export const HistoryProvider: React.FC = ({
updateHistory: false,
});
},
+ addCheckConstraint: ({ redoData: { tableId, constraint } }) => {
+ return addCheckConstraint(tableId, constraint, {
+ updateHistory: false,
+ });
+ },
+ removeCheckConstraint: ({
+ redoData: { tableId, constraintId },
+ }) => {
+ return removeCheckConstraint(tableId, constraintId, {
+ updateHistory: false,
+ });
+ },
+ updateCheckConstraint: ({
+ redoData: { tableId, constraintId, constraint },
+ }) => {
+ return updateCheckConstraint(
+ tableId,
+ constraintId,
+ constraint,
+ {
+ updateHistory: false,
+ }
+ );
+ },
addAreas: ({ redoData: { areas } }) => {
return addAreas(areas, { updateHistory: false });
},
@@ -135,6 +165,15 @@ export const HistoryProvider: React.FC = ({
updateHistory: false,
});
},
+ addNotes: ({ redoData: { notes } }) => {
+ return addNotes(notes, { updateHistory: false });
+ },
+ removeNotes: ({ redoData: { noteIds } }) => {
+ return removeNotes(noteIds, { updateHistory: false });
+ },
+ updateNote: ({ redoData: { noteId, note } }) => {
+ return updateNote(noteId, note, { updateHistory: false });
+ },
}),
[
addTables,
@@ -150,6 +189,9 @@ export const HistoryProvider: React.FC = ({
addIndex,
removeIndex,
updateIndex,
+ addCheckConstraint,
+ removeCheckConstraint,
+ updateCheckConstraint,
removeRelationships,
addDependencies,
removeDependencies,
@@ -160,6 +202,9 @@ export const HistoryProvider: React.FC = ({
addCustomTypes,
removeCustomTypes,
updateCustomType,
+ addNotes,
+ removeNotes,
+ updateNote,
]
);
@@ -249,6 +294,28 @@ export const HistoryProvider: React.FC = ({
updateHistory: false,
});
},
+ addCheckConstraint: ({ undoData: { tableId, constraintId } }) => {
+ return removeCheckConstraint(tableId, constraintId, {
+ updateHistory: false,
+ });
+ },
+ removeCheckConstraint: ({ undoData: { tableId, constraint } }) => {
+ return addCheckConstraint(tableId, constraint, {
+ updateHistory: false,
+ });
+ },
+ updateCheckConstraint: ({
+ undoData: { tableId, constraintId, constraint },
+ }) => {
+ return updateCheckConstraint(
+ tableId,
+ constraintId,
+ constraint,
+ {
+ updateHistory: false,
+ }
+ );
+ },
addAreas: ({ undoData: { areaIds } }) => {
return removeAreas(areaIds, { updateHistory: false });
},
@@ -271,6 +338,15 @@ export const HistoryProvider: React.FC = ({
updateHistory: false,
});
},
+ addNotes: ({ undoData: { noteIds } }) => {
+ return removeNotes(noteIds, { updateHistory: false });
+ },
+ removeNotes: ({ undoData: { notes } }) => {
+ return addNotes(notes, { updateHistory: false });
+ },
+ updateNote: ({ undoData: { noteId, note } }) => {
+ return updateNote(noteId, note, { updateHistory: false });
+ },
}),
[
addTables,
@@ -286,6 +362,9 @@ export const HistoryProvider: React.FC = ({
addIndex,
removeIndex,
updateIndex,
+ addCheckConstraint,
+ removeCheckConstraint,
+ updateCheckConstraint,
removeRelationships,
addDependencies,
removeDependencies,
@@ -296,6 +375,9 @@ export const HistoryProvider: React.FC = ({
addCustomTypes,
removeCustomTypes,
updateCustomType,
+ addNotes,
+ removeNotes,
+ updateNote,
]
);
diff --git a/src/context/history-context/redo-undo-action.ts b/src/context/history-context/redo-undo-action.ts
index ec73da8a4..f618c8453 100644
--- a/src/context/history-context/redo-undo-action.ts
+++ b/src/context/history-context/redo-undo-action.ts
@@ -2,10 +2,12 @@ import type { DBTable } from '@/lib/domain/db-table';
import type { ChartDBContext } from '../chartdb-context/chartdb-context';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
+import type { DBCheckConstraint } from '@/lib/domain/db-check-constraint';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
+import type { Note } from '@/lib/domain/note';
type Action = keyof ChartDBContext;
@@ -89,6 +91,32 @@ type RedoUndoActionUpdateIndex = RedoUndoActionBase<
{ tableId: string; indexId: string; index: Partial }
>;
+type RedoUndoActionAddCheckConstraint = RedoUndoActionBase<
+ 'addCheckConstraint',
+ { tableId: string; constraint: DBCheckConstraint },
+ { tableId: string; constraintId: string }
+>;
+
+type RedoUndoActionRemoveCheckConstraint = RedoUndoActionBase<
+ 'removeCheckConstraint',
+ { tableId: string; constraintId: string },
+ { tableId: string; constraint: DBCheckConstraint }
+>;
+
+type RedoUndoActionUpdateCheckConstraint = RedoUndoActionBase<
+ 'updateCheckConstraint',
+ {
+ tableId: string;
+ constraintId: string;
+ constraint: Partial;
+ },
+ {
+ tableId: string;
+ constraintId: string;
+ constraint: Partial;
+ }
+>;
+
type RedoUndoActionAddRelationships = RedoUndoActionBase<
'addRelationships',
{ relationships: DBRelationship[] },
@@ -161,6 +189,24 @@ type RedoUndoActionRemoveCustomTypes = RedoUndoActionBase<
{ customTypes: DBCustomType[] }
>;
+type RedoUndoActionAddNotes = RedoUndoActionBase<
+ 'addNotes',
+ { notes: Note[] },
+ { noteIds: string[] }
+>;
+
+type RedoUndoActionUpdateNote = RedoUndoActionBase<
+ 'updateNote',
+ { noteId: string; note: Partial },
+ { noteId: string; note: Partial }
+>;
+
+type RedoUndoActionRemoveNotes = RedoUndoActionBase<
+ 'removeNotes',
+ { noteIds: string[] },
+ { notes: Note[] }
+>;
+
export type RedoUndoAction =
| RedoUndoActionAddTables
| RedoUndoActionRemoveTables
@@ -173,6 +219,9 @@ export type RedoUndoAction =
| RedoUndoActionAddIndex
| RedoUndoActionRemoveIndex
| RedoUndoActionUpdateIndex
+ | RedoUndoActionAddCheckConstraint
+ | RedoUndoActionRemoveCheckConstraint
+ | RedoUndoActionUpdateCheckConstraint
| RedoUndoActionAddRelationships
| RedoUndoActionUpdateRelationship
| RedoUndoActionRemoveRelationships
@@ -184,7 +233,10 @@ export type RedoUndoAction =
| RedoUndoActionRemoveAreas
| RedoUndoActionAddCustomTypes
| RedoUndoActionUpdateCustomType
- | RedoUndoActionRemoveCustomTypes;
+ | RedoUndoActionRemoveCustomTypes
+ | RedoUndoActionAddNotes
+ | RedoUndoActionUpdateNote
+ | RedoUndoActionRemoveNotes;
export type RedoActionData = Extract<
RedoUndoAction,
diff --git a/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx b/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx
index 4214fb7c9..fdc1f87cf 100644
--- a/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx
+++ b/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx
@@ -11,9 +11,14 @@ import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { useReactFlow } from '@xyflow/react';
-export const KeyboardShortcutsProvider: React.FC = ({
- children,
-}) => {
+export interface KeyboardShortcutsProviderProps
+ extends React.PropsWithChildren {
+ enabled?: boolean;
+}
+
+export const KeyboardShortcutsProvider: React.FC<
+ KeyboardShortcutsProviderProps
+> = ({ children, enabled = true }) => {
const { redo, undo } = useHistory();
const { openOpenDiagramDialog } = useDialog();
const { updateDiagramUpdatedAt } = useChartDB();
@@ -25,6 +30,7 @@ export const KeyboardShortcutsProvider: React.FC = ({
redo,
{
preventDefault: true,
+ enabled,
},
[redo]
);
@@ -33,6 +39,7 @@ export const KeyboardShortcutsProvider: React.FC = ({
undo,
{
preventDefault: true,
+ enabled,
},
[undo]
);
@@ -42,6 +49,7 @@ export const KeyboardShortcutsProvider: React.FC = ({
() => openOpenDiagramDialog(),
{
preventDefault: true,
+ enabled,
},
[openOpenDiagramDialog]
);
@@ -51,6 +59,7 @@ export const KeyboardShortcutsProvider: React.FC = ({
updateDiagramUpdatedAt,
{
preventDefault: true,
+ enabled,
},
[updateDiagramUpdatedAt]
);
@@ -60,6 +69,7 @@ export const KeyboardShortcutsProvider: React.FC = ({
toggleSidePanel,
{
preventDefault: true,
+ enabled,
},
[toggleSidePanel]
);
@@ -74,6 +84,7 @@ export const KeyboardShortcutsProvider: React.FC = ({
},
{
preventDefault: true,
+ enabled,
},
[fitView]
);
diff --git a/src/context/layout-context/layout-context.tsx b/src/context/layout-context/layout-context.tsx
index cfd39e88e..0487fa171 100644
--- a/src/context/layout-context/layout-context.tsx
+++ b/src/context/layout-context/layout-context.tsx
@@ -5,8 +5,10 @@ export type SidebarSection =
| 'dbml'
| 'tables'
| 'refs'
- | 'areas'
- | 'customTypes';
+ | 'customTypes'
+ | 'visuals';
+
+export type VisualsTab = 'areas' | 'notes';
export interface LayoutContext {
openedTableInSidebar: string | undefined;
@@ -27,6 +29,10 @@ export interface LayoutContext {
openAreaFromSidebar: (areaId: string) => void;
closeAllAreasInSidebar: () => void;
+ openedNoteInSidebar: string | undefined;
+ openNoteFromSidebar: (noteId: string) => void;
+ closeAllNotesInSidebar: () => void;
+
openedCustomTypeInSidebar: string | undefined;
openCustomTypeFromSidebar: (customTypeId: string) => void;
closeAllCustomTypesInSidebar: () => void;
@@ -34,6 +40,9 @@ export interface LayoutContext {
selectedSidebarSection: SidebarSection;
selectSidebarSection: (section: SidebarSection) => void;
+ selectedVisualsTab: VisualsTab;
+ selectVisualsTab: (tab: VisualsTab) => void;
+
isSidePanelShowed: boolean;
hideSidePanel: () => void;
showSidePanel: () => void;
@@ -58,6 +67,10 @@ export const layoutContext = createContext({
openAreaFromSidebar: emptyFn,
closeAllAreasInSidebar: emptyFn,
+ openedNoteInSidebar: undefined,
+ openNoteFromSidebar: emptyFn,
+ closeAllNotesInSidebar: emptyFn,
+
openedCustomTypeInSidebar: undefined,
openCustomTypeFromSidebar: emptyFn,
closeAllCustomTypesInSidebar: emptyFn,
@@ -66,6 +79,9 @@ export const layoutContext = createContext({
openTableFromSidebar: emptyFn,
closeAllTablesInSidebar: emptyFn,
+ selectedVisualsTab: 'areas',
+ selectVisualsTab: emptyFn,
+
isSidePanelShowed: false,
hideSidePanel: emptyFn,
showSidePanel: emptyFn,
diff --git a/src/context/layout-context/layout-provider.tsx b/src/context/layout-context/layout-provider.tsx
index 5849387fd..e7241838c 100644
--- a/src/context/layout-context/layout-provider.tsx
+++ b/src/context/layout-context/layout-provider.tsx
@@ -1,5 +1,9 @@
import React from 'react';
-import type { LayoutContext, SidebarSection } from './layout-context';
+import type {
+ LayoutContext,
+ SidebarSection,
+ VisualsTab,
+} from './layout-context';
import { layoutContext } from './layout-context';
import { useBreakpoint } from '@/hooks/use-breakpoint';
@@ -16,10 +20,15 @@ export const LayoutProvider: React.FC = ({
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
string | undefined
>();
+ const [openedNoteInSidebar, setOpenedNoteInSidebar] = React.useState<
+ string | undefined
+ >();
const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
React.useState();
const [selectedSidebarSection, setSelectedSidebarSection] =
React.useState('tables');
+ const [selectedVisualsTab, setSelectedVisualsTab] =
+ React.useState('areas');
const [isSidePanelShowed, setIsSidePanelShowed] =
React.useState(isDesktop);
@@ -38,6 +47,9 @@ export const LayoutProvider: React.FC = ({
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
() => setOpenedAreaInSidebar('');
+ const closeAllNotesInSidebar: LayoutContext['closeAllNotesInSidebar'] =
+ () => setOpenedNoteInSidebar('');
+
const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
() => setOpenedCustomTypeInSidebar('');
@@ -83,10 +95,20 @@ export const LayoutProvider: React.FC = ({
areaId
) => {
showSidePanel();
- setSelectedSidebarSection('areas');
+ setSelectedSidebarSection('visuals');
+ setSelectedVisualsTab('areas');
setOpenedAreaInSidebar(areaId);
};
+ const openNoteFromSidebar: LayoutContext['openNoteFromSidebar'] = (
+ noteId
+ ) => {
+ showSidePanel();
+ setSelectedSidebarSection('visuals');
+ setSelectedVisualsTab('notes');
+ setOpenedNoteInSidebar(noteId);
+ };
+
const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
(customTypeId) => {
showSidePanel();
@@ -116,9 +138,14 @@ export const LayoutProvider: React.FC = ({
openedAreaInSidebar,
openAreaFromSidebar,
closeAllAreasInSidebar,
+ openedNoteInSidebar,
+ openNoteFromSidebar,
+ closeAllNotesInSidebar,
openedCustomTypeInSidebar,
openCustomTypeFromSidebar,
closeAllCustomTypesInSidebar,
+ selectedVisualsTab,
+ selectVisualsTab: setSelectedVisualsTab,
}}
>
{children}
diff --git a/src/context/storage-context/storage-context.tsx b/src/context/storage-context/storage-context.tsx
index 565f4997a..a56ee06bd 100644
--- a/src/context/storage-context/storage-context.tsx
+++ b/src/context/storage-context/storage-context.tsx
@@ -8,6 +8,7 @@ import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
+import type { Note } from '@/lib/domain/note';
export interface StorageContext {
// Config operations
@@ -30,6 +31,7 @@ export interface StorageContext {
includeDependencies?: boolean;
includeAreas?: boolean;
includeCustomTypes?: boolean;
+ includeNotes?: boolean;
}) => Promise;
getDiagram: (
id: string,
@@ -39,6 +41,7 @@ export interface StorageContext {
includeDependencies?: boolean;
includeAreas?: boolean;
includeCustomTypes?: boolean;
+ includeNotes?: boolean;
}
) => Promise;
updateDiagram: (params: {
@@ -135,6 +138,20 @@ export interface StorageContext {
}) => Promise;
listCustomTypes: (diagramId: string) => Promise;
deleteDiagramCustomTypes: (diagramId: string) => Promise;
+
+ // Note operations
+ addNote: (params: { diagramId: string; note: Note }) => Promise;
+ getNote: (params: {
+ diagramId: string;
+ id: string;
+ }) => Promise;
+ updateNote: (params: {
+ id: string;
+ attributes: Partial;
+ }) => Promise;
+ deleteNote: (params: { diagramId: string; id: string }) => Promise;
+ listNotes: (diagramId: string) => Promise;
+ deleteDiagramNotes: (diagramId: string) => Promise;
}
export const storageInitialValue: StorageContext = {
@@ -187,6 +204,14 @@ export const storageInitialValue: StorageContext = {
deleteCustomType: emptyFn,
listCustomTypes: emptyFn,
deleteDiagramCustomTypes: emptyFn,
+
+ // Note operations
+ addNote: emptyFn,
+ getNote: emptyFn,
+ updateNote: emptyFn,
+ deleteNote: emptyFn,
+ listNotes: emptyFn,
+ deleteDiagramNotes: emptyFn,
};
export const storageContext =
diff --git a/src/context/storage-context/storage-provider.tsx b/src/context/storage-context/storage-provider.tsx
index b5ff26223..72fca7cd4 100644
--- a/src/context/storage-context/storage-provider.tsx
+++ b/src/context/storage-context/storage-provider.tsx
@@ -1,774 +1,1013 @@
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback, useRef } from 'react';
import type { StorageContext } from './storage-context';
import { storageContext } from './storage-context';
-import Dexie, { type EntityTable } from 'dexie';
import type { Diagram } from '@/lib/domain/diagram';
-import type { DBTable } from '@/lib/domain/db-table';
-import type { DBRelationship } from '@/lib/domain/db-relationship';
-import { determineCardinalities } from '@/lib/domain/db-relationship';
import type { ChartDBConfig } from '@/lib/domain/config';
-import type { DBDependency } from '@/lib/domain/db-dependency';
-import type { Area } from '@/lib/domain/area';
-import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
+const API_BASE = '/api';
+const defaultConfig: ChartDBConfig = {
+ defaultDiagramId: '',
+ hideSocialLinks: false,
+};
+
+type DiagramPayload = Omit & {
+ createdAt: string;
+ updatedAt: string;
+};
+
+type DiagramCacheEntry = {
+ diagram: Diagram;
+ isFull: boolean;
+};
+
+const serializeDiagram = (diagram: Diagram): DiagramPayload => ({
+ ...diagram,
+ createdAt: diagram.createdAt.toISOString(),
+ updatedAt: diagram.updatedAt.toISOString(),
+});
+
+const deserializeDiagram = (diagram: DiagramPayload): Diagram => ({
+ ...diagram,
+ createdAt: new Date(diagram.createdAt),
+ updatedAt: new Date(diagram.updatedAt),
+});
+
+const buildIncludeParam = (options?: {
+ includeTables?: boolean;
+ includeRelationships?: boolean;
+ includeDependencies?: boolean;
+ includeAreas?: boolean;
+ includeCustomTypes?: boolean;
+ includeNotes?: boolean;
+}) => {
+ if (!options) {
+ return '';
+ }
+
+ const includes: string[] = [];
+
+ if (options.includeTables) includes.push('tables');
+ if (options.includeRelationships) includes.push('relationships');
+ if (options.includeDependencies) includes.push('dependencies');
+ if (options.includeAreas) includes.push('areas');
+ if (options.includeCustomTypes) includes.push('customTypes');
+ if (options.includeNotes) includes.push('notes');
+
+ if (includes.length === 0) return '';
+
+ return `?include=${includes.join(',')}`;
+};
+
+const isFullInclude = (options?: {
+ includeTables?: boolean;
+ includeRelationships?: boolean;
+ includeDependencies?: boolean;
+ includeAreas?: boolean;
+ includeCustomTypes?: boolean;
+ includeNotes?: boolean;
+}) => {
+ if (!options) return false;
+ return (
+ options.includeTables === true &&
+ options.includeRelationships === true &&
+ options.includeDependencies === true &&
+ options.includeAreas === true &&
+ options.includeCustomTypes === true &&
+ options.includeNotes === true
+ );
+};
+
+const fetchJson = async (
+ url: string,
+ options?: RequestInit,
+ allowNotFound = false
+): Promise => {
+ const headers = new Headers(options?.headers ?? undefined);
+ if (options?.body && !headers.has('Content-Type')) {
+ headers.set('Content-Type', 'application/json');
+ }
+
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ });
+
+ if (allowNotFound && response.status === 404) {
+ return undefined;
+ }
+
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status}`);
+ }
+
+ if (response.status === 204) {
+ return undefined;
+ }
+
+ return (await response.json()) as T;
+};
+
export const StorageProvider: React.FC = ({
children,
}) => {
- const db = useMemo(() => {
- const dexieDB = new Dexie('ChartDB') as Dexie & {
- diagrams: EntityTable<
- Diagram,
- 'id' // primary key "id" (for the typings only)
- >;
- db_tables: EntityTable<
- DBTable & { diagramId: string },
- 'id' // primary key "id" (for the typings only)
- >;
- db_relationships: EntityTable<
- DBRelationship & { diagramId: string },
- 'id' // primary key "id" (for the typings only)
- >;
- db_dependencies: EntityTable<
- DBDependency & { diagramId: string },
- 'id' // primary key "id" (for the typings only)
- >;
- areas: EntityTable<
- Area & { diagramId: string },
- 'id' // primary key "id" (for the typings only)
- >;
- db_custom_types: EntityTable<
- DBCustomType & { diagramId: string },
- 'id' // primary key "id" (for the typings only)
- >;
- config: EntityTable<
- ChartDBConfig & { id: number },
- 'id' // primary key "id" (for the typings only)
- >;
- diagram_filters: EntityTable<
- DiagramFilter & { diagramId: string },
- 'diagramId' // primary key "id" (for the typings only)
- >;
- };
-
- // Schema declaration:
- dexieDB.version(1).stores({
- diagrams: '++id, name, databaseType, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
- db_relationships:
- '++id, diagramId, name, sourceTableId, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(2).upgrade((tx) =>
- tx
- .table('db_tables')
- .toCollection()
- .modify((table) => {
- for (const field of table.fields) {
- field.type = {
- // @ts-expect-error string before
- id: (field.type as string).split(' ').join('_'),
- // @ts-expect-error string before
- name: field.type,
- };
- }
- })
- );
+ const diagramCacheRef = useRef(new Map());
+ const tableIndexRef = useRef(new Map());
+ const relationshipIndexRef = useRef(new Map());
+ const dependencyIndexRef = useRef(new Map());
+ const areaIndexRef = useRef(new Map());
+ const customTypeIndexRef = useRef(new Map());
+ const noteIndexRef = useRef(new Map());
+ const diagramLocksRef = useRef(new Map>());
+ const deletedDiagramsRef = useRef(new Set());
+
+ const removeDiagramFromIndex = useCallback((diagramId: string) => {
+ const indexes = [
+ tableIndexRef.current,
+ relationshipIndexRef.current,
+ dependencyIndexRef.current,
+ areaIndexRef.current,
+ customTypeIndexRef.current,
+ noteIndexRef.current,
+ ];
+
+ for (const index of indexes) {
+ for (const [key, value] of index.entries()) {
+ if (value === diagramId) {
+ index.delete(key);
+ }
+ }
+ }
+ }, []);
+
+ const indexDiagram = useCallback(
+ (diagram: Diagram) => {
+ removeDiagramFromIndex(diagram.id);
+
+ for (const table of diagram.tables ?? []) {
+ tableIndexRef.current.set(table.id, diagram.id);
+ }
+ for (const relationship of diagram.relationships ?? []) {
+ relationshipIndexRef.current.set(relationship.id, diagram.id);
+ }
+ for (const dependency of diagram.dependencies ?? []) {
+ dependencyIndexRef.current.set(dependency.id, diagram.id);
+ }
+ for (const area of diagram.areas ?? []) {
+ areaIndexRef.current.set(area.id, diagram.id);
+ }
+ for (const customType of diagram.customTypes ?? []) {
+ customTypeIndexRef.current.set(customType.id, diagram.id);
+ }
+ for (const note of diagram.notes ?? []) {
+ noteIndexRef.current.set(note.id, diagram.id);
+ }
+ },
+ [removeDiagramFromIndex]
+ );
+
+ const cacheDiagram = useCallback(
+ (diagram: Diagram, isFull: boolean) => {
+ diagramCacheRef.current.set(diagram.id, { diagram, isFull });
+ if (isFull) {
+ indexDiagram(diagram);
+ }
+ },
+ [indexDiagram]
+ );
+
+ const removeDiagramFromCache = useCallback(
+ (diagramId: string) => {
+ diagramCacheRef.current.delete(diagramId);
+ removeDiagramFromIndex(diagramId);
+ },
+ [removeDiagramFromIndex]
+ );
+
+ const resolveDiagramIdForEntity = useCallback(
+ (entityId: string, index: Map, key: keyof Diagram) => {
+ const cachedId = index.get(entityId);
+ if (cachedId) return cachedId;
+
+ for (const [
+ diagramId,
+ entry,
+ ] of diagramCacheRef.current.entries()) {
+ if (!entry.isFull) continue;
+ const collection = entry.diagram[key] as
+ | { id: string }[]
+ | undefined;
+ if (collection?.some((item) => item.id === entityId)) {
+ index.set(entityId, diagramId);
+ return diagramId;
+ }
+ }
+
+ return undefined;
+ },
+ []
+ );
- dexieDB.version(3).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
- db_relationships:
- '++id, diagramId, name, sourceTableId, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(4).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, x, y, fields, indexes, color, createdAt, width, comment',
- db_relationships:
- '++id, diagramId, name, sourceTableId, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(5).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment',
- db_relationships:
- '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(6).upgrade((tx) =>
- tx
- .table(
- 'db_relationships'
+ const enqueueDiagramTask = useCallback(
+ async (diagramId: string, task: () => Promise): Promise => {
+ const locks = diagramLocksRef.current;
+ const previous = locks.get(diagramId) ?? Promise.resolve();
+ const next = previous.then(task, task);
+ locks.set(
+ diagramId,
+ next.then(
+ () => undefined,
+ () => undefined
)
- .toCollection()
- .modify((relationship, ref) => {
- const { sourceCardinality, targetCardinality } =
- determineCardinalities(
- // @ts-expect-error string before
- relationship.type ?? 'one_to_one'
- );
-
- relationship.sourceCardinality = sourceCardinality;
- relationship.targetCardinality = targetCardinality;
-
- // @ts-expect-error string before
- delete ref.value.type;
- })
+ );
+ return next;
+ },
+ []
+ );
+
+ const saveDiagram = useCallback(async (diagram: Diagram) => {
+ await fetchJson(
+ `${API_BASE}/diagrams/${encodeURIComponent(diagram.id)}`,
+ {
+ method: 'PUT',
+ body: JSON.stringify(serializeDiagram(diagram)),
+ }
);
+ }, []);
- dexieDB.version(7).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment',
- db_relationships:
- '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- db_dependencies:
- '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(8).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
- db_relationships:
- '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- db_dependencies:
- '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(9).upgrade((tx) =>
- tx
- .table('db_tables')
- .toCollection()
- .modify((table) => {
- for (const field of table.fields) {
- if (typeof field.nullable === 'string') {
- field.nullable =
- (field.nullable as string).toLowerCase() ===
- 'true';
- }
- }
- })
+ const deleteDiagramFile = useCallback(async (diagramId: string) => {
+ await fetchJson(
+ `${API_BASE}/diagrams/${encodeURIComponent(diagramId)}`,
+ { method: 'DELETE' },
+ true
);
+ }, []);
- dexieDB.version(10).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
- db_relationships:
- '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- db_dependencies:
- '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
- areas: '++id, diagramId, name, x, y, width, height, color',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB.version(11).stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
- db_relationships:
- '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- db_dependencies:
- '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
- areas: '++id, diagramId, name, x, y, width, height, color',
- db_custom_types:
- '++id, diagramId, schema, type, kind, values, fields',
- config: '++id, defaultDiagramId',
- });
-
- dexieDB
- .version(12)
- .stores({
- diagrams:
- '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
- db_tables:
- '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
- db_relationships:
- '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
- db_dependencies:
- '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
- areas: '++id, diagramId, name, x, y, width, height, color',
- db_custom_types:
- '++id, diagramId, schema, type, kind, values, fields',
- config: '++id, defaultDiagramId',
- diagram_filters: 'diagramId, tableIds, schemasIds',
- })
- .upgrade((tx) => {
- tx.table('config').clear();
- });
+ const fetchDiagram = useCallback(
+ async (
+ diagramId: string,
+ options?: {
+ includeTables?: boolean;
+ includeRelationships?: boolean;
+ includeDependencies?: boolean;
+ includeAreas?: boolean;
+ includeCustomTypes?: boolean;
+ includeNotes?: boolean;
+ }
+ ) => {
+ const response = await fetchJson(
+ `${API_BASE}/diagrams/${encodeURIComponent(diagramId)}${buildIncludeParam(
+ options
+ )}`,
+ undefined,
+ true
+ );
- dexieDB.on('ready', async () => {
- const config = await dexieDB.config.get(1);
+ if (!response) return undefined;
+
+ const diagram = deserializeDiagram(response);
+ if (isFullInclude(options)) {
+ cacheDiagram(diagram, true);
+ }
+ return diagram;
+ },
+ [cacheDiagram]
+ );
- if (!config) {
- const diagrams = await dexieDB.diagrams.toArray();
+ const getFullDiagram = useCallback(
+ async (diagramId: string): Promise => {
+ const cached = diagramCacheRef.current.get(diagramId);
+ if (cached?.isFull) {
+ return cached.diagram;
+ }
- await dexieDB.config.add({
- id: 1,
- defaultDiagramId: diagrams?.[0]?.id ?? '',
- });
+ const diagram = await fetchDiagram(diagramId, {
+ includeTables: true,
+ includeRelationships: true,
+ includeDependencies: true,
+ includeAreas: true,
+ includeCustomTypes: true,
+ includeNotes: true,
+ });
+ if (diagram) {
+ cacheDiagram(diagram, true);
}
- });
- return dexieDB;
- }, []);
+ return diagram;
+ },
+ [cacheDiagram, fetchDiagram]
+ );
- const getConfig: StorageContext['getConfig'] =
- useCallback(async (): Promise => {
- return await db.config.get(1);
- }, [db]);
+ const getConfig: StorageContext['getConfig'] = useCallback(async () => {
+ const config = await fetchJson(
+ `${API_BASE}/config`,
+ undefined,
+ true
+ );
+ return config ?? defaultConfig;
+ }, []);
const updateConfig: StorageContext['updateConfig'] = useCallback(
async (config) => {
- await db.config.update(1, config);
+ await fetchJson(`${API_BASE}/config`, {
+ method: 'PUT',
+ body: JSON.stringify(config),
+ });
},
- [db]
+ []
);
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
- async (diagramId: string): Promise => {
- const filter = await db.diagram_filters.get({ diagramId });
-
- return filter;
+ async (diagramId: string) => {
+ return await fetchJson(
+ `${API_BASE}/diagram-filters/${encodeURIComponent(diagramId)}`,
+ undefined,
+ true
+ );
},
- [db]
+ []
);
const updateDiagramFilter: StorageContext['updateDiagramFilter'] =
- useCallback(
- async (diagramId, filter): Promise => {
- await db.diagram_filters.put({
- diagramId,
- ...filter,
- });
- },
- [db]
- );
+ useCallback(async (diagramId, filter) => {
+ await fetchJson(
+ `${API_BASE}/diagram-filters/${encodeURIComponent(diagramId)}`,
+ {
+ method: 'PUT',
+ body: JSON.stringify(filter),
+ }
+ );
+ }, []);
const deleteDiagramFilter: StorageContext['deleteDiagramFilter'] =
- useCallback(
- async (diagramId: string): Promise => {
- await db.diagram_filters.where({ diagramId }).delete();
- },
- [db]
- );
+ useCallback(async (diagramId: string) => {
+ await fetchJson(
+ `${API_BASE}/diagram-filters/${encodeURIComponent(diagramId)}`,
+ { method: 'DELETE' },
+ true
+ );
+ }, []);
+
+ const addDiagram: StorageContext['addDiagram'] = useCallback(
+ async ({ diagram }) => {
+ deletedDiagramsRef.current.delete(diagram.id);
+ cacheDiagram(diagram, true);
+ await saveDiagram(diagram);
+ },
+ [cacheDiagram, saveDiagram]
+ );
+
+ const listDiagrams: StorageContext['listDiagrams'] = useCallback(
+ async (options) => {
+ const response = await fetchJson(
+ `${API_BASE}/diagrams${buildIncludeParam(options)}`
+ );
+ const diagrams = (response ?? []).map(deserializeDiagram);
+
+ if (isFullInclude(options)) {
+ for (const diagram of diagrams) {
+ cacheDiagram(diagram, true);
+ }
+ }
+
+ return diagrams;
+ },
+ [cacheDiagram]
+ );
+
+ const getDiagram: StorageContext['getDiagram'] = useCallback(
+ async (id, options) => {
+ return await fetchDiagram(id, options);
+ },
+ [fetchDiagram]
+ );
+
+ const updateDiagram: StorageContext['updateDiagram'] = useCallback(
+ async ({ id, attributes }) => {
+ await enqueueDiagramTask(id, async () => {
+ if (deletedDiagramsRef.current.has(id)) return;
+
+ const diagram = await getFullDiagram(id);
+ if (!diagram) return;
+
+ const updated = { ...diagram, ...attributes };
+ const newId = attributes.id ?? id;
+ updated.id = newId;
+
+ if (newId !== id) {
+ removeDiagramFromCache(id);
+ }
+
+ cacheDiagram(updated, true);
+ await saveDiagram(updated);
+
+ if (newId !== id) {
+ deletedDiagramsRef.current.add(id);
+ await deleteDiagramFile(id);
+ deletedDiagramsRef.current.delete(id);
+ }
+ });
+ },
+ [
+ cacheDiagram,
+ deleteDiagramFile,
+ enqueueDiagramTask,
+ getFullDiagram,
+ removeDiagramFromCache,
+ saveDiagram,
+ ]
+ );
+
+ const deleteDiagram: StorageContext['deleteDiagram'] = useCallback(
+ async (id) => {
+ deletedDiagramsRef.current.add(id);
+ removeDiagramFromCache(id);
+ await deleteDiagramFile(id);
+ },
+ [deleteDiagramFile, removeDiagramFromCache]
+ );
const addTable: StorageContext['addTable'] = useCallback(
async ({ diagramId, table }) => {
- await db.db_tables.add({
- ...table,
- diagramId,
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const tables = diagram.tables ?? [];
+ diagram.tables = [...tables, table];
+ tableIndexRef.current.set(table.id, diagramId);
+ await saveDiagram(diagram);
});
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const getTable: StorageContext['getTable'] = useCallback(
- async ({ id, diagramId }): Promise => {
- return await db.db_tables.get({ id, diagramId });
+ async ({ id, diagramId }) => {
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.tables?.find((table) => table.id === id);
},
- [db]
+ [getFullDiagram]
);
const deleteDiagramTables: StorageContext['deleteDiagramTables'] =
useCallback(
async (diagramId) => {
- await db.db_tables
- .where('diagramId')
- .equals(diagramId)
- .delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ diagram.tables = [];
+ indexDiagram(diagram);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, indexDiagram, saveDiagram]
);
const updateTable: StorageContext['updateTable'] = useCallback(
async ({ id, attributes }) => {
- await db.db_tables.update(id, attributes);
+ const diagramId = resolveDiagramIdForEntity(
+ id,
+ tableIndexRef.current,
+ 'tables'
+ );
+ if (!diagramId) return;
+
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.tables) return;
+ const tables = [...diagram.tables];
+ const index = tables.findIndex((table) => table.id === id);
+ if (index === -1) return;
+ tables[index] = { ...tables[index], ...attributes };
+ diagram.tables = tables;
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [
+ enqueueDiagramTask,
+ getFullDiagram,
+ resolveDiagramIdForEntity,
+ saveDiagram,
+ ]
);
const putTable: StorageContext['putTable'] = useCallback(
async ({ diagramId, table }) => {
- await db.db_tables.put({ ...table, diagramId });
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const tables = [...(diagram.tables ?? [])];
+ const index = tables.findIndex((item) => item.id === table.id);
+ if (index === -1) {
+ tables.push(table);
+ } else {
+ tables[index] = table;
+ }
+ diagram.tables = tables;
+ tableIndexRef.current.set(table.id, diagramId);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const deleteTable: StorageContext['deleteTable'] = useCallback(
async ({ id, diagramId }) => {
- await db.db_tables.where({ id, diagramId }).delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.tables) return;
+ diagram.tables = diagram.tables.filter(
+ (table) => table.id !== id
+ );
+ tableIndexRef.current.delete(id);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const listTables: StorageContext['listTables'] = useCallback(
- async (diagramId): Promise => {
- // Fetch all tables associated with the diagram
- const tables = await db.db_tables
- .where('diagramId')
- .equals(diagramId)
- .toArray();
-
- return tables;
+ async (diagramId) => {
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.tables ?? [];
},
- [db]
+ [getFullDiagram]
);
const addRelationship: StorageContext['addRelationship'] = useCallback(
async ({ diagramId, relationship }) => {
- await db.db_relationships.add({
- ...relationship,
- diagramId,
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const relationships = diagram.relationships ?? [];
+ diagram.relationships = [...relationships, relationship];
+ relationshipIndexRef.current.set(relationship.id, diagramId);
+ await saveDiagram(diagram);
});
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const deleteDiagramRelationships: StorageContext['deleteDiagramRelationships'] =
useCallback(
async (diagramId) => {
- await db.db_relationships
- .where('diagramId')
- .equals(diagramId)
- .delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ diagram.relationships = [];
+ indexDiagram(diagram);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, indexDiagram, saveDiagram]
);
const getRelationship: StorageContext['getRelationship'] = useCallback(
- async ({ id, diagramId }): Promise => {
- return await db.db_relationships.get({ id, diagramId });
+ async ({ id, diagramId }) => {
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.relationships?.find((rel) => rel.id === id);
},
- [db]
+ [getFullDiagram]
);
const updateRelationship: StorageContext['updateRelationship'] =
useCallback(
async ({ id, attributes }) => {
- await db.db_relationships.update(id, attributes);
+ const diagramId = resolveDiagramIdForEntity(
+ id,
+ relationshipIndexRef.current,
+ 'relationships'
+ );
+ if (!diagramId) return;
+
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.relationships) return;
+ const relationships = [...diagram.relationships];
+ const index = relationships.findIndex(
+ (rel) => rel.id === id
+ );
+ if (index === -1) return;
+ relationships[index] = {
+ ...relationships[index],
+ ...attributes,
+ };
+ diagram.relationships = relationships;
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [
+ enqueueDiagramTask,
+ getFullDiagram,
+ resolveDiagramIdForEntity,
+ saveDiagram,
+ ]
);
const deleteRelationship: StorageContext['deleteRelationship'] =
useCallback(
async ({ id, diagramId }) => {
- await db.db_relationships.where({ id, diagramId }).delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.relationships) return;
+ diagram.relationships = diagram.relationships.filter(
+ (rel) => rel.id !== id
+ );
+ relationshipIndexRef.current.delete(id);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const listRelationships: StorageContext['listRelationships'] = useCallback(
- async (diagramId): Promise => {
- // Sort relationships alphabetically
- return (
- await db.db_relationships
- .where('diagramId')
- .equals(diagramId)
- .toArray()
- ).sort((a, b) => {
- return a.name.localeCompare(b.name);
- });
+ async (diagramId) => {
+ const diagram = await getFullDiagram(diagramId);
+ return (diagram?.relationships ?? []).sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
},
- [db]
+ [getFullDiagram]
);
const addDependency: StorageContext['addDependency'] = useCallback(
async ({ diagramId, dependency }) => {
- await db.db_dependencies.add({
- ...dependency,
- diagramId,
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const dependencies = diagram.dependencies ?? [];
+ diagram.dependencies = [...dependencies, dependency];
+ dependencyIndexRef.current.set(dependency.id, diagramId);
+ await saveDiagram(diagram);
});
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const getDependency: StorageContext['getDependency'] = useCallback(
async ({ diagramId, id }) => {
- return await db.db_dependencies.get({ id, diagramId });
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.dependencies?.find(
+ (dependency) => dependency.id === id
+ );
},
- [db]
+ [getFullDiagram]
);
const updateDependency: StorageContext['updateDependency'] = useCallback(
async ({ id, attributes }) => {
- await db.db_dependencies.update(id, attributes);
+ const diagramId = resolveDiagramIdForEntity(
+ id,
+ dependencyIndexRef.current,
+ 'dependencies'
+ );
+ if (!diagramId) return;
+
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.dependencies) return;
+ const dependencies = [...diagram.dependencies];
+ const index = dependencies.findIndex(
+ (dependency) => dependency.id === id
+ );
+ if (index === -1) return;
+ dependencies[index] = { ...dependencies[index], ...attributes };
+ diagram.dependencies = dependencies;
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [
+ enqueueDiagramTask,
+ getFullDiagram,
+ resolveDiagramIdForEntity,
+ saveDiagram,
+ ]
);
const deleteDependency: StorageContext['deleteDependency'] = useCallback(
async ({ diagramId, id }) => {
- await db.db_dependencies.where({ id, diagramId }).delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.dependencies) return;
+ diagram.dependencies = diagram.dependencies.filter(
+ (dependency) => dependency.id !== id
+ );
+ dependencyIndexRef.current.delete(id);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const listDependencies: StorageContext['listDependencies'] = useCallback(
async (diagramId) => {
- return await db.db_dependencies
- .where('diagramId')
- .equals(diagramId)
- .toArray();
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.dependencies ?? [];
},
- [db]
+ [getFullDiagram]
);
const deleteDiagramDependencies: StorageContext['deleteDiagramDependencies'] =
useCallback(
async (diagramId) => {
- await db.db_dependencies
- .where('diagramId')
- .equals(diagramId)
- .delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ diagram.dependencies = [];
+ indexDiagram(diagram);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, indexDiagram, saveDiagram]
);
const addArea: StorageContext['addArea'] = useCallback(
async ({ area, diagramId }) => {
- await db.areas.add({
- ...area,
- diagramId,
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const areas = diagram.areas ?? [];
+ diagram.areas = [...areas, area];
+ areaIndexRef.current.set(area.id, diagramId);
+ await saveDiagram(diagram);
});
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const getArea: StorageContext['getArea'] = useCallback(
async ({ diagramId, id }) => {
- return await db.areas.get({ id, diagramId });
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.areas?.find((area) => area.id === id);
},
- [db]
+ [getFullDiagram]
);
const updateArea: StorageContext['updateArea'] = useCallback(
async ({ id, attributes }) => {
- await db.areas.update(id, attributes);
+ const diagramId = resolveDiagramIdForEntity(
+ id,
+ areaIndexRef.current,
+ 'areas'
+ );
+ if (!diagramId) return;
+
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.areas) return;
+ const areas = [...diagram.areas];
+ const index = areas.findIndex((area) => area.id === id);
+ if (index === -1) return;
+ areas[index] = { ...areas[index], ...attributes };
+ diagram.areas = areas;
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [
+ enqueueDiagramTask,
+ getFullDiagram,
+ resolveDiagramIdForEntity,
+ saveDiagram,
+ ]
);
const deleteArea: StorageContext['deleteArea'] = useCallback(
async ({ diagramId, id }) => {
- await db.areas.where({ id, diagramId }).delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.areas) return;
+ diagram.areas = diagram.areas.filter((area) => area.id !== id);
+ areaIndexRef.current.delete(id);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const listAreas: StorageContext['listAreas'] = useCallback(
async (diagramId) => {
- return await db.areas
- .where('diagramId')
- .equals(diagramId)
- .toArray();
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.areas ?? [];
},
- [db]
+ [getFullDiagram]
);
const deleteDiagramAreas: StorageContext['deleteDiagramAreas'] =
useCallback(
async (diagramId) => {
- await db.areas.where('diagramId').equals(diagramId).delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ diagram.areas = [];
+ indexDiagram(diagram);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, indexDiagram, saveDiagram]
);
- // Custom type operations
const addCustomType: StorageContext['addCustomType'] = useCallback(
async ({ diagramId, customType }) => {
- await db.db_custom_types.add({
- ...customType,
- diagramId,
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const customTypes = diagram.customTypes ?? [];
+ diagram.customTypes = [...customTypes, customType];
+ customTypeIndexRef.current.set(customType.id, diagramId);
+ await saveDiagram(diagram);
});
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const getCustomType: StorageContext['getCustomType'] = useCallback(
- async ({ diagramId, id }): Promise => {
- return await db.db_custom_types.get({ id, diagramId });
+ async ({ diagramId, id }) => {
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.customTypes?.find(
+ (customType) => customType.id === id
+ );
},
- [db]
+ [getFullDiagram]
);
const updateCustomType: StorageContext['updateCustomType'] = useCallback(
async ({ id, attributes }) => {
- await db.db_custom_types.update(id, attributes);
+ const diagramId = resolveDiagramIdForEntity(
+ id,
+ customTypeIndexRef.current,
+ 'customTypes'
+ );
+ if (!diagramId) return;
+
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.customTypes) return;
+ const customTypes = [...diagram.customTypes];
+ const index = customTypes.findIndex(
+ (customType) => customType.id === id
+ );
+ if (index === -1) return;
+ customTypes[index] = { ...customTypes[index], ...attributes };
+ diagram.customTypes = customTypes;
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [
+ enqueueDiagramTask,
+ getFullDiagram,
+ resolveDiagramIdForEntity,
+ saveDiagram,
+ ]
);
const deleteCustomType: StorageContext['deleteCustomType'] = useCallback(
async ({ diagramId, id }) => {
- await db.db_custom_types.where({ id, diagramId }).delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.customTypes) return;
+ diagram.customTypes = diagram.customTypes.filter(
+ (customType) => customType.id !== id
+ );
+ customTypeIndexRef.current.delete(id);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
const listCustomTypes: StorageContext['listCustomTypes'] = useCallback(
- async (diagramId): Promise => {
- return (
- await db.db_custom_types
- .where('diagramId')
- .equals(diagramId)
- .toArray()
- ).sort((a, b) => {
- return a.name.localeCompare(b.name);
- });
+ async (diagramId) => {
+ const diagram = await getFullDiagram(diagramId);
+ return (diagram?.customTypes ?? []).sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
},
- [db]
+ [getFullDiagram]
);
const deleteDiagramCustomTypes: StorageContext['deleteDiagramCustomTypes'] =
useCallback(
async (diagramId) => {
- await db.db_custom_types
- .where('diagramId')
- .equals(diagramId)
- .delete();
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ diagram.customTypes = [];
+ indexDiagram(diagram);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, indexDiagram, saveDiagram]
);
- const addDiagram: StorageContext['addDiagram'] = useCallback(
- async ({ diagram }) => {
- const promises = [];
- promises.push(
- db.diagrams.add({
- id: diagram.id,
- name: diagram.name,
- databaseType: diagram.databaseType,
- databaseEdition: diagram.databaseEdition,
- createdAt: diagram.createdAt,
- updatedAt: diagram.updatedAt,
- })
- );
-
- const tables = diagram.tables ?? [];
- promises.push(
- ...tables.map((table) =>
- addTable({ diagramId: diagram.id, table })
- )
- );
-
- const relationships = diagram.relationships ?? [];
- promises.push(
- ...relationships.map((relationship) =>
- addRelationship({ diagramId: diagram.id, relationship })
- )
- );
-
- const dependencies = diagram.dependencies ?? [];
- promises.push(
- ...dependencies.map((dependency) =>
- addDependency({ diagramId: diagram.id, dependency })
- )
- );
-
- const areas = diagram.areas ?? [];
- promises.push(
- ...areas.map((area) => addArea({ diagramId: diagram.id, area }))
- );
-
- const customTypes = diagram.customTypes ?? [];
- promises.push(
- ...customTypes.map((customType) =>
- addCustomType({ diagramId: diagram.id, customType })
- )
- );
-
- await Promise.all(promises);
+ const addNote: StorageContext['addNote'] = useCallback(
+ async ({ note, diagramId }) => {
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ const notes = diagram.notes ?? [];
+ diagram.notes = [...notes, note];
+ noteIndexRef.current.set(note.id, diagramId);
+ await saveDiagram(diagram);
+ });
},
- [db, addArea, addCustomType, addDependency, addRelationship, addTable]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
- const listDiagrams: StorageContext['listDiagrams'] = useCallback(
- async (
- options = {
- includeRelationships: false,
- includeTables: false,
- includeDependencies: false,
- includeAreas: false,
- includeCustomTypes: false,
- }
- ): Promise => {
- let diagrams = await db.diagrams.toArray();
-
- if (options.includeTables) {
- diagrams = await Promise.all(
- diagrams.map(async (diagram) => {
- diagram.tables = await listTables(diagram.id);
- return diagram;
- })
- );
- }
-
- if (options.includeRelationships) {
- diagrams = await Promise.all(
- diagrams.map(async (diagram) => {
- diagram.relationships = await listRelationships(
- diagram.id
- );
- return diagram;
- })
- );
- }
-
- if (options.includeDependencies) {
- diagrams = await Promise.all(
- diagrams.map(async (diagram) => {
- diagram.dependencies = await listDependencies(
- diagram.id
- );
- return diagram;
- })
- );
- }
-
- if (options.includeAreas) {
- diagrams = await Promise.all(
- diagrams.map(async (diagram) => {
- diagram.areas = await listAreas(diagram.id);
- return diagram;
- })
- );
- }
-
- if (options.includeCustomTypes) {
- diagrams = await Promise.all(
- diagrams.map(async (diagram) => {
- diagram.customTypes = await listCustomTypes(diagram.id);
- return diagram;
- })
- );
- }
-
- return diagrams;
+ const getNote: StorageContext['getNote'] = useCallback(
+ async ({ diagramId, id }) => {
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.notes?.find((note) => note.id === id);
},
- [
- db,
- listAreas,
- listCustomTypes,
- listDependencies,
- listRelationships,
- listTables,
- ]
+ [getFullDiagram]
);
- const getDiagram: StorageContext['getDiagram'] = useCallback(
- async (
- id,
- options = {
- includeRelationships: false,
- includeTables: false,
- includeDependencies: false,
- includeAreas: false,
- includeCustomTypes: false,
- }
- ): Promise => {
- const diagram = await db.diagrams.get(id);
-
- if (!diagram) {
- return undefined;
- }
-
- if (options.includeTables) {
- diagram.tables = await listTables(id);
- }
-
- if (options.includeRelationships) {
- diagram.relationships = await listRelationships(id);
- }
-
- if (options.includeDependencies) {
- diagram.dependencies = await listDependencies(id);
- }
-
- if (options.includeAreas) {
- diagram.areas = await listAreas(id);
- }
-
- if (options.includeCustomTypes) {
- diagram.customTypes = await listCustomTypes(id);
- }
-
- return diagram;
+ const updateNote: StorageContext['updateNote'] = useCallback(
+ async ({ id, attributes }) => {
+ const diagramId = resolveDiagramIdForEntity(
+ id,
+ noteIndexRef.current,
+ 'notes'
+ );
+ if (!diagramId) return;
+
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.notes) return;
+ const notes = [...diagram.notes];
+ const index = notes.findIndex((note) => note.id === id);
+ if (index === -1) return;
+ notes[index] = { ...notes[index], ...attributes };
+ diagram.notes = notes;
+ await saveDiagram(diagram);
+ });
},
[
- db,
- listAreas,
- listCustomTypes,
- listDependencies,
- listRelationships,
- listTables,
+ enqueueDiagramTask,
+ getFullDiagram,
+ resolveDiagramIdForEntity,
+ saveDiagram,
]
);
- const updateDiagram: StorageContext['updateDiagram'] = useCallback(
- async ({ id, attributes }) => {
- await db.diagrams.update(id, attributes);
-
- if (attributes.id) {
- await Promise.all([
- db.db_tables
- .where('diagramId')
- .equals(id)
- .modify({ diagramId: attributes.id }),
- db.db_relationships
- .where('diagramId')
- .equals(id)
- .modify({ diagramId: attributes.id }),
- db.db_dependencies
- .where('diagramId')
- .equals(id)
- .modify({ diagramId: attributes.id }),
- db.areas.where('diagramId').equals(id).modify({
- diagramId: attributes.id,
- }),
- db.db_custom_types
- .where('diagramId')
- .equals(id)
- .modify({ diagramId: attributes.id }),
- ]);
- }
+ const deleteNote: StorageContext['deleteNote'] = useCallback(
+ async ({ diagramId, id }) => {
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram?.notes) return;
+ diagram.notes = diagram.notes.filter((note) => note.id !== id);
+ noteIndexRef.current.delete(id);
+ await saveDiagram(diagram);
+ });
},
- [db]
+ [enqueueDiagramTask, getFullDiagram, saveDiagram]
);
- const deleteDiagram: StorageContext['deleteDiagram'] = useCallback(
- async (id) => {
- await Promise.all([
- db.diagrams.delete(id),
- db.db_tables.where('diagramId').equals(id).delete(),
- db.db_relationships.where('diagramId').equals(id).delete(),
- db.db_dependencies.where('diagramId').equals(id).delete(),
- db.areas.where('diagramId').equals(id).delete(),
- db.db_custom_types.where('diagramId').equals(id).delete(),
- ]);
+ const listNotes: StorageContext['listNotes'] = useCallback(
+ async (diagramId) => {
+ const diagram = await getFullDiagram(diagramId);
+ return diagram?.notes ?? [];
},
- [db]
+ [getFullDiagram]
);
+ const deleteDiagramNotes: StorageContext['deleteDiagramNotes'] =
+ useCallback(
+ async (diagramId) => {
+ await enqueueDiagramTask(diagramId, async () => {
+ if (deletedDiagramsRef.current.has(diagramId)) return;
+ const diagram = await getFullDiagram(diagramId);
+ if (!diagram) return;
+ diagram.notes = [];
+ indexDiagram(diagram);
+ await saveDiagram(diagram);
+ });
+ },
+ [enqueueDiagramTask, getFullDiagram, indexDiagram, saveDiagram]
+ );
+
return (
= ({
deleteCustomType,
listCustomTypes,
deleteDiagramCustomTypes,
+ addNote,
+ getNote,
+ updateNote,
+ deleteNote,
+ listNotes,
+ deleteDiagramNotes,
getDiagramFilter,
updateDiagramFilter,
deleteDiagramFilter,
diff --git a/src/context/theme-context/theme-provider.tsx b/src/context/theme-context/theme-provider.tsx
index d81f663b7..435762233 100644
--- a/src/context/theme-context/theme-provider.tsx
+++ b/src/context/theme-context/theme-provider.tsx
@@ -9,8 +9,13 @@ import {
keyboardShortcutsForOS,
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
-export const ThemeProvider: React.FC = ({
+export interface ThemeProviderProps extends React.PropsWithChildren {
+ disableHotkeys?: boolean;
+}
+
+export const ThemeProvider: React.FC = ({
children,
+ disableHotkeys = false,
}) => {
const { theme, setTheme } = useLocalConfig();
const isDarkSystemTheme = useMediaQuery({
@@ -49,6 +54,7 @@ export const ThemeProvider: React.FC = ({
{
preventDefault: true,
enableOnFormTags: true,
+ enabled: !disableHotkeys,
},
[handleThemeToggle]
);
diff --git a/src/dialogs/common/import-database/import-database.tsx b/src/dialogs/common/import-database/import-database.tsx
index e818da5f5..3c97edd75 100644
--- a/src/dialogs/common/import-database/import-database.tsx
+++ b/src/dialogs/common/import-database/import-database.tsx
@@ -42,6 +42,16 @@ import {
type ValidationResult,
} from '@/lib/data/sql-import/sql-validator';
import { SQLValidationStatus } from './sql-validation-status';
+import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
+import type { ImportMethod } from '@/lib/import-method/import-method';
+import { detectImportMethod } from '@/lib/import-method/detect-import-method';
+import { verifyDBML } from '@/lib/dbml/dbml-import/verify-dbml';
+import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
+import { sqlImportToDiagram } from '@/lib/data/sql-import';
+import {
+ clearErrorHighlight,
+ highlightErrorLine,
+} from '@/components/code-snippet/dbml/utils';
const calculateContentSizeMB = (content: string): number => {
return content.length / (1024 * 1024); // Convert to MB
@@ -55,49 +65,6 @@ const calculateIsLargeFile = (content: string): boolean => {
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
-// Helper to detect if content is likely SQL DDL or JSON
-const detectContentType = (content: string): 'query' | 'ddl' | null => {
- if (!content || content.trim().length === 0) return null;
-
- // Common SQL DDL keywords
- const ddlKeywords = [
- 'CREATE TABLE',
- 'ALTER TABLE',
- 'DROP TABLE',
- 'CREATE INDEX',
- 'CREATE VIEW',
- 'CREATE PROCEDURE',
- 'CREATE FUNCTION',
- 'CREATE SCHEMA',
- 'CREATE DATABASE',
- ];
-
- const upperContent = content.toUpperCase();
-
- // Check for SQL DDL patterns
- const hasDDLKeywords = ddlKeywords.some((keyword) =>
- upperContent.includes(keyword)
- );
- if (hasDDLKeywords) return 'ddl';
-
- // Check if it looks like JSON
- try {
- // Just check structure, don't need full parse for detection
- if (
- (content.trim().startsWith('{') && content.trim().endsWith('}')) ||
- (content.trim().startsWith('[') && content.trim().endsWith(']'))
- ) {
- return 'query';
- }
- } catch (error) {
- // Not valid JSON, might be partial
- console.error('Error detecting content type:', error);
- }
-
- // If we can't confidently detect, return null
- return null;
-};
-
export interface ImportDatabaseProps {
goBack?: () => void;
onImport: () => void;
@@ -111,8 +78,9 @@ export interface ImportDatabaseProps {
>;
keepDialogAfterImport?: boolean;
title: string;
- importMethod: 'query' | 'ddl';
- setImportMethod: (method: 'query' | 'ddl') => void;
+ importMethod: ImportMethod;
+ setImportMethod: (method: ImportMethod) => void;
+ importMethods?: ImportMethod[];
}
export const ImportDatabase: React.FC = ({
@@ -128,10 +96,12 @@ export const ImportDatabase: React.FC = ({
title,
importMethod,
setImportMethod,
+ importMethods,
}) => {
const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState('');
const editorRef = useRef(null);
+ const decorationsCollection = useRef();
const pasteDisposableRef = useRef(null);
const { t } = useTranslation();
@@ -139,6 +109,7 @@ export const ImportDatabase: React.FC = ({
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
const [isCheckingJson, setIsCheckingJson] = useState(false);
+ const [jsonCheckAttempts, setJsonCheckAttempts] = useState(0);
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
const [sqlValidation, setSqlValidation] = useState(
null
@@ -146,15 +117,21 @@ export const ImportDatabase: React.FC = ({
const [isAutoFixing, setIsAutoFixing] = useState(false);
const [showAutoFixButton, setShowAutoFixButton] = useState(false);
+ const clearDecorations = useCallback(() => {
+ clearErrorHighlight(decorationsCollection.current);
+ }, []);
+
useEffect(() => {
setScriptResult('');
setErrorMessage('');
setShowCheckJsonButton(false);
+ setJsonCheckAttempts(0);
}, [importMethod, setScriptResult]);
- // Check if the ddl is valid
+ // Check if the ddl or dbml is valid
useEffect(() => {
- if (importMethod !== 'ddl') {
+ clearDecorations();
+ if (importMethod === 'query') {
setSqlValidation(null);
setShowAutoFixButton(false);
return;
@@ -163,15 +140,80 @@ export const ImportDatabase: React.FC = ({
if (!scriptResult.trim()) {
setSqlValidation(null);
setShowAutoFixButton(false);
+ setErrorMessage('');
return;
}
+ if (importMethod === 'dbml') {
+ // Validate DBML by parsing it
+ const validateResponse = verifyDBML(scriptResult, { databaseType });
+ if (!validateResponse.hasError) {
+ setErrorMessage('');
+
+ // Try to count tables/relationships for DBML
+ (async () => {
+ try {
+ const diagram = await importDBMLToDiagram(
+ scriptResult,
+ { databaseType }
+ );
+ setSqlValidation({
+ isValid: true,
+ errors: [],
+ warnings: [],
+ tableCount: diagram.tables?.length ?? 0,
+ relationshipCount:
+ diagram.relationships?.length ?? 0,
+ });
+ } catch {
+ // If parsing fails, just show validation without counts
+ setSqlValidation({
+ isValid: true,
+ errors: [],
+ warnings: [],
+ });
+ }
+ })();
+ } else {
+ let errorMsg = 'Invalid DBML syntax';
+ let line: number = 1;
+
+ if (validateResponse.parsedError) {
+ errorMsg = validateResponse.parsedError.message;
+ line = validateResponse.parsedError.line;
+ highlightErrorLine({
+ error: validateResponse.parsedError,
+ model: editorRef.current?.getModel(),
+ editorDecorationsCollection:
+ decorationsCollection.current,
+ });
+ }
+
+ setSqlValidation({
+ isValid: false,
+ errors: [
+ {
+ message: errorMsg,
+ line: line,
+ type: 'syntax' as const,
+ },
+ ],
+ warnings: [],
+ });
+ setErrorMessage(errorMsg);
+ }
+
+ setShowAutoFixButton(false);
+ return;
+ }
+
+ // SQL validation
// First run our validation based on database type
const validation = validateSQL(scriptResult, databaseType);
- setSqlValidation(validation);
// If we have auto-fixable errors, show the auto-fix button
if (validation.fixedSQL && validation.errors.length > 0) {
+ setSqlValidation(validation);
setShowAutoFixButton(true);
// Don't try to parse invalid SQL
setErrorMessage('SQL contains syntax errors');
@@ -181,18 +223,37 @@ export const ImportDatabase: React.FC = ({
// Hide auto-fix button if no fixes available
setShowAutoFixButton(false);
- // Validate the SQL (either original or already fixed)
+ // Validate the SQL (either original or already fixed) and count tables/relationships
parseSQLError({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
- }).then((result) => {
+ }).then(async (result) => {
if (result.success) {
setErrorMessage('');
+
+ // Try to parse and count tables/relationships for successful validation
+ try {
+ const diagram = await sqlImportToDiagram({
+ sqlContent: scriptResult,
+ sourceDatabaseType: databaseType,
+ targetDatabaseType: databaseType,
+ });
+
+ setSqlValidation({
+ ...validation,
+ tableCount: diagram.tables?.length ?? 0,
+ relationshipCount: diagram.relationships?.length ?? 0,
+ });
+ } catch {
+ // If parsing fails, just show validation without counts
+ setSqlValidation(validation);
+ }
} else if (!result.success && result.error) {
+ setSqlValidation(validation);
setErrorMessage(result.error);
}
});
- }, [importMethod, scriptResult, databaseType]);
+ }, [importMethod, scriptResult, databaseType, clearDecorations]);
// Check if the script result is a valid JSON
useEffect(() => {
@@ -203,23 +264,29 @@ export const ImportDatabase: React.FC = ({
if (scriptResult.trim().length === 0) {
setErrorMessage('');
setShowCheckJsonButton(false);
+ setJsonCheckAttempts(0);
return;
}
if (isStringMetadataJson(scriptResult)) {
setErrorMessage('');
setShowCheckJsonButton(false);
+ setJsonCheckAttempts(0);
} else if (
scriptResult.trim().includes('{') &&
scriptResult.trim().includes('}')
) {
- setShowCheckJsonButton(true);
- setErrorMessage('');
+ // Only show the check button if we haven't exhausted all attempts (2 tries)
+ if (jsonCheckAttempts < 2) {
+ setShowCheckJsonButton(true);
+ setErrorMessage('');
+ }
+ // If we've exhausted attempts, keep the error message and don't show the button
} else {
setErrorMessage(errorScriptOutputMessage);
setShowCheckJsonButton(false);
}
- }, [scriptResult, importMethod]);
+ }, [scriptResult, importMethod, jsonCheckAttempts]);
const handleImport = useCallback(() => {
if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
@@ -296,10 +363,12 @@ export const ImportDatabase: React.FC = ({
if (isStringMetadataJson(fixedJson)) {
setScriptResult(fixedJson);
setErrorMessage('');
+ setJsonCheckAttempts(0);
formatEditor();
} else {
setScriptResult(fixedJson);
setErrorMessage(errorScriptOutputMessage);
+ setJsonCheckAttempts((prev) => prev + 1);
formatEditor();
}
@@ -317,9 +386,17 @@ export const ImportDatabase: React.FC = ({
};
}, []);
+ // Use ref to track current import method to avoid closure issues
+ const importMethodRef = useRef(importMethod);
+ useEffect(() => {
+ importMethodRef.current = importMethod;
+ }, [importMethod]);
+
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
+ decorationsCollection.current =
+ editor.createDecorationsCollection();
// Cleanup previous disposable if it exists
if (pasteDisposableRef.current) {
@@ -338,8 +415,9 @@ export const ImportDatabase: React.FC = ({
const isLargeFile = calculateIsLargeFile(content);
// First, detect content type to determine if we should switch modes
- const detectedType = detectContentType(content);
- if (detectedType && detectedType !== importMethod) {
+ const detectedType = detectImportMethod(content);
+ const currentMethod = importMethodRef.current;
+ if (detectedType && detectedType !== currentMethod) {
// Switch to the detected mode immediately
setImportMethod(detectedType);
@@ -352,10 +430,10 @@ export const ImportDatabase: React.FC = ({
?.run();
}, 100);
}
- // For DDL mode, do NOT format as it can break the SQL
+ // For DDL and DBML modes, do NOT format as it can break the syntax
} else {
// Content type didn't change, apply formatting based on current mode
- if (importMethod === 'query' && !isLargeFile) {
+ if (currentMethod === 'query' && !isLargeFile) {
// Only format JSON content if not too large
setTimeout(() => {
editor
@@ -363,13 +441,13 @@ export const ImportDatabase: React.FC = ({
?.run();
}, 100);
}
- // For DDL mode or large files, do NOT format
+ // For DDL and DBML modes or large files, do NOT format
}
});
pasteDisposableRef.current = disposable;
},
- [importMethod, setImportMethod]
+ [setImportMethod]
);
const renderHeader = useCallback(() => {
@@ -391,6 +469,7 @@ export const ImportDatabase: React.FC = ({
databaseEdition={databaseEdition}
setShowSSMSInfoDialog={setShowSSMSInfoDialog}
showSSMSInfoDialog={showSSMSInfoDialog}
+ importMethods={importMethods}
/>
),
[
@@ -401,6 +480,7 @@ export const ImportDatabase: React.FC = ({
databaseEdition,
setShowSSMSInfoDialog,
showSSMSInfoDialog,
+ importMethods,
]
);
@@ -410,27 +490,36 @@ export const ImportDatabase: React.FC = ({
{importMethod === 'query'
? 'Smart Query Output'
- : 'SQL Script'}
+ : importMethod === 'dbml'
+ ? 'DBML Script'
+ : 'SQL Script'}
}>
}
onMount={handleEditorDidMount}
+ beforeMount={setupDBMLLanguage}
theme={
effectiveTheme === 'dark'
? 'dbml-dark'
: 'dbml-light'
}
options={{
+ editContext: false,
formatOnPaste: false, // Never format on paste - we handle it manually
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
- glyphMargin: false,
lineNumbers: 'on',
guides: {
indentation: false,
@@ -455,12 +544,15 @@ export const ImportDatabase: React.FC = ({
- {errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
+ {errorMessage ||
+ ((importMethod === 'ddl' || importMethod === 'dbml') &&
+ sqlValidation) ? (
) : null}
diff --git a/src/dialogs/common/import-database/instructions-section/instructions-section.tsx b/src/dialogs/common/import-database/instructions-section/instructions-section.tsx
index e43bc4036..e6a8842cd 100644
--- a/src/dialogs/common/import-database/instructions-section/instructions-section.tsx
+++ b/src/dialogs/common/import-database/instructions-section/instructions-section.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import logo from '@/assets/logo-2.png';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
@@ -15,9 +15,11 @@ import {
AvatarImage,
} from '@/components/avatar/avatar';
import { useTranslation } from 'react-i18next';
-import { Code } from 'lucide-react';
+import { Code, FileCode } from 'lucide-react';
import { SmartQueryInstructions } from './instructions/smart-query-instructions';
import { DDLInstructions } from './instructions/ddl-instructions';
+import { DBMLInstructions } from './instructions/dbml-instructions';
+import type { ImportMethod } from '@/lib/import-method/import-method';
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
DatabaseType.CLICKHOUSE,
@@ -30,12 +32,15 @@ export interface InstructionsSectionProps {
setDatabaseEdition: React.Dispatch<
React.SetStateAction
>;
- importMethod: 'query' | 'ddl';
- setImportMethod: (method: 'query' | 'ddl') => void;
+ importMethod: ImportMethod;
+ setImportMethod: (method: ImportMethod) => void;
showSSMSInfoDialog: boolean;
setShowSSMSInfoDialog: (show: boolean) => void;
+ importMethods?: ImportMethod[];
}
+const defaultImportMethods: ImportMethod[] = ['query', 'ddl', 'dbml'];
+
export const InstructionsSection: React.FC = ({
databaseType,
databaseEdition,
@@ -44,12 +49,27 @@ export const InstructionsSection: React.FC = ({
setImportMethod,
setShowSSMSInfoDialog,
showSSMSInfoDialog,
+ importMethods = defaultImportMethods,
}) => {
const { t } = useTranslation();
+ const showSmartQuery = useMemo(
+ () => importMethods.includes('query'),
+ [importMethods]
+ );
+ const showDDL = useMemo(
+ () => importMethods.includes('ddl'),
+ [importMethods]
+ );
+ const showDBML = useMemo(
+ () => importMethods.includes('dbml'),
+ [importMethods]
+ );
+
return (
- {databaseTypeToEditionMap[databaseType].length > 0 ? (
+ {showSmartQuery &&
+ databaseTypeToEditionMap[databaseType].length > 0 ? (
{t(
@@ -115,24 +135,24 @@ export const InstructionsSection: React.FC = ({
) : null}
- {DatabasesWithoutDDLInstructions.includes(databaseType) ? null : (
-
-
- How would you like to import?
-
-
{
- let selectedImportMethod: 'query' | 'ddl' = 'query';
- if (value) {
- selectedImportMethod = value as 'query' | 'ddl';
- }
+
+
+ How would you like to import?
+
+
{
+ let selectedImportMethod: ImportMethod = 'query';
+ if (value) {
+ selectedImportMethod = value as ImportMethod;
+ }
- setImportMethod(selectedImportMethod);
- }}
- >
+ setImportMethod(selectedImportMethod);
+ }}
+ >
+ {showSmartQuery && (
= ({
Smart Query
+ )}
+ {showDDL &&
+ !DatabasesWithoutDDLInstructions.includes(
+ databaseType
+ ) && (
+
+
+
+
+ SQL Script
+
+ )}
+ {showDBML && (
- SQL Script
+ DBML
-
-
- )}
+ )}
+
+
Instructions:
- {importMethod === 'query' ? (
+ {importMethod === 'query' && showSmartQuery ? (
- ) : (
+ ) : importMethod === 'ddl' ? (
+ ) : (
+
)}
diff --git a/src/dialogs/common/import-database/instructions-section/instructions/dbml-instructions.tsx b/src/dialogs/common/import-database/instructions-section/instructions/dbml-instructions.tsx
new file mode 100644
index 000000000..41f168ead
--- /dev/null
+++ b/src/dialogs/common/import-database/instructions-section/instructions/dbml-instructions.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import type { DatabaseType } from '@/lib/domain/database-type';
+import type { DatabaseEdition } from '@/lib/domain/database-edition';
+import { CodeSnippet } from '@/components/code-snippet/code-snippet';
+import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
+
+export interface DBMLInstructionsProps {
+ databaseType: DatabaseType;
+ databaseEdition?: DatabaseEdition;
+}
+
+export const DBMLInstructions: React.FC = () => {
+ return (
+ <>
+
+
+ Paste your DBML (Database Markup Language) schema definition
+ here →
+
+
+
+
+
Example:
+ users.id]
+ title varchar
+ content text
+}`}
+ language={'dbml'}
+ />
+
+ >
+ );
+};
diff --git a/src/dialogs/common/import-database/instructions-section/instructions/ddl-instructions.tsx b/src/dialogs/common/import-database/instructions-section/instructions/ddl-instructions.tsx
index 1f02c5182..8d4935265 100644
--- a/src/dialogs/common/import-database/instructions-section/instructions/ddl-instructions.tsx
+++ b/src/dialogs/common/import-database/instructions-section/instructions/ddl-instructions.tsx
@@ -43,8 +43,8 @@ const DDLInstructionsMap: Record = {
},
{
text: 'Execute the following command in your terminal:',
- code: `sqlite3 \n.dump > `,
- example: `sqlite3 my_db.db\n.dump > schema_export.sql`,
+ code: `sqlite3 \n".schema" > `,
+ example: `sqlite3 my_db.db\n".schema" > schema_export.sql`,
},
{
text: 'Open the exported SQL file, copy its contents, and paste them here.',
diff --git a/src/dialogs/common/import-database/instructions-section/instructions/smart-query-instructions.tsx b/src/dialogs/common/import-database/instructions-section/instructions/smart-query-instructions.tsx
index 52ea1b34c..e6089e999 100644
--- a/src/dialogs/common/import-database/instructions-section/instructions/smart-query-instructions.tsx
+++ b/src/dialogs/common/import-database/instructions-section/instructions/smart-query-instructions.tsx
@@ -64,9 +64,8 @@ export const SmartQueryInstructions: React.FC = ({
useEffect(() => {
const loadScripts = async () => {
- const { importMetadataScripts } = await import(
- '@/lib/data/import-metadata/scripts/scripts'
- );
+ const { importMetadataScripts } =
+ await import('@/lib/data/import-metadata/scripts/scripts');
setImportMetadataScripts(importMetadataScripts);
};
loadScripts();
diff --git a/src/dialogs/common/import-database/sql-validation-status.tsx b/src/dialogs/common/import-database/sql-validation-status.tsx
index ed8d64f5c..3dccc670f 100644
--- a/src/dialogs/common/import-database/sql-validation-status.tsx
+++ b/src/dialogs/common/import-database/sql-validation-status.tsx
@@ -11,6 +11,7 @@ interface SQLValidationStatusProps {
errorMessage: string;
isAutoFixing?: boolean;
onErrorClick?: (line: number) => void;
+ importMethod?: 'ddl' | 'dbml' | 'query';
}
export const SQLValidationStatus: React.FC = ({
@@ -18,6 +19,7 @@ export const SQLValidationStatus: React.FC = ({
errorMessage,
isAutoFixing = false,
onErrorClick,
+ importMethod = 'ddl',
}) => {
const hasErrors = useMemo(
() => validation?.errors.length && validation.errors.length > 0,
@@ -73,7 +75,7 @@ export const SQLValidationStatus: React.FC = ({
{hasErrors ? (
-
+
{validation?.errors
.slice(0, 3)
@@ -137,7 +139,7 @@ export const SQLValidationStatus: React.FC
= ({
{hasWarnings && !hasErrors ? (
-
+
@@ -168,7 +170,46 @@ export const SQLValidationStatus: React.FC
= ({
- SQL syntax validated successfully
+
+ {importMethod === 'dbml'
+ ? 'DBML syntax validated successfully'
+ : 'SQL syntax validated successfully'}
+
+ {(validation.tableCount !== undefined ||
+ validation.relationshipCount !==
+ undefined) && (
+
+ {validation.tableCount !== undefined &&
+ validation.tableCount > 0 && (
+
+
+ {validation.tableCount}
+ {' '}
+ table
+ {validation.tableCount !== 1
+ ? 's'
+ : ''}
+
+ )}
+ {validation.relationshipCount !==
+ undefined &&
+ validation.relationshipCount >
+ 0 && (
+
+
+ {
+ validation.relationshipCount
+ }
+ {' '}
+ relationship
+ {validation.relationshipCount !==
+ 1
+ ? 's'
+ : ''}
+
+ )}
+
+ )}
diff --git a/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx b/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx
index b08a8390b..5faee1665 100644
--- a/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx
+++ b/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx
@@ -22,6 +22,11 @@ import { sqlImportToDiagram } from '@/lib/data/sql-import';
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
+import {
+ defaultDBMLDiagramName,
+ importDBMLToDiagram,
+} from '@/lib/dbml/dbml-import/dbml-import';
+import type { ImportMethod } from '@/lib/import-method/import-method';
export interface CreateDiagramDialogProps extends BaseDialogProps {}
@@ -30,11 +35,11 @@ export const CreateDiagramDialog: React.FC
= ({
}) => {
const { diagramId } = useChartDB();
const { t } = useTranslation();
- const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
+ const [importMethod, setImportMethod] = useState('query');
const [databaseType, setDatabaseType] = useState(
DatabaseType.GENERIC
);
- const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
+ const { closeCreateDiagramDialog } = useDialog();
const { updateConfig } = useConfig();
const [scriptResult, setScriptResult] = useState('');
const [databaseEdition, setDatabaseEdition] = useState<
@@ -89,6 +94,14 @@ export const CreateDiagramDialog: React.FC = ({
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
+ } else if (importMethod === 'dbml') {
+ diagram = await importDBMLToDiagram(scriptResult, {
+ databaseType,
+ });
+ // Update the diagram name if it's the default
+ if (diagram.name === defaultDBMLDiagramName) {
+ diagram.name = `Diagram ${diagramNumber}`;
+ }
} else {
let metadata: DatabaseMetadata | undefined = databaseMetadata;
@@ -152,10 +165,6 @@ export const CreateDiagramDialog: React.FC = ({
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
- setTimeout(
- () => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
- 700
- );
}, [
databaseType,
addDiagram,
@@ -164,14 +173,13 @@ export const CreateDiagramDialog: React.FC = ({
navigate,
updateConfig,
diagramNumber,
- openImportDBMLDialog,
]);
const importNewDiagramOrFilterTables = useCallback(async () => {
try {
setIsParsingMetadata(true);
- if (importMethod === 'ddl') {
+ if (importMethod === 'ddl' || importMethod === 'dbml') {
await importNewDiagram();
} else {
// Parse metadata asynchronously to avoid blocking the UI
diff --git a/src/dialogs/create-diagram-dialog/select-database/select-database-content.tsx b/src/dialogs/create-diagram-dialog/select-database/select-database-content.tsx
index ae30d1e6a..ce5615e82 100644
--- a/src/dialogs/create-diagram-dialog/select-database/select-database-content.tsx
+++ b/src/dialogs/create-diagram-dialog/select-database/select-database-content.tsx
@@ -1,20 +1,28 @@
-import React, { useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { ToggleGroup } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
import { DatabaseOption } from './database-option';
import { ExampleOption } from './example-option';
import { Button } from '@/components/button/button';
import { ChevronDown, ChevronUp } from 'lucide-react';
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from '@/components/tabs/tabs';
export interface SelectDatabaseContentProps {
databaseType: DatabaseType;
setDatabaseType: React.Dispatch>;
- onContinue: () => void;
+ onContinue: (selectedDatabaseType: DatabaseType) => void;
}
const ROW_SIZE = 3;
const ROWS = 2;
const TOTAL_SLOTS = ROW_SIZE * ROWS;
-const SUPPORTED_DB_TYPES: DatabaseType[] = [
+
+// Transactional databases - OLTP systems optimized for frequent read/write operations
+const TRANSACTIONAL_DB_TYPES: DatabaseType[] = [
DatabaseType.MYSQL,
DatabaseType.POSTGRESQL,
DatabaseType.MARIADB,
@@ -22,69 +30,87 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
DatabaseType.SQL_SERVER,
DatabaseType.ORACLE,
DatabaseType.COCKROACHDB,
- DatabaseType.CLICKHOUSE,
];
+// Analytical databases - OLAP systems optimized for complex queries and analytics
+const ANALYTICAL_DB_TYPES: DatabaseType[] = [DatabaseType.CLICKHOUSE];
+
export const SelectDatabaseContent: React.FC = ({
databaseType,
setDatabaseType,
onContinue,
}) => {
+ const [activeTab, setActiveTab] = useState<'transactional' | 'analytical'>(
+ 'transactional'
+ );
const [currentRow, setCurrentRow] = useState(0);
+
+ const currentDbTypes =
+ activeTab === 'transactional'
+ ? TRANSACTIONAL_DB_TYPES
+ : ANALYTICAL_DB_TYPES;
+
const currentDatabasesTypes = useMemo(
() =>
- SUPPORTED_DB_TYPES.slice(
+ currentDbTypes.slice(
currentRow * ROW_SIZE,
currentRow * ROW_SIZE + TOTAL_SLOTS
),
- [currentRow]
+ [currentRow, currentDbTypes]
);
const hasNextRow = useMemo(
- () => (currentRow + 1) * ROW_SIZE < SUPPORTED_DB_TYPES.length,
- [currentRow]
+ () => (currentRow + 1) * ROW_SIZE < currentDbTypes.length,
+ [currentRow, currentDbTypes]
);
const hasPreviousRow = useMemo(() => currentRow > 0, [currentRow]);
- const toggleRow = () => {
+ const toggleRow = useCallback(() => {
if (currentRow === 0 && hasNextRow) {
setCurrentRow(currentRow + 1);
} else if (currentRow > 0) {
setCurrentRow(currentRow - 1);
}
- };
+ }, [currentRow, hasNextRow]);
- return (
-
-
{
- if (!value) {
- setDatabaseType(DatabaseType.GENERIC);
- } else {
- setDatabaseType(value);
- onContinue();
- }
- }}
- type="single"
- className="grid grid-flow-row grid-cols-3 gap-6"
- >
- {Array.from({ length: TOTAL_SLOTS }).map((_, index) =>
- currentDatabasesTypes?.[index] ? (
-
- ) : null
- )}
+ const handleTabChange = useCallback((value: string) => {
+ setActiveTab(value as 'transactional' | 'analytical');
+ setCurrentRow(0); // Reset to first row when switching tabs
+ }, []);
+
+ const renderDatabaseGrid = useCallback(
+ () => (
+
+
{
+ if (!value) {
+ setDatabaseType(DatabaseType.GENERIC);
+ } else {
+ setDatabaseType(value);
+ onContinue(value);
+ }
+ }}
+ type="single"
+ className="grid grid-flow-row grid-cols-3 content-start gap-4"
+ >
+ {Array.from({ length: TOTAL_SLOTS }).map((_, index) =>
+ currentDatabasesTypes?.[index] ? (
+
+ ) : null
+ )}
+
-
+
{hasNextRow || hasPreviousRow ? (
{currentRow === 0 ? (
@@ -105,7 +131,55 @@ export const SelectDatabaseContent: React.FC = ({
) : null}
-
+
+ ),
+ [
+ databaseType,
+ currentDatabasesTypes,
+ hasNextRow,
+ hasPreviousRow,
+ onContinue,
+ setDatabaseType,
+ toggleRow,
+ currentRow,
+ ]
+ );
+
+ return (
+
+
+
+
+ Transactional
+
+
+ Analytical
+
+
+
+ {renderDatabaseGrid()}
+
+
+ {renderDatabaseGrid()}
+
+
);
};
diff --git a/src/dialogs/export-image-dialog/export-image-dialog.tsx b/src/dialogs/export-image-dialog/export-image-dialog.tsx
index bb26636ff..f9c1c29d3 100644
--- a/src/dialogs/export-image-dialog/export-image-dialog.tsx
+++ b/src/dialogs/export-image-dialog/export-image-dialog.tsx
@@ -30,7 +30,7 @@ export interface ExportImageDialogProps extends BaseDialogProps {
const DEFAULT_INCLUDE_PATTERN_BG = true;
const DEFAULT_TRANSPARENT = false;
-const DEFAULT_SCALE = '2';
+const DEFAULT_SCALE = '4';
export const ExportImageDialog: React.FC
= ({
dialog,
format,
@@ -62,7 +62,7 @@ export const ExportImageDialog: React.FC = ({
const scaleOptions: SelectBoxOption[] = useMemo(
() =>
- ['1', '2', '3', '4'].map((scale) => ({
+ ['1', '2', '4'].map((scale) => ({
value: scale,
label: t(`export_image_dialog.scale_${scale}x`),
})),
diff --git a/src/dialogs/export-sql-dialog/export-sql-dialog.tsx b/src/dialogs/export-sql-dialog/export-sql-dialog.tsx
index df9d29cd0..230bae272 100644
--- a/src/dialogs/export-sql-dialog/export-sql-dialog.tsx
+++ b/src/dialogs/export-sql-dialog/export-sql-dialog.tsx
@@ -18,10 +18,11 @@ import {
exportBaseSQL,
exportSQL,
} from '@/lib/data/sql-export/export-sql-script';
+import { hasCrossDialectSupport } from '@/lib/data/sql-export/cross-dialect';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
-import { Annoyed, Sparkles } from 'lucide-react';
-import React, { useCallback, useEffect, useRef } from 'react';
+import { Annoyed, Sparkles, Blocks, Wand2 } from 'lucide-react';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import type { Diagram } from '@/lib/domain/diagram';
@@ -49,8 +50,30 @@ export const ExportSQLDialog: React.FC = ({
const [error, setError] = React.useState(false);
const [isScriptLoading, setIsScriptLoading] =
React.useState(false);
+ const [useAIExport, setUseAIExport] = React.useState(false);
const abortControllerRef = useRef(null);
+ // Check if a deterministic export path is available
+ const hasDeterministicPath = useMemo(() => {
+ return (
+ targetDatabaseType === DatabaseType.GENERIC ||
+ currentDiagram.databaseType === targetDatabaseType ||
+ hasCrossDialectSupport(
+ currentDiagram.databaseType,
+ targetDatabaseType
+ )
+ );
+ }, [targetDatabaseType, currentDiagram.databaseType]);
+
+ // Show toggle only for cross-dialect exports where both options are available
+ const showExportModeToggle = useMemo(() => {
+ return (
+ hasDeterministicPath &&
+ currentDiagram.databaseType !== targetDatabaseType &&
+ targetDatabaseType !== DatabaseType.GENERIC
+ );
+ }, [hasDeterministicPath, currentDiagram.databaseType, targetDatabaseType]);
+
const exportSQLScript = useCallback(async () => {
const filteredDiagram: Diagram = {
...currentDiagram,
@@ -120,7 +143,8 @@ export const ExportSQLDialog: React.FC = ({
}),
};
- if (targetDatabaseType === DatabaseType.GENERIC) {
+ // Use deterministic export if available and AI export is not selected
+ if (hasDeterministicPath && !useAIExport) {
return Promise.resolve(
exportBaseSQL({
diagram: filteredDiagram,
@@ -135,7 +159,13 @@ export const ExportSQLDialog: React.FC = ({
signal: abortControllerRef.current?.signal,
});
}
- }, [targetDatabaseType, currentDiagram, filter]);
+ }, [
+ targetDatabaseType,
+ currentDiagram,
+ filter,
+ hasDeterministicPath,
+ useAIExport,
+ ]);
useEffect(() => {
if (!dialog.open) {
@@ -249,6 +279,36 @@ export const ExportSQLDialog: React.FC = ({
],
})}
+ {showExportModeToggle && (
+
+
+ setUseAIExport(false)}
+ >
+
+ Deterministic
+
+ setUseAIExport(true)}
+ >
+
+ AI
+
+
+
+ )}
diff --git a/src/dialogs/import-database-dialog/import-database-dialog.tsx b/src/dialogs/import-database-dialog/import-database-dialog.tsx
index e7dd28af1..6fb79b253 100644
--- a/src/dialogs/import-database-dialog/import-database-dialog.tsx
+++ b/src/dialogs/import-database-dialog/import-database-dialog.tsx
@@ -10,37 +10,40 @@ import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
-import { Trans, useTranslation } from 'react-i18next';
-import { useReactFlow } from '@xyflow/react';
+import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
-import { useAlert } from '@/context/alert-context/alert-context';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
+import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
+import type { ImportMethod } from '@/lib/import-method/import-method';
export interface ImportDatabaseDialogProps extends BaseDialogProps {
databaseType: DatabaseType;
+ importMethods?: ImportMethod[];
+ initialImportMethod?: ImportMethod;
}
+const defaultImportMethods: ImportMethod[] = ['query', 'ddl', 'dbml'];
+
export const ImportDatabaseDialog: React.FC
= ({
dialog,
databaseType,
+ importMethods = defaultImportMethods,
+ initialImportMethod,
}) => {
- const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
+ const [importMethod, setImportMethod] = useState(
+ initialImportMethod ?? importMethods[0]
+ );
const { closeImportDatabaseDialog } = useDialog();
- const { showAlert } = useAlert();
const {
- tables,
- relationships,
- removeTables,
- removeRelationships,
addTables,
addRelationships,
diagramName,
databaseType: currentDatabaseType,
updateDatabaseType,
+ tables: existingTables,
} = useChartDB();
const [scriptResult, setScriptResult] = useState('');
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
- const { setNodes } = useReactFlow();
const { t } = useTranslation();
const [databaseEdition, setDatabaseEdition] = useState<
DatabaseEdition | undefined
@@ -54,7 +57,8 @@ export const ImportDatabaseDialog: React.FC = ({
if (!dialog.open) return;
setDatabaseEdition(undefined);
setScriptResult('');
- }, [dialog.open]);
+ setImportMethod(initialImportMethod ?? importMethods[0]);
+ }, [dialog.open, importMethods, initialImportMethod]);
const importDatabase = useCallback(async () => {
let diagram: Diagram | undefined;
@@ -65,6 +69,10 @@ export const ImportDatabaseDialog: React.FC = ({
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
+ } else if (importMethod === 'dbml') {
+ diagram = await importDBMLToDiagram(scriptResult, {
+ databaseType,
+ });
} else {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
@@ -79,247 +87,54 @@ export const ImportDatabaseDialog: React.FC = ({
});
}
- const tableIdsToRemove = tables
- .filter((table) =>
- diagram.tables?.some(
- (t) => t.name === table.name && t.schema === table.schema
- )
- )
- .map((table) => table.id);
-
- const relationshipIdsToRemove = relationships
- .filter((relationship) => {
- const sourceTable = tables.find(
- (table) => table.id === relationship.sourceTableId
- );
-
- const targetTable = tables.find(
- (table) => table.id === relationship.targetTableId
- );
-
- if (!sourceTable || !targetTable) return true; // should not happen
-
- const sourceField = sourceTable.fields.find(
- (field) => field.id === relationship.sourceFieldId
- );
-
- const targetField = targetTable.fields.find(
- (field) => field.id === relationship.targetFieldId
- );
-
- if (!sourceField || !targetField) return true; // should not happen
-
- const replacementSourceTable = diagram.tables?.find(
- (table) =>
- table.name === sourceTable.name &&
- table.schema === sourceTable.schema
- );
-
- const replacementTargetTable = diagram.tables?.find(
- (table) =>
- table.name === targetTable.name &&
- table.schema === targetTable.schema
- );
-
- // if the source or target field of the relationship is not in the new table, remove the relationship
- if (
- (replacementSourceTable &&
- !replacementSourceTable.fields.some(
- (field) => field.name === sourceField.name
- )) ||
- (replacementTargetTable &&
- !replacementTargetTable.fields.some(
- (field) => field.name === targetField.name
- ))
- ) {
- return true;
- }
-
- return diagram.relationships?.some((r) => {
- const sourceNewTable = diagram.tables?.find(
- (table) => table.id === r.sourceTableId
- );
-
- const targetNewTable = diagram.tables?.find(
- (table) => table.id === r.targetTableId
- );
-
- const sourceNewField = sourceNewTable?.fields.find(
- (field) => field.id === r.sourceFieldId
- );
-
- const targetNewField = targetNewTable?.fields.find(
- (field) => field.id === r.targetFieldId
- );
-
- return (
- sourceField.name === sourceNewField?.name &&
- sourceTable.name === sourceNewTable?.name &&
- sourceTable.schema === sourceNewTable?.schema &&
- targetField.name === targetNewField?.name &&
- targetTable.name === targetNewTable?.name &&
- targetTable.schema === targetNewTable?.schema
- );
- });
- })
- .map((relationship) => relationship.id);
-
- const newRelationshipsNumber = diagram.relationships?.filter(
- (relationship) => {
- const newSourceTable = diagram.tables?.find(
- (table) => table.id === relationship.sourceTableId
- );
- const newTargetTable = diagram.tables?.find(
- (table) => table.id === relationship.targetTableId
- );
- const newSourceField = newSourceTable?.fields.find(
- (field) => field.id === relationship.sourceFieldId
- );
- const newTargetField = newTargetTable?.fields.find(
- (field) => field.id === relationship.targetFieldId
- );
-
- return !relationships.some((r) => {
- const sourceTable = tables.find(
- (table) => table.id === r.sourceTableId
- );
- const targetTable = tables.find(
- (table) => table.id === r.targetTableId
- );
- const sourceField = sourceTable?.fields.find(
- (field) => field.id === r.sourceFieldId
- );
- const targetField = targetTable?.fields.find(
- (field) => field.id === r.targetFieldId
- );
- return (
- sourceField?.name === newSourceField?.name &&
- sourceTable?.name === newSourceTable?.name &&
- sourceTable?.schema === newSourceTable?.schema &&
- targetField?.name === newTargetField?.name &&
- targetTable?.name === newTargetTable?.name &&
- targetTable?.schema === newTargetTable?.schema
- );
- });
- }
- ).length;
-
- const newTablesNumber = diagram.tables?.filter(
- (table) =>
- !tables.some(
- (t) => t.name === table.name && t.schema === table.schema
- )
- ).length;
-
- const shouldRemove = new Promise((resolve) => {
- if (
- tableIdsToRemove.length === 0 &&
- relationshipIdsToRemove.length === 0 &&
- newTablesNumber === 0 &&
- newRelationshipsNumber === 0
- ) {
- resolve(true);
- return;
- }
+ // Skip if nothing to import
+ const newTablesNumber = diagram.tables?.length ?? 0;
+ const newRelationshipsNumber = diagram.relationships?.length ?? 0;
+ if (newTablesNumber === 0 && newRelationshipsNumber === 0) {
+ return;
+ }
- const content = (
- <>
-
- {t(
- 'import_database_dialog.override_alert.content.alert'
- )}
-
- {(newTablesNumber ?? 0 > 0) ? (
-
- ,
- }}
- />
-
- ) : null}
- {(newRelationshipsNumber ?? 0 > 0) ? (
-
- ,
- }}
- />
-
- ) : null}
- {tableIdsToRemove.length > 0 && (
-
- ,
- }}
- />
-
- )}
-
- {t(
- 'import_database_dialog.override_alert.content.proceed'
- )}
-
- >
- );
+ // Close dialog immediately to prevent re-render blocking
+ closeImportDatabaseDialog();
- showAlert({
- title: t('import_database_dialog.override_alert.title'),
- content,
- actionLabel: t('import_database_dialog.override_alert.import'),
- closeLabel: t('import_database_dialog.override_alert.cancel'),
- onAction: () => resolve(true),
- onClose: () => resolve(false),
+ // Calculate position offset for new tables to avoid overlap
+ let offsetX = 0;
+ if (existingTables.length > 0) {
+ // Find the rightmost table
+ const rightmostTable = existingTables.reduce((max, table) => {
+ const tableRight = table.x + (table.width ?? 250);
+ const maxRight = max.x + (max.width ?? 250);
+ return tableRight > maxRight ? table : max;
});
- });
-
- if (!(await shouldRemove)) return;
-
- await Promise.all([
- removeTables(tableIdsToRemove, { updateHistory: false }),
- removeRelationships(relationshipIdsToRemove, {
- updateHistory: false,
- }),
- ]);
-
- await Promise.all([
- addTables(diagram.tables ?? [], { updateHistory: false }),
- addRelationships(diagram.relationships ?? [], {
- updateHistory: false,
- }),
- ]);
-
- if (currentDatabaseType === DatabaseType.GENERIC) {
- await updateDatabaseType(databaseType);
+ // Position new tables 150px to the right of the rightmost table
+ offsetX = rightmostTable.x + (rightmostTable.width ?? 250) + 150;
}
- setNodes((nodes) =>
- nodes.map((node) => ({
- ...node,
- selected:
- diagram.tables?.some((table) => table.id === node.id) ??
- false,
- }))
- );
-
- resetRedoStack();
- resetUndoStack();
+ // Apply offset to imported tables
+ const positionedTables =
+ diagram.tables?.map((table) => ({
+ ...table,
+ x: table.x + offsetX,
+ })) ?? [];
+
+ // Use queueMicrotask to defer work after dialog closes but before next paint
+ queueMicrotask(async () => {
+ // Add tables and relationships
+ await Promise.all([
+ addTables(positionedTables, { updateHistory: false }),
+ addRelationships(diagram.relationships ?? [], {
+ updateHistory: false,
+ }),
+ ]);
+
+ if (currentDatabaseType === DatabaseType.GENERIC) {
+ await updateDatabaseType(databaseType);
+ }
- closeImportDatabaseDialog();
+ // Reset undo/redo stacks
+ resetRedoStack();
+ resetUndoStack();
+ });
}, [
importMethod,
databaseEdition,
@@ -327,18 +142,12 @@ export const ImportDatabaseDialog: React.FC = ({
updateDatabaseType,
databaseType,
scriptResult,
- tables,
addRelationships,
addTables,
- closeImportDatabaseDialog,
- relationships,
- removeRelationships,
- removeTables,
resetRedoStack,
resetUndoStack,
- showAlert,
- setNodes,
- t,
+ closeImportDatabaseDialog,
+ existingTables,
]);
return (
@@ -365,6 +174,7 @@ export const ImportDatabaseDialog: React.FC = ({
title={t('import_database_dialog.title', { diagramName })}
importMethod={importMethod}
setImportMethod={setImportMethod}
+ importMethods={importMethods}
/>
diff --git a/src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx b/src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
deleted file mode 100644
index b03b4ad48..000000000
--- a/src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
+++ /dev/null
@@ -1,359 +0,0 @@
-import React, {
- useCallback,
- useEffect,
- useState,
- Suspense,
- useRef,
-} from 'react';
-import type * as monaco from 'monaco-editor';
-import { useDialog } from '@/hooks/use-dialog';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogInternalContent,
- DialogTitle,
-} from '@/components/dialog/dialog';
-import { Button } from '@/components/button/button';
-import type { BaseDialogProps } from '../common/base-dialog-props';
-import { useTranslation } from 'react-i18next';
-import { Editor } from '@/components/code-snippet/code-snippet';
-import { useTheme } from '@/hooks/use-theme';
-import { AlertCircle } from 'lucide-react';
-import {
- importDBMLToDiagram,
- sanitizeDBML,
- preprocessDBML,
-} from '@/lib/dbml/dbml-import/dbml-import';
-import { useChartDB } from '@/hooks/use-chartdb';
-import { Parser } from '@dbml/core';
-import { useCanvas } from '@/hooks/use-canvas';
-import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
-import type { DBTable } from '@/lib/domain/db-table';
-import { useToast } from '@/components/toast/use-toast';
-import { Spinner } from '@/components/spinner/spinner';
-import { debounce } from '@/lib/utils';
-import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
-import {
- clearErrorHighlight,
- highlightErrorLine,
-} from '@/components/code-snippet/dbml/utils';
-
-export interface ImportDBMLDialogProps extends BaseDialogProps {
- withCreateEmptyDiagram?: boolean;
-}
-
-export const ImportDBMLDialog: React.FC = ({
- dialog,
- withCreateEmptyDiagram,
-}) => {
- const { t } = useTranslation();
- const initialDBML = `// Use DBML to define your database structure
-// Simple Blog System with Comments Example
-
-Table users {
- id integer [primary key]
- name varchar
- email varchar
-}
-
-Table posts {
- id integer [primary key]
- title varchar
- content text
- user_id integer
- created_at timestamp
-}
-
-Table comments {
- id integer [primary key]
- content text
- post_id integer
- user_id integer
- created_at timestamp
-}
-
-// Relationships
-Ref: posts.user_id > users.id // Each post belongs to one user
-Ref: comments.post_id > posts.id // Each comment belongs to one post
-Ref: comments.user_id > users.id // Each comment is written by one user`;
-
- const [dbmlContent, setDBMLContent] = useState(initialDBML);
- const { closeImportDBMLDialog } = useDialog();
- const [errorMessage, setErrorMessage] = useState();
- const { effectiveTheme } = useTheme();
- const { toast } = useToast();
- const {
- addTables,
- addRelationships,
- tables,
- relationships,
- removeTables,
- removeRelationships,
- } = useChartDB();
- const { reorderTables } = useCanvas();
- const [reorder, setReorder] = useState(false);
- const editorRef = useRef();
- const decorationsCollection =
- useRef();
-
- const handleEditorDidMount = (
- editor: monaco.editor.IStandaloneCodeEditor
- ) => {
- editorRef.current = editor;
- decorationsCollection.current = editor.createDecorationsCollection();
- };
-
- useEffect(() => {
- if (reorder) {
- reorderTables({
- updateHistory: false,
- });
- setReorder(false);
- }
- }, [reorder, reorderTables]);
-
- const clearDecorations = useCallback(() => {
- clearErrorHighlight(decorationsCollection.current);
- }, []);
-
- const validateDBML = useCallback(
- async (content: string) => {
- // Clear previous errors
- setErrorMessage(undefined);
- clearDecorations();
-
- if (!content.trim()) return;
-
- try {
- const preprocessedContent = preprocessDBML(content);
- const sanitizedContent = sanitizeDBML(preprocessedContent);
- const parser = new Parser();
- parser.parse(sanitizedContent, 'dbmlv2');
- } catch (e) {
- const parsedError = parseDBMLError(e);
- if (parsedError) {
- setErrorMessage(
- t('import_dbml_dialog.error.description') +
- ` (1 error found - in line ${parsedError.line})`
- );
- highlightErrorLine({
- error: parsedError,
- model: editorRef.current?.getModel(),
- editorDecorationsCollection:
- decorationsCollection.current,
- });
- } else {
- setErrorMessage(
- e instanceof Error ? e.message : JSON.stringify(e)
- );
- }
- }
- },
- [clearDecorations, t]
- );
-
- const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
-
- // Set up debounced validation
- useEffect(() => {
- debouncedValidateRef.current = debounce((value: string) => {
- validateDBML(value);
- }, 500);
-
- return () => {
- debouncedValidateRef.current = null;
- };
- }, [validateDBML]);
-
- // Trigger validation when content changes
- useEffect(() => {
- if (debouncedValidateRef.current) {
- debouncedValidateRef.current(dbmlContent);
- }
- }, [dbmlContent]);
-
- useEffect(() => {
- if (!dialog.open) {
- setErrorMessage(undefined);
- clearDecorations();
- setDBMLContent(initialDBML);
- }
- }, [dialog.open, initialDBML, clearDecorations]);
-
- const handleImport = useCallback(async () => {
- if (!dbmlContent.trim() || errorMessage) return;
-
- try {
- const importedDiagram = await importDBMLToDiagram(dbmlContent);
- const tableIdsToRemove = tables
- .filter((table) =>
- importedDiagram.tables?.some(
- (t: DBTable) =>
- t.name === table.name && t.schema === table.schema
- )
- )
- .map((table) => table.id);
- // Find relationships that need to be removed
- const relationshipIdsToRemove = relationships
- .filter((relationship) => {
- const sourceTable = tables.find(
- (table: DBTable) =>
- table.id === relationship.sourceTableId
- );
- const targetTable = tables.find(
- (table: DBTable) =>
- table.id === relationship.targetTableId
- );
- if (!sourceTable || !targetTable) return true;
- const replacementSourceTable = importedDiagram.tables?.find(
- (table: DBTable) =>
- table.name === sourceTable.name &&
- table.schema === sourceTable.schema
- );
- const replacementTargetTable = importedDiagram.tables?.find(
- (table: DBTable) =>
- table.name === targetTable.name &&
- table.schema === targetTable.schema
- );
- return replacementSourceTable || replacementTargetTable;
- })
- .map((relationship) => relationship.id);
-
- // Remove existing items
- await Promise.all([
- removeTables(tableIdsToRemove, { updateHistory: false }),
- removeRelationships(relationshipIdsToRemove, {
- updateHistory: false,
- }),
- ]);
-
- // Add new items
- await Promise.all([
- addTables(importedDiagram.tables ?? [], {
- updateHistory: false,
- }),
- addRelationships(importedDiagram.relationships ?? [], {
- updateHistory: false,
- }),
- ]);
- setReorder(true);
- closeImportDBMLDialog();
- } catch (e) {
- toast({
- title: t('import_dbml_dialog.error.title'),
- variant: 'destructive',
- description: (
- <>
- {t('import_dbml_dialog.error.description')}
- {e instanceof Error ? e.message : JSON.stringify(e)}
- >
- ),
- });
- }
- }, [
- dbmlContent,
- closeImportDBMLDialog,
- tables,
- relationships,
- removeTables,
- removeRelationships,
- addTables,
- addRelationships,
- errorMessage,
- toast,
- setReorder,
- t,
- ]);
-
- return (
- {
- if (!open) {
- closeImportDBMLDialog();
- }
- }}
- >
-
-
-
- {withCreateEmptyDiagram
- ? t('import_dbml_dialog.example_title')
- : t('import_dbml_dialog.title')}
-
-
- {t('import_dbml_dialog.description')}
-
-
-
- }>
- setDBMLContent(value || '')}
- language="dbml"
- onMount={handleEditorDidMount}
- theme={
- effectiveTheme === 'dark'
- ? 'dbml-dark'
- : 'dbml-light'
- }
- beforeMount={setupDBMLLanguage}
- options={{
- minimap: { enabled: false },
- scrollBeyondLastLine: false,
- automaticLayout: true,
- glyphMargin: true,
- lineNumbers: 'on',
- scrollbar: {
- vertical: 'visible',
- horizontal: 'visible',
- },
- }}
- className="size-full"
- />
-
-
-
-
-
-
-
- {withCreateEmptyDiagram
- ? t('import_dbml_dialog.skip_and_empty')
- : t('import_dbml_dialog.cancel')}
-
-
- {errorMessage ? (
-
-
-
-
- {errorMessage ||
- t(
- 'import_dbml_dialog.error.description'
- )}
-
-
- ) : null}
-
-
- {withCreateEmptyDiagram
- ? t('import_dbml_dialog.show_example')
- : t('import_dbml_dialog.import')}
-
-
-
-
-
- );
-};
diff --git a/src/dialogs/open-diagram-dialog/open-diagram-dialog.tsx b/src/dialogs/open-diagram-dialog/open-diagram-dialog.tsx
index ba42f749f..1e4525d82 100644
--- a/src/dialogs/open-diagram-dialog/open-diagram-dialog.tsx
+++ b/src/dialogs/open-diagram-dialog/open-diagram-dialog.tsx
@@ -37,7 +37,7 @@ export const OpenDiagramDialog: React.FC = ({
dialog,
canClose = true,
}) => {
- const { closeOpenDiagramDialog } = useDialog();
+ const { closeOpenDiagramDialog, openCreateDiagramDialog } = useDialog();
const { t } = useTranslation();
const { updateConfig } = useConfig();
const navigate = useNavigate();
@@ -254,15 +254,29 @@ export const OpenDiagramDialog: React.FC = ({
) : (
)}
-
+
openDiagram(selectedDiagramId ?? '')}
+ type="button"
+ variant="secondary"
+ onClick={() => {
+ closeOpenDiagramDialog();
+ openCreateDiagramDialog();
+ }}
>
- {t('open_diagram_dialog.open')}
+ {t('open_diagram_dialog.new_database')}
-
+
+
+ openDiagram(selectedDiagramId ?? '')
+ }
+ >
+ {t('open_diagram_dialog.open')}
+
+
+
diff --git a/src/globals.css b/src/globals.css
index 9e050f615..78eb02352 100644
--- a/src/globals.css
+++ b/src/globals.css
@@ -81,10 +81,13 @@
* {
@apply border-border;
}
+ html,
body {
- @apply bg-background text-foreground;
overscroll-behavior-x: none;
}
+ body {
+ @apply bg-background text-foreground;
+ }
.text-editable {
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
diff --git a/src/hooks/use-clean-mode.ts b/src/hooks/use-clean-mode.ts
new file mode 100644
index 000000000..e28d28b9e
--- /dev/null
+++ b/src/hooks/use-clean-mode.ts
@@ -0,0 +1,11 @@
+import { useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
+
+export const useCleanMode = (): boolean => {
+ const { search } = useLocation();
+
+ return useMemo(() => {
+ const params = new URLSearchParams(search);
+ return params.get('clean') === 'true';
+ }, [search]);
+};
diff --git a/src/hooks/use-clean-table-id.ts b/src/hooks/use-clean-table-id.ts
new file mode 100644
index 000000000..f6efc4540
--- /dev/null
+++ b/src/hooks/use-clean-table-id.ts
@@ -0,0 +1,11 @@
+import { useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
+
+export const useCleanTableId = (): string | undefined => {
+ const { search } = useLocation();
+
+ return useMemo(() => {
+ const params = new URLSearchParams(search);
+ return params.get('tableId') ?? undefined;
+ }, [search]);
+};
diff --git a/src/hooks/use-focus-on.ts b/src/hooks/use-focus-on.ts
index 17ca76c7f..a00f061d2 100644
--- a/src/hooks/use-focus-on.ts
+++ b/src/hooks/use-focus-on.ts
@@ -88,6 +88,44 @@ export const useFocusOn = () => {
[fitView, setNodes, hideSidePanel, isDesktop]
);
+ const focusOnNote = useCallback(
+ (noteId: string, options: FocusOptions = {}) => {
+ const { select = true } = options;
+
+ if (select) {
+ setNodes((nodes) =>
+ nodes.map((node) =>
+ node.id === noteId
+ ? {
+ ...node,
+ selected: true,
+ }
+ : {
+ ...node,
+ selected: false,
+ }
+ )
+ );
+ }
+
+ fitView({
+ duration: 500,
+ maxZoom: 1,
+ minZoom: 1,
+ nodes: [
+ {
+ id: noteId,
+ },
+ ],
+ });
+
+ if (!isDesktop) {
+ hideSidePanel();
+ }
+ },
+ [fitView, setNodes, hideSidePanel, isDesktop]
+ );
+
const focusOnRelationship = useCallback(
(
relationshipId: string,
@@ -137,6 +175,7 @@ export const useFocusOn = () => {
return {
focusOnArea,
focusOnTable,
+ focusOnNote,
focusOnRelationship,
};
};
diff --git a/src/hooks/use-update-table-field.ts b/src/hooks/use-update-table-field.ts
new file mode 100644
index 000000000..e38306a1c
--- /dev/null
+++ b/src/hooks/use-update-table-field.ts
@@ -0,0 +1,398 @@
+import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
+import { useChartDB } from './use-chartdb';
+import { useDebounce } from './use-debounce-v2';
+import type { DatabaseType, DBField, DBTable } from '@/lib/domain';
+import type {
+ SelectBoxOption,
+ SelectBoxProps,
+} from '@/components/select-box/select-box';
+import {
+ dataTypeDataToDataType,
+ sortedDataTypeMap,
+ supportsArrayDataType,
+ autoIncrementAlwaysOn,
+ requiresNotNull,
+} from '@/lib/data/data-types/data-types';
+import { generateDBFieldSuffix } from '@/lib/domain/db-field';
+import type { DataTypeData } from '@/lib/data/data-types/data-types';
+
+const generateFieldRegexPatterns = (
+ dataType: DataTypeData,
+ databaseType: DatabaseType
+): {
+ regex?: string;
+ extractRegex?: RegExp;
+} => {
+ const typeName = dataType.name;
+ const supportsArrays = supportsArrayDataType(dataType.id, databaseType);
+ const arrayPattern = supportsArrays ? '(\\[\\])?' : '';
+
+ if (!dataType.fieldAttributes) {
+ // For types without field attributes, support plain type + optional array notation
+ return {
+ regex: `^${typeName}${arrayPattern}$`,
+ extractRegex: new RegExp(`^${typeName}${arrayPattern}$`),
+ };
+ }
+
+ const fieldAttributes = dataType.fieldAttributes;
+
+ if (fieldAttributes.hasCharMaxLength) {
+ if (fieldAttributes.hasCharMaxLengthOption) {
+ return {
+ regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)${arrayPattern}$`,
+ extractRegex: supportsArrays
+ ? /\((\d+|max)\)(\[\])?/i
+ : /\((\d+|max)\)/i,
+ };
+ }
+ return {
+ regex: `^${typeName}\\(\\d+\\)${arrayPattern}$`,
+ extractRegex: supportsArrays ? /\((\d+)\)(\[\])?/ : /\((\d+)\)/,
+ };
+ }
+
+ if (fieldAttributes.precision && fieldAttributes.scale) {
+ return {
+ regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)${arrayPattern}$`,
+ extractRegex: new RegExp(
+ `${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)${arrayPattern}`
+ ),
+ };
+ }
+
+ if (fieldAttributes.precision) {
+ return {
+ regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)${arrayPattern}$`,
+ extractRegex: supportsArrays ? /\((\d+)\)(\[\])?/ : /\((\d+)\)/,
+ };
+ }
+
+ return { regex: undefined, extractRegex: undefined };
+};
+
+export const useUpdateTableField = (
+ table: DBTable,
+ field: DBField,
+ customUpdateField?: (attrs: Partial) => void
+) => {
+ const {
+ databaseType,
+ customTypes,
+ updateField: chartDBUpdateField,
+ removeField: chartDBRemoveField,
+ } = useChartDB();
+
+ // Local state for responsive UI
+ const [localFieldName, setLocalFieldName] = useState(field.name);
+ const [localNullable, setLocalNullable] = useState(field.nullable);
+ const [localPrimaryKey, setLocalPrimaryKey] = useState(field.primaryKey);
+
+ const lastFieldNameRef = useRef(field.name);
+
+ useEffect(() => {
+ if (localFieldName === lastFieldNameRef.current) {
+ lastFieldNameRef.current = field.name;
+ setLocalFieldName(field.name);
+ }
+ }, [field.name, localFieldName]);
+
+ // Update local state when field properties change externally
+ useEffect(() => {
+ setLocalNullable(field.nullable);
+ setLocalPrimaryKey(field.primaryKey);
+ }, [field.nullable, field.primaryKey]);
+
+ // Auto-correct: Primary key fields must be NOT NULL
+ // This fixes any existing data where PK columns were incorrectly set as nullable
+ useEffect(() => {
+ if (field.primaryKey && field.nullable) {
+ chartDBUpdateField(table.id, field.id, { nullable: false });
+ }
+ }, [
+ field.primaryKey,
+ field.nullable,
+ table.id,
+ field.id,
+ chartDBUpdateField,
+ ]);
+
+ // Use custom updateField if provided, otherwise use the chartDB one
+ const updateField = useMemo(
+ () =>
+ customUpdateField
+ ? (
+ _tableId: string,
+ _fieldId: string,
+ attrs: Partial
+ ) => customUpdateField(attrs)
+ : chartDBUpdateField,
+ [customUpdateField, chartDBUpdateField]
+ );
+
+ // Calculate primary key fields for validation
+ const primaryKeyFields = useMemo(() => {
+ return table.fields.filter((f) => f.primaryKey);
+ }, [table.fields]);
+
+ const primaryKeyCount = useMemo(
+ () => primaryKeyFields.length,
+ [primaryKeyFields.length]
+ );
+
+ // Generate data type options for select box
+ const dataFieldOptions = useMemo(() => {
+ const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
+ databaseType
+ ].map((type) => {
+ const regexPatterns = generateFieldRegexPatterns(
+ type,
+ databaseType
+ );
+
+ return {
+ label: type.name,
+ value: type.id,
+ regex: regexPatterns.regex,
+ extractRegex: regexPatterns.extractRegex,
+ group: customTypes?.length ? 'Standard Types' : undefined,
+ };
+ });
+
+ if (!customTypes?.length) {
+ return standardTypes;
+ }
+
+ // Add custom types as options
+ const customTypeOptions: SelectBoxOption[] = customTypes.map(
+ (type) => ({
+ label: type.name,
+ value: type.name,
+ description:
+ type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
+ group: 'Custom Types',
+ })
+ );
+
+ return [...standardTypes, ...customTypeOptions];
+ }, [databaseType, customTypes]);
+
+ // Handle data type change
+ const handleDataTypeChange = useCallback<
+ NonNullable
+ >(
+ (value, regexMatches) => {
+ const dataType = sortedDataTypeMap[databaseType].find(
+ (v) => v.id === value
+ ) ?? {
+ id: value as string,
+ name: value as string,
+ };
+
+ let characterMaximumLength: string | undefined = undefined;
+ let precision: number | undefined = undefined;
+ let scale: number | undefined = undefined;
+ let isArray: boolean | undefined = undefined;
+
+ if (regexMatches?.length) {
+ // Check if the last captured group is the array indicator []
+ const lastMatch = regexMatches[regexMatches.length - 1];
+ const hasArrayIndicator = lastMatch === '[]';
+
+ if (dataType?.fieldAttributes?.hasCharMaxLength) {
+ characterMaximumLength = regexMatches[1]?.toLowerCase();
+ } else if (
+ dataType?.fieldAttributes?.precision &&
+ dataType?.fieldAttributes?.scale
+ ) {
+ precision = parseInt(regexMatches[1]);
+ scale = regexMatches[2]
+ ? parseInt(regexMatches[2])
+ : undefined;
+ } else if (dataType?.fieldAttributes?.precision) {
+ precision = parseInt(regexMatches[1]);
+ }
+
+ // Set isArray if the array indicator was found and the type supports arrays
+ if (hasArrayIndicator) {
+ const typeId = value as string;
+ if (supportsArrayDataType(typeId, databaseType)) {
+ isArray = true;
+ }
+ } else {
+ // Explicitly set to false/undefined if no array indicator
+ isArray = undefined;
+ }
+ } else {
+ if (
+ dataType?.fieldAttributes?.hasCharMaxLength &&
+ field.characterMaximumLength
+ ) {
+ characterMaximumLength = field.characterMaximumLength;
+ }
+
+ if (dataType?.fieldAttributes?.precision && field.precision) {
+ precision = field.precision;
+ }
+
+ if (dataType?.fieldAttributes?.scale && field.scale) {
+ scale = field.scale;
+ }
+ }
+
+ const newTypeName = dataType?.name ?? (value as string);
+ const typeRequiresNotNull = requiresNotNull(newTypeName);
+ const shouldForceIncrement = autoIncrementAlwaysOn(newTypeName);
+
+ updateField(table.id, field.id, {
+ characterMaximumLength,
+ precision,
+ scale,
+ isArray,
+ ...(typeRequiresNotNull ? { nullable: false } : {}),
+ increment: shouldForceIncrement ? true : undefined,
+ default: undefined,
+ type: dataTypeDataToDataType(
+ dataType ?? {
+ id: value as string,
+ name: value as string,
+ }
+ ),
+ });
+ },
+ [
+ updateField,
+ databaseType,
+ field.characterMaximumLength,
+ field.precision,
+ field.scale,
+ field.id,
+ table.id,
+ ]
+ );
+
+ // Debounced update for field name
+ const debouncedNameUpdate = useDebounce(
+ useCallback(
+ (value: string) => {
+ if (value.trim() !== field.name) {
+ updateField(table.id, field.id, { name: value });
+ }
+ },
+ [updateField, table.id, field.id, field.name]
+ ),
+ 300 // 300ms debounce for text input
+ );
+
+ // Debounced update for nullable toggle
+ const debouncedNullableUpdate = useDebounce(
+ useCallback(
+ (value: boolean) => {
+ const updates: Partial = { nullable: value };
+
+ // If setting to nullable, clear increment (auto-increment requires NOT NULL)
+ if (value && field.increment) {
+ updates.increment = undefined;
+ }
+
+ updateField(table.id, field.id, updates);
+ },
+ [updateField, table.id, field.id, field.increment]
+ ),
+ 100 // 100ms debounce for toggle
+ );
+
+ // Debounced update for primary key toggle
+ const debouncedPrimaryKeyUpdate = useDebounce(
+ useCallback(
+ (value: boolean, primaryKeyCount: number) => {
+ if (value) {
+ // When setting as primary key
+ const updates: Partial = {
+ primaryKey: true,
+ nullable: false, // Primary keys must be NOT NULL
+ };
+ // Only auto-set unique if this will be the only primary key
+ if (primaryKeyCount === 0) {
+ updates.unique = true;
+ }
+ updateField(table.id, field.id, updates);
+ } else {
+ // When removing primary key
+ updateField(table.id, field.id, {
+ primaryKey: false,
+ });
+ }
+ },
+ [updateField, table.id, field.id]
+ ),
+ 100 // 100ms debounce for toggle
+ );
+
+ // Handle primary key toggle with optimistic update
+ const handlePrimaryKeyToggle = useCallback(
+ (value: boolean) => {
+ setLocalPrimaryKey(value);
+ // Primary keys must be NOT NULL - update local state immediately for responsive UI
+ if (value) {
+ setLocalNullable(false);
+ }
+ debouncedPrimaryKeyUpdate(value, primaryKeyCount);
+ },
+ [primaryKeyCount, debouncedPrimaryKeyUpdate]
+ );
+
+ // Handle nullable toggle with optimistic update
+ const handleNullableToggle = useCallback(
+ (value: boolean) => {
+ setLocalNullable(value);
+ debouncedNullableUpdate(value);
+ },
+ [debouncedNullableUpdate]
+ );
+
+ // Handle name change with optimistic update
+ const handleNameChange = useCallback(
+ (value: string) => {
+ setLocalFieldName(value);
+ debouncedNameUpdate(value);
+ },
+ [debouncedNameUpdate]
+ );
+
+ // Utility function to generate field suffix for display
+ const generateFieldSuffix = useCallback(
+ (typeId?: string) => {
+ return generateDBFieldSuffix(
+ {
+ ...field,
+ isArray: field.isArray && typeId === field.type.id,
+ },
+ {
+ databaseType,
+ forceExtended: true,
+ typeId,
+ }
+ );
+ },
+ [field, databaseType]
+ );
+
+ const removeField = useCallback(() => {
+ chartDBRemoveField(table.id, field.id);
+ }, [chartDBRemoveField, table.id, field.id]);
+
+ return {
+ dataFieldOptions,
+ handleDataTypeChange,
+ handlePrimaryKeyToggle,
+ handleNullableToggle,
+ handleNameChange,
+ generateFieldSuffix,
+ primaryKeyCount,
+ fieldName: localFieldName,
+ nullable: localNullable,
+ primaryKey: localPrimaryKey,
+ removeField,
+ };
+};
diff --git a/src/hooks/use-update-table.ts b/src/hooks/use-update-table.ts
new file mode 100644
index 000000000..e160c0c0f
--- /dev/null
+++ b/src/hooks/use-update-table.ts
@@ -0,0 +1,42 @@
+import { useCallback, useState, useEffect } from 'react';
+import { useChartDB } from './use-chartdb';
+import { useDebounce } from './use-debounce-v2';
+import type { DBTable } from '@/lib/domain';
+
+// Hook for updating table properties with debouncing for performance
+export const useUpdateTable = (table: DBTable) => {
+ const { updateTable: chartDBUpdateTable } = useChartDB();
+ const [localTableName, setLocalTableName] = useState(table.name);
+
+ // Debounced update function
+ const debouncedUpdate = useDebounce(
+ useCallback(
+ (value: string) => {
+ if (value.trim() && value.trim() !== table.name) {
+ chartDBUpdateTable(table.id, { name: value.trim() });
+ }
+ },
+ [chartDBUpdateTable, table.id, table.name]
+ ),
+ 1000 // 1000ms debounce
+ );
+
+ // Update local state immediately for responsive UI
+ const handleTableNameChange = useCallback(
+ (value: string) => {
+ setLocalTableName(value);
+ debouncedUpdate(value);
+ },
+ [debouncedUpdate]
+ );
+
+ // Update local state when table name changes externally
+ useEffect(() => {
+ setLocalTableName(table.name);
+ }, [table.name]);
+
+ return {
+ tableName: localTableName,
+ handleTableNameChange,
+ };
+};
diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts
index ffe995564..7e25ce917 100644
--- a/src/i18n/locales/ar.ts
+++ b/src/i18n/locales/ar.ts
@@ -4,18 +4,18 @@ export const ar: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'جديد',
- browse: 'تصفح',
+ browse: 'فتح',
tables: 'الجداول',
refs: 'المراجع',
- areas: 'المناطق',
dependencies: 'التبعيات',
custom_types: 'الأنواع المخصصة',
+ visuals: 'مرئيات',
},
menu: {
actions: {
actions: 'الإجراءات',
new: 'جديد...',
- browse: 'تصفح...',
+ browse: 'جميع قواعد البيانات...',
save: 'حفظ',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
@@ -44,6 +44,8 @@ export const ar: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'النسخ الاحتياطي',
@@ -128,16 +130,20 @@ export const ar: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'جميع الجداول مخفية',
+ show_all: 'عرض الكل',
table: {
fields: 'الحقول',
nullable: 'يمكن ان يكون فارغاً؟',
primary_key: 'المفتاح الأساسي',
indexes: 'الفهارس',
+ check_constraints: 'قيود التحقق',
comments: 'تعليقات',
no_comments: 'لا توجد تعليقات',
add_field: 'إضافة حقل',
add_index: 'إضافة فهرس',
+ add_check: 'إضافة تحقق',
index_select_fields: 'حدد الحقول',
no_types_found: 'لا يوجد أنواع',
field_name: 'الإسم',
@@ -163,6 +169,11 @@ export const ar: LanguageTranslation = {
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس',
},
+ check_constraint_actions: {
+ title: 'قيد التحقق',
+ expression: 'التعبير',
+ delete: 'حذف قيد التحقق',
+ },
table_actions: {
title: 'إجراءات الجدول',
change_schema: 'تغيير المخطط',
@@ -190,6 +201,7 @@ export const ar: LanguageTranslation = {
foreign: 'الجدول المرتبط',
cardinality: 'الكاردينالية',
delete_relationship: 'حذف',
+ switch_tables: 'تبديل الجداول',
relationship_actions: {
title: 'إجراءات',
delete_relationship: 'حذف',
@@ -211,55 +223,81 @@ export const ar: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'المناطق',
+ add_area: 'إضافة منطقة',
+ filter: 'تصفية',
+ clear: 'مسح التصفية',
+ no_results: 'لم يتم العثور على مناطق مطابقة للتصفية.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'إجراءات المنطقة',
+ edit_name: 'تحرير الاسم',
+ delete_area: 'حذف المنطقة',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'لا توجد مناطق',
+ description: 'أنشئ منطقة للبدء',
+ },
+ },
+
+ visuals_section: {
+ visuals: 'مرئيات',
+ tabs: {
+ areas: 'المناطق',
+ notes: 'ملاحظات',
+ },
+ },
+
+ notes_section: {
+ filter: 'تصفية',
+ add_note: 'إضافة ملاحظة',
+ no_results: 'لم يتم العثور على ملاحظات',
+ clear: 'مسح التصفية',
+ empty_state: {
+ title: 'لا توجد ملاحظات',
+ description: 'أنشئ ملاحظة لإضافة تعليقات نصية على اللوحة',
+ },
+ note: {
+ empty_note: 'ملاحظة فارغة',
+ note_actions: {
+ title: 'إجراءات الملاحظة',
+ edit_content: 'تحرير المحتوى',
+ delete_note: 'حذف الملاحظة',
+ },
},
},
- // TODO: Translate
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'الأنواع المخصصة',
+ filter: 'تصفية',
+ clear: 'مسح التصفية',
+ no_results: 'لم يتم العثور على أنواع مخصصة مطابقة للتصفية.',
+ new_type: 'نوع جديد',
empty_state: {
- title: 'No custom types',
+ title: 'لا توجد أنواع مخصصة',
description:
- 'Custom types will appear here when they are available in your database',
+ 'ستظهر الأنواع المخصصة هنا عندما تكون متاحة في قاعدة البيانات الخاصة بك',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'النوع',
+ enum_values: 'قيم التعداد',
+ composite_fields: 'الحقول',
+ no_fields: 'لم يتم تحديد حقول',
no_values: 'لم يتم تحديد قيم التعداد',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'اسم الحقل',
+ field_type_placeholder: 'اختر النوع',
+ add_field: 'إضافة حقل',
+ no_fields_tooltip: 'لم يتم تحديد حقول لهذا النوع المخصص',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'إجراءات',
+ highlight_fields: 'تمييز الحقول',
+ delete_custom_type: 'حذف',
+ clear_field_highlight: 'إزالة التمييز',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'حذف النوع',
},
},
},
@@ -273,8 +311,7 @@ export const ar: LanguageTranslation = {
redo: 'إعادة',
reorder_diagram: 'ترتيب تلقائي للرسم البياني',
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'تصفية الجداول',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
@@ -308,7 +345,7 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
import_from_file: 'استيراد من ملف',
back: 'رجوع',
- empty_diagram: 'مخطط فارغ',
+ empty_diagram: 'قاعدة بيانات فارغة',
continue: 'متابعة',
import: 'استيراد',
},
@@ -324,6 +361,7 @@ export const ar: LanguageTranslation = {
},
cancel: 'إلغاء',
open: 'فتح',
+ new_database: 'قاعدة بيانات جديدة',
diagram_actions: {
open: 'فتح',
@@ -387,10 +425,9 @@ export const ar: LanguageTranslation = {
export_image_dialog: {
title: 'تصدير الصورة',
description: ':اختر عامل المقياس للتصدير',
- scale_1x: '1x عادي',
- scale_2x: '2x (موصى به)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (جودة منخفضة)',
+ scale_2x: '2x (جودة عادية)',
+ scale_4x: '4x (أفضل جودة)',
cancel: 'إلغاء',
export: 'تصدير',
// TODO: Translate
@@ -400,7 +437,12 @@ export const ar: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'اختر مخططاً',
description:
@@ -478,7 +520,8 @@ export const ar: LanguageTranslation = {
new_view: 'عرض جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'منطقة جديدة',
+ new_note: 'ملاحظة جديدة',
},
table_node_context_menu: {
@@ -488,6 +531,22 @@ export const ar: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'جميع الجداول مخفية',
+ show_all_tables: 'عرض الكل',
+ },
+
+ canvas_filter: {
+ title: 'تصفية الجداول',
+ search_placeholder: 'البحث في الجداول...',
+ group_by_schema: 'تجميع حسب المخطط',
+ group_by_area: 'تجميع حسب المنطقة',
+ no_tables_found: 'لم يتم العثور على جداول',
+ empty_diagram_description: 'أنشئ جدولاً للبدء',
+ no_tables_description: 'جرب تعديل البحث أو التصفية',
+ clear_filter: 'مسح التصفية',
+ },
+
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',
tool_tips: {
diff --git a/src/i18n/locales/bn.ts b/src/i18n/locales/bn.ts
index 56de55e3d..6cef9e6b9 100644
--- a/src/i18n/locales/bn.ts
+++ b/src/i18n/locales/bn.ts
@@ -4,18 +4,18 @@ export const bn: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'নতুন',
- browse: 'ব্রাউজ',
+ browse: 'খুলুন',
tables: 'টেবিল',
refs: 'রেফস',
- areas: 'এলাকা',
dependencies: 'নির্ভরতা',
custom_types: 'কাস্টম টাইপ',
+ visuals: 'ভিজ্যুয়াল',
},
menu: {
actions: {
actions: 'কার্য',
new: 'নতুন...',
- browse: 'ব্রাউজ করুন...',
+ browse: 'সমস্ত ডেটাবেস...',
save: 'সংরক্ষণ করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
@@ -44,6 +44,8 @@ export const bn: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
@@ -129,16 +131,20 @@ export const bn: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'সব টেবিল লুকানো আছে',
+ show_all: 'সব দেখান',
table: {
fields: 'ফিল্ড',
nullable: 'নালযোগ্য?',
primary_key: 'প্রাথমিক কী',
indexes: 'ইনডেক্স',
+ check_constraints: 'চেক সীমাবদ্ধতা',
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
add_field: 'ফিল্ড যোগ করুন',
add_index: 'ইনডেক্স যোগ করুন',
+ add_check: 'চেক যোগ করুন',
index_select_fields: 'ফিল্ড নির্বাচন করুন',
no_types_found: 'কোনো ধরন পাওয়া যায়নি',
field_name: 'নাম',
@@ -165,6 +171,11 @@ export const bn: LanguageTranslation = {
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন',
},
+ check_constraint_actions: {
+ title: 'চেক সীমাবদ্ধতা',
+ expression: 'এক্সপ্রেশন',
+ delete: 'সীমাবদ্ধতা মুছুন',
+ },
table_actions: {
title: 'টেবিল কর্ম',
change_schema: 'স্কিমা পরিবর্তন করুন',
@@ -189,9 +200,10 @@ export const bn: LanguageTranslation = {
relationship: {
relationship: 'সম্পর্ক',
primary: 'প্রাথমিক টেবিল',
- foreign: 'রেফারেন্স করা টেবিল',
+ foreign: 'সম্পর্কিত টেবিল',
cardinality: 'কার্ডিনালিটি',
delete_relationship: 'মুছুন',
+ switch_tables: 'টেবিল বদল করুন',
relationship_actions: {
title: 'কর্ম',
delete_relationship: 'মুছুন',
@@ -213,54 +225,85 @@ export const bn: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'এলাকা',
+ add_area: 'এলাকা যোগ করুন',
+ filter: 'ফিল্টার',
+ clear: 'ফিল্টার সাফ করুন',
+ no_results:
+ 'আপনার ফিল্টারের সাথে মেলে এমন কোনো এলাকা পাওয়া যায়নি।',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'এলাকা ক্রিয়া',
+ edit_name: 'নাম সম্পাদনা করুন',
+ delete_area: 'এলাকা মুছুন',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'কোনো এলাকা নেই',
+ description: 'শুরু করতে একটি এলাকা তৈরি করুন',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'ভিজ্যুয়াল',
+ tabs: {
+ areas: 'এলাকা',
+ notes: 'নোট',
+ },
+ },
+
+ notes_section: {
+ filter: 'ফিল্টার',
+ add_note: 'নোট যোগ করুন',
+ no_results: 'কোনো নোট পাওয়া যায়নি',
+ clear: 'ফিল্টার সাফ করুন',
+ empty_state: {
+ title: 'কোনো নোট নেই',
+ description:
+ 'ক্যানভাসে টেক্সট টীকা যোগ করতে একটি নোট তৈরি করুন',
+ },
+ note: {
+ empty_note: 'খালি নোট',
+ note_actions: {
+ title: 'নোট ক্রিয়া',
+ edit_content: 'বিষয়বস্তু সম্পাদনা',
+ delete_note: 'নোট মুছুন',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'কাস্টম টাইপ',
+ filter: 'ফিল্টার',
+ clear: 'ফিল্টার সাফ করুন',
+ no_results:
+ 'আপনার ফিল্টারের সাথে মেলে এমন কোনো কাস্টম টাইপ পাওয়া যায়নি।',
+ new_type: 'নতুন টাইপ',
empty_state: {
- title: 'No custom types',
+ title: 'কোনো কাস্টম টাইপ নেই',
description:
- 'Custom types will appear here when they are available in your database',
+ 'আপনার ডাটাবেসে উপলব্ধ হলে কাস্টম টাইপ এখানে দেখা যাবে',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'ধরন',
+ enum_values: 'Enum মান',
+ composite_fields: 'ফিল্ড',
+ no_fields: 'কোনো ফিল্ড সংজ্ঞায়িত নেই',
no_values: 'কোন enum মান সংজ্ঞায়িত নেই',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'ফিল্ডের নাম',
+ field_type_placeholder: 'টাইপ নির্বাচন করুন',
+ add_field: 'ফিল্ড যোগ করুন',
+ no_fields_tooltip:
+ 'এই কাস্টম টাইপের জন্য কোনো ফিল্ড সংজ্ঞায়িত নেই',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'ক্রিয়া',
+ highlight_fields: 'ফিল্ড হাইলাইট করুন',
+ delete_custom_type: 'মুছুন',
+ clear_field_highlight: 'হাইলাইট সরান',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'টাইপ মুছুন',
},
},
},
@@ -279,7 +322,7 @@ export const bn: LanguageTranslation = {
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
- filter: 'Filter Tables',
+ filter: 'টেবিল ফিল্টার করুন',
},
new_diagram_dialog: {
@@ -310,7 +353,7 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
back: 'ফিরে যান',
import_from_file: 'ফাইল থেকে আমদানি করুন',
- empty_diagram: 'ফাঁকা চিত্র',
+ empty_diagram: 'খালি ডাটাবেস',
continue: 'চালিয়ে যান',
import: 'আমদানি করুন',
},
@@ -326,6 +369,7 @@ export const bn: LanguageTranslation = {
},
cancel: 'বাতিল করুন',
open: 'খুলুন',
+ new_database: 'নতুন ডেটাবেস',
diagram_actions: {
open: 'খুলুন',
@@ -389,10 +433,9 @@ export const bn: LanguageTranslation = {
export_image_dialog: {
title: 'চিত্র রপ্তানি করুন',
description: 'রপ্তানির জন্য স্কেল ফ্যাক্টর নির্বাচন করুন:',
- scale_1x: '1x স্বাভাবিক',
- scale_2x: '2x (প্রস্তাবিত)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (নিম্ন মান)',
+ scale_2x: '2x (সাধারণ মান)',
+ scale_4x: '4x (সেরা মান)',
cancel: 'বাতিল করুন',
export: 'রপ্তানি করুন',
// TODO: Translate
@@ -402,7 +445,12 @@ export const bn: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'স্কিমা নির্বাচন করুন',
description:
@@ -483,7 +531,8 @@ export const bn: LanguageTranslation = {
new_view: 'নতুন ভিউ',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'নতুন এলাকা',
+ new_note: 'নতুন নোট',
},
table_node_context_menu: {
@@ -493,6 +542,22 @@ export const bn: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'সব টেবিল লুকানো আছে',
+ show_all_tables: 'সব দেখান',
+ },
+
+ canvas_filter: {
+ title: 'টেবিল ফিল্টার করুন',
+ search_placeholder: 'টেবিল খুঁজুন...',
+ group_by_schema: 'স্কিমা অনুযায়ী গ্রুপ করুন',
+ group_by_area: 'এলাকা অনুযায়ী গ্রুপ করুন',
+ no_tables_found: 'কোনো টেবিল পাওয়া যায়নি',
+ empty_diagram_description: 'শুরু করতে একটি টেবিল তৈরি করুন',
+ no_tables_description: 'আপনার অনুসন্ধান বা ফিল্টার সামঞ্জস্য করুন',
+ clear_filter: 'ফিল্টার মুছুন',
+ },
+
snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 06d977bf2..9030909ad 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -4,18 +4,18 @@ export const de: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Neu',
- browse: 'Durchsuchen',
+ browse: 'Öffnen',
tables: 'Tabellen',
refs: 'Refs',
- areas: 'Bereiche',
dependencies: 'Abhängigkeiten',
custom_types: 'Benutzerdefinierte Typen',
+ visuals: 'Darstellungen',
},
menu: {
actions: {
actions: 'Aktionen',
new: 'Neu...',
- browse: 'Durchsuchen...',
+ browse: 'Alle Datenbanken...',
save: 'Speichern',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
@@ -44,12 +44,13 @@ export const de: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
- // TODO: Translate
backup: {
- backup: 'Backup',
- export_diagram: 'Export Diagram',
- restore_diagram: 'Restore Diagram',
+ backup: 'Sicherung',
+ export_diagram: 'Diagramm exportieren',
+ restore_diagram: 'Diagramm wiederherstellen',
},
help: {
help: 'Hilfe',
@@ -130,16 +131,20 @@ export const de: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Alle Tabellen sind ausgeblendet',
+ show_all: 'Alle anzeigen',
table: {
fields: 'Felder',
nullable: 'Nullable?',
primary_key: 'Primärschlüssel',
indexes: 'Indizes',
+ check_constraints: 'Prüfbedingungen',
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
add_field: 'Feld hinzufügen',
add_index: 'Index hinzufügen',
+ add_check: 'Prüfung hinzufügen',
index_select_fields: 'Felder auswählen',
no_types_found: 'Keine Datentypen gefunden',
field_name: 'Name',
@@ -166,6 +171,11 @@ export const de: LanguageTranslation = {
index_type: 'Indextyp',
delete_index: 'Index löschen',
},
+ check_constraint_actions: {
+ title: 'Prüfbedingung',
+ expression: 'Ausdruck',
+ delete: 'Prüfbedingung löschen',
+ },
table_actions: {
title: 'Tabellenaktionen',
change_schema: 'Schema ändern',
@@ -190,9 +200,10 @@ export const de: LanguageTranslation = {
relationship: {
relationship: 'Beziehung',
primary: 'Primäre Tabelle',
- foreign: 'Referenzierte Tabelle',
+ foreign: 'Verknüpfte Tabelle',
cardinality: 'Kardinalität',
delete_relationship: 'Löschen',
+ switch_tables: 'Tabellen tauschen',
relationship_actions: {
title: 'Aktionen',
delete_relationship: 'Löschen',
@@ -214,54 +225,85 @@ export const de: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
+ areas: 'Bereiche',
+ add_area: 'Bereich hinzufügen',
filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ clear: 'Filter löschen',
+ no_results:
+ 'Keine Bereiche gefunden, die Ihrem Filter entsprechen.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Bereich-Aktionen',
+ edit_name: 'Name bearbeiten',
+ delete_area: 'Bereich löschen',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Keine Bereiche',
+ description: 'Erstellen Sie einen Bereich, um zu beginnen',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Darstellungen',
+ tabs: {
+ areas: 'Bereiche',
+ notes: 'Notizen',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filter',
+ add_note: 'Notiz hinzufügen',
+ no_results: 'Keine Notizen gefunden',
+ clear: 'Filter löschen',
+ empty_state: {
+ title: 'Keine Notizen',
+ description:
+ 'Erstellen Sie eine Notiz, um Textanmerkungen auf der Leinwand hinzuzufügen',
+ },
+ note: {
+ empty_note: 'Leere Notiz',
+ note_actions: {
+ title: 'Notiz-Aktionen',
+ edit_content: 'Inhalt bearbeiten',
+ delete_note: 'Notiz löschen',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
+ custom_types: 'Benutzerdefinierte Typen',
filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ clear: 'Filter löschen',
+ no_results:
+ 'Keine benutzerdefinierten Typen gefunden, die Ihrem Filter entsprechen.',
+ new_type: 'Neuer Typ',
empty_state: {
- title: 'No custom types',
+ title: 'Keine benutzerdefinierten Typen',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Benutzerdefinierte Typen werden hier angezeigt, wenn sie in Ihrer Datenbank verfügbar sind',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Art',
+ enum_values: 'Enum-Werte',
+ composite_fields: 'Felder',
+ no_fields: 'Keine Felder definiert',
no_values: 'Keine Enum-Werte definiert',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Feldname',
+ field_type_placeholder: 'Typ auswählen',
+ add_field: 'Feld hinzufügen',
+ no_fields_tooltip:
+ 'Keine Felder für diesen benutzerdefinierten Typ definiert',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Aktionen',
+ highlight_fields: 'Felder hervorheben',
+ delete_custom_type: 'Löschen',
+ clear_field_highlight: 'Hervorhebung entfernen',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Typ löschen',
},
},
},
@@ -280,8 +322,7 @@ export const de: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Tabellen filtern',
},
new_diagram_dialog: {
@@ -313,7 +354,7 @@ export const de: LanguageTranslation = {
back: 'Zurück',
// TODO: Translate
import_from_file: 'Import from File',
- empty_diagram: 'Leeres Diagramm',
+ empty_diagram: 'Leere Datenbank',
continue: 'Weiter',
import: 'Importieren',
},
@@ -329,6 +370,7 @@ export const de: LanguageTranslation = {
},
cancel: 'Abbrechen',
open: 'Öffnen',
+ new_database: 'Neue Datenbank',
diagram_actions: {
open: 'Öffnen',
@@ -392,10 +434,9 @@ export const de: LanguageTranslation = {
export_image_dialog: {
title: 'Bild exportieren',
description: 'Wählen Sie den Skalierungsfaktor für den Export:',
- scale_1x: '1x Normal',
- scale_2x: '2x (Empfohlen)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Niedrige Qualität)',
+ scale_2x: '2x (Normale Qualität)',
+ scale_4x: '4x (Beste Qualität)',
cancel: 'Abbrechen',
export: 'Exportieren',
// TODO: Translate
@@ -405,6 +446,13 @@ export const de: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
+ share_table_dialog: {
+ title: 'Teile Tabelle',
+ description:
+ 'Kopieren Sie den folgenden Link, um diese Tabelle zu teilen.',
+ close: 'Schlie?en',
+ copy_aria_label: 'Freigabelink kopieren',
+ },
new_table_schema_dialog: {
title: 'Schema auswählen',
@@ -485,8 +533,8 @@ export const de: LanguageTranslation = {
new_table: 'Neue Tabelle',
new_view: 'Neue Ansicht',
new_relationship: 'Neue Beziehung',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: 'Neuer Bereich',
+ new_note: 'Neue Notiz',
},
table_node_context_menu: {
@@ -496,6 +544,24 @@ export const de: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Alle Tabellen sind ausgeblendet',
+ show_all_tables: 'Alle anzeigen',
+ },
+
+ canvas_filter: {
+ title: 'Tabellen filtern',
+ search_placeholder: 'Tabellen suchen...',
+ group_by_schema: 'Nach Schema gruppieren',
+ group_by_area: 'Nach Bereich gruppieren',
+ no_tables_found: 'Keine Tabellen gefunden',
+ empty_diagram_description:
+ 'Erstellen Sie eine Tabelle, um zu beginnen',
+ no_tables_description:
+ 'Versuchen Sie, Ihre Suche oder Filter anzupassen',
+ clear_filter: 'Filter löschen',
+ },
+
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index f8a5a4505..c71940d4e 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -4,18 +4,18 @@ export const en = {
translation: {
editor_sidebar: {
new_diagram: 'New',
- browse: 'Browse',
+ browse: 'Open',
tables: 'Tables',
refs: 'Refs',
- areas: 'Areas',
dependencies: 'Dependencies',
custom_types: 'Custom Types',
+ visuals: 'Visuals',
},
menu: {
actions: {
actions: 'Actions',
new: 'New...',
- browse: 'Browse...',
+ browse: 'All Databases...',
save: 'Save',
import: 'Import',
export_sql: 'Export SQL',
@@ -43,6 +43,8 @@ export const en = {
hide_dependencies: 'Hide Dependencies',
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Backup',
@@ -125,16 +127,20 @@ export const en = {
no_results: 'No tables found matching your filter.',
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'All tables are hidden',
+ show_all: 'Show all',
table: {
fields: 'Fields',
nullable: 'Nullable?',
primary_key: 'Primary Key',
indexes: 'Indexes',
+ check_constraints: 'Check Constraints',
comments: 'Comments',
no_comments: 'No comments',
add_field: 'Add Field',
add_index: 'Add Index',
+ add_check: 'Add Check',
index_select_fields: 'Select fields',
no_types_found: 'No types found',
field_name: 'Name',
@@ -159,6 +165,11 @@ export const en = {
index_type: 'Index Type',
delete_index: 'Delete Index',
},
+ check_constraint_actions: {
+ title: 'Check Constraint',
+ expression: 'Expression',
+ delete: 'Delete Check Constraint',
+ },
table_actions: {
title: 'Table Actions',
change_schema: 'Change Schema',
@@ -183,9 +194,10 @@ export const en = {
relationship: {
relationship: 'Relationship',
primary: 'Primary Table',
- foreign: 'Referenced Table',
+ foreign: 'Related Table',
cardinality: 'Cardinality',
delete_relationship: 'Delete',
+ switch_tables: 'Switch Tables',
relationship_actions: {
title: 'Actions',
delete_relationship: 'Delete',
@@ -227,11 +239,40 @@ export const en = {
},
},
+ visuals_section: {
+ visuals: 'Visuals',
+ tabs: {
+ areas: 'Areas',
+ notes: 'Notes',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filter',
+ add_note: 'Add Note',
+ no_results: 'No notes found',
+ clear: 'Clear Filter',
+ empty_state: {
+ title: 'No Notes',
+ description:
+ 'Create a note to add text annotations on the canvas',
+ },
+ note: {
+ empty_note: 'Empty note',
+ note_actions: {
+ title: 'Note Actions',
+ edit_content: 'Edit Content',
+ delete_note: 'Delete Note',
+ },
+ },
+ },
+
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
+ new_type: 'New Type',
empty_state: {
title: 'No custom types',
description:
@@ -301,7 +342,7 @@ export const en = {
cancel: 'Cancel',
import_from_file: 'Import from File',
back: 'Back',
- empty_diagram: 'Empty diagram',
+ empty_diagram: 'Empty database',
continue: 'Continue',
import: 'Import',
},
@@ -317,6 +358,7 @@ export const en = {
},
cancel: 'Cancel',
open: 'Open',
+ new_database: 'New Database',
diagram_actions: {
open: 'Open',
@@ -380,10 +422,9 @@ export const en = {
export_image_dialog: {
title: 'Export Image',
description: 'Choose the scale factor for export:',
- scale_1x: '1x Regular',
- scale_2x: '2x (Recommended)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Low Quality)',
+ scale_2x: '2x (Normal Quality)',
+ scale_4x: '4x (Best Quality)',
cancel: 'Cancel',
export: 'Export',
advanced_options: 'Advanced Options',
@@ -392,7 +433,12 @@ export const en = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Select Schema',
description:
@@ -473,6 +519,7 @@ export const en = {
new_view: 'New View',
new_relationship: 'New Relationship',
new_area: 'New Area',
+ new_note: 'New Note',
},
table_node_context_menu: {
@@ -482,6 +529,22 @@ export const en = {
add_relationship: 'Add Relationship',
},
+ canvas: {
+ all_tables_hidden: 'All tables are hidden',
+ show_all_tables: 'Show all',
+ },
+
+ canvas_filter: {
+ title: 'Filter Tables',
+ search_placeholder: 'Search tables...',
+ group_by_schema: 'Group by Schema',
+ group_by_area: 'Group by Area',
+ no_tables_found: 'No tables found',
+ empty_diagram_description: 'Create a table to get started',
+ no_tables_description: 'Try adjusting your search or filter',
+ clear_filter: 'Clear filter',
+ },
+
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts
index 13f3f5cbd..dfafbaead 100644
--- a/src/i18n/locales/es.ts
+++ b/src/i18n/locales/es.ts
@@ -4,18 +4,18 @@ export const es: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nuevo',
- browse: 'Examinar',
+ browse: 'Abrir',
tables: 'Tablas',
refs: 'Refs',
- areas: 'Áreas',
dependencies: 'Dependencias',
custom_types: 'Tipos Personalizados',
+ visuals: 'Visuales',
},
menu: {
actions: {
actions: 'Acciones',
new: 'Nuevo...',
- browse: 'Examinar...',
+ browse: 'Todas las bases de datos...',
save: 'Guardar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
@@ -44,6 +44,8 @@ export const es: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Respaldo',
@@ -128,16 +130,20 @@ export const es: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Todas las tablas están ocultas',
+ show_all: 'Mostrar todo',
table: {
fields: 'Campos',
nullable: '¿Opcional?',
primary_key: 'Clave Primaria',
indexes: 'Índices',
+ check_constraints: 'Restricciones de verificación',
comments: 'Comentarios',
no_comments: 'Sin comentarios',
add_field: 'Agregar Campo',
add_index: 'Agregar Índice',
+ add_check: 'Agregar verificación',
index_select_fields: 'Seleccionar campos',
field_name: 'Nombre',
field_type: 'Tipo',
@@ -164,6 +170,11 @@ export const es: LanguageTranslation = {
index_type: 'Tipo de Índice',
delete_index: 'Eliminar Índice',
},
+ check_constraint_actions: {
+ title: 'Restricción de verificación',
+ expression: 'Expresión',
+ delete: 'Eliminar restricción',
+ },
table_actions: {
title: 'Acciones de la Tabla',
change_schema: 'Cambiar Esquema',
@@ -188,9 +199,10 @@ export const es: LanguageTranslation = {
relationship: {
relationship: 'Relación',
primary: 'Tabla Primaria',
- foreign: 'Tabla Referenciada',
+ foreign: 'Tabla Relacionada',
cardinality: 'Cardinalidad',
delete_relationship: 'Eliminar',
+ switch_tables: 'Intercambiar tablas',
relationship_actions: {
title: 'Acciones',
delete_relationship: 'Eliminar',
@@ -212,54 +224,85 @@ export const es: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'Áreas',
+ add_area: 'Agregar Área',
+ filter: 'Filtrar',
+ clear: 'Limpiar Filtro',
+ no_results:
+ 'No se encontraron áreas que coincidan con tu filtro.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Acciones del Área',
+ edit_name: 'Editar Nombre',
+ delete_area: 'Eliminar Área',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Sin áreas',
+ description: 'Crea un área para comenzar',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuales',
+ tabs: {
+ areas: 'Áreas',
+ notes: 'Notas',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filtrar',
+ add_note: 'Agregar Nota',
+ no_results: 'No se encontraron notas',
+ clear: 'Limpiar Filtro',
+ empty_state: {
+ title: 'Sin Notas',
+ description:
+ 'Crea una nota para agregar anotaciones de texto en el lienzo',
+ },
+ note: {
+ empty_note: 'Nota vacía',
+ note_actions: {
+ title: 'Acciones de Nota',
+ edit_content: 'Editar Contenido',
+ delete_note: 'Eliminar Nota',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Tipos Personalizados',
+ filter: 'Filtrar',
+ clear: 'Limpiar Filtro',
+ no_results:
+ 'No se encontraron tipos personalizados que coincidan con tu filtro.',
+ new_type: 'Nuevo Tipo',
empty_state: {
- title: 'No custom types',
+ title: 'Sin tipos personalizados',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Los tipos personalizados aparecerán aquí cuando estén disponibles en tu base de datos',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Tipo',
+ enum_values: 'Valores Enum',
+ composite_fields: 'Campos',
+ no_fields: 'Sin campos definidos',
no_values: 'No hay valores de enum definidos',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Nombre del campo',
+ field_type_placeholder: 'Seleccionar tipo',
+ add_field: 'Agregar Campo',
+ no_fields_tooltip:
+ 'Sin campos definidos para este tipo personalizado',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Acciones',
+ highlight_fields: 'Resaltar Campos',
+ delete_custom_type: 'Eliminar',
+ clear_field_highlight: 'Quitar Resaltado',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Eliminar Tipo',
},
},
},
@@ -277,8 +320,7 @@ export const es: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Filtrar Tablas',
},
new_diagram_dialog: {
@@ -310,7 +352,7 @@ export const es: LanguageTranslation = {
back: 'Atrás',
// TODO: Translate
import_from_file: 'Import from File',
- empty_diagram: 'Diagrama vacío',
+ empty_diagram: 'Base de datos vacía',
continue: 'Continuar',
import: 'Importar',
},
@@ -327,6 +369,7 @@ export const es: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
+ new_database: 'Nueva Base de Datos',
diagram_actions: {
open: 'Abrir',
@@ -390,10 +433,9 @@ export const es: LanguageTranslation = {
export_image_dialog: {
title: 'Exportar imagen',
description: 'Escoge el factor de escalamiento para exportar:',
- scale_1x: '1x regular',
- scale_2x: '2x (recomendado)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Baja calidad)',
+ scale_2x: '2x (Calidad normal)',
+ scale_4x: '4x (Mejor calidad)',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
@@ -403,7 +445,12 @@ export const es: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Seleccionar Esquema',
description:
@@ -484,8 +531,8 @@ export const es: LanguageTranslation = {
new_table: 'Nueva Tabla',
new_view: 'Nueva Vista',
new_relationship: 'Nueva Relación',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: 'Nueva Área',
+ new_note: 'Nueva Nota',
},
table_node_context_menu: {
@@ -495,6 +542,22 @@ export const es: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Todas las tablas están ocultas',
+ show_all_tables: 'Mostrar todo',
+ },
+
+ canvas_filter: {
+ title: 'Filtrar Tablas',
+ search_placeholder: 'Buscar tablas...',
+ group_by_schema: 'Agrupar por Esquema',
+ group_by_area: 'Agrupar por Área',
+ no_tables_found: 'No se encontraron tablas',
+ empty_diagram_description: 'Crea una tabla para comenzar',
+ no_tables_description: 'Intenta ajustar tu búsqueda o filtro',
+ clear_filter: 'Limpiar filtro',
+ },
+
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts
index df07b6880..7a5d0a50c 100644
--- a/src/i18n/locales/fr.ts
+++ b/src/i18n/locales/fr.ts
@@ -4,18 +4,18 @@ export const fr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nouveau',
- browse: 'Parcourir',
+ browse: 'Ouvrir',
tables: 'Tables',
refs: 'Refs',
- areas: 'Zones',
dependencies: 'Dépendances',
custom_types: 'Types Personnalisés',
+ visuals: 'Visuels',
},
menu: {
actions: {
actions: 'Actions',
new: 'Nouveau...',
- browse: 'Parcourir...',
+ browse: 'Toutes les bases de données...',
save: 'Enregistrer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
@@ -43,6 +43,8 @@ export const fr: LanguageTranslation = {
hide_dependencies: 'Masquer les Dépendances',
show_minimap: 'Afficher la Mini Carte',
hide_minimap: 'Masquer la Mini Carte',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Sauvegarde',
@@ -126,16 +128,20 @@ export const fr: LanguageTranslation = {
'Aucune table trouvée correspondant à votre filtre.',
show_list: 'Afficher la Liste des Tableaux',
show_dbml: "Afficher l'éditeur DBML",
+ all_hidden: 'Toutes les tables sont masquées',
+ show_all: 'Tout afficher',
table: {
fields: 'Champs',
nullable: 'Nullable?',
primary_key: 'Clé Primaire',
indexes: 'Index',
+ check_constraints: 'Contraintes de vérification',
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
add_field: 'Ajouter un Champ',
add_index: 'Ajouter un Index',
+ add_check: 'Ajouter une vérification',
index_select_fields: 'Sélectionner des champs',
no_types_found: 'Aucun type trouvé',
field_name: 'Nom',
@@ -162,6 +168,11 @@ export const fr: LanguageTranslation = {
index_type: "Type d'index",
delete_index: "Supprimer l'Index",
},
+ check_constraint_actions: {
+ title: 'Contrainte de vérification',
+ expression: 'Expression',
+ delete: 'Supprimer la contrainte',
+ },
table_actions: {
title: 'Actions de la Table',
add_field: 'Ajouter un Champ',
@@ -186,9 +197,10 @@ export const fr: LanguageTranslation = {
relationship: {
relationship: 'Relation',
primary: 'Table Principale',
- foreign: 'Table Référencée',
+ foreign: 'Table Liée',
cardinality: 'Cardinalité',
delete_relationship: 'Supprimer',
+ switch_tables: 'Inverser les tables',
relationship_actions: {
title: 'Actions',
delete_relationship: 'Supprimer',
@@ -210,54 +222,84 @@ export const fr: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'Zones',
+ add_area: 'Ajouter une Zone',
+ filter: 'Filtrer',
+ clear: 'Effacer le Filtre',
+ no_results: 'Aucune zone trouvée correspondant à votre filtre.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Actions de la Zone',
+ edit_name: 'Modifier le Nom',
+ delete_area: 'Supprimer la Zone',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Aucune zone',
+ description: 'Créez une zone pour commencer',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuels',
+ tabs: {
+ areas: 'Zones',
+ notes: 'Notes',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filtrer',
+ add_note: 'Ajouter une Note',
+ no_results: 'Aucune note trouvée',
+ clear: 'Effacer le Filtre',
+ empty_state: {
+ title: 'Pas de Notes',
+ description:
+ 'Créez une note pour ajouter des annotations de texte sur le canevas',
+ },
+ note: {
+ empty_note: 'Note vide',
+ note_actions: {
+ title: 'Actions de Note',
+ edit_content: 'Modifier le Contenu',
+ delete_note: 'Supprimer la Note',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Types Personnalisés',
+ filter: 'Filtrer',
+ clear: 'Effacer le Filtre',
+ no_results:
+ 'Aucun type personnalisé trouvé correspondant à votre filtre.',
+ new_type: 'Nouveau Type',
empty_state: {
- title: 'No custom types',
+ title: 'Aucun type personnalisé',
description:
- 'Custom types will appear here when they are available in your database',
+ "Les types personnalisés apparaîtront ici lorsqu'ils seront disponibles dans votre base de données",
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Type',
+ enum_values: 'Valeurs Enum',
+ composite_fields: 'Champs',
+ no_fields: 'Aucun champ défini',
no_values: "Aucune valeur d'énumération définie",
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Nom du champ',
+ field_type_placeholder: 'Sélectionner le type',
+ add_field: 'Ajouter un Champ',
+ no_fields_tooltip:
+ 'Aucun champ défini pour ce type personnalisé',
custom_type_actions: {
title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ highlight_fields: 'Surligner les Champs',
+ delete_custom_type: 'Supprimer',
+ clear_field_highlight: 'Effacer le Surlignage',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Supprimer le Type',
},
},
},
@@ -275,8 +317,7 @@ export const fr: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Surligner les tables chevauchées',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Filtrer les Tables',
},
new_diagram_dialog: {
@@ -307,7 +348,7 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
back: 'Retour',
import_from_file: "Importer à partir d'un fichier",
- empty_diagram: 'Diagramme vide',
+ empty_diagram: 'Base de données vide',
continue: 'Continuer',
import: 'Importer',
},
@@ -324,6 +365,7 @@ export const fr: LanguageTranslation = {
},
cancel: 'Annuler',
open: 'Ouvrir',
+ new_database: 'Nouvelle Base de Données',
diagram_actions: {
open: 'Ouvrir',
@@ -353,10 +395,9 @@ export const fr: LanguageTranslation = {
title: "Exporter l'image",
description:
"Choisissez le facteur d'échelle pour l'image exportée.",
- scale_1x: '1x Normal',
- scale_2x: '2x (Recommandé)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Basse qualité)',
+ scale_2x: '2x (Qualité normale)',
+ scale_4x: '4x (Meilleure qualité)',
cancel: 'Annuler',
export: 'Exporter',
// TODO: Translate
@@ -366,7 +407,12 @@ export const fr: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Sélectionner un Schéma',
description:
@@ -480,8 +526,8 @@ export const fr: LanguageTranslation = {
new_table: 'Nouvelle Table',
new_view: 'Nouvelle Vue',
new_relationship: 'Nouvelle Relation',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: 'Nouvelle Zone',
+ new_note: 'Nouvelle Note',
},
table_node_context_menu: {
@@ -491,6 +537,23 @@ export const fr: LanguageTranslation = {
add_relationship: 'Ajouter une Relation',
},
+ canvas: {
+ all_tables_hidden: 'Toutes les tables sont masquées',
+ show_all_tables: 'Tout afficher',
+ },
+
+ canvas_filter: {
+ title: 'Filtrer les Tables',
+ search_placeholder: 'Rechercher des tables...',
+ group_by_schema: 'Grouper par Schéma',
+ group_by_area: 'Grouper par Zone',
+ no_tables_found: 'Aucune table trouvée',
+ empty_diagram_description: 'Créez une table pour commencer',
+ no_tables_description:
+ 'Essayez de modifier votre recherche ou filtre',
+ clear_filter: 'Effacer le filtre',
+ },
+
snap_to_grid_tooltip:
'Aligner sur la grille (maintenir la touche {{key}})',
diff --git a/src/i18n/locales/gu.ts b/src/i18n/locales/gu.ts
index 028007d59..62d8209e8 100644
--- a/src/i18n/locales/gu.ts
+++ b/src/i18n/locales/gu.ts
@@ -4,18 +4,18 @@ export const gu: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'નવું',
- browse: 'બ્રાઉજ',
+ browse: 'ખોલો',
tables: 'ટેબલો',
refs: 'રેફ્સ',
- areas: 'ક્ષેત્રો',
dependencies: 'નિર્ભરતાઓ',
custom_types: 'કસ્ટમ ટાઇપ',
+ visuals: 'Visuals',
},
menu: {
actions: {
actions: 'ક્રિયાઓ',
new: 'નવું...',
- browse: 'બ્રાઉજ કરો...',
+ browse: 'બધા ડેટાબેસ...',
save: 'સાચવો',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
@@ -44,6 +44,8 @@ export const gu: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
@@ -129,6 +131,8 @@ export const gu: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'બધી ટેબલ્સ છુપાયેલી છે',
+ show_all: 'બધું બતાવો',
table: {
fields: 'ફીલ્ડ્સ',
@@ -136,10 +140,12 @@ export const gu: LanguageTranslation = {
nullable: 'Nullable?',
primary_key: 'પ્રાથમિક કી',
indexes: 'ઈન્ડેક્સ',
+ check_constraints: 'ચકાસણી નિયંત્રણો',
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
add_field: 'ફીલ્ડ ઉમેરો',
add_index: 'ઈન્ડેક્સ ઉમેરો',
+ add_check: 'ચકાસણી ઉમેરો',
index_select_fields: 'ફીલ્ડ્સ પસંદ કરો',
no_types_found: 'કોઈ પ્રકાર મળ્યા નથી',
field_name: 'નામ',
@@ -166,6 +172,11 @@ export const gu: LanguageTranslation = {
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
},
+ check_constraint_actions: {
+ title: 'ચકાસણી નિયંત્રણ',
+ expression: 'અભિવ્યક્તિ',
+ delete: 'નિયંત્રણ કાઢી નાખો',
+ },
table_actions: {
title: 'ટેબલ ક્રિયાઓ',
change_schema: 'સ્કીમા બદલો',
@@ -190,9 +201,10 @@ export const gu: LanguageTranslation = {
relationship: {
relationship: 'સંબંધ',
primary: 'પ્રાથમિક ટેબલ',
- foreign: 'સંદર્ભિત ટેબલ',
+ foreign: 'સંબંધિત ટેબલ',
cardinality: 'કાર્ડિનાલિટી',
delete_relationship: 'કાઢી નાખો',
+ switch_tables: 'ટેબલ બદલો',
relationship_actions: {
title: 'ક્રિયાઓ',
delete_relationship: 'કાઢી નાખો',
@@ -214,54 +226,83 @@ export const gu: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'વિસ્તારો',
+ add_area: 'વિસ્તાર ઉમેરો',
+ filter: 'ફિલ્ટર',
+ clear: 'ફિલ્ટર સાફ કરો',
+ no_results: 'તમારા ફિલ્ટરને અનુરૂપ કોઈ વિસ્તાર મળ્યો નથી.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'વિસ્તાર ક્રિયાઓ',
+ edit_name: 'નામ સંપાદિત કરો',
+ delete_area: 'વિસ્તાર કાઢી નાખો',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'કોઈ વિસ્તાર નથી',
+ description: 'શરૂ કરવા માટે વિસ્તાર બનાવો',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuals',
+ tabs: {
+ areas: 'વિસ્તારો',
+ notes: 'નોંધો',
+ },
+ },
+
+ notes_section: {
+ filter: 'ફિલ્ટર',
+ add_note: 'નોંધ ઉમેરો',
+ no_results: 'કોઈ નોંધો મળી નથી',
+ clear: 'ફિલ્ટર સાફ કરો',
+ empty_state: {
+ title: 'કોઈ નોંધો નથી',
+ description:
+ 'કેનવાસ પર ટેક્સ્ટ એનોટેશન ઉમેરવા માટે નોંધ બનાવો',
+ },
+ note: {
+ empty_note: 'ખાલી નોંધ',
+ note_actions: {
+ title: 'નોંધ ક્રિયાઓ',
+ edit_content: 'સામગ્રી સંપાદિત કરો',
+ delete_note: 'નોંધ કાઢી નાખો',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'કસ્ટમ પ્રકાર',
+ filter: 'ફિલ્ટર',
+ clear: 'ફિલ્ટર સાફ કરો',
+ no_results: 'તમારા ફિલ્ટરને અનુરૂપ કોઈ કસ્ટમ પ્રકાર મળ્યો નથી.',
+ new_type: 'નવો પ્રકાર',
empty_state: {
- title: 'No custom types',
+ title: 'કોઈ કસ્ટમ પ્રકાર નથી',
description:
- 'Custom types will appear here when they are available in your database',
+ 'જ્યારે તમારા ડેટાબેસમાં ઉપલબ્ધ હશે ત્યારે કસ્ટમ પ્રકાર અહીં દેખાશે',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'પ્રકાર',
+ enum_values: 'Enum મૂલ્યો',
+ composite_fields: 'ફીલ્ડ્સ',
+ no_fields: 'કોઈ ફીલ્ડ વ્યાખ્યાયિત નથી',
no_values: 'કોઈ enum મૂલ્યો વ્યાખ્યાયિત નથી',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'ફીલ્ડનું નામ',
+ field_type_placeholder: 'પ્રકાર પસંદ કરો',
+ add_field: 'ફીલ્ડ ઉમેરો',
+ no_fields_tooltip:
+ 'આ કસ્ટમ પ્રકાર માટે કોઈ ફીલ્ડ વ્યાખ્યાયિત નથી',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'ક્રિયાઓ',
+ highlight_fields: 'ફીલ્ડ્સ હાઇલાઇટ કરો',
+ delete_custom_type: 'કાઢી નાખો',
+ clear_field_highlight: 'હાઇલાઇટ કાઢો',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'પ્રકાર કાઢી નાખો',
},
},
},
@@ -279,8 +320,7 @@ export const gu: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'ટેબલ ફિલ્ટર કરો',
},
new_diagram_dialog: {
@@ -310,7 +350,7 @@ export const gu: LanguageTranslation = {
cancel: 'રદ કરો',
back: 'પાછા',
import_from_file: 'ફાઇલમાંથી આયાત કરો',
- empty_diagram: 'ખાલી ડાયાગ્રામ',
+ empty_diagram: 'ખાલી ડેટાબેસ',
continue: 'ચાલુ રાખો',
import: 'આયાત કરો',
},
@@ -326,6 +366,7 @@ export const gu: LanguageTranslation = {
},
cancel: 'રદ કરો',
open: 'ખોલો',
+ new_database: 'નવું ડેટાબેસ',
diagram_actions: {
open: 'ખોલો',
@@ -389,10 +430,9 @@ export const gu: LanguageTranslation = {
export_image_dialog: {
title: 'છબી નિકાસ કરો',
description: 'નિકાસ માટે સ્કેલ ફેક્ટર પસંદ કરો:',
- scale_1x: '1x સામાન્ય',
- scale_2x: '2x (ભલામણ કરેલું)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (નીચી ગુણવત્તા)',
+ scale_2x: '2x (સામાન્ય ગુણવત્તા)',
+ scale_4x: '4x (શ્રેષ્ઠ ગુણવત્તા)',
cancel: 'રદ કરો',
export: 'નિકાસ કરો',
// TODO: Translate
@@ -402,7 +442,12 @@ export const gu: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'સ્કીમા પસંદ કરો',
description:
@@ -484,7 +529,8 @@ export const gu: LanguageTranslation = {
new_view: 'નવું વ્યૂ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'નવો વિસ્તાર',
+ new_note: 'નવી નોંધ',
},
table_node_context_menu: {
@@ -494,6 +540,23 @@ export const gu: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'બધી ટેબલ્સ છુપાયેલી છે',
+ show_all_tables: 'બધું બતાવો',
+ },
+
+ canvas_filter: {
+ title: 'ટેબલ્સ ફિલ્ટર કરો',
+ search_placeholder: 'ટેબલ્સ શોધો...',
+ group_by_schema: 'સ્કીમા પ્રમાણે ગ્રુપ કરો',
+ group_by_area: 'વિસ્તાર પ્રમાણે ગ્રુપ કરો',
+ no_tables_found: 'કોઈ ટેબલ મળી નથી',
+ empty_diagram_description: 'શરૂ કરવા માટે ટેબલ બનાવો',
+ no_tables_description:
+ 'તમારી શોધ અથવા ફિલ્ટર સમાયોજિત કરવાનો પ્રયાસ કરો',
+ clear_filter: 'ફિલ્ટર સાફ કરો',
+ },
+
snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts
index fdbd56405..ff2e04a28 100644
--- a/src/i18n/locales/hi.ts
+++ b/src/i18n/locales/hi.ts
@@ -4,18 +4,18 @@ export const hi: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नया',
- browse: 'ब्राउज़',
+ browse: 'खोलें',
tables: 'टेबल',
refs: 'रेफ्स',
- areas: 'क्षेत्र',
dependencies: 'निर्भरताएं',
custom_types: 'कस्टम टाइप',
+ visuals: 'Visuals',
},
menu: {
actions: {
actions: 'कार्य',
new: 'नया...',
- browse: 'ब्राउज़ करें...',
+ browse: 'सभी डेटाबेस...',
save: 'सहेजें',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
@@ -44,6 +44,8 @@ export const hi: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'बैकअप',
@@ -129,16 +131,20 @@ export const hi: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'सभी तालिकाएँ छिपी हुई हैं',
+ show_all: 'सभी दिखाएं',
table: {
fields: 'फ़ील्ड्स',
nullable: 'Nullable?',
primary_key: 'प्राथमिक कुंजी',
indexes: 'सूचकांक',
+ check_constraints: 'जाँच प्रतिबंध',
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
add_field: 'फ़ील्ड जोड़ें',
add_index: 'सूचकांक जोड़ें',
+ add_check: 'जाँच जोड़ें',
index_select_fields: 'फ़ील्ड्स चुनें',
no_types_found: 'कोई प्रकार नहीं मिला',
field_name: 'नाम',
@@ -165,6 +171,11 @@ export const hi: LanguageTranslation = {
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ',
},
+ check_constraint_actions: {
+ title: 'जाँच प्रतिबंध',
+ expression: 'अभिव्यक्ति',
+ delete: 'प्रतिबंध हटाएं',
+ },
table_actions: {
title: 'तालिका क्रियाएँ',
change_schema: 'स्कीमा बदलें',
@@ -189,9 +200,10 @@ export const hi: LanguageTranslation = {
relationship: {
relationship: 'संबंध',
primary: 'प्राथमिक तालिका',
- foreign: 'संदर्भित तालिका',
+ foreign: 'संबंधित तालिका',
cardinality: 'कार्डिनैलिटी',
delete_relationship: 'हटाएँ',
+ switch_tables: 'टेबल बदलें',
relationship_actions: {
title: 'क्रियाएँ',
delete_relationship: 'हटाएँ',
@@ -213,54 +225,85 @@ export const hi: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'क्षेत्र',
+ add_area: 'क्षेत्र जोड़ें',
+ filter: 'फ़िल्टर',
+ clear: 'फ़िल्टर साफ़ करें',
+ no_results:
+ 'आपके फ़िल्टर से मेल खाने वाला कोई क्षेत्र नहीं मिला।',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'क्षेत्र क्रियाएं',
+ edit_name: 'नाम संपादित करें',
+ delete_area: 'क्षेत्र हटाएं',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'कोई क्षेत्र नहीं',
+ description: 'शुरू करने के लिए एक क्षेत्र बनाएं',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuals',
+ tabs: {
+ areas: 'क्षेत्र',
+ notes: 'नोट्स',
+ },
+ },
+
+ notes_section: {
+ filter: 'फ़िल्टर',
+ add_note: 'नोट जोड़ें',
+ no_results: 'कोई नोट नहीं मिला',
+ clear: 'फ़िल्टर साफ़ करें',
+ empty_state: {
+ title: 'कोई नोट नहीं',
+ description:
+ 'कैनवास पर टेक्स्ट एनोटेशन जोड़ने के लिए एक नोट बनाएं',
+ },
+ note: {
+ empty_note: 'खाली नोट',
+ note_actions: {
+ title: 'नोट क्रियाएं',
+ edit_content: 'सामग्री संपादित करें',
+ delete_note: 'नोट हटाएं',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'कस्टम प्रकार',
+ filter: 'फ़िल्टर',
+ clear: 'फ़िल्टर साफ़ करें',
+ no_results:
+ 'आपके फ़िल्टर से मेल खाने वाला कोई कस्टम प्रकार नहीं मिला।',
+ new_type: 'नया प्रकार',
empty_state: {
- title: 'No custom types',
+ title: 'कोई कस्टम प्रकार नहीं',
description:
- 'Custom types will appear here when they are available in your database',
+ 'जब आपके डेटाबेस में उपलब्ध होंगे तो कस्टम प्रकार यहाँ दिखाई देंगे',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'प्रकार',
+ enum_values: 'Enum मान',
+ composite_fields: 'फ़ील्ड',
+ no_fields: 'कोई फ़ील्ड परिभाषित नहीं',
no_values: 'कोई enum मान परिभाषित नहीं',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'फ़ील्ड का नाम',
+ field_type_placeholder: 'प्रकार चुनें',
+ add_field: 'फ़ील्ड जोड़ें',
+ no_fields_tooltip:
+ 'इस कस्टम प्रकार के लिए कोई फ़ील्ड परिभाषित नहीं',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'क्रियाएं',
+ highlight_fields: 'फ़ील्ड हाइलाइट करें',
+ delete_custom_type: 'हटाएं',
+ clear_field_highlight: 'हाइलाइट हटाएं',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'प्रकार हटाएं',
},
},
},
@@ -278,8 +321,7 @@ export const hi: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'टेबल फ़िल्टर करें',
},
new_diagram_dialog: {
@@ -312,7 +354,7 @@ export const hi: LanguageTranslation = {
back: 'वापस',
// TODO: Translate
import_from_file: 'Import from File',
- empty_diagram: 'खाली आरेख',
+ empty_diagram: 'खाली डेटाबेस',
continue: 'जारी रखें',
import: 'आयात करें',
},
@@ -328,6 +370,7 @@ export const hi: LanguageTranslation = {
},
cancel: 'रद्द करें',
open: 'खोलें',
+ new_database: 'नया डेटाबेस',
diagram_actions: {
open: 'खोलें',
@@ -391,10 +434,9 @@ export const hi: LanguageTranslation = {
export_image_dialog: {
title: 'छवि निर्यात करें',
description: 'निर्यात के लिए स्केल फ़ैक्टर चुनें:',
- scale_1x: '1x सामान्य',
- scale_2x: '2x (अनुशंसित)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (निम्न गुणवत्ता)',
+ scale_2x: '2x (सामान्य गुणवत्ता)',
+ scale_4x: '4x (सर्वोत्तम गुणवत्ता)',
cancel: 'रद्द करें',
export: 'निर्यात करें',
// TODO: Translate
@@ -404,7 +446,12 @@ export const hi: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'स्कीमा चुनें',
description:
@@ -486,7 +533,8 @@ export const hi: LanguageTranslation = {
new_view: 'नया व्यू',
new_relationship: 'नया संबंध',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'नया क्षेत्र',
+ new_note: 'नया नोट',
},
table_node_context_menu: {
@@ -496,6 +544,23 @@ export const hi: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'सभी तालिकाएँ छिपी हुई हैं',
+ show_all_tables: 'सभी दिखाएं',
+ },
+
+ canvas_filter: {
+ title: 'तालिकाएँ फ़िल्टर करें',
+ search_placeholder: 'तालिकाएँ खोजें...',
+ group_by_schema: 'स्कीमा के अनुसार समूहित करें',
+ group_by_area: 'क्षेत्र के अनुसार समूहित करें',
+ no_tables_found: 'कोई तालिका नहीं मिली',
+ empty_diagram_description: 'शुरू करने के लिए एक तालिका बनाएं',
+ no_tables_description:
+ 'अपनी खोज या फ़िल्टर समायोजित करने का प्रयास करें',
+ clear_filter: 'फ़िल्टर साफ़ करें',
+ },
+
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/hr.ts b/src/i18n/locales/hr.ts
index 1814e8a6e..47133675f 100644
--- a/src/i18n/locales/hr.ts
+++ b/src/i18n/locales/hr.ts
@@ -4,18 +4,18 @@ export const hr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Novi',
- browse: 'Pregledaj',
+ browse: 'Otvori',
tables: 'Tablice',
refs: 'Refs',
- areas: 'Područja',
dependencies: 'Ovisnosti',
custom_types: 'Prilagođeni Tipovi',
+ visuals: 'Vizuali',
},
menu: {
actions: {
actions: 'Akcije',
new: 'Novi...',
- browse: 'Pregledaj...',
+ browse: 'Sve baze podataka...',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
@@ -43,6 +43,8 @@ export const hr: LanguageTranslation = {
hide_dependencies: 'Sakrij ovisnosti',
show_minimap: 'Prikaži mini kartu',
hide_minimap: 'Sakrij mini kartu',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Sigurnosna kopija',
@@ -126,16 +128,20 @@ export const hr: LanguageTranslation = {
'Nema pronađenih tablica koje odgovaraju vašem filteru.',
show_list: 'Prikaži popis tablica',
show_dbml: 'Prikaži DBML uređivač',
+ all_hidden: 'Sve tablice su skrivene',
+ show_all: 'Prikaži sve',
table: {
fields: 'Polja',
nullable: 'Može biti null?',
primary_key: 'Primarni ključ',
indexes: 'Indeksi',
+ check_constraints: 'Provjerna ograničenja',
comments: 'Komentari',
no_comments: 'Nema komentara',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
+ add_check: 'Dodaj provjeru',
index_select_fields: 'Odaberi polja',
no_types_found: 'Nema pronađenih tipova',
field_name: 'Naziv',
@@ -160,6 +166,11 @@ export const hr: LanguageTranslation = {
index_type: 'Vrsta indeksa',
delete_index: 'Izbriši indeks',
},
+ check_constraint_actions: {
+ title: 'Provjerno ograničenje',
+ expression: 'Izraz',
+ delete: 'Obriši ograničenje',
+ },
table_actions: {
title: 'Radnje nad tablicom',
change_schema: 'Promijeni shemu',
@@ -184,9 +195,10 @@ export const hr: LanguageTranslation = {
relationship: {
relationship: 'Veza',
primary: 'Primarna tablica',
- foreign: 'Referentna tablica',
+ foreign: 'Povezana tablica',
cardinality: 'Kardinalnost',
delete_relationship: 'Izbriši',
+ switch_tables: 'Zamijeni tablice',
relationship_actions: {
title: 'Radnje',
delete_relationship: 'Izbriši',
@@ -229,12 +241,41 @@ export const hr: LanguageTranslation = {
},
},
+ visuals_section: {
+ visuals: 'Vizuali',
+ tabs: {
+ areas: 'Područja',
+ notes: 'Bilješke',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filtriraj',
+ add_note: 'Dodaj Bilješku',
+ no_results: 'Nije pronađena nijedna bilješka',
+ clear: 'Očisti Filter',
+ empty_state: {
+ title: 'Nema Bilješki',
+ description:
+ 'Kreirajte bilješku za dodavanje tekstualnih napomena na platnu',
+ },
+ note: {
+ empty_note: 'Prazna bilješka',
+ note_actions: {
+ title: 'Akcije Bilješke',
+ edit_content: 'Uredi Sadržaj',
+ delete_note: 'Obriši Bilješku',
+ },
+ },
+ },
+
custom_types_section: {
custom_types: 'Prilagođeni tipovi',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih prilagođenih tipova koji odgovaraju vašem filteru.',
+ new_type: 'Novi tip',
empty_state: {
title: 'Nema prilagođenih tipova',
description:
@@ -305,7 +346,7 @@ export const hr: LanguageTranslation = {
cancel: 'Odustani',
import_from_file: 'Uvezi iz datoteke',
back: 'Natrag',
- empty_diagram: 'Prazan dijagram',
+ empty_diagram: 'Prazna baza podataka',
continue: 'Nastavi',
import: 'Uvezi',
},
@@ -321,6 +362,7 @@ export const hr: LanguageTranslation = {
},
cancel: 'Odustani',
open: 'Otvori',
+ new_database: 'Nova baza podataka',
diagram_actions: {
open: 'Otvori',
@@ -384,10 +426,9 @@ export const hr: LanguageTranslation = {
export_image_dialog: {
title: 'Izvezi sliku',
description: 'Odaberite faktor veličine za izvoz:',
- scale_1x: '1x Obično',
- scale_2x: '2x (Preporučeno)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Niska kvaliteta)',
+ scale_2x: '2x (Normalna kvaliteta)',
+ scale_4x: '4x (Najbolja kvaliteta)',
cancel: 'Odustani',
export: 'Izvezi',
advanced_options: 'Napredne opcije',
@@ -396,7 +437,12 @@ export const hr: LanguageTranslation = {
transparent: 'Prozirna pozadina',
transparent_description: 'Ukloni boju pozadine iz slike.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Odaberi shemu',
description:
@@ -478,6 +524,7 @@ export const hr: LanguageTranslation = {
new_view: 'Novi Pogled',
new_relationship: 'Nova veza',
new_area: 'Novo područje',
+ new_note: 'Nova Bilješka',
},
table_node_context_menu: {
@@ -487,6 +534,22 @@ export const hr: LanguageTranslation = {
add_relationship: 'Dodaj vezu',
},
+ canvas: {
+ all_tables_hidden: 'Sve tablice su skrivene',
+ show_all_tables: 'Prikaži sve',
+ },
+
+ canvas_filter: {
+ title: 'Filtriraj tablice',
+ search_placeholder: 'Pretraži tablice...',
+ group_by_schema: 'Grupiraj po shemi',
+ group_by_area: 'Grupiraj po području',
+ no_tables_found: 'Nisu pronađene tablice',
+ empty_diagram_description: 'Kreirajte tablicu za početak',
+ no_tables_description: 'Pokušajte prilagoditi pretragu ili filter',
+ clear_filter: 'Očisti filter',
+ },
+
snap_to_grid_tooltip: 'Priljepljivanje na mrežu (Drži {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/id_ID.ts b/src/i18n/locales/id_ID.ts
index 8185903aa..8a883c098 100644
--- a/src/i18n/locales/id_ID.ts
+++ b/src/i18n/locales/id_ID.ts
@@ -4,18 +4,18 @@ export const id_ID: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Baru',
- browse: 'Jelajahi',
+ browse: 'Buka',
tables: 'Tabel',
refs: 'Refs',
- areas: 'Area',
dependencies: 'Ketergantungan',
custom_types: 'Tipe Kustom',
+ visuals: 'Visual',
},
menu: {
actions: {
actions: 'Aksi',
new: 'Baru...',
- browse: 'Jelajahi...',
+ browse: 'Semua database...',
save: 'Simpan',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
@@ -44,6 +44,8 @@ export const id_ID: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Cadangan',
@@ -128,16 +130,20 @@ export const id_ID: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Semua tabel tersembunyi',
+ show_all: 'Tampilkan semua',
table: {
fields: 'Kolom',
nullable: 'Bisa Kosong?',
primary_key: 'Kunci Utama',
indexes: 'Indeks',
+ check_constraints: 'Batasan Pemeriksaan',
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
add_field: 'Tambah Kolom',
add_index: 'Tambah Indeks',
+ add_check: 'Tambah Pemeriksaan',
index_select_fields: 'Pilih kolom',
no_types_found: 'Tidak ada tipe yang ditemukan',
field_name: 'Nama',
@@ -164,6 +170,11 @@ export const id_ID: LanguageTranslation = {
index_type: 'Tipe Indeks',
delete_index: 'Hapus Indeks',
},
+ check_constraint_actions: {
+ title: 'Batasan Pemeriksaan',
+ expression: 'Ekspresi',
+ delete: 'Hapus Batasan',
+ },
table_actions: {
title: 'Aksi Tabel',
change_schema: 'Ubah Skema',
@@ -188,9 +199,10 @@ export const id_ID: LanguageTranslation = {
relationship: {
relationship: 'Hubungan',
primary: 'Tabel Primer',
- foreign: 'Tabel Referensi',
+ foreign: 'Tabel Terkait',
cardinality: 'Kardinalitas',
delete_relationship: 'Hapus',
+ switch_tables: 'Tukar Tabel',
relationship_actions: {
title: 'Aksi',
delete_relationship: 'Hapus',
@@ -212,54 +224,84 @@ export const id_ID: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
+ areas: 'Area',
+ add_area: 'Tambah Area',
filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ clear: 'Hapus Filter',
+ no_results: 'Tidak ada area yang cocok dengan filter Anda.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Aksi Area',
+ edit_name: 'Edit Nama',
+ delete_area: 'Hapus Area',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Tidak ada area',
+ description: 'Buat area untuk memulai',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visual',
+ tabs: {
+ areas: 'Area',
+ notes: 'Catatan',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filter',
+ add_note: 'Tambah Catatan',
+ no_results: 'Tidak ada catatan ditemukan',
+ clear: 'Hapus Filter',
+ empty_state: {
+ title: 'Tidak Ada Catatan',
+ description:
+ 'Buat catatan untuk menambahkan anotasi teks di kanvas',
+ },
+ note: {
+ empty_note: 'Catatan kosong',
+ note_actions: {
+ title: 'Aksi Catatan',
+ edit_content: 'Edit Konten',
+ delete_note: 'Hapus Catatan',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
+ custom_types: 'Tipe Kustom',
filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ clear: 'Hapus Filter',
+ no_results:
+ 'Tidak ada tipe kustom yang cocok dengan filter Anda.',
+ new_type: 'Tipe Baru',
empty_state: {
- title: 'No custom types',
+ title: 'Tidak ada tipe kustom',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Tipe kustom akan muncul di sini ketika tersedia di database Anda',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Jenis',
+ enum_values: 'Nilai Enum',
+ composite_fields: 'Field',
+ no_fields: 'Tidak ada field yang ditentukan',
no_values: 'Tidak ada nilai enum yang ditentukan',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Nama field',
+ field_type_placeholder: 'Pilih tipe',
+ add_field: 'Tambah Field',
+ no_fields_tooltip:
+ 'Tidak ada field yang ditentukan untuk tipe kustom ini',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Aksi',
+ highlight_fields: 'Sorot Field',
+ delete_custom_type: 'Hapus',
+ clear_field_highlight: 'Hapus Sorotan',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Hapus Tipe',
},
},
},
@@ -277,8 +319,7 @@ export const id_ID: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Filter Tabel',
},
new_diagram_dialog: {
@@ -309,7 +350,7 @@ export const id_ID: LanguageTranslation = {
cancel: 'Batal',
import_from_file: 'Impor dari file',
back: 'Kembali',
- empty_diagram: 'Diagram Kosong',
+ empty_diagram: 'Database Kosong',
continue: 'Lanjutkan',
import: 'Impor',
},
@@ -325,6 +366,7 @@ export const id_ID: LanguageTranslation = {
},
cancel: 'Batal',
open: 'Buka',
+ new_database: 'Database Baru',
diagram_actions: {
open: 'Buka',
@@ -387,10 +429,9 @@ export const id_ID: LanguageTranslation = {
export_image_dialog: {
title: 'Ekspor Gambar',
description: 'Pilih faktor skala untuk ekspor:',
- scale_1x: '1x Biasa',
- scale_2x: '2x (Disarankan)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Kualitas Rendah)',
+ scale_2x: '2x (Kualitas Normal)',
+ scale_4x: '4x (Kualitas Terbaik)',
cancel: 'Batal',
export: 'Ekspor',
// TODO: Translate
@@ -400,7 +441,12 @@ export const id_ID: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Pilih Skema',
description:
@@ -483,7 +529,8 @@ export const id_ID: LanguageTranslation = {
new_view: 'Tampilan Baru',
new_relationship: 'Hubungan Baru',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'Area Baru',
+ new_note: 'Catatan Baru',
},
table_node_context_menu: {
@@ -493,6 +540,22 @@ export const id_ID: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Semua tabel tersembunyi',
+ show_all_tables: 'Tampilkan semua',
+ },
+
+ canvas_filter: {
+ title: 'Filter Tabel',
+ search_placeholder: 'Cari tabel...',
+ group_by_schema: 'Kelompokkan berdasarkan Skema',
+ group_by_area: 'Kelompokkan berdasarkan Area',
+ no_tables_found: 'Tidak ada tabel ditemukan',
+ empty_diagram_description: 'Buat tabel untuk memulai',
+ no_tables_description: 'Coba sesuaikan pencarian atau filter Anda',
+ clear_filter: 'Hapus filter',
+ },
+
snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts
index 47e0f3b62..c3031ad67 100644
--- a/src/i18n/locales/ja.ts
+++ b/src/i18n/locales/ja.ts
@@ -4,18 +4,18 @@ export const ja: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新規',
- browse: '参照',
+ browse: '開く',
tables: 'テーブル',
refs: '参照',
- areas: 'エリア',
dependencies: '依存関係',
custom_types: 'カスタムタイプ',
+ visuals: 'ビジュアル',
},
menu: {
actions: {
actions: 'アクション',
new: '新規...',
- browse: '参照...',
+ browse: 'すべてのデータベース...',
save: '保存',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
@@ -45,12 +45,13 @@ export const ja: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
- // TODO: Translate
backup: {
- backup: 'Backup',
- export_diagram: 'Export Diagram',
- restore_diagram: 'Restore Diagram',
+ backup: 'バックアップ',
+ export_diagram: 'ダイアグラムをエクスポート',
+ restore_diagram: 'ダイアグラムを復元',
},
help: {
help: 'ヘルプ',
@@ -132,16 +133,20 @@ export const ja: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'すべてのテーブルが非表示です',
+ show_all: 'すべて表示',
table: {
fields: 'フィールド',
nullable: 'NULL可能?',
primary_key: '主キー',
indexes: 'インデックス',
+ check_constraints: 'チェック制約',
comments: 'コメント',
no_comments: 'コメントがありません',
add_field: 'フィールドを追加',
add_index: 'インデックスを追加',
+ add_check: 'チェックを追加',
index_select_fields: 'フィールドを選択',
no_types_found: 'タイプが見つかりません',
field_name: '名前',
@@ -168,6 +173,11 @@ export const ja: LanguageTranslation = {
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除',
},
+ check_constraint_actions: {
+ title: 'チェック制約',
+ expression: '式',
+ delete: 'チェック制約を削除',
+ },
table_actions: {
title: 'テーブル操作',
change_schema: 'スキーマを変更',
@@ -192,9 +202,10 @@ export const ja: LanguageTranslation = {
relationship: {
relationship: 'リレーションシップ',
primary: '主テーブル',
- foreign: '参照テーブル',
+ foreign: '関連テーブル',
cardinality: 'カーディナリティ',
delete_relationship: '削除',
+ switch_tables: 'テーブルを入れ替え',
relationship_actions: {
title: '操作',
delete_relationship: '削除',
@@ -217,54 +228,83 @@ export const ja: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'エリア',
+ add_area: 'エリアを追加',
+ filter: 'フィルタ',
+ clear: 'フィルタをクリア',
+ no_results: 'フィルタに一致するエリアが見つかりません。',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'エリア操作',
+ edit_name: '名前を編集',
+ delete_area: 'エリアを削除',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'エリアがありません',
+ description: 'エリアを作成して開始してください',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'ビジュアル',
+ tabs: {
+ areas: 'エリア',
+ notes: 'ノート',
+ },
+ },
+
+ notes_section: {
+ filter: 'フィルター',
+ add_note: 'ノートを追加',
+ no_results: 'ノートが見つかりません',
+ clear: 'フィルターをクリア',
+ empty_state: {
+ title: 'ノートがありません',
+ description:
+ 'キャンバス上にテキスト注釈を追加するためのノートを作成',
+ },
+ note: {
+ empty_note: '空のノート',
+ note_actions: {
+ title: 'ノートアクション',
+ edit_content: 'コンテンツを編集',
+ delete_note: 'ノートを削除',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'カスタム型',
+ filter: 'フィルタ',
+ clear: 'フィルタをクリア',
+ no_results: 'フィルタに一致するカスタム型が見つかりません。',
+ new_type: '新しい型',
empty_state: {
- title: 'No custom types',
+ title: 'カスタム型がありません',
description:
- 'Custom types will appear here when they are available in your database',
+ 'データベースで利用可能になると、カスタム型がここに表示されます',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: '種類',
+ enum_values: '列挙値',
+ composite_fields: 'フィールド',
+ no_fields: 'フィールドが定義されていません',
no_values: '列挙値が定義されていません',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'フィールド名',
+ field_type_placeholder: '型を選択',
+ add_field: 'フィールドを追加',
+ no_fields_tooltip:
+ 'このカスタム型にはフィールドが定義されていません',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: '操作',
+ highlight_fields: 'フィールドをハイライト',
+ delete_custom_type: '削除',
+ clear_field_highlight: 'ハイライトを解除',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: '型を削除',
},
},
},
@@ -281,8 +321,8 @@ export const ja: LanguageTranslation = {
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
- 'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
- filter: 'Filter Tables',
+ '「{{typeName}}」をハイライト中 - クリックで解除',
+ filter: 'テーブルをフィルタ',
},
new_diagram_dialog: {
@@ -314,7 +354,7 @@ export const ja: LanguageTranslation = {
back: '戻る',
// TODO: Translate
import_from_file: 'Import from File',
- empty_diagram: '空のダイアグラム',
+ empty_diagram: '空のデータベース',
continue: '続行',
import: 'インポート',
},
@@ -330,6 +370,7 @@ export const ja: LanguageTranslation = {
},
cancel: 'キャンセル',
open: '開く',
+ new_database: '新しいデータベース',
diagram_actions: {
open: '開く',
@@ -393,10 +434,9 @@ export const ja: LanguageTranslation = {
export_image_dialog: {
title: '画像をエクスポート',
description: 'エクスポートの倍率を選択してください:',
- scale_1x: '1x 標準',
- scale_2x: '2x (推奨)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (低画質)',
+ scale_2x: '2x (通常画質)',
+ scale_4x: '4x (最高画質)',
cancel: 'キャンセル',
export: 'エクスポート',
// TODO: Translate
@@ -406,7 +446,12 @@ export const ja: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'スキーマを選択',
description:
@@ -487,8 +532,8 @@ export const ja: LanguageTranslation = {
new_table: '新しいテーブル',
new_view: '新しいビュー',
new_relationship: '新しいリレーションシップ',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: '新しいエリア',
+ new_note: '新しいメモ',
},
table_node_context_menu: {
@@ -498,6 +543,22 @@ export const ja: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'すべてのテーブルが非表示です',
+ show_all_tables: 'すべて表示',
+ },
+
+ canvas_filter: {
+ title: 'テーブルをフィルター',
+ search_placeholder: 'テーブルを検索...',
+ group_by_schema: 'スキーマでグループ化',
+ group_by_area: 'エリアでグループ化',
+ no_tables_found: 'テーブルが見つかりません',
+ empty_diagram_description: 'テーブルを作成して開始',
+ no_tables_description: '検索またはフィルターを調整してください',
+ clear_filter: 'フィルターをクリア',
+ },
+
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/ko_KR.ts b/src/i18n/locales/ko_KR.ts
index b660397d6..06fa7aad0 100644
--- a/src/i18n/locales/ko_KR.ts
+++ b/src/i18n/locales/ko_KR.ts
@@ -4,18 +4,18 @@ export const ko_KR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '새로 만들기',
- browse: '찾아보기',
+ browse: '열기',
tables: '테이블',
refs: 'Refs',
- areas: '영역',
dependencies: '종속성',
custom_types: '사용자 지정 타입',
+ visuals: '시각화',
},
menu: {
actions: {
actions: '작업',
new: '새로 만들기...',
- browse: '찾아보기...',
+ browse: '모든 데이터베이스...',
save: '저장',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
@@ -44,6 +44,8 @@ export const ko_KR: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: '백업',
@@ -128,16 +130,20 @@ export const ko_KR: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: '모든 테이블이 숨겨져 있습니다',
+ show_all: '모두 표시',
table: {
fields: '필드',
nullable: 'null 여부',
primary_key: '기본키',
indexes: '인덱스',
+ check_constraints: '체크 제약조건',
comments: '주석',
no_comments: '주석 없음',
add_field: '필드 추가',
add_index: '인덱스 추가',
+ add_check: '체크 추가',
index_select_fields: '필드 선택',
no_types_found: '타입을 찾을 수 없습니다.',
field_name: '이름',
@@ -164,6 +170,11 @@ export const ko_KR: LanguageTranslation = {
index_type: '인덱스 타입',
delete_index: '인덱스 삭제',
},
+ check_constraint_actions: {
+ title: '체크 제약조건',
+ expression: '표현식',
+ delete: '체크 제약조건 삭제',
+ },
table_actions: {
title: '테이블 작업',
change_schema: '스키마 변경',
@@ -188,9 +199,10 @@ export const ko_KR: LanguageTranslation = {
relationship: {
relationship: '연관 관계',
primary: '주 테이블',
- foreign: '참조 테이블',
+ foreign: '관련 테이블',
cardinality: '카디널리티',
delete_relationship: '제거',
+ switch_tables: '테이블 전환',
relationship_actions: {
title: '연관 관계 작업',
delete_relationship: '연관 관계 삭제',
@@ -212,54 +224,84 @@ export const ko_KR: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: '영역',
+ add_area: '영역 추가',
+ filter: '필터',
+ clear: '필터 지우기',
+ no_results: '필터와 일치하는 영역을 찾을 수 없습니다.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: '영역 작업',
+ edit_name: '이름 편집',
+ delete_area: '영역 삭제',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: '영역 없음',
+ description: '영역을 만들어 시작하세요',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: '시각화',
+ tabs: {
+ areas: '영역',
+ notes: '메모',
+ },
+ },
+
+ notes_section: {
+ filter: '필터',
+ add_note: '메모 추가',
+ no_results: '메모를 찾을 수 없습니다',
+ clear: '필터 지우기',
+ empty_state: {
+ title: '메모 없음',
+ description:
+ '캔버스에 텍스트 주석을 추가하려면 메모를 만드세요',
+ },
+ note: {
+ empty_note: '빈 메모',
+ note_actions: {
+ title: '메모 작업',
+ edit_content: '내용 편집',
+ delete_note: '메모 삭제',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: '사용자 정의 타입',
+ filter: '필터',
+ clear: '필터 지우기',
+ no_results:
+ '필터와 일치하는 사용자 정의 타입을 찾을 수 없습니다.',
+ new_type: '새 타입',
empty_state: {
- title: 'No custom types',
+ title: '사용자 정의 타입 없음',
description:
- 'Custom types will appear here when they are available in your database',
+ '데이터베이스에서 사용 가능한 사용자 정의 타입이 여기에 표시됩니다',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: '종류',
+ enum_values: '열거형 값',
+ composite_fields: '필드',
+ no_fields: '정의된 필드 없음',
no_values: '정의된 열거형 값이 없습니다',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: '필드 이름',
+ field_type_placeholder: '타입 선택',
+ add_field: '필드 추가',
+ no_fields_tooltip:
+ '이 사용자 정의 타입에 정의된 필드가 없습니다',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: '작업',
+ highlight_fields: '필드 강조 표시',
+ delete_custom_type: '삭제',
+ clear_field_highlight: '강조 표시 지우기',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: '타입 삭제',
},
},
},
@@ -277,8 +319,7 @@ export const ko_KR: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: '테이블 필터',
},
new_diagram_dialog: {
@@ -309,7 +350,7 @@ export const ko_KR: LanguageTranslation = {
cancel: '취소',
back: '뒤로가기',
import_from_file: '파일에서 가져오기',
- empty_diagram: '빈 다이어그램으로 시작',
+ empty_diagram: '빈 데이터베이스',
continue: '계속',
import: '가져오기',
},
@@ -325,6 +366,7 @@ export const ko_KR: LanguageTranslation = {
},
cancel: '취소',
open: '열기',
+ new_database: '새 데이터베이스',
diagram_actions: {
open: '열기',
@@ -387,10 +429,9 @@ export const ko_KR: LanguageTranslation = {
export_image_dialog: {
title: '이미지로 내보내기',
description: '내보낼 배율을 선택해주세요:',
- scale_1x: '1x 기본',
- scale_2x: '2x (권장)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (저화질)',
+ scale_2x: '2x (일반 화질)',
+ scale_4x: '4x (최고 화질)',
cancel: '취소',
export: '내보내기',
// TODO: Translate
@@ -400,7 +441,12 @@ export const ko_KR: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: '스키마 선택',
description:
@@ -479,8 +525,8 @@ export const ko_KR: LanguageTranslation = {
new_table: '새 테이블',
new_view: '새 뷰',
new_relationship: '새 연관관계',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: '새 영역',
+ new_note: '새 메모',
},
table_node_context_menu: {
@@ -490,6 +536,22 @@ export const ko_KR: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: '모든 테이블이 숨겨져 있습니다',
+ show_all_tables: '모두 표시',
+ },
+
+ canvas_filter: {
+ title: '테이블 필터',
+ search_placeholder: '테이블 검색...',
+ group_by_schema: '스키마별 그룹화',
+ group_by_area: '영역별 그룹화',
+ no_tables_found: '테이블을 찾을 수 없습니다',
+ empty_diagram_description: '시작하려면 테이블을 만드세요',
+ no_tables_description: '검색 또는 필터를 조정해 보세요',
+ clear_filter: '필터 지우기',
+ },
+
snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',
tool_tips: {
diff --git a/src/i18n/locales/mr.ts b/src/i18n/locales/mr.ts
index 18b0f74b7..67a42f2a0 100644
--- a/src/i18n/locales/mr.ts
+++ b/src/i18n/locales/mr.ts
@@ -4,18 +4,18 @@ export const mr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नवीन',
- browse: 'ब्राउज',
+ browse: 'उघडा',
tables: 'टेबल',
refs: 'Refs',
- areas: 'क्षेत्रे',
dependencies: 'अवलंबने',
custom_types: 'कस्टम प्रकार',
+ visuals: 'Visuals',
},
menu: {
actions: {
actions: 'क्रिया',
new: 'नवीन...',
- browse: 'ब्राउज करा...',
+ browse: 'सर्व डेटाबेस...',
save: 'जतन करा',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
@@ -44,6 +44,8 @@ export const mr: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
// TODO: Add translations
@@ -131,16 +133,20 @@ export const mr: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'सर्व टेबल्स लपवलेले आहेत',
+ show_all: 'सर्व दाखवा',
table: {
fields: 'फील्ड्स',
nullable: 'नल करण्यायोग्य?',
primary_key: 'प्राथमिक की',
indexes: 'सूचकांक',
+ check_constraints: 'तपासणी निर्बंध',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
add_field: 'फील्ड जोडा',
add_index: 'सूचकांक जोडा',
+ add_check: 'तपासणी जोडा',
index_select_fields: 'फील्ड निवडा',
no_types_found: 'कोणतेही प्रकार सापडले नाहीत',
field_name: 'नाव',
@@ -167,6 +173,11 @@ export const mr: LanguageTranslation = {
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा',
},
+ check_constraint_actions: {
+ title: 'तपासणी निर्बंध',
+ expression: 'अभिव्यक्ती',
+ delete: 'निर्बंध हटवा',
+ },
table_actions: {
title: 'टेबल एक्शन',
change_schema: 'स्कीमा बदला',
@@ -192,9 +203,10 @@ export const mr: LanguageTranslation = {
relationship: {
relationship: 'रिलेशनशिप',
primary: 'प्राथमिक टेबल',
- foreign: 'रेफरंस टेबल',
+ foreign: 'संबंधित टेबल',
cardinality: 'कार्डिनॅलिटी',
delete_relationship: 'हटवा',
+ switch_tables: 'टेबल बदला',
relationship_actions: {
title: 'क्रिया',
delete_relationship: 'हटवा',
@@ -216,54 +228,85 @@ export const mr: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'क्षेत्रे',
+ add_area: 'क्षेत्र जोडा',
+ filter: 'फिल्टर',
+ clear: 'फिल्टर साफ करा',
+ no_results:
+ 'तुमच्या फिल्टरशी जुळणारे कोणतेही क्षेत्र सापडले नाही।',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'क्षेत्र क्रिया',
+ edit_name: 'नाव संपादित करा',
+ delete_area: 'क्षेत्र हटवा',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'क्षेत्रे नाहीत',
+ description: 'सुरू करण्यासाठी क्षेत्र तयार करा',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuals',
+ tabs: {
+ areas: 'क्षेत्रे',
+ notes: 'नोट्स',
+ },
+ },
+
+ notes_section: {
+ filter: 'फिल्टर',
+ add_note: 'नोट जोडा',
+ no_results: 'कोणत्याही नोट्स सापडल्या नाहीत',
+ clear: 'फिल्टर साफ करा',
+ empty_state: {
+ title: 'नोट्स नाहीत',
+ description:
+ 'कॅनव्हासवर मजकूर भाष्य जोडण्यासाठी एक नोट तयार करा',
+ },
+ note: {
+ empty_note: 'रिकामी नोट',
+ note_actions: {
+ title: 'नोट क्रिया',
+ edit_content: 'सामग्री संपादित करा',
+ delete_note: 'नोट हटवा',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'कस्टम प्रकार',
+ filter: 'फिल्टर',
+ clear: 'फिल्टर साफ करा',
+ no_results:
+ 'तुमच्या फिल्टरशी जुळणारा कोणताही कस्टम प्रकार सापडला नाही.',
+ new_type: 'नवीन प्रकार',
empty_state: {
- title: 'No custom types',
+ title: 'कस्टम प्रकार नाहीत',
description:
- 'Custom types will appear here when they are available in your database',
+ 'तुमच्या डेटाबेसमध्ये उपलब्ध असताना कस्टम प्रकार येथे दिसतील',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'प्रकार',
+ enum_values: 'Enum मूल्ये',
+ composite_fields: 'फील्ड्स',
+ no_fields: 'कोणतेही फील्ड परिभाषित नाहीत',
no_values: 'कोणतीही enum मूल्ये परिभाषित नाहीत',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'फील्डचे नाव',
+ field_type_placeholder: 'प्रकार निवडा',
+ add_field: 'फील्ड जोडा',
+ no_fields_tooltip:
+ 'या कस्टम प्रकारासाठी कोणतेही फील्ड परिभाषित नाहीत',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'क्रिया',
+ highlight_fields: 'फील्ड्स हायलाइट करा',
+ delete_custom_type: 'हटवा',
+ clear_field_highlight: 'हायलाइट काढा',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'प्रकार हटवा',
},
},
},
@@ -281,8 +324,7 @@ export const mr: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'टेबल्स फिल्टर करा',
},
new_diagram_dialog: {
@@ -315,7 +357,7 @@ export const mr: LanguageTranslation = {
// TODO: Add translations
import_from_file: 'Import from File',
back: 'मागे',
- empty_diagram: 'रिक्त आरेख',
+ empty_diagram: 'रिक्त डेटाबेस',
continue: 'सुरू ठेवा',
import: 'आयात करा',
},
@@ -331,6 +373,7 @@ export const mr: LanguageTranslation = {
},
cancel: 'रद्द करा',
open: 'उघडा',
+ new_database: 'नवीन डेटाबेस',
diagram_actions: {
open: 'उघडा',
@@ -394,10 +437,9 @@ export const mr: LanguageTranslation = {
export_image_dialog: {
title: 'इमेज निर्यात करा',
description: 'एक्स्पोर्ट करण्यासाठी स्केल फॅक्टर निवडा:',
- scale_1x: '1x नियमित',
- scale_2x: '2x (शिफारस केलेले)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (कमी गुणवत्ता)',
+ scale_2x: '2x (सामान्य गुणवत्ता)',
+ scale_4x: '4x (सर्वोत्तम गुणवत्ता)',
cancel: 'रद्द करा',
export: 'निर्यात करा',
// TODO: Translate
@@ -407,7 +449,12 @@ export const mr: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'स्कीमा निवडा',
description:
@@ -492,7 +539,8 @@ export const mr: LanguageTranslation = {
new_view: 'नवीन व्ह्यू',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'नवीन क्षेत्र',
+ new_note: 'नवीन टीप',
},
table_node_context_menu: {
@@ -502,6 +550,23 @@ export const mr: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'सर्व टेबल्स लपवलेले आहेत',
+ show_all_tables: 'सर्व दाखवा',
+ },
+
+ canvas_filter: {
+ title: 'टेबल्स फिल्टर करा',
+ search_placeholder: 'टेबल्स शोधा...',
+ group_by_schema: 'स्कीमानुसार गट करा',
+ group_by_area: 'क्षेत्रानुसार गट करा',
+ no_tables_found: 'कोणतेही टेबल सापडले नाही',
+ empty_diagram_description: 'सुरू करण्यासाठी टेबल तयार करा',
+ no_tables_description:
+ 'तुमची शोध किंवा फिल्टर समायोजित करण्याचा प्रयत्न करा',
+ clear_filter: 'फिल्टर साफ करा',
+ },
+
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/ne.ts b/src/i18n/locales/ne.ts
index d99922019..9ad8d6f55 100644
--- a/src/i18n/locales/ne.ts
+++ b/src/i18n/locales/ne.ts
@@ -4,18 +4,18 @@ export const ne: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नयाँ',
- browse: 'ब्राउज',
+ browse: 'खोल्नुहोस्',
tables: 'टेबलहरू',
refs: 'Refs',
- areas: 'क्षेत्रहरू',
dependencies: 'निर्भरताहरू',
custom_types: 'कस्टम प्रकारहरू',
+ visuals: 'Visuals',
},
menu: {
actions: {
actions: 'कार्यहरू',
new: 'नयाँ...',
- browse: 'ब्राउज गर्नुहोस्...',
+ browse: 'सबै डाटाबेसहरू...',
save: 'सुरक्षित गर्नुहोस्',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
@@ -44,6 +44,8 @@ export const ne: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
// TODO: Translate
backup: {
@@ -129,16 +131,20 @@ export const ne: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'सबै तालिकाहरू लुकेका छन्',
+ show_all: 'सबै देखाउनुहोस्',
table: {
fields: 'क्षेत्रहरू',
nullable: 'नलेबल?',
primary_key: 'प्राथमिक कुंजी',
indexes: 'सूचकहरू',
+ check_constraints: 'जाँच प्रतिबन्धहरू',
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
add_field: 'क्षेत्र थप्नुहोस्',
add_index: 'सूचक थप्नुहोस्',
+ add_check: 'जाँच थप्नुहोस्',
index_select_fields: 'क्षेत्रहरू चयन गर्नुहोस्',
no_types_found: 'कुनै प्रकारहरू फेला परेनन्',
field_name: 'नाम',
@@ -165,6 +171,11 @@ export const ne: LanguageTranslation = {
index_type: 'इन्डेक्स प्रकार',
delete_index: 'सूचक हटाउनुहोस्',
},
+ check_constraint_actions: {
+ title: 'जाँच प्रतिबन्ध',
+ expression: 'अभिव्यक्ति',
+ delete: 'प्रतिबन्ध हटाउनुहोस्',
+ },
table_actions: {
title: 'तालिका विशेषताहरू',
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
@@ -189,9 +200,10 @@ export const ne: LanguageTranslation = {
relationship: {
relationship: 'सम्बन्ध',
primary: 'मुख्य तालिका',
- foreign: 'परिचित तालिका',
+ foreign: 'सम्बन्धित तालिका',
cardinality: 'कार्डिन्यालिटी',
delete_relationship: 'हटाउनुहोस्',
+ switch_tables: 'तालिकाहरू साट्नुहोस्',
relationship_actions: {
title: 'कार्यहरू',
delete_relationship: 'हटाउनुहोस्',
@@ -213,54 +225,84 @@ export const ne: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'क्षेत्रहरू',
+ add_area: 'क्षेत्र थप्नुहोस्',
+ filter: 'फिल्टर',
+ clear: 'फिल्टर खाली गर्नुहोस्',
+ no_results: 'तपाईंको फिल्टरसँग मिल्ने कुनै क्षेत्र फेला परेन।',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'क्षेत्र कार्यहरू',
+ edit_name: 'नाम सम्पादन गर्नुहोस्',
+ delete_area: 'क्षेत्र मेट्नुहोस्',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'कुनै क्षेत्र छैन',
+ description: 'सुरु गर्न क्षेत्र बनाउनुहोस्',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuals',
+ tabs: {
+ areas: 'क्षेत्रहरू',
+ notes: 'टिप्पणीहरू',
+ },
+ },
+
+ notes_section: {
+ filter: 'फिल्टर',
+ add_note: 'टिप्पणी थप्नुहोस्',
+ no_results: 'कुनै टिप्पणी फेला परेन',
+ clear: 'फिल्टर खाली गर्नुहोस्',
+ empty_state: {
+ title: 'कुनै टिप्पणी छैन',
+ description:
+ 'क्यानभासमा पाठ टिप्पणी थप्न टिप्पणी सिर्जना गर्नुहोस्',
+ },
+ note: {
+ empty_note: 'खाली टिप्पणी',
+ note_actions: {
+ title: 'टिप्पणी कार्यहरू',
+ edit_content: 'सामग्री सम्पादन गर्नुहोस्',
+ delete_note: 'टिप्पणी मेटाउनुहोस्',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'कस्टम प्रकारहरू',
+ filter: 'फिल्टर',
+ clear: 'फिल्टर खाली गर्नुहोस्',
+ no_results:
+ 'तपाईंको फिल्टरसँग मिल्ने कुनै कस्टम प्रकार फेला परेन।',
+ new_type: 'नयाँ प्रकार',
empty_state: {
- title: 'No custom types',
+ title: 'कुनै कस्टम प्रकार छैन',
description:
- 'Custom types will appear here when they are available in your database',
+ 'तपाईंको डाटाबेसमा उपलब्ध हुँदा कस्टम प्रकारहरू यहाँ देखिनेछन्',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'प्रकार',
+ enum_values: 'Enum मानहरू',
+ composite_fields: 'फिल्डहरू',
+ no_fields: 'कुनै फिल्ड परिभाषित छैन',
no_values: 'कुनै enum मानहरू परिभाषित छैनन्',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'फिल्डको नाम',
+ field_type_placeholder: 'प्रकार छान्नुहोस्',
+ add_field: 'फिल्ड थप्नुहोस्',
+ no_fields_tooltip:
+ 'यस कस्टम प्रकारका लागि कुनै फिल्ड परिभाषित छैन',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'कार्यहरू',
+ highlight_fields: 'फिल्डहरू हाइलाइट गर्नुहोस्',
+ delete_custom_type: 'मेट्नुहोस्',
+ clear_field_highlight: 'हाइलाइट हटाउनुहोस्',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'प्रकार मेट्नुहोस्',
},
},
},
@@ -279,8 +321,7 @@ export const ne: LanguageTranslation = {
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables:
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'तालिकाहरू फिल्टर गर्नुहोस्',
},
new_diagram_dialog: {
@@ -311,7 +352,7 @@ export const ne: LanguageTranslation = {
cancel: 'रद्द गर्नुहोस्',
import_from_file: 'फाइलबाट आयात गर्नुहोस्',
back: 'फर्क',
- empty_diagram: 'रिक्त डायाग्राम',
+ empty_diagram: 'खाली डाटाबेस',
continue: 'जारी राख्नुहोस्',
import: 'आयात गर्नुहोस्',
},
@@ -328,6 +369,7 @@ export const ne: LanguageTranslation = {
},
cancel: 'रद्द गर्नुहोस्',
open: 'खोल्नुहोस्',
+ new_database: 'नयाँ डाटाबेस',
diagram_actions: {
open: 'खोल्नुहोस्',
@@ -391,10 +433,9 @@ export const ne: LanguageTranslation = {
export_image_dialog: {
title: 'इमेज निर्यात गर्नुहोस्',
description: 'निर्यात गर्नका लागि गणना कारक छान्नुहोस्:',
- scale_1x: '१x सामान्य',
- scale_2x: '२x (सिफारिस गरिएको)',
- scale_3x: '३x',
- scale_4x: '४x',
+ scale_1x: '१x (कम गुणस्तर)',
+ scale_2x: '२x (सामान्य गुणस्तर)',
+ scale_4x: '४x (उत्तम गुणस्तर)',
cancel: 'रद्द गर्नुहोस्',
export: 'निर्यात गर्नुहोस्',
// TODO: Translate
@@ -404,7 +445,12 @@ export const ne: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'स्कीम चयन गर्नुहोस्',
description:
@@ -486,7 +532,8 @@ export const ne: LanguageTranslation = {
new_view: 'नयाँ भ्यू',
new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'नयाँ क्षेत्र',
+ new_note: 'नयाँ नोट',
},
table_node_context_menu: {
@@ -496,6 +543,23 @@ export const ne: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'सबै तालिकाहरू लुकेका छन्',
+ show_all_tables: 'सबै देखाउनुहोस्',
+ },
+
+ canvas_filter: {
+ title: 'तालिकाहरू फिल्टर गर्नुहोस्',
+ search_placeholder: 'तालिकाहरू खोज्नुहोस्...',
+ group_by_schema: 'स्कीमा अनुसार समूह गर्नुहोस्',
+ group_by_area: 'क्षेत्र अनुसार समूह गर्नुहोस्',
+ no_tables_found: 'कुनै तालिका भेटिएन',
+ empty_diagram_description: 'सुरु गर्न तालिका बनाउनुहोस्',
+ no_tables_description:
+ 'तपाईंको खोज वा फिल्टर समायोजन गर्ने प्रयास गर्नुहोस्',
+ clear_filter: 'फिल्टर हटाउनुहोस्',
+ },
+
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',
tool_tips: {
diff --git a/src/i18n/locales/pt_BR.ts b/src/i18n/locales/pt_BR.ts
index 1f30024fd..0b522933f 100644
--- a/src/i18n/locales/pt_BR.ts
+++ b/src/i18n/locales/pt_BR.ts
@@ -4,18 +4,18 @@ export const pt_BR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Novo',
- browse: 'Navegar',
+ browse: 'Abrir',
tables: 'Tabelas',
refs: 'Refs',
- areas: 'Áreas',
dependencies: 'Dependências',
custom_types: 'Tipos Personalizados',
+ visuals: 'Visuais',
},
menu: {
actions: {
actions: 'Ações',
new: 'Novo...',
- browse: 'Navegar...',
+ browse: 'Todos os bancos de dados...',
save: 'Salvar',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
@@ -44,6 +44,8 @@ export const pt_BR: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
// TODO: Translate
backup: {
@@ -129,16 +131,20 @@ export const pt_BR: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Todas as tabelas estão ocultas',
+ show_all: 'Mostrar tudo',
table: {
fields: 'Campos',
nullable: 'Permite Nulo?',
primary_key: 'Chave Primária',
indexes: 'Índices',
+ check_constraints: 'Restrições de verificação',
comments: 'Comentários',
no_comments: 'Sem comentários',
add_field: 'Adicionar Campo',
add_index: 'Adicionar Índice',
+ add_check: 'Adicionar verificação',
index_select_fields: 'Selecionar campos',
no_types_found: 'Nenhum tipo encontrado',
field_name: 'Nome',
@@ -165,6 +171,11 @@ export const pt_BR: LanguageTranslation = {
index_type: 'Tipo de Índice',
delete_index: 'Excluir Índice',
},
+ check_constraint_actions: {
+ title: 'Restrição de verificação',
+ expression: 'Expressão',
+ delete: 'Excluir restrição',
+ },
table_actions: {
title: 'Ações da Tabela',
change_schema: 'Alterar Esquema',
@@ -189,9 +200,10 @@ export const pt_BR: LanguageTranslation = {
relationship: {
relationship: 'Relacionamento',
primary: 'Tabela Primária',
- foreign: 'Tabela Referenciada',
+ foreign: 'Tabela Relacionada',
cardinality: 'Cardinalidade',
delete_relationship: 'Excluir',
+ switch_tables: 'Trocar Tabelas',
relationship_actions: {
title: 'Ações',
delete_relationship: 'Excluir',
@@ -213,54 +225,85 @@ export const pt_BR: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'Áreas',
+ add_area: 'Adicionar Área',
+ filter: 'Filtrar',
+ clear: 'Limpar Filtro',
+ no_results:
+ 'Nenhuma área encontrada correspondente ao seu filtro.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Ações da Área',
+ edit_name: 'Editar Nome',
+ delete_area: 'Excluir Área',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Sem áreas',
+ description: 'Crie uma área para começar',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuais',
+ tabs: {
+ areas: 'Áreas',
+ notes: 'Notas',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filtrar',
+ add_note: 'Adicionar Nota',
+ no_results: 'Nenhuma nota encontrada',
+ clear: 'Limpar Filtro',
+ empty_state: {
+ title: 'Sem Notas',
+ description:
+ 'Crie uma nota para adicionar anotações de texto na tela',
+ },
+ note: {
+ empty_note: 'Nota vazia',
+ note_actions: {
+ title: 'Ações de Nota',
+ edit_content: 'Editar Conteúdo',
+ delete_note: 'Excluir Nota',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Tipos Personalizados',
+ filter: 'Filtrar',
+ clear: 'Limpar Filtro',
+ no_results:
+ 'Nenhum tipo personalizado encontrado correspondente ao seu filtro.',
+ new_type: 'Novo Tipo',
empty_state: {
- title: 'No custom types',
+ title: 'Sem tipos personalizados',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Os tipos personalizados aparecerão aqui quando estiverem disponíveis no seu banco de dados',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Tipo',
+ enum_values: 'Valores Enum',
+ composite_fields: 'Campos',
+ no_fields: 'Nenhum campo definido',
no_values: 'Nenhum valor de enum definido',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Nome do campo',
+ field_type_placeholder: 'Selecionar tipo',
+ add_field: 'Adicionar Campo',
+ no_fields_tooltip:
+ 'Nenhum campo definido para este tipo personalizado',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Ações',
+ highlight_fields: 'Destacar Campos',
+ delete_custom_type: 'Excluir',
+ clear_field_highlight: 'Remover Destaque',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Excluir Tipo',
},
},
},
@@ -278,8 +321,7 @@ export const pt_BR: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Filtrar Tabelas',
},
new_diagram_dialog: {
@@ -311,7 +353,7 @@ export const pt_BR: LanguageTranslation = {
back: 'Voltar',
// TODO: Translate
import_from_file: 'Import from File',
- empty_diagram: 'Diagrama vazio',
+ empty_diagram: 'Banco de dados vazio',
continue: 'Continuar',
import: 'Importar',
},
@@ -327,6 +369,7 @@ export const pt_BR: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
+ new_database: 'Novo Banco de Dados',
diagram_actions: {
open: 'Abrir',
@@ -390,10 +433,9 @@ export const pt_BR: LanguageTranslation = {
export_image_dialog: {
title: 'Exportar Imagem',
description: 'Escolha o fator de escala para exportação:',
- scale_1x: '1x Normal',
- scale_2x: '2x (Recomendado)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Baixa Qualidade)',
+ scale_2x: '2x (Qualidade Normal)',
+ scale_4x: '4x (Melhor Qualidade)',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
@@ -403,7 +445,12 @@ export const pt_BR: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Selecionar Esquema',
description:
@@ -484,8 +531,8 @@ export const pt_BR: LanguageTranslation = {
new_table: 'Nova Tabela',
new_view: 'Nova Visualização',
new_relationship: 'Novo Relacionamento',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: 'Nova Área',
+ new_note: 'Nova Nota',
},
table_node_context_menu: {
@@ -495,6 +542,22 @@ export const pt_BR: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Todas as tabelas estão ocultas',
+ show_all_tables: 'Mostrar tudo',
+ },
+
+ canvas_filter: {
+ title: 'Filtrar Tabelas',
+ search_placeholder: 'Pesquisar tabelas...',
+ group_by_schema: 'Agrupar por Esquema',
+ group_by_area: 'Agrupar por Área',
+ no_tables_found: 'Nenhuma tabela encontrada',
+ empty_diagram_description: 'Crie uma tabela para começar',
+ no_tables_description: 'Tente ajustar sua pesquisa ou filtro',
+ clear_filter: 'Limpar filtro',
+ },
+
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts
index 8a3de3dba..f02b5cc3d 100644
--- a/src/i18n/locales/ru.ts
+++ b/src/i18n/locales/ru.ts
@@ -4,18 +4,18 @@ export const ru: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Новая',
- browse: 'Обзор',
+ browse: 'Открыть',
tables: 'Таблицы',
refs: 'Ссылки',
- areas: 'Области',
dependencies: 'Зависимости',
custom_types: 'Пользовательские типы',
+ visuals: 'Визуальные элементы',
},
menu: {
actions: {
actions: 'Действия',
new: 'Новая...',
- browse: 'Обзор...',
+ browse: 'Все базы данных...',
save: 'Сохранить',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
@@ -43,6 +43,8 @@ export const ru: LanguageTranslation = {
hide_dependencies: 'Скрыть зависимости',
show_minimap: 'Показать мини-карту',
hide_minimap: 'Скрыть мини-карту',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Бэкап',
@@ -126,16 +128,20 @@ export const ru: LanguageTranslation = {
'Таблицы не найдены, соответствующие вашему фильтру.',
show_list: 'Переключиться на список таблиц',
show_dbml: 'Переключиться на редактор DBML',
+ all_hidden: 'Все таблицы скрыты',
+ show_all: 'Показать все',
table: {
fields: 'Поля',
nullable: 'Может быть NULL?',
primary_key: 'Первичный ключ',
indexes: 'Индексы',
+ check_constraints: 'Проверочные ограничения',
comments: 'Комментарии',
no_comments: 'Нет комментария',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
+ add_check: 'Добавить проверку',
index_select_fields: 'Выберите поля',
no_types_found: 'Типы не найдены',
field_name: 'Имя',
@@ -161,6 +167,11 @@ export const ru: LanguageTranslation = {
index_type: 'Тип индекса',
delete_index: 'Удалить индекс',
},
+ check_constraint_actions: {
+ title: 'Проверочное ограничение',
+ expression: 'Выражение',
+ delete: 'Удалить ограничение',
+ },
table_actions: {
title: 'Действия',
change_schema: 'Изменить схему',
@@ -185,9 +196,10 @@ export const ru: LanguageTranslation = {
relationship: {
relationship: 'Отношение',
primary: 'Основная таблица',
- foreign: 'Справочная таблица',
+ foreign: 'Связанная таблица',
cardinality: 'Тип множественной связи',
delete_relationship: 'Удалить',
+ switch_tables: 'Поменять таблицы',
relationship_actions: {
title: 'Действия',
delete_relationship: 'Удалить',
@@ -230,34 +242,65 @@ export const ru: LanguageTranslation = {
description: 'Создайте область, чтобы начать',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Визуальные элементы',
+ tabs: {
+ areas: 'Области',
+ notes: 'Заметки',
+ },
+ },
+
+ notes_section: {
+ filter: 'Фильтр',
+ add_note: 'Добавить Заметку',
+ no_results: 'Заметки не найдены',
+ clear: 'Очистить Фильтр',
+ empty_state: {
+ title: 'Нет Заметок',
+ description:
+ 'Создайте заметку, чтобы добавить текстовые аннотации на холсте',
+ },
+ note: {
+ empty_note: 'Пустая заметка',
+ note_actions: {
+ title: 'Действия с Заметкой',
+ edit_content: 'Редактировать Содержимое',
+ delete_note: 'Удалить Заметку',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Пользовательские типы',
+ filter: 'Фильтр',
+ clear: 'Очистить фильтр',
+ no_results:
+ 'Не найдено пользовательских типов, соответствующих фильтру.',
+ new_type: 'Новый тип',
empty_state: {
- title: 'No custom types',
+ title: 'Нет пользовательских типов',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Пользовательские типы появятся здесь, когда будут доступны в вашей базе данных',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Вид',
+ enum_values: 'Значения перечисления',
+ composite_fields: 'Поля',
+ no_fields: 'Поля не определены',
no_values: 'Значения перечисления не определены',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Имя поля',
+ field_type_placeholder: 'Выберите тип',
+ add_field: 'Добавить поле',
+ no_fields_tooltip:
+ 'Для этого пользовательского типа поля не определены',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Действия',
+ highlight_fields: 'Выделить поля',
+ delete_custom_type: 'Удалить',
+ clear_field_highlight: 'Снять выделение',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Удалить тип',
},
},
},
@@ -275,8 +318,7 @@ export const ru: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Фильтровать таблицы',
},
new_diagram_dialog: {
@@ -307,7 +349,7 @@ export const ru: LanguageTranslation = {
cancel: 'Отменить',
back: 'Назад',
import_from_file: 'Импортировать из файла',
- empty_diagram: 'Пустая диаграмма',
+ empty_diagram: 'Пустая база данных',
continue: 'Продолжить',
import: 'Импорт',
},
@@ -324,6 +366,7 @@ export const ru: LanguageTranslation = {
},
cancel: 'Отмена',
open: 'Открыть',
+ new_database: 'Новая база данных',
diagram_actions: {
open: 'Открыть',
@@ -387,10 +430,9 @@ export const ru: LanguageTranslation = {
export_image_dialog: {
title: 'Экспортировать изображение',
description: 'Выберите детализацию изображения при экспорте:',
- scale_1x: '1x Обычный',
- scale_2x: '2x (Рекомендовано)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Низкое качество)',
+ scale_2x: '2x (Обычное качество)',
+ scale_4x: '4x (Лучшее качество)',
cancel: 'Отменить',
export: 'Экспортировать',
// TODO: Translate
@@ -400,7 +442,12 @@ export const ru: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Выбрать схему',
description:
@@ -481,6 +528,7 @@ export const ru: LanguageTranslation = {
new_view: 'Новое представление',
new_relationship: 'Создать отношение',
new_area: 'Новая область',
+ new_note: 'Новая Заметка',
},
table_node_context_menu: {
@@ -490,6 +538,22 @@ export const ru: LanguageTranslation = {
add_relationship: 'Добавить связь',
},
+ canvas: {
+ all_tables_hidden: 'Все таблицы скрыты',
+ show_all_tables: 'Показать все',
+ },
+
+ canvas_filter: {
+ title: 'Фильтр таблиц',
+ search_placeholder: 'Поиск таблиц...',
+ group_by_schema: 'Группировать по схеме',
+ group_by_area: 'Группировать по области',
+ no_tables_found: 'Таблицы не найдены',
+ empty_diagram_description: 'Создайте таблицу, чтобы начать',
+ no_tables_description: 'Попробуйте изменить поиск или фильтр',
+ clear_filter: 'Очистить фильтр',
+ },
+
copy_to_clipboard: 'Скопировать в буфер обмена',
copied: 'Скопировано!',
snap_to_grid_tooltip: 'Выравнивание по сетке (Удерживайте {{key}})',
diff --git a/src/i18n/locales/te.ts b/src/i18n/locales/te.ts
index ed258e6ee..7770d52a7 100644
--- a/src/i18n/locales/te.ts
+++ b/src/i18n/locales/te.ts
@@ -4,18 +4,18 @@ export const te: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'కొత్తది',
- browse: 'బ్రాఉజ్',
+ browse: 'తెరవు',
tables: 'టేబల్లు',
refs: 'సంబంధాలు',
- areas: 'ప్రదేశాలు',
dependencies: 'ఆధారతలు',
custom_types: 'కస్టమ్ టైప్స్',
+ visuals: 'Visuals',
},
menu: {
actions: {
actions: 'చర్యలు',
new: 'కొత్తది...',
- browse: 'బ్రాఉజ్ చేయండి...',
+ browse: 'అన్ని డేటాబేస్లు...',
save: 'సేవ్',
import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
@@ -44,6 +44,8 @@ export const te: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
// TODO: Translate
backup: {
@@ -129,16 +131,20 @@ export const te: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'అన్ని పట్టికలు దాచబడ్డాయి',
+ show_all: 'అన్ని చూపించు',
table: {
fields: 'ఫీల్డులు',
nullable: 'నల్వాలు?',
primary_key: 'ప్రాథమిక కీ',
indexes: 'ఇండెక్సులు',
+ check_constraints: 'తనిఖీ పరిమితులు',
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
add_field: 'ఫీల్డ్ జోడించు',
add_index: 'ఇండెక్స్ జోడించు',
+ add_check: 'తనిఖీ జోడించు',
index_select_fields: 'ఫీల్డ్స్ ఎంచుకోండి',
no_types_found: 'ప్రకృతులు కనుగొనబడలేదు',
field_name: 'పేరు',
@@ -165,6 +171,11 @@ export const te: LanguageTranslation = {
index_type: 'ఇండెక్స్ రకం',
delete_index: 'ఇండెక్స్ తొలగించు',
},
+ check_constraint_actions: {
+ title: 'తనిఖీ పరిమితి',
+ expression: 'వ్యక్తీకరణ',
+ delete: 'పరిమితిని తొలగించు',
+ },
table_actions: {
title: 'పట్టిక చర్యలు',
change_schema: 'స్కీమాను మార్చు',
@@ -190,9 +201,10 @@ export const te: LanguageTranslation = {
relationship: {
relationship: 'సంబంధం',
primary: 'ప్రాథమిక పట్టిక',
- foreign: 'సూచించబడిన పట్టిక',
+ foreign: 'సంబంధిత పట్టిక',
cardinality: 'కార్డినాలిటీ',
delete_relationship: 'సంబంధం తొలగించు',
+ switch_tables: 'పట్టికలను మార్చు',
relationship_actions: {
title: 'చర్యలు',
delete_relationship: 'సంబంధం తొలగించు',
@@ -214,54 +226,83 @@ export const te: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'ప్రాంతాలు',
+ add_area: 'ప్రాంతం జోడించండి',
+ filter: 'ఫిల్టర్',
+ clear: 'ఫిల్టర్ను క్లియర్ చేయండి',
+ no_results: 'మీ ఫిల్టర్కు సరిపోలే ప్రాంతాలు కనుగొనబడలేదు.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'ప్రాంత చర్యలు',
+ edit_name: 'పేరు సవరించండి',
+ delete_area: 'ప్రాంతాన్ని తొలగించండి',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'ప్రాంతాలు లేవు',
+ description: 'ప్రారంభించడానికి ఒక ప్రాంతం సృష్టించండి',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Visuals',
+ tabs: {
+ areas: 'ప్రాంతాలు',
+ notes: 'గమనికలు',
+ },
+ },
+
+ notes_section: {
+ filter: 'ఫిల్టర్',
+ add_note: 'గమనిక జోడించండి',
+ no_results: 'గమనికలు కనుగొనబడలేదు',
+ clear: 'ఫిల్టర్ను క్లియర్ చేయండి',
+ empty_state: {
+ title: 'గమనికలు లేవు',
+ description:
+ 'కాన్వాస్పై టెక్స్ట్ ఉల్లేఖనలను జోడించడానికి ఒక గమనికను సృష్టించండి',
+ },
+ note: {
+ empty_note: 'ఖాళీ గమనిక',
+ note_actions: {
+ title: 'గమనిక చర్యలు',
+ edit_content: 'కంటెంట్ను సవరించండి',
+ delete_note: 'గమనికను తొలగించండి',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'అనుకూల రకాలు',
+ filter: 'ఫిల్టర్',
+ clear: 'ఫిల్టర్ను క్లియర్ చేయండి',
+ no_results: 'మీ ఫిల్టర్కు సరిపోలే అనుకూల రకాలు కనుగొనబడలేదు.',
+ new_type: 'కొత్త రకం',
empty_state: {
- title: 'No custom types',
+ title: 'అనుకూల రకాలు లేవు',
description:
- 'Custom types will appear here when they are available in your database',
+ 'మీ డేటాబేస్లో అందుబాటులో ఉన్నప్పుడు అనుకూల రకాలు ఇక్కడ కనిపిస్తాయి',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'రకం',
+ enum_values: 'Enum విలువలు',
+ composite_fields: 'ఫీల్డ్లు',
+ no_fields: 'ఫీల్డ్లు నిర్వచించబడలేదు',
no_values: 'ఏ enum విలువలు నిర్వచించబడలేదు',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'ఫీల్డ్ పేరు',
+ field_type_placeholder: 'రకాన్ని ఎంచుకోండి',
+ add_field: 'ఫీల్డ్ జోడించండి',
+ no_fields_tooltip:
+ 'ఈ అనుకూల రకానికి ఫీల్డ్లు నిర్వచించబడలేదు',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'చర్యలు',
+ highlight_fields: 'ఫీల్డ్లను హైలైట్ చేయండి',
+ delete_custom_type: 'తొలగించండి',
+ clear_field_highlight: 'హైలైట్ తొలగించండి',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'రకాన్ని తొలగించండి',
},
},
},
@@ -279,8 +320,7 @@ export const te: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'పట్టికలను ఫిల్టర్ చేయండి',
},
new_diagram_dialog: {
@@ -312,7 +352,7 @@ export const te: LanguageTranslation = {
// TODO: Translate
import_from_file: 'Import from File',
back: 'తిరుగు',
- empty_diagram: 'ఖాళీ చిత్రము',
+ empty_diagram: 'ఖాళీ డేటాబేస్',
continue: 'కొనసాగించు',
import: 'డిగుమతి',
},
@@ -328,6 +368,7 @@ export const te: LanguageTranslation = {
},
cancel: 'రద్దు',
open: 'తెరవు',
+ new_database: 'కొత్త డేటాబేస్',
diagram_actions: {
open: 'తెరవు',
@@ -391,10 +432,9 @@ export const te: LanguageTranslation = {
export_image_dialog: {
title: 'చిత్రం ఎగుమతి',
description: 'ఎగుమతి కోసం స్కేల్ ఫ్యాక్టర్ ఎంచుకోండి:',
- scale_1x: '1x సాధారణ',
- scale_2x: '2x (సిఫార్సు చేయబడినది)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (తక్కువ నాణ్యత)',
+ scale_2x: '2x (సాధారణ నాణ్యత)',
+ scale_4x: '4x (అత్యుత్తమ నాణ్యత)',
cancel: 'రద్దు',
export: 'ఎగుమతి',
// TODO: Translate
@@ -404,7 +444,12 @@ export const te: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'స్కీమాను ఎంచుకోండి',
description:
@@ -489,7 +534,8 @@ export const te: LanguageTranslation = {
new_view: 'కొత్త వ్యూ',
new_relationship: 'కొత్త సంబంధం',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'కొత్త ప్రాంతం',
+ new_note: 'కొత్త నోట్',
},
table_node_context_menu: {
@@ -499,6 +545,23 @@ export const te: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'అన్ని పట్టికలు దాచబడ్డాయి',
+ show_all_tables: 'అన్ని చూపించు',
+ },
+
+ canvas_filter: {
+ title: 'పట్టికలను ఫిల్టర్ చేయండి',
+ search_placeholder: 'పట్టికలను శోధించండి...',
+ group_by_schema: 'స్కీమా ద్వారా గ్రూప్ చేయండి',
+ group_by_area: 'ప్రాంతం ద్వారా గ్రూప్ చేయండి',
+ no_tables_found: 'పట్టికలు కనుగొనబడలేదు',
+ empty_diagram_description: 'ప్రారంభించడానికి పట్టికను సృష్టించండి',
+ no_tables_description:
+ 'మీ శోధన లేదా ఫిల్టర్ను సర్దుబాటు చేయడానికి ప్రయత్నించండి',
+ clear_filter: 'ఫిల్టర్ క్లియర్ చేయండి',
+ },
+
// TODO: Translate
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/tr.ts b/src/i18n/locales/tr.ts
index da0790caa..0f6e527be 100644
--- a/src/i18n/locales/tr.ts
+++ b/src/i18n/locales/tr.ts
@@ -4,18 +4,18 @@ export const tr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Yeni',
- browse: 'Gözat',
+ browse: 'Aç',
tables: 'Tablolar',
refs: 'Refs',
- areas: 'Alanlar',
dependencies: 'Bağımlılıklar',
custom_types: 'Özel Tipler',
+ visuals: 'Görseller',
},
menu: {
actions: {
actions: 'Eylemler',
new: 'Yeni...',
- browse: 'Gözat...',
+ browse: 'Tüm veritabanları...',
save: 'Kaydet',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
@@ -44,6 +44,8 @@ export const tr: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
// TODO: Translate
backup: {
@@ -128,16 +130,20 @@ export const tr: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Tüm tablolar gizli',
+ show_all: 'Tümünü göster',
table: {
fields: 'Alanlar',
nullable: 'Boş Bırakılabilir?',
primary_key: 'Birincil Anahtar',
indexes: 'İndeksler',
+ check_constraints: 'Kontrol Kısıtlamaları',
comments: 'Yorumlar',
no_comments: 'Yorum yok',
add_field: 'Alan Ekle',
add_index: 'İndeks Ekle',
+ add_check: 'Kontrol Ekle',
index_select_fields: 'Alanları Seç',
no_types_found: 'Tür bulunamadı',
field_name: 'Ad',
@@ -164,6 +170,11 @@ export const tr: LanguageTranslation = {
index_type: 'İndeks Türü',
delete_index: 'İndeksi Sil',
},
+ check_constraint_actions: {
+ title: 'Kontrol Kısıtlaması',
+ expression: 'İfade',
+ delete: 'Kısıtlamayı Sil',
+ },
table_actions: {
title: 'Tablo İşlemleri',
change_schema: 'Şemayı Değiştir',
@@ -189,9 +200,10 @@ export const tr: LanguageTranslation = {
relationship: {
relationship: 'İlişki',
primary: 'Birincil Tablo',
- foreign: 'Referans Tablo',
+ foreign: 'İlişkili Tablo',
cardinality: 'Kardinalite',
delete_relationship: 'Sil',
+ switch_tables: 'Tabloları Değiştir',
relationship_actions: {
title: 'İşlemler',
delete_relationship: 'Sil',
@@ -213,54 +225,82 @@ export const tr: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'Alanlar',
+ add_area: 'Alan Ekle',
+ filter: 'Filtrele',
+ clear: 'Filtreyi Temizle',
+ no_results: 'Filtrenizle eşleşen alan bulunamadı.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Alan İşlemleri',
+ edit_name: 'Adı Düzenle',
+ delete_area: 'Alanı Sil',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Alan yok',
+ description: 'Başlamak için bir alan oluşturun',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Görseller',
+ tabs: {
+ areas: 'Alanlar',
+ notes: 'Notlar',
+ },
+ },
+
+ notes_section: {
+ filter: 'Filtrele',
+ add_note: 'Not Ekle',
+ no_results: 'Not bulunamadı',
+ clear: 'Filtreyi Temizle',
+ empty_state: {
+ title: 'Not Yok',
+ description:
+ 'Tuval üzerinde metin açıklamaları eklemek için bir not oluşturun',
+ },
+ note: {
+ empty_note: 'Boş not',
+ note_actions: {
+ title: 'Not İşlemleri',
+ edit_content: 'İçeriği Düzenle',
+ delete_note: 'Notu Sil',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Özel Tipler',
+ filter: 'Filtrele',
+ clear: 'Filtreyi Temizle',
+ no_results: 'Filtrenizle eşleşen özel tip bulunamadı.',
+ new_type: 'Yeni Tip',
empty_state: {
- title: 'No custom types',
+ title: 'Özel tip yok',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Veritabanınızda mevcut olduğunda özel tipler burada görünecektir',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Tür',
+ enum_values: 'Enum Değerleri',
+ composite_fields: 'Alanlar',
+ no_fields: 'Alan tanımlanmamış',
no_values: 'Tanımlanmış enum değeri yok',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Alan adı',
+ field_type_placeholder: 'Tip seçin',
+ add_field: 'Alan Ekle',
+ no_fields_tooltip: 'Bu özel tip için alan tanımlanmamış',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'İşlemler',
+ highlight_fields: 'Alanları Vurgula',
+ delete_custom_type: 'Sil',
+ clear_field_highlight: 'Vurguyu Kaldır',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Tipi Sil',
},
},
},
@@ -277,8 +317,7 @@ export const tr: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Tabloları Filtrele',
},
new_diagram_dialog: {
database_selection: {
@@ -308,7 +347,7 @@ export const tr: LanguageTranslation = {
import_from_file: 'Import from File',
cancel: 'İptal',
back: 'Geri',
- empty_diagram: 'Boş diyagram',
+ empty_diagram: 'Boş veritabanı',
continue: 'Devam',
import: 'İçe Aktar',
},
@@ -323,6 +362,7 @@ export const tr: LanguageTranslation = {
},
cancel: 'İptal',
open: 'Aç',
+ new_database: 'Yeni Veritabanı',
diagram_actions: {
open: 'Aç',
@@ -383,10 +423,9 @@ export const tr: LanguageTranslation = {
export_image_dialog: {
title: 'Resmi Dışa Aktar',
description: 'Dışa aktarım için ölçek faktörünü seçin:',
- scale_1x: '1x Normal',
- scale_2x: '2x (Önerilen)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Düşük Kalite)',
+ scale_2x: '2x (Normal Kalite)',
+ scale_4x: '4x (En İyi Kalite)',
cancel: 'İptal',
export: 'Dışa Aktar',
// TODO: Translate
@@ -396,6 +435,13 @@ export const tr: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
+
new_table_schema_dialog: {
title: 'Şema Seç',
description:
@@ -474,7 +520,8 @@ export const tr: LanguageTranslation = {
new_view: 'Yeni Görünüm',
new_relationship: 'Yeni İlişki',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'Yeni Alan',
+ new_note: 'Yeni Not',
},
table_node_context_menu: {
edit_table: 'Tabloyu Düzenle',
@@ -483,6 +530,23 @@ export const tr: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Tüm tablolar gizli',
+ show_all_tables: 'Tümünü göster',
+ },
+
+ canvas_filter: {
+ title: 'Tabloları Filtrele',
+ search_placeholder: 'Tablo ara...',
+ group_by_schema: 'Şemaya Göre Grupla',
+ group_by_area: 'Alana Göre Grupla',
+ no_tables_found: 'Tablo bulunamadı',
+ empty_diagram_description: 'Başlamak için bir tablo oluşturun',
+ no_tables_description:
+ 'Aramanızı veya filtrenizi ayarlamayı deneyin',
+ clear_filter: 'Filtreyi temizle',
+ },
+
// TODO: Translate
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
diff --git a/src/i18n/locales/uk.ts b/src/i18n/locales/uk.ts
index d0f673b74..d8fd195a3 100644
--- a/src/i18n/locales/uk.ts
+++ b/src/i18n/locales/uk.ts
@@ -4,18 +4,18 @@ export const uk: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Нова',
- browse: 'Огляд',
+ browse: 'Відкрити',
tables: 'Таблиці',
refs: 'Зв’язки',
- areas: 'Області',
dependencies: 'Залежності',
custom_types: 'Користувацькі типи',
+ visuals: 'Візуальні елементи',
},
menu: {
actions: {
actions: 'Дії',
new: 'Нова...',
- browse: 'Огляд...',
+ browse: 'Усі бази даних...',
save: 'Зберегти',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
@@ -43,6 +43,8 @@ export const uk: LanguageTranslation = {
hide_dependencies: 'Приховати залежності',
show_minimap: 'Показати мінімапу',
hide_minimap: 'Приховати мінімапу',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Резервне копіювання',
@@ -127,16 +129,20 @@ export const uk: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Всі таблиці приховані',
+ show_all: 'Показати все',
table: {
fields: 'Поля',
nullable: 'Може бути Null?',
primary_key: 'Первинний ключ',
indexes: 'Індекси',
+ check_constraints: 'Перевірочні обмеження',
comments: 'Коментарі',
no_comments: 'Немає коментарів',
add_field: 'Додати поле',
add_index: 'Додати індекс',
+ add_check: 'Додати перевірку',
index_select_fields: 'Виберіть поля',
no_types_found: 'Типи не знайдено',
field_name: 'Назва поля',
@@ -163,6 +169,11 @@ export const uk: LanguageTranslation = {
index_type: 'Тип індексу',
delete_index: 'Видалити індекс',
},
+ check_constraint_actions: {
+ title: 'Перевірочне обмеження',
+ expression: 'Вираз',
+ delete: 'Видалити обмеження',
+ },
table_actions: {
title: 'Дії з таблицею',
change_schema: 'Змінити схему',
@@ -187,9 +198,10 @@ export const uk: LanguageTranslation = {
relationship: {
relationship: 'Звʼязок',
primary: 'Первинна таблиця',
- foreign: 'Посилання на таблицю',
+ foreign: 'Повʼязана таблиця',
cardinality: 'Звʼязок',
delete_relationship: 'Видалити',
+ switch_tables: 'Поміняти таблиці',
relationship_actions: {
title: 'Дії',
delete_relationship: 'Видалити',
@@ -211,54 +223,85 @@ export const uk: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'Області',
+ add_area: 'Додати область',
+ filter: 'Фільтр',
+ clear: 'Очистити фільтр',
+ no_results:
+ 'Області не знайдені, які відповідають вашому фільтру.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Дії з областю',
+ edit_name: 'Редагувати назву',
+ delete_area: 'Видалити область',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Немає областей',
+ description: 'Створіть область, щоб почати',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Візуальні елементи',
+ tabs: {
+ areas: 'Області',
+ notes: 'Нотатки',
+ },
+ },
+
+ notes_section: {
+ filter: 'Фільтр',
+ add_note: 'Додати Нотатку',
+ no_results: 'Нотатки не знайдено',
+ clear: 'Очистити Фільтр',
+ empty_state: {
+ title: 'Немає Нотаток',
+ description:
+ 'Створіть нотатку, щоб додати текстові анотації на полотні',
+ },
+ note: {
+ empty_note: 'Порожня нотатка',
+ note_actions: {
+ title: 'Дії з Нотаткою',
+ edit_content: 'Редагувати Вміст',
+ delete_note: 'Видалити Нотатку',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Користувацькі типи',
+ filter: 'Фільтр',
+ clear: 'Очистити фільтр',
+ no_results:
+ 'Не знайдено користувацьких типів, що відповідають фільтру.',
+ new_type: 'Новий тип',
empty_state: {
- title: 'No custom types',
+ title: 'Немає користувацьких типів',
description:
- 'Custom types will appear here when they are available in your database',
+ "Користувацькі типи з'являться тут, коли вони будуть доступні у вашій базі даних",
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Вид',
+ enum_values: 'Значення переліку',
+ composite_fields: 'Поля',
+ no_fields: 'Поля не визначені',
no_values: 'Значення переліку не визначені',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Назва поля',
+ field_type_placeholder: 'Виберіть тип',
+ add_field: 'Додати поле',
+ no_fields_tooltip:
+ 'Для цього користувацького типу поля не визначені',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Дії',
+ highlight_fields: 'Виділити поля',
+ delete_custom_type: 'Видалити',
+ clear_field_highlight: 'Зняти виділення',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Видалити тип',
},
},
},
@@ -276,8 +319,7 @@ export const uk: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Фільтрувати таблиці',
},
new_diagram_dialog: {
@@ -308,7 +350,7 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
back: 'Назад',
import_from_file: 'Імпортувати з файлу',
- empty_diagram: 'Порожня діаграма',
+ empty_diagram: 'Порожня база даних',
continue: 'Продовжити',
import: 'Імпорт',
},
@@ -325,6 +367,7 @@ export const uk: LanguageTranslation = {
},
cancel: 'Скасувати',
open: 'Відкрити',
+ new_database: 'Нова база даних',
diagram_actions: {
open: 'Відкрити',
@@ -388,10 +431,9 @@ export const uk: LanguageTranslation = {
export_image_dialog: {
title: 'Експорт зображення',
description: 'Виберіть коефіцієнт масштабування для експорту:',
- scale_1x: '1x Звичайний',
- scale_2x: '2x (Рекомендовано)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Низька якість)',
+ scale_2x: '2x (Звичайна якість)',
+ scale_4x: '4x (Найкраща якість)',
cancel: 'Скасувати',
export: 'Експортувати',
// TODO: Translate
@@ -401,7 +443,12 @@ export const uk: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Виберіть Схему',
description:
@@ -480,7 +527,8 @@ export const uk: LanguageTranslation = {
new_view: 'Нове представлення',
new_relationship: 'Новий звʼязок',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'Нова область',
+ new_note: 'Нова Нотатка',
},
table_node_context_menu: {
@@ -490,6 +538,22 @@ export const uk: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Всі таблиці приховані',
+ show_all_tables: 'Показати все',
+ },
+
+ canvas_filter: {
+ title: 'Фільтрувати таблиці',
+ search_placeholder: 'Пошук таблиць...',
+ group_by_schema: 'Групувати за схемою',
+ group_by_area: 'Групувати за областю',
+ no_tables_found: 'Таблиці не знайдено',
+ empty_diagram_description: 'Створіть таблицю, щоб почати',
+ no_tables_description: 'Спробуйте налаштувати пошук або фільтр',
+ clear_filter: 'Очистити фільтр',
+ },
+
snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/vi.ts b/src/i18n/locales/vi.ts
index a3532d0d3..20b18f3b7 100644
--- a/src/i18n/locales/vi.ts
+++ b/src/i18n/locales/vi.ts
@@ -4,18 +4,18 @@ export const vi: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Mới',
- browse: 'Duyệt',
+ browse: 'Mở',
tables: 'Bảng',
refs: 'Refs',
- areas: 'Khu vực',
dependencies: 'Phụ thuộc',
custom_types: 'Kiểu tùy chỉnh',
+ visuals: 'Hình ảnh',
},
menu: {
actions: {
actions: 'Hành động',
new: 'Mới...',
- browse: 'Duyệt...',
+ browse: 'Tất cả cơ sở dữ liệu...',
save: 'Lưu',
import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL',
@@ -44,6 +44,8 @@ export const vi: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: 'Hỗ trợ',
@@ -128,16 +130,20 @@ export const vi: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: 'Tất cả bảng đã bị ẩn',
+ show_all: 'Hiển thị tất cả',
table: {
fields: 'Trường',
nullable: 'Có thể NULL?',
primary_key: 'Khóa chính',
indexes: 'Chỉ mục',
+ check_constraints: 'Ràng buộc kiểm tra',
comments: 'Bình luận',
no_comments: 'Không có bình luận',
add_field: 'Thêm trường',
add_index: 'Thêm chỉ mục',
+ add_check: 'Thêm kiểm tra',
index_select_fields: 'Chọn trường',
no_types_found: 'Không tìm thấy',
field_name: 'Tên trường',
@@ -164,6 +170,11 @@ export const vi: LanguageTranslation = {
index_type: 'Loại chỉ mục',
delete_index: 'Xóa chỉ mục',
},
+ check_constraint_actions: {
+ title: 'Ràng buộc kiểm tra',
+ expression: 'Biểu thức',
+ delete: 'Xóa ràng buộc',
+ },
table_actions: {
title: 'Hành động',
change_schema: 'Thay đổi lược đồ',
@@ -187,10 +198,11 @@ export const vi: LanguageTranslation = {
dependencies: 'Phụ thuộc',
relationship: {
relationship: 'Quan hệ',
- primary: 'Bảng khóa chính',
- foreign: 'Bảng khóa ngoại',
+ primary: 'Bảng chính',
+ foreign: 'Bảng liên quan',
cardinality: 'Quan hệ',
delete_relationship: 'Xóa',
+ switch_tables: 'Đổi Bảng',
relationship_actions: {
title: 'Hành động',
delete_relationship: 'Xóa',
@@ -212,54 +224,84 @@ export const vi: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: 'Khu vực',
+ add_area: 'Thêm Khu vực',
+ filter: 'Lọc',
+ clear: 'Xóa Bộ Lọc',
+ no_results: 'Không tìm thấy khu vực nào phù hợp với bộ lọc.',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: 'Hành động Khu vực',
+ edit_name: 'Sửa Tên',
+ delete_area: 'Xóa Khu vực',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: 'Không có khu vực',
+ description: 'Tạo khu vực để bắt đầu',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: 'Hình ảnh',
+ tabs: {
+ areas: 'Khu vực',
+ notes: 'Ghi chú',
+ },
+ },
+
+ notes_section: {
+ filter: 'Lọc',
+ add_note: 'Thêm Ghi Chú',
+ no_results: 'Không tìm thấy ghi chú',
+ clear: 'Xóa Bộ Lọc',
+ empty_state: {
+ title: 'Không Có Ghi Chú',
+ description:
+ 'Tạo ghi chú để thêm chú thích văn bản trên canvas',
+ },
+ note: {
+ empty_note: 'Ghi chú trống',
+ note_actions: {
+ title: 'Hành Động Ghi Chú',
+ edit_content: 'Chỉnh Sửa Nội Dung',
+ delete_note: 'Xóa Ghi Chú',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: 'Loại Tùy Chỉnh',
+ filter: 'Lọc',
+ clear: 'Xóa Bộ Lọc',
+ no_results:
+ 'Không tìm thấy loại tùy chỉnh nào phù hợp với bộ lọc.',
+ new_type: 'Loại Mới',
empty_state: {
- title: 'No custom types',
+ title: 'Không có loại tùy chỉnh',
description:
- 'Custom types will appear here when they are available in your database',
+ 'Các loại tùy chỉnh sẽ xuất hiện ở đây khi có sẵn trong cơ sở dữ liệu của bạn',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: 'Loại',
+ enum_values: 'Giá Trị Enum',
+ composite_fields: 'Trường',
+ no_fields: 'Chưa định nghĩa trường',
no_values: 'Không có giá trị enum được định nghĩa',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: 'Tên trường',
+ field_type_placeholder: 'Chọn loại',
+ add_field: 'Thêm Trường',
+ no_fields_tooltip:
+ 'Chưa định nghĩa trường cho loại tùy chỉnh này',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: 'Hành động',
+ highlight_fields: 'Làm Nổi Bật Trường',
+ delete_custom_type: 'Xóa',
+ clear_field_highlight: 'Xóa Làm Nổi Bật',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: 'Xóa Loại',
},
},
},
@@ -277,8 +319,7 @@ export const vi: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: 'Lọc Bảng',
},
new_diagram_dialog: {
@@ -309,7 +350,7 @@ export const vi: LanguageTranslation = {
cancel: 'Hủy',
import_from_file: 'Nhập từ tệp',
back: 'Trở lại',
- empty_diagram: 'Sơ đồ trống',
+ empty_diagram: 'Cơ sở dữ liệu trống',
continue: 'Tiếp tục',
import: 'Nhập',
},
@@ -325,6 +366,7 @@ export const vi: LanguageTranslation = {
},
cancel: 'Hủy',
open: 'Mở',
+ new_database: 'Cơ sở dữ liệu mới',
diagram_actions: {
open: 'Mở',
@@ -387,10 +429,9 @@ export const vi: LanguageTranslation = {
export_image_dialog: {
title: 'Xuất ảnh',
description: 'Chọn tỉ lệ để xuất:',
- scale_1x: '1x Thông thường',
- scale_2x: '2x (Khuyến khích)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (Chất lượng thấp)',
+ scale_2x: '2x (Chất lượng bình thường)',
+ scale_4x: '4x (Chất lượng tốt nhất)',
cancel: 'Hủy',
export: 'Xuất',
// TODO: Translate
@@ -400,7 +441,12 @@ export const vi: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: 'Chọn lược đồ',
description:
@@ -481,7 +527,8 @@ export const vi: LanguageTranslation = {
new_view: 'Chế độ xem Mới',
new_relationship: 'Tạo quan hệ mới',
// TODO: Translate
- new_area: 'New Area',
+ new_area: 'Khu vực mới',
+ new_note: 'Ghi Chú Mới',
},
table_node_context_menu: {
@@ -491,6 +538,23 @@ export const vi: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: 'Tất cả bảng đã bị ẩn',
+ show_all_tables: 'Hiển thị tất cả',
+ },
+
+ canvas_filter: {
+ title: 'Lọc bảng',
+ search_placeholder: 'Tìm kiếm bảng...',
+ group_by_schema: 'Nhóm theo Schema',
+ group_by_area: 'Nhóm theo Khu vực',
+ no_tables_found: 'Không tìm thấy bảng',
+ empty_diagram_description: 'Tạo bảng để bắt đầu',
+ no_tables_description:
+ 'Thử điều chỉnh tìm kiếm hoặc bộ lọc của bạn',
+ clear_filter: 'Xóa bộ lọc',
+ },
+
snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/zh_CN.ts b/src/i18n/locales/zh_CN.ts
index d61ad374f..66ff7e2ed 100644
--- a/src/i18n/locales/zh_CN.ts
+++ b/src/i18n/locales/zh_CN.ts
@@ -4,18 +4,18 @@ export const zh_CN: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新建',
- browse: '浏览',
+ browse: '打开',
tables: '表',
refs: '引用',
- areas: '区域',
dependencies: '依赖关系',
custom_types: '自定义类型',
+ visuals: '视觉效果',
},
menu: {
actions: {
actions: '操作',
new: '新建...',
- browse: '浏览...',
+ browse: '所有数据库...',
save: '保存',
import: '导入数据库',
export_sql: '导出 SQL 语句',
@@ -44,6 +44,8 @@ export const zh_CN: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: '备份',
@@ -125,16 +127,20 @@ export const zh_CN: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: '所有表格已隐藏',
+ show_all: '显示全部',
table: {
fields: '字段',
nullable: '可为空?',
primary_key: '主键',
indexes: '索引',
+ check_constraints: '检查约束',
comments: '注释',
no_comments: '空',
add_field: '添加字段',
add_index: '添加索引',
+ add_check: '添加检查',
index_select_fields: '选择字段',
no_types_found: '未找到类型',
field_name: '名称',
@@ -161,6 +167,11 @@ export const zh_CN: LanguageTranslation = {
index_type: '索引类型',
delete_index: '删除索引',
},
+ check_constraint_actions: {
+ title: '检查约束',
+ expression: '表达式',
+ delete: '删除检查约束',
+ },
table_actions: {
title: '表操作',
change_schema: '更改模式',
@@ -185,9 +196,10 @@ export const zh_CN: LanguageTranslation = {
relationship: {
relationship: '关系',
primary: '主表',
- foreign: '被引用表',
+ foreign: '关联表',
cardinality: '基数',
delete_relationship: '删除',
+ switch_tables: '切换表',
relationship_actions: {
title: '操作',
delete_relationship: '删除',
@@ -209,54 +221,81 @@ export const zh_CN: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: '区域',
+ add_area: '添加区域',
+ filter: '筛选',
+ clear: '清除筛选',
+ no_results: '未找到符合筛选条件的区域。',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: '区域操作',
+ edit_name: '编辑名称',
+ delete_area: '删除区域',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: '没有区域',
+ description: '创建区域以开始',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: '视觉效果',
+ tabs: {
+ areas: '区域',
+ notes: '笔记',
+ },
+ },
+
+ notes_section: {
+ filter: '筛选',
+ add_note: '添加笔记',
+ no_results: '未找到笔记',
+ clear: '清除筛选',
+ empty_state: {
+ title: '没有笔记',
+ description: '创建笔记以在画布上添加文本注释',
+ },
+ note: {
+ empty_note: '空笔记',
+ note_actions: {
+ title: '笔记操作',
+ edit_content: '编辑内容',
+ delete_note: '删除笔记',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: '自定义类型',
+ filter: '筛选',
+ clear: '清除筛选',
+ no_results: '未找到符合筛选条件的自定义类型。',
+ new_type: '新类型',
empty_state: {
- title: 'No custom types',
+ title: '没有自定义类型',
description:
- 'Custom types will appear here when they are available in your database',
+ '当数据库中有可用的自定义类型时,它们将显示在这里',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: '类型',
+ enum_values: '枚举值',
+ composite_fields: '字段',
+ no_fields: '未定义字段',
no_values: '没有定义枚举值',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: '字段名称',
+ field_type_placeholder: '选择类型',
+ add_field: '添加字段',
+ no_fields_tooltip: '此自定义类型未定义字段',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: '操作',
+ highlight_fields: '高亮字段',
+ delete_custom_type: '删除',
+ clear_field_highlight: '清除高亮',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: '删除类型',
},
},
},
@@ -274,8 +313,7 @@ export const zh_CN: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出显示重叠的表',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: '筛选表',
},
new_diagram_dialog: {
@@ -306,7 +344,7 @@ export const zh_CN: LanguageTranslation = {
cancel: '取消',
import_from_file: '从文件导入',
back: '上一步',
- empty_diagram: '新建空关系图',
+ empty_diagram: '空数据库',
continue: '下一步',
import: '导入',
},
@@ -322,6 +360,7 @@ export const zh_CN: LanguageTranslation = {
},
cancel: '取消',
open: '打开',
+ new_database: '新建数据库',
diagram_actions: {
open: '打开',
@@ -384,10 +423,9 @@ export const zh_CN: LanguageTranslation = {
export_image_dialog: {
title: '导出图片',
description: '选择导出的缩放比例:',
- scale_1x: '1x 常规',
- scale_2x: '2x (推荐)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (低质量)',
+ scale_2x: '2x (普通质量)',
+ scale_4x: '4x (最佳质量)',
cancel: '取消',
export: '导出',
// TODO: Translate
@@ -397,7 +435,12 @@ export const zh_CN: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: '选择模式',
description: '当前显示多个模式。请选择一个用于新表。',
@@ -475,8 +518,8 @@ export const zh_CN: LanguageTranslation = {
new_table: '新建表',
new_view: '新建视图',
new_relationship: '新建关系',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: '新建区域',
+ new_note: '新笔记',
},
table_node_context_menu: {
@@ -486,6 +529,22 @@ export const zh_CN: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: '所有表格已隐藏',
+ show_all_tables: '显示全部',
+ },
+
+ canvas_filter: {
+ title: '筛选表格',
+ search_placeholder: '搜索表格...',
+ group_by_schema: '按模式分组',
+ group_by_area: '按区域分组',
+ no_tables_found: '未找到表格',
+ empty_diagram_description: '创建表格以开始',
+ no_tables_description: '尝试调整您的搜索或筛选',
+ clear_filter: '清除筛选',
+ },
+
snap_to_grid_tooltip: '对齐到网格(按住 {{key}})',
tool_tips: {
diff --git a/src/i18n/locales/zh_TW.ts b/src/i18n/locales/zh_TW.ts
index f92060613..942631d6e 100644
--- a/src/i18n/locales/zh_TW.ts
+++ b/src/i18n/locales/zh_TW.ts
@@ -4,18 +4,18 @@ export const zh_TW: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新建',
- browse: '瀏覽',
+ browse: '開啟',
tables: '表格',
refs: 'Refs',
- areas: '區域',
dependencies: '相依性',
custom_types: '自定義類型',
+ visuals: '視覺效果',
},
menu: {
actions: {
actions: '操作',
new: '新增...',
- browse: '瀏覽...',
+ browse: '所有資料庫...',
save: '儲存',
import: '匯入資料庫',
export_sql: '匯出 SQL',
@@ -44,6 +44,8 @@ export const zh_TW: LanguageTranslation = {
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
+ expand_all_tables: 'Expand All Tables',
+ collapse_all_tables: 'Collapse All Tables',
},
backup: {
backup: '備份',
@@ -125,16 +127,20 @@ export const zh_TW: LanguageTranslation = {
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
+ all_hidden: '所有表格已隱藏',
+ show_all: '顯示全部',
table: {
fields: '欄位',
nullable: '可為 NULL?',
primary_key: '主鍵',
indexes: '索引',
+ check_constraints: '檢查約束',
comments: '註解',
no_comments: '無註解',
add_field: '新增欄位',
add_index: '新增索引',
+ add_check: '新增檢查',
index_select_fields: '選擇欄位',
no_types_found: '未找到類型',
field_name: '名稱',
@@ -161,6 +167,11 @@ export const zh_TW: LanguageTranslation = {
index_type: '索引類型',
delete_index: '刪除索引',
},
+ check_constraint_actions: {
+ title: '檢查約束',
+ expression: '運算式',
+ delete: '刪除檢查約束',
+ },
table_actions: {
title: '表格操作',
change_schema: '變更 Schema',
@@ -185,9 +196,10 @@ export const zh_TW: LanguageTranslation = {
relationship: {
relationship: '關聯',
primary: '主表格',
- foreign: '參照表格',
+ foreign: '關聯表格',
cardinality: '基數',
delete_relationship: '刪除',
+ switch_tables: '切換表格',
relationship_actions: {
title: '操作',
delete_relationship: '刪除',
@@ -209,54 +221,81 @@ export const zh_TW: LanguageTranslation = {
},
},
- // TODO: Translate
areas_section: {
- areas: 'Areas',
- add_area: 'Add Area',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No areas found matching your filter.',
+ areas: '區域',
+ add_area: '新增區域',
+ filter: '篩選',
+ clear: '清除篩選',
+ no_results: '未找到符合篩選條件的區域。',
area: {
area_actions: {
- title: 'Area Actions',
- edit_name: 'Edit Name',
- delete_area: 'Delete Area',
+ title: '區域操作',
+ edit_name: '編輯名稱',
+ delete_area: '刪除區域',
},
},
empty_state: {
- title: 'No areas',
- description: 'Create an area to get started',
+ title: '沒有區域',
+ description: '建立區域以開始',
},
},
- // TODO: Translate
+
+ visuals_section: {
+ visuals: '視覺效果',
+ tabs: {
+ areas: '區域',
+ notes: '筆記',
+ },
+ },
+
+ notes_section: {
+ filter: '篩選',
+ add_note: '新增筆記',
+ no_results: '未找到筆記',
+ clear: '清除篩選',
+ empty_state: {
+ title: '沒有筆記',
+ description: '建立筆記以在畫布上新增文字註解',
+ },
+ note: {
+ empty_note: '空白筆記',
+ note_actions: {
+ title: '筆記操作',
+ edit_content: '編輯內容',
+ delete_note: '刪除筆記',
+ },
+ },
+ },
+
custom_types_section: {
- custom_types: 'Custom Types',
- filter: 'Filter',
- clear: 'Clear Filter',
- no_results: 'No custom types found matching your filter.',
+ custom_types: '自訂類型',
+ filter: '篩選',
+ clear: '清除篩選',
+ no_results: '未找到符合篩選條件的自訂類型。',
+ new_type: '新類型',
empty_state: {
- title: 'No custom types',
+ title: '沒有自訂類型',
description:
- 'Custom types will appear here when they are available in your database',
+ '當資料庫中有可用的自訂類型時,它們將顯示在這裡',
},
custom_type: {
- kind: 'Kind',
- enum_values: 'Enum Values',
- composite_fields: 'Fields',
- no_fields: 'No fields defined',
+ kind: '類型',
+ enum_values: '列舉值',
+ composite_fields: '欄位',
+ no_fields: '未定義欄位',
no_values: '沒有定義列舉值',
- field_name_placeholder: 'Field name',
- field_type_placeholder: 'Select type',
- add_field: 'Add Field',
- no_fields_tooltip: 'No fields defined for this custom type',
+ field_name_placeholder: '欄位名稱',
+ field_type_placeholder: '選擇類型',
+ add_field: '新增欄位',
+ no_fields_tooltip: '此自訂類型未定義欄位',
custom_type_actions: {
- title: 'Actions',
- highlight_fields: 'Highlight Fields',
- delete_custom_type: 'Delete',
- clear_field_highlight: 'Clear Highlight',
+ title: '操作',
+ highlight_fields: '突出顯示欄位',
+ delete_custom_type: '刪除',
+ clear_field_highlight: '清除突出顯示',
},
- delete_custom_type: 'Delete Type',
+ delete_custom_type: '刪除類型',
},
},
},
@@ -274,8 +313,7 @@ export const zh_TW: LanguageTranslation = {
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出顯示重疊表格',
- // TODO: Translate
- filter: 'Filter Tables',
+ filter: '篩選表格',
},
new_diagram_dialog: {
@@ -305,7 +343,7 @@ export const zh_TW: LanguageTranslation = {
cancel: '取消',
import_from_file: '從檔案匯入',
back: '返回',
- empty_diagram: '空白圖表',
+ empty_diagram: '空資料庫',
continue: '繼續',
import: '匯入',
},
@@ -321,6 +359,7 @@ export const zh_TW: LanguageTranslation = {
},
cancel: '取消',
open: '開啟',
+ new_database: '新建資料庫',
diagram_actions: {
open: '開啟',
@@ -383,10 +422,9 @@ export const zh_TW: LanguageTranslation = {
export_image_dialog: {
title: '匯出圖片',
description: '請選擇匯出的倍率:',
- scale_1x: '1x 標準',
- scale_2x: '2x (推薦)',
- scale_3x: '3x',
- scale_4x: '4x',
+ scale_1x: '1x (低品質)',
+ scale_2x: '2x (普通品質)',
+ scale_4x: '4x (最佳品質)',
cancel: '取消',
export: '匯出',
// TODO: Translate
@@ -396,7 +434,12 @@ export const zh_TW: LanguageTranslation = {
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
-
+ share_table_dialog: {
+ title: 'Share Table',
+ description: 'Copy the following link to share this table.',
+ close: 'Close',
+ copy_aria_label: 'Copy share URL',
+ },
new_table_schema_dialog: {
title: '選擇 Schema',
description: '目前顯示多個 Schema,請為新表格選擇一個。',
@@ -475,8 +518,8 @@ export const zh_TW: LanguageTranslation = {
new_table: '新建表格',
new_view: '新檢視',
new_relationship: '新建關聯',
- // TODO: Translate
- new_area: 'New Area',
+ new_area: '新區域',
+ new_note: '新筆記',
},
table_node_context_menu: {
@@ -486,6 +529,22 @@ export const zh_TW: LanguageTranslation = {
add_relationship: 'Add Relationship', // TODO: Translate
},
+ canvas: {
+ all_tables_hidden: '所有表格已隱藏',
+ show_all_tables: '顯示全部',
+ },
+
+ canvas_filter: {
+ title: '篩選表格',
+ search_placeholder: '搜尋表格...',
+ group_by_schema: '依架構分組',
+ group_by_area: '依區域分組',
+ no_tables_found: '找不到表格',
+ empty_diagram_description: '建立表格以開始',
+ no_tables_description: '嘗試調整您的搜尋或篩選',
+ clear_filter: '清除篩選',
+ },
+
snap_to_grid_tooltip: '對齊網格(按住 {{key}})',
tool_tips: {
diff --git a/src/index.css b/src/index.css
index fae1a2eb8..8149ea1b9 100644
--- a/src/index.css
+++ b/src/index.css
@@ -7,6 +7,11 @@
@apply cursor-default;
}
+ .react-flow.canvas-cursor-multi-select .react-flow__pane,
+ .react-flow.canvas-cursor-multi-select .react-flow__node {
+ cursor: crosshair !important;
+ }
+
.react-flow.nodes-animated .react-flow__node {
@apply transition-[width];
@apply duration-100;
@@ -18,4 +23,7 @@
.marker-definitions {
}
+
+ .nodrag {
+ }
}
diff --git a/src/lib/check-constraints/__tests__/check-constraints-validator.test.ts b/src/lib/check-constraints/__tests__/check-constraints-validator.test.ts
new file mode 100644
index 000000000..9d983b325
--- /dev/null
+++ b/src/lib/check-constraints/__tests__/check-constraints-validator.test.ts
@@ -0,0 +1,522 @@
+import { describe, it, expect } from 'vitest';
+import {
+ validateCheckConstraint,
+ validateCheckConstraintWithDetails,
+} from '../check-constraints-validator';
+
+describe('Check Constraint Validator', () => {
+ describe('Valid expressions', () => {
+ describe('Simple comparisons', () => {
+ it('should validate simple less than', () => {
+ expect(validateCheckConstraint('a < b')).toBe(true);
+ });
+
+ it('should validate simple greater than', () => {
+ expect(validateCheckConstraint('a > b')).toBe(true);
+ });
+
+ it('should validate simple equal', () => {
+ expect(validateCheckConstraint('a = b')).toBe(true);
+ });
+
+ it('should validate less than or equal', () => {
+ expect(validateCheckConstraint('a <= b')).toBe(true);
+ });
+
+ it('should validate greater than or equal', () => {
+ expect(validateCheckConstraint('a >= b')).toBe(true);
+ });
+
+ it('should validate not equal (!=)', () => {
+ expect(validateCheckConstraint('a != b')).toBe(true);
+ });
+
+ it('should validate not equal (<>)', () => {
+ expect(validateCheckConstraint('a <> b')).toBe(true);
+ });
+
+ it('should validate comparison with number', () => {
+ expect(validateCheckConstraint('price > 0')).toBe(true);
+ });
+
+ it('should validate comparison with negative number', () => {
+ expect(validateCheckConstraint('balance >= -100')).toBe(true);
+ });
+
+ it('should validate comparison with decimal', () => {
+ expect(validateCheckConstraint('rate < 0.05')).toBe(true);
+ });
+ });
+
+ describe('Compound expressions', () => {
+ it('should validate AND expression', () => {
+ expect(validateCheckConstraint('a > 0 AND b < 100')).toBe(true);
+ });
+
+ it('should validate OR expression', () => {
+ expect(validateCheckConstraint('a > 0 OR a < -10')).toBe(true);
+ });
+
+ it('should validate complex AND/OR', () => {
+ expect(
+ validateCheckConstraint('a > 0 AND b < 100 OR c = 5')
+ ).toBe(true);
+ });
+
+ it('should validate range check with AND', () => {
+ expect(
+ validateCheckConstraint('grade >= 0 AND grade <= 100')
+ ).toBe(true);
+ });
+
+ it('should validate multiple conditions', () => {
+ expect(
+ validateCheckConstraint('age >= 18 AND age <= 120')
+ ).toBe(true);
+ });
+ });
+
+ describe('BETWEEN expressions', () => {
+ it('should validate BETWEEN with numbers', () => {
+ expect(
+ validateCheckConstraint('quantity BETWEEN 1 AND 100')
+ ).toBe(true);
+ });
+
+ it('should validate BETWEEN with identifiers', () => {
+ expect(
+ validateCheckConstraint('value BETWEEN min_val AND max_val')
+ ).toBe(true);
+ });
+
+ it('should validate danger_level BETWEEN', () => {
+ expect(
+ validateCheckConstraint('danger_level BETWEEN 1 AND 10')
+ ).toBe(true);
+ });
+
+ it('should validate day_of_week BETWEEN', () => {
+ expect(
+ validateCheckConstraint('day_of_week BETWEEN 1 AND 7')
+ ).toBe(true);
+ });
+ });
+
+ describe('IN expressions', () => {
+ it('should validate IN with strings', () => {
+ expect(
+ validateCheckConstraint(
+ "status IN ('active', 'inactive', 'pending')"
+ )
+ ).toBe(true);
+ });
+
+ it('should validate IN with numbers', () => {
+ expect(
+ validateCheckConstraint('priority IN (1, 2, 3, 4, 5)')
+ ).toBe(true);
+ });
+
+ it('should validate IN with single value', () => {
+ expect(validateCheckConstraint("type IN ('default')")).toBe(
+ true
+ );
+ });
+
+ it('should validate complex IN expression', () => {
+ expect(
+ validateCheckConstraint(
+ "status IN ('scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show')"
+ )
+ ).toBe(true);
+ });
+ });
+
+ describe('IS NULL / IS NOT NULL', () => {
+ it('should validate IS NULL', () => {
+ expect(validateCheckConstraint('deleted_at IS NULL')).toBe(
+ true
+ );
+ });
+
+ it('should validate IS NOT NULL', () => {
+ expect(validateCheckConstraint('name IS NOT NULL')).toBe(true);
+ });
+ });
+
+ describe('LIKE expressions', () => {
+ it('should validate LIKE with pattern', () => {
+ expect(validateCheckConstraint("email LIKE '%@%'")).toBe(true);
+ });
+
+ it('should validate NOT LIKE', () => {
+ expect(validateCheckConstraint("code NOT LIKE 'TEST%'")).toBe(
+ true
+ );
+ });
+ });
+
+ describe('NOT IN and NOT BETWEEN', () => {
+ it('should validate NOT IN', () => {
+ expect(
+ validateCheckConstraint(
+ "status NOT IN ('deleted', 'archived')"
+ )
+ ).toBe(true);
+ });
+
+ it('should validate NOT BETWEEN', () => {
+ expect(
+ validateCheckConstraint('value NOT BETWEEN 0 AND 10')
+ ).toBe(true);
+ });
+ });
+
+ describe('Function calls', () => {
+ it('should validate LENGTH function', () => {
+ expect(validateCheckConstraint('LENGTH(name) > 0')).toBe(true);
+ });
+
+ it('should validate UPPER function comparison', () => {
+ expect(validateCheckConstraint('UPPER(code) = code')).toBe(
+ true
+ );
+ });
+
+ it('should validate nested function calls', () => {
+ expect(
+ validateCheckConstraint('TRIM(UPPER(name)) = name')
+ ).toBe(true);
+ });
+
+ it('should validate ISJSON function', () => {
+ expect(validateCheckConstraint('ISJSON(metadata) = 1')).toBe(
+ true
+ );
+ });
+
+ it('should validate YEAR function', () => {
+ expect(
+ validateCheckConstraint('YEAR(created_at) >= 2020')
+ ).toBe(true);
+ });
+ });
+
+ describe('Arithmetic expressions', () => {
+ it('should validate addition', () => {
+ expect(validateCheckConstraint('a + b < 100')).toBe(true);
+ });
+
+ it('should validate subtraction', () => {
+ expect(
+ validateCheckConstraint('end_date - start_date > 0')
+ ).toBe(true);
+ });
+
+ it('should validate multiplication', () => {
+ expect(
+ validateCheckConstraint('quantity * price < 10000')
+ ).toBe(true);
+ });
+
+ it('should validate division', () => {
+ expect(validateCheckConstraint('total / count > 0')).toBe(true);
+ });
+
+ it('should validate modulo', () => {
+ expect(validateCheckConstraint('id % 2 = 0')).toBe(true);
+ });
+ });
+
+ describe('Parenthesized expressions', () => {
+ it('should validate simple parentheses', () => {
+ expect(validateCheckConstraint('(a < b)')).toBe(true);
+ });
+
+ it('should validate nested parentheses', () => {
+ expect(validateCheckConstraint('((a < b) AND (c > d))')).toBe(
+ true
+ );
+ });
+
+ it('should validate grouped OR with AND', () => {
+ expect(
+ validateCheckConstraint('(a = 1 OR a = 2) AND b > 0')
+ ).toBe(true);
+ });
+ });
+
+ describe('Quoted identifiers', () => {
+ it('should validate double-quoted identifier', () => {
+ expect(validateCheckConstraint('"column name" > 0')).toBe(true);
+ });
+
+ it('should validate backtick-quoted identifier (MySQL)', () => {
+ expect(validateCheckConstraint('`column name` > 0')).toBe(true);
+ });
+
+ it('should validate bracket-quoted identifier (SQL Server)', () => {
+ expect(validateCheckConstraint('[price] >= 0')).toBe(true);
+ });
+
+ it('should validate SQL Server style with multiple brackets', () => {
+ expect(
+ validateCheckConstraint('[grade] >= 0 AND [grade] <= 100')
+ ).toBe(true);
+ });
+ });
+
+ describe('NOT expressions', () => {
+ it('should validate NOT with parentheses', () => {
+ expect(validateCheckConstraint('NOT (a = b)')).toBe(true);
+ });
+
+ it('should validate NOT with comparison', () => {
+ expect(validateCheckConstraint('NOT deleted')).toBe(true);
+ });
+ });
+
+ describe('Boolean literals', () => {
+ it('should validate TRUE', () => {
+ expect(validateCheckConstraint('is_active = TRUE')).toBe(true);
+ });
+
+ it('should validate FALSE', () => {
+ expect(validateCheckConstraint('is_deleted = FALSE')).toBe(
+ true
+ );
+ });
+ });
+
+ describe('Type casting (PostgreSQL)', () => {
+ it('should validate PostgreSQL type cast', () => {
+ expect(validateCheckConstraint('value::integer > 0')).toBe(
+ true
+ );
+ });
+ });
+
+ describe('Real-world examples from codebase', () => {
+ it('should validate price >= 0', () => {
+ expect(validateCheckConstraint('price >= 0')).toBe(true);
+ });
+
+ it('should validate enchantment_charges >= 0', () => {
+ expect(
+ validateCheckConstraint('enchantment_charges >= 0')
+ ).toBe(true);
+ });
+
+ it('should validate quantity > 0', () => {
+ expect(validateCheckConstraint('quantity > 0')).toBe(true);
+ });
+
+ it('should validate mana_cost > 0', () => {
+ expect(validateCheckConstraint('mana_cost > 0')).toBe(true);
+ });
+
+ it('should validate enchantment_level BETWEEN 0 AND 10', () => {
+ expect(
+ validateCheckConstraint(
+ 'enchantment_level BETWEEN 0 AND 10'
+ )
+ ).toBe(true);
+ });
+
+ it('should validate period_end >= period_start', () => {
+ expect(
+ validateCheckConstraint('period_end >= period_start')
+ ).toBe(true);
+ });
+
+ it('should validate total_cents >= 0', () => {
+ expect(validateCheckConstraint('total_cents >= 0')).toBe(true);
+ });
+
+ it('should validate amount_cents > 0', () => {
+ expect(validateCheckConstraint('amount_cents > 0')).toBe(true);
+ });
+ });
+ });
+
+ describe('Invalid expressions', () => {
+ describe('Incomplete expressions', () => {
+ it('should reject expression with missing right operand', () => {
+ expect(validateCheckConstraint('a<')).toBe(false);
+ });
+
+ it('should reject expression with missing left operand', () => {
+ expect(validateCheckConstraint(' {
+ expect(validateCheckConstraint('a > 0 AND')).toBe(false);
+ });
+
+ it('should reject expression with missing operand after OR', () => {
+ expect(validateCheckConstraint('a > 0 OR')).toBe(false);
+ });
+
+ it('should reject expression ending with operator', () => {
+ expect(validateCheckConstraint('price >')).toBe(false);
+ });
+
+ it('should reject expression ending with >=', () => {
+ expect(validateCheckConstraint('value >=')).toBe(false);
+ });
+ });
+
+ describe('Invalid identifiers', () => {
+ it('should reject identifier starting with digit', () => {
+ expect(validateCheckConstraint('a < 2b')).toBe(false);
+ });
+
+ it('should reject number followed by letters without space', () => {
+ expect(validateCheckConstraint('a < 123abc')).toBe(false);
+ });
+ });
+
+ describe('Unbalanced parentheses', () => {
+ it('should reject missing closing parenthesis', () => {
+ expect(validateCheckConstraint('(a < b')).toBe(false);
+ });
+
+ it('should reject missing opening parenthesis', () => {
+ expect(validateCheckConstraint('a < b)')).toBe(false);
+ });
+
+ it('should reject nested unbalanced parentheses', () => {
+ expect(validateCheckConstraint('((a < b)')).toBe(false);
+ });
+
+ it('should reject function with missing closing paren', () => {
+ expect(validateCheckConstraint('LENGTH(name > 0')).toBe(false);
+ });
+ });
+
+ describe('Empty expressions', () => {
+ it('should reject empty string', () => {
+ expect(validateCheckConstraint('')).toBe(false);
+ });
+
+ it('should reject whitespace only', () => {
+ expect(validateCheckConstraint(' ')).toBe(false);
+ });
+
+ it('should reject tabs and newlines only', () => {
+ expect(validateCheckConstraint('\t\n')).toBe(false);
+ });
+ });
+
+ describe('Invalid operator placement', () => {
+ it('should reject double operators', () => {
+ expect(validateCheckConstraint('a < > b')).toBe(false);
+ });
+
+ it('should reject AND at start', () => {
+ expect(validateCheckConstraint('AND a > 0')).toBe(false);
+ });
+
+ it('should reject OR at start', () => {
+ expect(validateCheckConstraint('OR a > 0')).toBe(false);
+ });
+ });
+
+ describe('Invalid BETWEEN', () => {
+ it('should reject BETWEEN without value before', () => {
+ expect(validateCheckConstraint('BETWEEN 1 AND 10')).toBe(false);
+ });
+ });
+
+ describe('Invalid IN', () => {
+ it('should reject IN without value before', () => {
+ expect(validateCheckConstraint("IN ('a', 'b')")).toBe(false);
+ });
+ });
+
+ describe('Invalid IS', () => {
+ it('should reject IS without value before', () => {
+ expect(validateCheckConstraint('IS NULL')).toBe(false);
+ });
+ });
+ });
+
+ describe('validateCheckConstraintWithDetails', () => {
+ it('should return error details for invalid expression', () => {
+ const result = validateCheckConstraintWithDetails('a<');
+ expect(result.isValid).toBe(false);
+ expect(result.error).toBeDefined();
+ expect(result.error).toContain('incomplete');
+ });
+
+ it('should return error details for unbalanced parentheses', () => {
+ const result = validateCheckConstraintWithDetails('(a < b');
+ expect(result.isValid).toBe(false);
+ expect(result.error).toContain('parenthes');
+ });
+
+ it('should return error details for empty expression', () => {
+ const result = validateCheckConstraintWithDetails('');
+ expect(result.isValid).toBe(false);
+ expect(result.error).toContain('empty');
+ });
+
+ it('should return valid result for good expression', () => {
+ const result = validateCheckConstraintWithDetails('a < b');
+ expect(result.isValid).toBe(true);
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should return position for certain errors', () => {
+ const result = validateCheckConstraintWithDetails('a < b)');
+ expect(result.isValid).toBe(false);
+ expect(result.position).toBeDefined();
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should handle underscores in identifiers', () => {
+ expect(validateCheckConstraint('my_column > 0')).toBe(true);
+ });
+
+ it('should handle multiple underscores', () => {
+ expect(validateCheckConstraint('my__very__long__column > 0')).toBe(
+ true
+ );
+ });
+
+ it('should handle $ in identifier', () => {
+ expect(validateCheckConstraint('col$1 > 0')).toBe(true);
+ });
+
+ it('should handle scientific notation', () => {
+ expect(validateCheckConstraint('value < 1e10')).toBe(true);
+ });
+
+ it('should handle negative scientific notation', () => {
+ expect(validateCheckConstraint('value > 1e-5')).toBe(true);
+ });
+
+ it('should handle string with escaped quote', () => {
+ expect(validateCheckConstraint("name = 'O''Brien'")).toBe(true);
+ });
+
+ it('should handle empty parentheses in function call', () => {
+ expect(validateCheckConstraint('NOW() > created_at')).toBe(true);
+ });
+
+ it('should handle comparison between two columns', () => {
+ expect(validateCheckConstraint('end_date >= start_date')).toBe(
+ true
+ );
+ });
+
+ it('should handle just an identifier (boolean column)', () => {
+ expect(validateCheckConstraint('is_valid')).toBe(true);
+ });
+
+ it('should handle NOT with identifier', () => {
+ expect(validateCheckConstraint('NOT is_deleted')).toBe(true);
+ });
+ });
+});
diff --git a/src/lib/check-constraints/check-constraints-validator.ts b/src/lib/check-constraints/check-constraints-validator.ts
new file mode 100644
index 000000000..f3ec796ba
--- /dev/null
+++ b/src/lib/check-constraints/check-constraints-validator.ts
@@ -0,0 +1,616 @@
+/**
+ * SQL Check Constraint Expression Validator
+ *
+ * Validates SQL check constraint expressions for syntactic correctness.
+ * This is a syntax validator - it doesn't verify that column names exist.
+ *
+ * Valid examples:
+ * - "price > 0"
+ * - "age >= 18 AND age <= 120"
+ * - "status IN ('active', 'inactive')"
+ * - "quantity BETWEEN 1 AND 100"
+ *
+ * Invalid examples:
+ * - "a<" (incomplete expression)
+ * - "a<2b" (invalid identifier starting with digit)
+ * - "(a < b" (unbalanced parentheses)
+ */
+
+// Token types for the lexer
+type TokenType =
+ | 'IDENTIFIER' // column names, table names
+ | 'NUMBER' // numeric literals
+ | 'STRING' // 'string' or "string"
+ | 'OPERATOR' // comparison and arithmetic operators
+ | 'LOGICAL' // AND, OR, NOT
+ | 'KEYWORD' // BETWEEN, IN, IS, NULL, LIKE, TRUE, FALSE, ISNULL, etc.
+ | 'LPAREN' // (
+ | 'RPAREN' // )
+ | 'LBRACKET' // [ (SQL Server style)
+ | 'RBRACKET' // ] (SQL Server style)
+ | 'COMMA' // ,
+ | 'UNKNOWN'; // unrecognized token
+
+interface Token {
+ type: TokenType;
+ value: string;
+ position: number;
+}
+
+// SQL keywords used in check constraints
+const SQL_KEYWORDS = new Set([
+ 'BETWEEN',
+ 'IN',
+ 'IS',
+ 'NULL',
+ 'NOT',
+ 'LIKE',
+ 'ILIKE',
+ 'TRUE',
+ 'FALSE',
+ 'UNKNOWN',
+ 'ESCAPE',
+ 'SIMILAR',
+ 'TO',
+ 'ANY',
+ 'ALL',
+ 'SOME',
+ 'EXISTS',
+ 'CASE',
+ 'WHEN',
+ 'THEN',
+ 'ELSE',
+ 'END',
+ 'CAST',
+ 'AS',
+ 'COLLATE',
+]);
+
+// Logical operators
+const LOGICAL_OPERATORS = new Set(['AND', 'OR', 'NOT']);
+
+// Comparison operators (including multi-char)
+const COMPARISON_OPERATORS = [
+ '<>',
+ '!=',
+ '<=',
+ '>=',
+ '::',
+ '<',
+ '>',
+ '=',
+ '+',
+ '-',
+ '*',
+ '/',
+ '%',
+ '||',
+ '~',
+ '!~',
+ '~*',
+ '!~*',
+];
+
+/**
+ * Tokenizes a SQL check constraint expression
+ */
+function tokenize(expression: string): Token[] {
+ const tokens: Token[] = [];
+ let i = 0;
+
+ while (i < expression.length) {
+ // Skip whitespace
+ if (/\s/.test(expression[i])) {
+ i++;
+ continue;
+ }
+
+ const startPos = i;
+
+ // Single-quoted string
+ if (expression[i] === "'") {
+ let value = "'";
+ i++;
+ while (i < expression.length) {
+ if (expression[i] === "'" && expression[i + 1] === "'") {
+ // Escaped quote
+ value += "''";
+ i += 2;
+ } else if (expression[i] === "'") {
+ value += "'";
+ i++;
+ break;
+ } else {
+ value += expression[i];
+ i++;
+ }
+ }
+ tokens.push({ type: 'STRING', value, position: startPos });
+ continue;
+ }
+
+ // Double-quoted identifier (standard SQL)
+ if (expression[i] === '"') {
+ let value = '"';
+ i++;
+ while (i < expression.length && expression[i] !== '"') {
+ value += expression[i];
+ i++;
+ }
+ if (i < expression.length) {
+ value += '"';
+ i++;
+ }
+ tokens.push({ type: 'IDENTIFIER', value, position: startPos });
+ continue;
+ }
+
+ // Backtick-quoted identifier (MySQL style)
+ if (expression[i] === '`') {
+ let value = '`';
+ i++;
+ while (i < expression.length && expression[i] !== '`') {
+ value += expression[i];
+ i++;
+ }
+ if (i < expression.length) {
+ value += '`';
+ i++;
+ }
+ tokens.push({ type: 'IDENTIFIER', value, position: startPos });
+ continue;
+ }
+
+ // SQL Server bracket-quoted identifier [column]
+ if (expression[i] === '[') {
+ let value = '[';
+ i++;
+ while (i < expression.length && expression[i] !== ']') {
+ value += expression[i];
+ i++;
+ }
+ if (i < expression.length) {
+ value += ']';
+ i++;
+ }
+ tokens.push({ type: 'IDENTIFIER', value, position: startPos });
+ continue;
+ }
+
+ // Parentheses
+ if (expression[i] === '(') {
+ tokens.push({ type: 'LPAREN', value: '(', position: startPos });
+ i++;
+ continue;
+ }
+ if (expression[i] === ')') {
+ tokens.push({ type: 'RPAREN', value: ')', position: startPos });
+ i++;
+ continue;
+ }
+
+ // Comma
+ if (expression[i] === ',') {
+ tokens.push({ type: 'COMMA', value: ',', position: startPos });
+ i++;
+ continue;
+ }
+
+ // Check for multi-character operators first
+ let foundOperator = false;
+ for (const op of COMPARISON_OPERATORS) {
+ if (expression.substring(i, i + op.length) === op) {
+ tokens.push({
+ type: 'OPERATOR',
+ value: op,
+ position: startPos,
+ });
+ i += op.length;
+ foundOperator = true;
+ break;
+ }
+ }
+ if (foundOperator) continue;
+
+ // Number (including decimals and negative numbers handled by context)
+ if (/[0-9]/.test(expression[i])) {
+ let value = '';
+ while (
+ i < expression.length &&
+ /[0-9.]/.test(expression[i]) &&
+ !(expression[i] === '.' && value.includes('.'))
+ ) {
+ value += expression[i];
+ i++;
+ }
+ // Handle scientific notation
+ if (
+ i < expression.length &&
+ (expression[i] === 'e' || expression[i] === 'E')
+ ) {
+ value += expression[i];
+ i++;
+ if (
+ i < expression.length &&
+ (expression[i] === '+' || expression[i] === '-')
+ ) {
+ value += expression[i];
+ i++;
+ }
+ while (i < expression.length && /[0-9]/.test(expression[i])) {
+ value += expression[i];
+ i++;
+ }
+ }
+ tokens.push({ type: 'NUMBER', value, position: startPos });
+ continue;
+ }
+
+ // Identifier or keyword (starts with letter or underscore)
+ if (/[a-zA-Z_]/.test(expression[i])) {
+ let value = '';
+ while (
+ i < expression.length &&
+ /[a-zA-Z0-9_$]/.test(expression[i])
+ ) {
+ value += expression[i];
+ i++;
+ }
+ const upperValue = value.toUpperCase();
+ if (LOGICAL_OPERATORS.has(upperValue)) {
+ tokens.push({
+ type: 'LOGICAL',
+ value: upperValue,
+ position: startPos,
+ });
+ } else if (SQL_KEYWORDS.has(upperValue)) {
+ tokens.push({
+ type: 'KEYWORD',
+ value: upperValue,
+ position: startPos,
+ });
+ } else {
+ tokens.push({ type: 'IDENTIFIER', value, position: startPos });
+ }
+ continue;
+ }
+
+ // Unknown character
+ tokens.push({
+ type: 'UNKNOWN',
+ value: expression[i],
+ position: startPos,
+ });
+ i++;
+ }
+
+ return tokens;
+}
+
+/**
+ * Validation result with detailed error information
+ */
+export interface CheckConstraintValidationResult {
+ isValid: boolean;
+ error?: string;
+ position?: number;
+}
+
+/**
+ * Validates a SQL check constraint expression.
+ *
+ * @param expression - The check constraint expression to validate (e.g., "price > 0")
+ * @returns true if the expression is syntactically valid, false otherwise
+ */
+export function validateCheckConstraint(expression: string): boolean {
+ return validateCheckConstraintWithDetails(expression).isValid;
+}
+
+/**
+ * Validates a SQL check constraint expression with detailed error information.
+ *
+ * @param expression - The check constraint expression to validate
+ * @returns Validation result with error details if invalid
+ */
+export function validateCheckConstraintWithDetails(
+ expression: string
+): CheckConstraintValidationResult {
+ // Empty or whitespace-only expressions are invalid
+ if (!expression || !expression.trim()) {
+ return { isValid: false, error: 'Expression cannot be empty' };
+ }
+
+ const trimmed = expression.trim();
+
+ // Tokenize the expression
+ const tokens = tokenize(trimmed);
+
+ // Check for unknown tokens (except handling edge cases)
+ for (const token of tokens) {
+ if (token.type === 'UNKNOWN') {
+ return {
+ isValid: false,
+ error: `Unexpected character '${token.value}'`,
+ position: token.position,
+ };
+ }
+ }
+
+ // Empty token list after trimming
+ if (tokens.length === 0) {
+ return { isValid: false, error: 'Expression cannot be empty' };
+ }
+
+ // Check for balanced parentheses
+ let parenDepth = 0;
+ for (const token of tokens) {
+ if (token.type === 'LPAREN') parenDepth++;
+ if (token.type === 'RPAREN') parenDepth--;
+ if (parenDepth < 0) {
+ return {
+ isValid: false,
+ error: 'Unbalanced parentheses: unexpected closing parenthesis',
+ position: token.position,
+ };
+ }
+ }
+ if (parenDepth !== 0) {
+ return {
+ isValid: false,
+ error: 'Unbalanced parentheses: missing closing parenthesis',
+ };
+ }
+
+ // Validate token sequence for complete expressions
+ const validationResult = validateTokenSequence(tokens);
+ if (!validationResult.isValid) {
+ return validationResult;
+ }
+
+ return { isValid: true };
+}
+
+/**
+ * Validates that the token sequence forms a valid expression.
+ * Checks that operators have operands on both sides, etc.
+ */
+function validateTokenSequence(
+ tokens: Token[]
+): CheckConstraintValidationResult {
+ if (tokens.length === 0) {
+ return { isValid: false, error: 'Expression cannot be empty' };
+ }
+
+ // Track context for validation
+ let expectingOperand = true; // Start expecting an operand
+ let lastToken: Token | null = null;
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ switch (token.type) {
+ case 'IDENTIFIER':
+ case 'NUMBER':
+ case 'STRING':
+ if (!expectingOperand && lastToken?.type !== 'KEYWORD') {
+ // Allow identifiers after certain keywords like IS, AS, CAST
+ if (
+ lastToken?.type === 'IDENTIFIER' ||
+ lastToken?.type === 'NUMBER' ||
+ lastToken?.type === 'STRING'
+ ) {
+ return {
+ isValid: false,
+ error: `Unexpected ${token.type.toLowerCase()} '${token.value}' - expected an operator`,
+ position: token.position,
+ };
+ }
+ }
+ expectingOperand = false;
+ break;
+
+ case 'OPERATOR':
+ // Unary minus/plus before numbers or identifiers is OK at start or after operator/lparen
+ if (
+ (token.value === '-' || token.value === '+') &&
+ expectingOperand
+ ) {
+ // This is a unary operator, still expecting an operand
+ break;
+ }
+ // Type cast operator :: doesn't require operand after
+ if (token.value === '::') {
+ if (expectingOperand) {
+ return {
+ isValid: false,
+ error: `Operator '${token.value}' requires a value before it`,
+ position: token.position,
+ };
+ }
+ expectingOperand = true;
+ break;
+ }
+ if (expectingOperand) {
+ return {
+ isValid: false,
+ error: `Operator '${token.value}' requires a value before it`,
+ position: token.position,
+ };
+ }
+ expectingOperand = true;
+ break;
+
+ case 'LOGICAL':
+ if (token.value === 'NOT') {
+ // NOT can appear:
+ // 1. Before an operand: NOT(a = b), NOT deleted
+ // 2. After IS: a IS NOT NULL
+ // 3. Before LIKE/IN/BETWEEN: a NOT LIKE '%x%', a NOT IN (1,2), a NOT BETWEEN 1 AND 10
+ // In case 3, NOT comes after a complete operand but is valid
+ // We'll allow it and keep expectingOperand = true
+ expectingOperand = true;
+ } else {
+ // AND, OR require operands on both sides
+ if (expectingOperand) {
+ return {
+ isValid: false,
+ error: `Logical operator '${token.value}' requires a value before it`,
+ position: token.position,
+ };
+ }
+ expectingOperand = true;
+ }
+ break;
+
+ case 'KEYWORD':
+ // Handle special keywords
+ if (
+ token.value === 'NULL' ||
+ token.value === 'TRUE' ||
+ token.value === 'FALSE'
+ ) {
+ expectingOperand = false;
+ } else if (token.value === 'NOT') {
+ // NOT as keyword (e.g., IS NOT NULL)
+ expectingOperand = true;
+ } else if (
+ token.value === 'IS' ||
+ token.value === 'IN' ||
+ token.value === 'LIKE' ||
+ token.value === 'ILIKE'
+ ) {
+ // These keywords require a value before them, BUT
+ // they can also come after NOT (e.g., "a NOT LIKE 'x'", "a NOT IN (1,2)")
+ if (expectingOperand && lastToken?.value !== 'NOT') {
+ return {
+ isValid: false,
+ error: `Keyword '${token.value}' requires a value before it`,
+ position: token.position,
+ };
+ }
+ expectingOperand = true;
+ } else if (token.value === 'BETWEEN') {
+ // BETWEEN requires a value before it, or can come after NOT
+ if (expectingOperand && lastToken?.value !== 'NOT') {
+ return {
+ isValid: false,
+ error: `Keyword 'BETWEEN' requires a value before it`,
+ position: token.position,
+ };
+ }
+ expectingOperand = true;
+ } else if (
+ token.value === 'CASE' ||
+ token.value === 'CAST' ||
+ token.value === 'EXISTS'
+ ) {
+ // These can start expressions
+ expectingOperand = false;
+ } else if (
+ token.value === 'WHEN' ||
+ token.value === 'THEN' ||
+ token.value === 'ELSE' ||
+ token.value === 'END'
+ ) {
+ // CASE-related keywords
+ if (token.value === 'END') {
+ expectingOperand = false;
+ } else {
+ expectingOperand = true;
+ }
+ } else if (token.value === 'AS' || token.value === 'TO') {
+ // AS in CAST, TO in SIMILAR TO
+ expectingOperand = true;
+ } else {
+ // Other keywords - generally need an operand after
+ expectingOperand = true;
+ }
+ break;
+
+ case 'LPAREN':
+ // Parenthesis can follow an identifier (function call) or start a group
+ if (
+ lastToken?.type === 'IDENTIFIER' ||
+ lastToken?.value === 'IN' ||
+ lastToken?.value === 'EXISTS'
+ ) {
+ // Function call or IN list - still expecting operand
+ expectingOperand = true;
+ } else {
+ // Grouping parenthesis
+ expectingOperand = true;
+ }
+ break;
+
+ case 'RPAREN':
+ // After closing paren, we have a complete subexpression
+ if (expectingOperand && lastToken?.type !== 'LPAREN') {
+ // Empty parens () are only valid for function calls
+ // But we already validated for balance, so this might be OK
+ }
+ expectingOperand = false;
+ break;
+
+ case 'COMMA':
+ // Commas separate list items (e.g., IN (..., ...))
+ if (expectingOperand) {
+ return {
+ isValid: false,
+ error: 'Unexpected comma - expected a value',
+ position: token.position,
+ };
+ }
+ expectingOperand = true;
+ break;
+ }
+
+ lastToken = token;
+ }
+
+ // At the end, we should not be expecting an operand (expression should be complete)
+ if (expectingOperand && lastToken?.type !== 'RPAREN') {
+ // Exception: expression ending with ) is OK
+ // Find the last non-RPAREN token
+ let lastNonParen: Token | undefined;
+ for (let j = tokens.length - 1; j >= 0; j--) {
+ if (tokens[j].type !== 'RPAREN') {
+ lastNonParen = tokens[j];
+ break;
+ }
+ }
+ if (lastNonParen) {
+ if (lastNonParen.type === 'OPERATOR') {
+ return {
+ isValid: false,
+ error: `Expression incomplete - operator '${lastNonParen.value}' requires a value after it`,
+ position: lastNonParen.position,
+ };
+ }
+ if (
+ lastNonParen.type === 'LOGICAL' &&
+ lastNonParen.value !== 'NOT'
+ ) {
+ return {
+ isValid: false,
+ error: `Expression incomplete - '${lastNonParen.value}' requires a value after it`,
+ position: lastNonParen.position,
+ };
+ }
+ if (
+ lastNonParen.type === 'KEYWORD' &&
+ lastNonParen.value !== 'NULL' &&
+ lastNonParen.value !== 'TRUE' &&
+ lastNonParen.value !== 'FALSE' &&
+ lastNonParen.value !== 'END'
+ ) {
+ return {
+ isValid: false,
+ error: `Expression incomplete - '${lastNonParen.value}' requires a value after it`,
+ position: lastNonParen.position,
+ };
+ }
+ }
+ }
+
+ return { isValid: true };
+}
diff --git a/src/lib/clone.ts b/src/lib/clone.ts
index 2050764ef..bca48bcab 100644
--- a/src/lib/clone.ts
+++ b/src/lib/clone.ts
@@ -6,6 +6,7 @@ import type { DBIndex } from './domain/db-index';
import type { DBRelationship } from './domain/db-relationship';
import type { DBTable } from './domain/db-table';
import type { Diagram } from './domain/diagram';
+import type { Note } from './domain/note';
import { generateId as defaultGenerateId } from './utils';
const generateIdsMapFromTable = (
@@ -49,6 +50,10 @@ const generateIdsMapFromDiagram = (
idsMap.set(area.id, generateId());
});
+ diagram.notes?.forEach((note) => {
+ idsMap.set(note.id, generateId());
+ });
+
diagram.customTypes?.forEach((customType) => {
idsMap.set(customType.id, generateId());
});
@@ -115,6 +120,9 @@ export const cloneTable = (
.map((id) => getNewId(id))
.filter((fieldId): fieldId is string => fieldId !== null),
id,
+ // Clear the name for primary key indexes to avoid duplicate constraint names
+ // when exporting SQL scripts (database will auto-generate unique names)
+ name: index.isPrimaryKey ? '' : index.name,
};
})
.filter((index): index is DBIndex => index !== null);
@@ -218,6 +226,21 @@ export const cloneDiagram = (
})
.filter((area): area is Area => area !== null) ?? [];
+ const notes: Note[] =
+ diagram.notes
+ ?.map((note) => {
+ const id = getNewId(note.id);
+ if (!id) {
+ return null;
+ }
+
+ return {
+ ...note,
+ id,
+ } satisfies Note;
+ })
+ .filter((note): note is Note => note !== null) ?? [];
+
const customTypes: DBCustomType[] =
diagram.customTypes
?.map((customType) => {
@@ -242,6 +265,7 @@ export const cloneDiagram = (
relationships,
tables,
areas,
+ notes,
customTypes,
createdAt: diagram.createdAt
? new Date(diagram.createdAt)
diff --git a/src/lib/data/data-types/clickhouse-data-types.ts b/src/lib/data/data-types/clickhouse-data-types.ts
index 6f4c2d68e..0199ac4f7 100644
--- a/src/lib/data/data-types/clickhouse-data-types.ts
+++ b/src/lib/data/data-types/clickhouse-data-types.ts
@@ -129,9 +129,6 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{ name: 'enum', id: 'enum' },
{ name: 'lowcardinality', id: 'lowcardinality' },
- // Array Type
- { name: 'array', id: 'array' },
-
// Tuple Type
{ name: 'tuple', id: 'tuple' },
{ name: 'map', id: 'map' },
diff --git a/src/lib/data/data-types/data-types.ts b/src/lib/data/data-types/data-types.ts
index e8cd17fae..e8b6734c1 100644
--- a/src/lib/data/data-types/data-types.ts
+++ b/src/lib/data/data-types/data-types.ts
@@ -1,10 +1,14 @@
import { z } from 'zod';
import { DatabaseType } from '../../domain/database-type';
+import { databaseSupportsArrays } from '../../domain/database-capabilities';
import { clickhouseDataTypes } from './clickhouse-data-types';
import { genericDataTypes } from './generic-data-types';
import { mariadbDataTypes } from './mariadb-data-types';
import { mysqlDataTypes } from './mysql-data-types';
-import { postgresDataTypes } from './postgres-data-types';
+import {
+ getPostgresPreferredSynonym,
+ postgresDataTypes,
+} from './postgres-data-types';
import { sqlServerDataTypes } from './sql-server-data-types';
import { sqliteDataTypes } from './sqlite-data-types';
import { oracleDataTypes } from './oracle-data-types';
@@ -165,3 +169,110 @@ export const supportsAutoIncrementDataType = (
'decimal',
].includes(dataTypeName.toLocaleLowerCase());
};
+
+export const autoIncrementAlwaysOn = (dataTypeName: string): boolean => {
+ return ['serial', 'bigserial', 'smallserial'].includes(
+ dataTypeName.toLowerCase()
+ );
+};
+
+export const requiresNotNull = (dataTypeName: string): boolean => {
+ return ['serial', 'bigserial', 'smallserial'].includes(
+ dataTypeName.toLowerCase()
+ );
+};
+
+const ARRAY_INCOMPATIBLE_TYPES = [
+ 'serial',
+ 'bigserial',
+ 'smallserial',
+] as const;
+
+export const supportsArrayDataType = (
+ dataTypeName: string,
+ databaseType: DatabaseType
+): boolean => {
+ if (!databaseSupportsArrays(databaseType)) {
+ return false;
+ }
+
+ return !ARRAY_INCOMPATIBLE_TYPES.includes(
+ dataTypeName.toLowerCase() as (typeof ARRAY_INCOMPATIBLE_TYPES)[number]
+ );
+};
+
+/**
+ * Resolves a data type to its preferred synonym if one exists for the given database type.
+ *
+ * This function acts as a dispatcher to database-specific synonym resolution functions.
+ * Currently supports PostgreSQL synonyms.
+ *
+ * @param typeName - The name of the data type to check (case-insensitive)
+ * @param databaseType - The database type (e.g., PostgreSQL, MySQL, etc.)
+ * @returns The DataTypeData of the preferred synonym, or null if the type
+ * is already the preferred form, has no synonyms, or the database type
+ * doesn't have synonym mappings
+ *
+ * @example
+ * ```ts
+ * getPreferredSynonym('character varying', DatabaseType.POSTGRESQL)
+ * // Returns: { name: 'varchar', id: 'varchar', ... }
+ *
+ * getPreferredSynonym('varchar', DatabaseType.POSTGRESQL)
+ * // Returns: null (already the preferred form)
+ *
+ * getPreferredSynonym('character varying', DatabaseType.MYSQL)
+ * // Returns: null (MySQL synonym mappings not implemented)
+ * ```
+ */
+export const getPreferredSynonym = (
+ typeName: string,
+ databaseType: DatabaseType
+): DataTypeData | null => {
+ const nameLower = typeName.toLowerCase().trim();
+
+ if (
+ databaseType === DatabaseType.POSTGRESQL ||
+ databaseType === DatabaseType.COCKROACHDB
+ ) {
+ return getPostgresPreferredSynonym(nameLower);
+ }
+
+ return null;
+};
+
+/**
+ * Returns the default primary key data type for a given database type.
+ *
+ * Different databases have different conventions for auto-increment primary key types:
+ * - SQLite: INTEGER (required for AUTOINCREMENT)
+ * - Oracle: NUMBER (Oracle doesn't have bigint - uses NUMBER for all numeric types)
+ * - Others: BIGINT (PostgreSQL, MySQL, SQL Server, MariaDB, etc.)
+ *
+ * @param databaseType - The database type
+ * @returns The DataType object with id and name for the default primary key type
+ *
+ * @example
+ * ```ts
+ * getDefaultPrimaryKeyType(DatabaseType.SQLITE)
+ * // Returns: { id: 'integer', name: 'integer' }
+ *
+ * getDefaultPrimaryKeyType(DatabaseType.ORACLE)
+ * // Returns: { id: 'number', name: 'number' }
+ *
+ * getDefaultPrimaryKeyType(DatabaseType.POSTGRESQL)
+ * // Returns: { id: 'bigint', name: 'bigint' }
+ * ```
+ */
+export const getDefaultPrimaryKeyType = (
+ databaseType: DatabaseType
+): DataType => {
+ switch (databaseType) {
+ case DatabaseType.SQLITE:
+ return { id: 'integer', name: 'integer' };
+ case DatabaseType.ORACLE:
+ return { id: 'number', name: 'number' };
+ default:
+ return { id: 'bigint', name: 'bigint' };
+ }
+};
diff --git a/src/lib/data/data-types/postgres-data-types.ts b/src/lib/data/data-types/postgres-data-types.ts
index 0c85058f6..1ef18d7db 100644
--- a/src/lib/data/data-types/postgres-data-types.ts
+++ b/src/lib/data/data-types/postgres-data-types.ts
@@ -2,7 +2,9 @@ import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
- { name: 'integer', id: 'integer', usageLevel: 1 },
+ // { name: 'integer', id: 'integer', usageLevel: 1 },
+ { name: 'int', id: 'int', usageLevel: 1 },
+ // { name: 'int4', id: 'int4', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
@@ -11,42 +13,47 @@ export const postgresDataTypes: readonly DataTypeData[] = [
},
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
+ // { name: 'bool', id: 'bool', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
+ { name: 'timestamptz', id: 'timestamptz', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
- {
- name: 'decimal',
- id: 'decimal',
- usageLevel: 2,
- fieldAttributes: {
- precision: {
- max: 131072,
- min: 0,
- default: 10,
- },
- scale: {
- max: 16383,
- min: 0,
- default: 2,
- },
- },
- },
+ // { name: 'int8', id: 'int8', usageLevel: 2 },
+ // {
+ // name: 'decimal',
+ // id: 'decimal',
+ // usageLevel: 2,
+ // fieldAttributes: {
+ // precision: {
+ // max: 131072,
+ // min: 0,
+ // default: 10,
+ // },
+ // scale: {
+ // max: 16383,
+ // min: 0,
+ // default: 2,
+ // },
+ // },
+ // },
{ name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
- {
- name: 'timestamp with time zone',
- id: 'timestamp_with_time_zone',
- usageLevel: 2,
- },
+ // {
+ // name: 'timestamp with time zone',
+ // id: 'timestamp_with_time_zone',
+ // usageLevel: 2,
+ // },
+ // { name: 'int', id: 'int', usageLevel: 2 },
// Less common types
{
name: 'numeric',
id: 'numeric',
+ usageLevel: 2,
fieldAttributes: {
precision: {
max: 131072,
@@ -61,21 +68,30 @@ export const postgresDataTypes: readonly DataTypeData[] = [
},
},
{ name: 'real', id: 'real' },
+ // { name: 'float4', id: 'float4' },
{ name: 'double precision', id: 'double_precision' },
+ // { name: 'float8', id: 'float8' },
{ name: 'smallserial', id: 'smallserial' },
{ name: 'bigserial', id: 'bigserial' },
{ name: 'money', id: 'money' },
{ name: 'smallint', id: 'smallint' },
+ // { name: 'int2', id: 'int2' },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
- {
- name: 'character varying',
- id: 'character_varying',
- fieldAttributes: { hasCharMaxLength: true },
- },
+ // {
+ // name: 'character',
+ // id: 'character',
+ // fieldAttributes: { hasCharMaxLength: true },
+ // },
+ // {
+ // name: 'character varying',
+ // id: 'character_varying',
+ // fieldAttributes: { hasCharMaxLength: true },
+ // },
{ name: 'time', id: 'time' },
- { name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
- { name: 'time with time zone', id: 'time_with_time_zone' },
- { name: 'time without time zone', id: 'time_without_time_zone' },
+ { name: 'timetz', id: 'timetz' },
+ // { name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
+ // { name: 'time with time zone', id: 'time_with_time_zone' },
+ // { name: 'time without time zone', id: 'time_without_time_zone' },
{ name: 'interval', id: 'interval' },
{ name: 'bytea', id: 'bytea' },
{ name: 'enum', id: 'enum' },
@@ -91,11 +107,11 @@ export const postgresDataTypes: readonly DataTypeData[] = [
{ name: 'macaddr', id: 'macaddr' },
{ name: 'macaddr8', id: 'macaddr8' },
{ name: 'bit', id: 'bit' },
- { name: 'bit varying', id: 'bit_varying' },
+ // { name: 'bit varying', id: 'bit_varying' },
+ { name: 'varbit', id: 'varbit' },
{ name: 'tsvector', id: 'tsvector' },
{ name: 'tsquery', id: 'tsquery' },
{ name: 'xml', id: 'xml' },
- { name: 'array', id: 'array' },
{ name: 'int4range', id: 'int4range' },
{ name: 'int8range', id: 'int8range' },
{ name: 'numrange', id: 'numrange' },
@@ -115,3 +131,92 @@ export const postgresDataTypes: readonly DataTypeData[] = [
{ name: 'regdictionary', id: 'regdictionary' },
{ name: 'user-defined', id: 'user-defined' },
] as const;
+
+/**
+ * Maps data type names to their preferred synonym names.
+ * The preferred synonym is typically the more commonly used or shorter form.
+ *
+ * Based on PostgreSQL official type names and common usage:
+ * - Verbose forms map to their shorter equivalents
+ * - Less common aliases map to their primary type names
+ * - Types with usageLevel: 1 are generally preferred over those with usageLevel: 2 or no level
+ *
+ * Note: Keys and values use the actual PostgreSQL type names (with spaces where applicable),
+ * not the internal ID format.
+ */
+const synonymMap: Record = {
+ // Character types
+ 'character varying': 'varchar',
+ character: 'char',
+
+ // Boolean types
+ bool: 'boolean',
+
+ // Integer types
+ integer: 'int',
+ int2: 'smallint',
+ int4: 'int',
+ int8: 'bigint',
+
+ // Floating point types
+ float4: 'real',
+ float8: 'double precision',
+
+ // Timestamp types
+ 'timestamp with time zone': 'timestamptz',
+ 'timestamp without time zone': 'timestamp',
+ datetime: 'timestamp',
+
+ // Time types
+ 'time with time zone': 'timetz',
+ 'time without time zone': 'time',
+
+ // Bit types
+ 'bit varying': 'varbit',
+
+ // Numeric types
+ decimal: 'numeric',
+} as const;
+
+/**
+ * Resolves a data type to its preferred synonym if one exists.
+ *
+ * For data types that have synonyms in PostgreSQL, this function returns
+ * the more commonly used variant. For example, 'character varying' resolves
+ * to 'varchar', and 'integer' resolves to 'int'.
+ *
+ * @param typeName - The name of the data type to check (case-insensitive)
+ * @returns The DataTypeData of the preferred synonym, or null if the type
+ * is already the preferred form or has no synonyms
+ *
+ * @example
+ * ```ts
+ * getPostgresPreferredSynonym('character varying')
+ * // Returns: { name: 'varchar', id: 'varchar', fieldAttributes: { hasCharMaxLength: true }, usageLevel: 1 }
+ *
+ * getPostgresPreferredSynonym('varchar')
+ * // Returns: null (already the preferred form)
+ *
+ * getPostgresPreferredSynonym('INTEGER')
+ * // Returns: { name: 'int', id: 'int', usageLevel: 1 } (case-insensitive)
+ * ```
+ */
+export const getPostgresPreferredSynonym = (
+ typeName: string
+): DataTypeData | null => {
+ // Normalize to lowercase for case-insensitive lookup
+ const normalizedTypeName = typeName.toLowerCase().trim();
+
+ const preferredName = synonymMap[normalizedTypeName];
+
+ if (!preferredName) {
+ return null;
+ }
+
+ return (
+ postgresDataTypes.find(
+ (dataType) =>
+ dataType.name.toLowerCase() === preferredName.toLowerCase()
+ ) ?? null
+ );
+};
diff --git a/src/lib/data/import-metadata/__tests__/fix-metadata-json.test.ts b/src/lib/data/import-metadata/__tests__/fix-metadata-json.test.ts
new file mode 100644
index 000000000..b8315839f
--- /dev/null
+++ b/src/lib/data/import-metadata/__tests__/fix-metadata-json.test.ts
@@ -0,0 +1,337 @@
+import { describe, it, expect, vi } from 'vitest';
+import { fixMetadataJson, isStringMetadataJson } from '../utils';
+
+describe('fixMetadataJson', () => {
+ describe('escaped quotes', () => {
+ it('should fix escaped double quotes (\\") to regular quotes', () => {
+ const input = '{\\"name\\": \\"test\\"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should handle deeply nested escaped quotes', () => {
+ const input =
+ '{\\"fk_info\\": [], \\"pk_info\\": [{\\"schema\\": \\"public\\", \\"table\\": \\"users\\"}]}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe(
+ '{"fk_info": [], "pk_info": [{"schema": "public", "table": "users"}]}'
+ );
+ });
+ });
+
+ describe('literal escape sequences', () => {
+ it('should remove literal \\n (backslash + n) from stringified JSON', () => {
+ const input = '{\\n "name": "test"\\n}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{ "name": "test"}');
+ });
+
+ it('should remove literal \\t (backslash + t) from stringified JSON', () => {
+ const input = '{\\t"name": "test"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should remove literal \\r (backslash + r) from stringified JSON', () => {
+ const input = '{\\r"name": "test"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should handle combined literal escape sequences', () => {
+ const input = '{\\r\\n\\t"name": "test"\\r\\n}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should handle fully stringified JSON with \\n and \\" combined', () => {
+ const input =
+ '{\\n \\"fk_info\\": [],\\n \\"pk_info\\": []\\n}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{ "fk_info": [], "pk_info": []}');
+ });
+ });
+
+ describe('double-double quotes', () => {
+ it('should convert :""value"" to :"value" for simple values', () => {
+ const input = '{"pk_def": ""PRIMARY KEY (id)""}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"pk_def": "PRIMARY KEY (id)"}');
+ });
+
+ it('should convert : ""value"" (with space) to : "value"', () => {
+ const input = '{"pk_def": ""PRIMARY KEY (id)""}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"pk_def": "PRIMARY KEY (id)"}');
+ });
+
+ it('should handle double-double quotes with complex values containing commas', () => {
+ const input =
+ '{"pk_def": ""PRIMARY KEY (audit_type_cd, channel_type_cd)""}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe(
+ '{"pk_def": "PRIMARY KEY (audit_type_cd, channel_type_cd)"}'
+ );
+ });
+
+ it('should handle multiple double-double quoted values in array', () => {
+ const input = `{
+ "pk_info": [
+ {"pk_def": ""PRIMARY KEY (id)""},
+ {"pk_def": ""PRIMARY KEY (a, b)""}
+ ]
+}`;
+ const result = fixMetadataJson(input);
+ const parsed = JSON.parse(result);
+ expect(parsed.pk_info[0].pk_def).toBe('PRIMARY KEY (id)');
+ expect(parsed.pk_info[1].pk_def).toBe('PRIMARY KEY (a, b)');
+ });
+ });
+
+ describe('extra content removal', () => {
+ it('should remove content before the first {', () => {
+ const input = 'some prefix text {"name": "test"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should remove content after the last }', () => {
+ const input = '{"name": "test"} some suffix text';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should remove both prefix and suffix content', () => {
+ const input = 'Result: {"name": "test"} -- end';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+ });
+
+ describe('string-wrapped JSON', () => {
+ it('should remove surrounding double quotes', () => {
+ const input = '"{\\"name\\": \\"test\\"}"';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should remove surrounding single quotes', () => {
+ const input = '\'{"name": "test"}\'';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+ });
+
+ describe('type conversions', () => {
+ it('should convert "precision": "null" to "precision": null', () => {
+ const input = '{"precision": "null", "scale": 2}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"precision": null, "scale": 2}');
+ });
+
+ it('should convert "nullable": "false" to "nullable": false', () => {
+ const input = '{"nullable": "false"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"nullable": false}');
+ });
+
+ it('should convert "nullable": "true" to "nullable": true', () => {
+ const input = '{"nullable": "true"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"nullable": true}');
+ });
+ });
+
+ describe('quadruple and triple quotes', () => {
+ it('should convert """" to ""', () => {
+ const input = '{"comment": """"}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"comment": ""}');
+ });
+
+ it('should convert """value""" to "value"', () => {
+ const input = '{"name": """test"""}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+ });
+
+ describe('real newline removal', () => {
+ it('should remove actual newline characters', () => {
+ const input = '{\n"name": "test"\n}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+ });
+
+ describe('escaped backslashes', () => {
+ it('should convert \\\\\\\\ to \\\\ (double-escaped to single-escaped)', () => {
+ // Input: {"path": "C:\\\\Users"} (double-escaped backslash in JSON string literal)
+ // Output: {"path": "C:\\Users"} (single-escaped backslash, valid JSON for path C:\Users)
+ const input = '{"path": "C:\\\\\\\\Users"}';
+ const result = fixMetadataJson(input);
+ // In JSON, \\ represents a single backslash, so this is correct
+ expect(result).toBe('{"path": "C:\\\\Users"}');
+ // Verify it parses correctly
+ const parsed = JSON.parse(result);
+ expect(parsed.path).toBe('C:\\Users');
+ });
+ });
+
+ describe('complex real-world scenarios', () => {
+ it('should handle MySQL smart query output with escaped quotes and newlines', () => {
+ const input = `{\\n \\"fk_info\\": [],\\n \\"pk_info\\": [\\n {\\n \\"schema\\": \\"mydb\\",\\n \\"table\\": \\"users\\",\\n \\"column\\": \\"id\\",\\n \\"pk_def\\": \\"PRIMARY KEY (id)\\"\\n }\\n ],\\n \\"columns\\": [],\\n \\"indexes\\": [],\\n \\"tables\\": [],\\n \\"views\\": [],\\n \\"database_name\\": \\"mydb\\",\\n \\"version\\": \\"8.0.39\\"\\n}`;
+
+ const result = fixMetadataJson(input);
+ const parsed = JSON.parse(result);
+
+ expect(parsed.fk_info).toEqual([]);
+ expect(parsed.pk_info).toHaveLength(1);
+ expect(parsed.pk_info[0].schema).toBe('mydb');
+ expect(parsed.pk_info[0].table).toBe('users');
+ expect(parsed.database_name).toBe('mydb');
+ expect(parsed.version).toBe('8.0.39');
+ });
+
+ it('should handle MySQL output with double-double quoted pk_def values', () => {
+ const input = `{
+ "fk_info": [],
+ "pk_info": [
+ {
+ "schema": "mydb",
+ "table": "audit_status",
+ "column": "audit_type_cd",
+ "pk_def": ""PRIMARY KEY (audit_type_cd, channel_type_cd)""
+ }
+ ],
+ "columns": [],
+ "indexes": [],
+ "tables": [],
+ "views": [],
+ "database_name": "mydb",
+ "version": "8.0.39"
+}`;
+
+ const result = fixMetadataJson(input);
+ const parsed = JSON.parse(result);
+
+ expect(parsed.pk_info[0].pk_def).toBe(
+ 'PRIMARY KEY (audit_type_cd, channel_type_cd)'
+ );
+ });
+
+ it('should handle combination of escaped quotes, literal newlines, and double-double quotes', () => {
+ const input = `{\\n \\"pk_info\\": [\\n {\\n \\"pk_def\\": \\"\\"PRIMARY KEY (a, b)\\"\\",\\n \\"nullable\\": \\"false\\"\\n }\\n ]\\n}`;
+
+ const result = fixMetadataJson(input);
+ const parsed = JSON.parse(result);
+
+ expect(parsed.pk_info[0].pk_def).toBe('PRIMARY KEY (a, b)');
+ expect(parsed.pk_info[0].nullable).toBe(false);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle empty objects', () => {
+ const input = '{}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{}');
+ });
+
+ it('should handle empty arrays in objects', () => {
+ const input = '{"items": []}';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"items": []}');
+ });
+
+ it('should handle whitespace-only prefix/suffix', () => {
+ const input = ' {"name": "test"} ';
+ const result = fixMetadataJson(input);
+ expect(result).toBe('{"name": "test"}');
+ });
+
+ it('should handle already valid JSON', () => {
+ const validJson =
+ '{"fk_info": [], "pk_info": [], "columns": [], "indexes": [], "tables": [], "views": [], "database_name": "test", "version": "1.0"}';
+ const result = fixMetadataJson(validJson);
+ expect(JSON.parse(result)).toEqual(JSON.parse(validJson));
+ });
+ });
+});
+
+describe('isStringMetadataJson', () => {
+ it('should return true for valid database metadata JSON', () => {
+ const validMetadata = JSON.stringify({
+ fk_info: [],
+ pk_info: [],
+ columns: [],
+ indexes: [],
+ tables: [],
+ views: [],
+ database_name: 'test_db',
+ version: '1.0',
+ });
+
+ expect(isStringMetadataJson(validMetadata)).toBe(true);
+ });
+
+ it('should return false for invalid JSON string', () => {
+ expect(isStringMetadataJson('not json')).toBe(false);
+ });
+
+ it('should return false for valid JSON but missing required fields', () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ const incompleteMetadata = JSON.stringify({
+ fk_info: [],
+ pk_info: [],
+ // missing other required fields
+ });
+
+ expect(isStringMetadataJson(incompleteMetadata)).toBe(false);
+
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should return false for empty string', () => {
+ expect(isStringMetadataJson('')).toBe(false);
+ });
+
+ it('should return false for null-like values', () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ expect(isStringMetadataJson('null')).toBe(false);
+ expect(isStringMetadataJson('undefined')).toBe(false);
+
+ consoleErrorSpy.mockRestore();
+ });
+});
+
+describe('fixMetadataJson + isStringMetadataJson integration', () => {
+ it('should fix and validate MySQL smart query output', () => {
+ const brokenInput = `{\\n \\"fk_info\\": [],\\n \\"pk_info\\": [],\\n \\"columns\\": [],\\n \\"indexes\\": [],\\n \\"tables\\": [],\\n \\"views\\": [],\\n \\"database_name\\": \\"testdb\\",\\n \\"version\\": \\"8.0.39\\"\\n}`;
+
+ const fixed = fixMetadataJson(brokenInput);
+ expect(isStringMetadataJson(fixed)).toBe(true);
+ });
+
+ it('should fix and validate output with double-double quoted values', () => {
+ const brokenInput = `{
+ "fk_info": [],
+ "pk_info": [{"schema": "db", "table": "t", "column": "c", "pk_def": ""PRIMARY KEY (a, b)""}],
+ "columns": [],
+ "indexes": [],
+ "tables": [],
+ "views": [],
+ "database_name": "testdb",
+ "version": "8.0"
+}`;
+
+ const fixed = fixMetadataJson(brokenInput);
+ expect(isStringMetadataJson(fixed)).toBe(true);
+ });
+});
diff --git a/src/lib/data/import-metadata/import/fields.ts b/src/lib/data/import-metadata/import/fields.ts
index 01c901c82..77d209dda 100644
--- a/src/lib/data/import-metadata/import/fields.ts
+++ b/src/lib/data/import-metadata/import/fields.ts
@@ -1,20 +1,24 @@
import type { DBField } from '@/lib/domain';
+import type { DatabaseType } from '@/lib/domain';
import type { ColumnInfo } from '../metadata-types/column-info';
import type { AggregatedIndexInfo } from '../metadata-types/index-info';
import type { PrimaryKeyInfo } from '../metadata-types/primary-key-info';
import type { TableInfo } from '../metadata-types/table-info';
import { generateId } from '@/lib/utils';
+import { getPreferredSynonym } from '@/lib/data/data-types/data-types';
export const createFieldsFromMetadata = ({
tableColumns,
tablePrimaryKeys,
aggregatedIndexes,
+ databaseType,
}: {
tableColumns: ColumnInfo[];
tableSchema?: string;
tableInfo: TableInfo;
tablePrimaryKeys: PrimaryKeyInfo[];
aggregatedIndexes: AggregatedIndexInfo[];
+ databaseType: DatabaseType;
}) => {
const uniqueColumns = tableColumns.reduce((acc, col) => {
if (!acc.has(col.name)) {
@@ -31,14 +35,28 @@ export const createFieldsFromMetadata = ({
pk.column.trim()
);
- return sortedColumns.map(
- (col: ColumnInfo): DBField => ({
+ return sortedColumns.map((col: ColumnInfo): DBField => {
+ // Create initial type from column metadata
+ const initialType = {
+ id: col.type.split(' ').join('_').toLowerCase(),
+ name: col.type.toLowerCase(),
+ };
+
+ // Check if there's a preferred synonym for this type
+ const preferredType = getPreferredSynonym(
+ initialType.name,
+ databaseType
+ );
+
+ // Use the preferred synonym if it exists, otherwise use the initial type
+ const finalType = preferredType
+ ? { id: preferredType.id, name: preferredType.name }
+ : initialType;
+
+ return {
id: generateId(),
name: col.name,
- type: {
- id: col.type.split(' ').join('_').toLowerCase(),
- name: col.type.toLowerCase(),
- },
+ type: finalType,
primaryKey: tablePrimaryKeysColumns.includes(col.name),
unique: Object.values(aggregatedIndexes).some(
(idx) =>
@@ -57,8 +75,12 @@ export const createFieldsFromMetadata = ({
...(col.precision?.scale ? { scale: col.precision.scale } : {}),
...(col.default ? { default: col.default } : {}),
...(col.collation ? { collation: col.collation } : {}),
+ ...(col.is_identity !== undefined
+ ? { increment: col.is_identity }
+ : {}),
+ ...(col.is_array !== undefined ? { isArray: col.is_array } : {}),
createdAt: Date.now(),
comments: col.comment ? col.comment : undefined,
- })
- );
+ };
+ });
};
diff --git a/src/lib/data/import-metadata/import/index.ts b/src/lib/data/import-metadata/import/index.ts
index ecc907fbc..81c09255c 100644
--- a/src/lib/data/import-metadata/import/index.ts
+++ b/src/lib/data/import-metadata/import/index.ts
@@ -1,5 +1,9 @@
import type { DatabaseEdition, Diagram } from '@/lib/domain';
-import { adjustTablePositions, DatabaseType } from '@/lib/domain';
+import {
+ adjustTablePositions,
+ DatabaseType,
+ getTableIndexesWithPrimaryKey,
+} from '@/lib/domain';
import { generateDiagramId } from '@/lib/utils';
import type { DatabaseMetadata } from '../metadata-types/database-metadata';
import { createCustomTypesFromMetadata } from './custom-types';
@@ -52,19 +56,24 @@ export const loadFromDatabaseMetadata = async ({
mode: 'perSchema',
});
- const sortedTables = adjustedTables.sort((a, b) => {
- if (a.isView === b.isView) {
- // Both are either tables or views, so sort alphabetically by name
- return a.name.localeCompare(b.name);
- }
- // If one is a view and the other is not, put tables first
- return a.isView ? 1 : -1;
- });
+ const sortedTables = adjustedTables
+ .map((table) => ({
+ ...table,
+ indexes: getTableIndexesWithPrimaryKey({ table }),
+ }))
+ .sort((a, b) => {
+ if (a.isView === b.isView) {
+ // Both are either tables or views, so sort alphabetically by name
+ return a.name.localeCompare(b.name);
+ }
+ // If one is a view and the other is not, put tables first
+ return a.isView ? 1 : -1;
+ });
const diagram: Diagram = {
id: generateDiagramId(),
name: databaseMetadata.database_name
- ? `${databaseMetadata.database_name}-db`
+ ? `${databaseMetadata.database_name}`
: diagramNumber
? `Diagram ${diagramNumber}`
: 'New Diagram',
diff --git a/src/lib/data/import-metadata/import/relationships.ts b/src/lib/data/import-metadata/import/relationships.ts
index 0b0fd7aae..044f617b7 100644
--- a/src/lib/data/import-metadata/import/relationships.ts
+++ b/src/lib/data/import-metadata/import/relationships.ts
@@ -55,24 +55,27 @@ export const createRelationshipsFromMetadata = ({
.length > 1;
if (sourceTable && targetTable && sourceField && targetField) {
+ // In ForeignKeyInfo: schema/table/column = FK table, reference_* = PK table
+ // In DBRelationship: source = referenced table (PK), target = FK table
+ // So we swap them here
const sourceCardinality = determineCardinality(
- sourceField,
- isSourceTablePKComplex
- );
- const targetCardinality = determineCardinality(
targetField,
isTargetTablePKComplex
);
+ const targetCardinality = determineCardinality(
+ sourceField,
+ isSourceTablePKComplex
+ );
return {
id: generateId(),
name: fk.foreign_key_name,
- sourceSchema: schema,
- targetSchema: targetSchema,
- sourceTableId: sourceTable.id,
- targetTableId: targetTable.id,
- sourceFieldId: sourceField.id,
- targetFieldId: targetField.id,
+ sourceSchema: targetSchema,
+ targetSchema: schema,
+ sourceTableId: targetTable.id,
+ targetTableId: sourceTable.id,
+ sourceFieldId: targetField.id,
+ targetFieldId: sourceField.id,
sourceCardinality,
targetCardinality,
createdAt: Date.now(),
diff --git a/src/lib/data/import-metadata/import/tables.ts b/src/lib/data/import-metadata/import/tables.ts
index b45689b84..7b3468205 100644
--- a/src/lib/data/import-metadata/import/tables.ts
+++ b/src/lib/data/import-metadata/import/tables.ts
@@ -1,4 +1,4 @@
-import type { DBIndex, DBTable } from '@/lib/domain';
+import type { DBCheckConstraint, DBIndex, DBTable } from '@/lib/domain';
import {
DatabaseType,
generateTableKey,
@@ -6,6 +6,7 @@ import {
} from '@/lib/domain';
import type { DatabaseMetadata } from '../metadata-types/database-metadata';
import type { TableInfo } from '../metadata-types/table-info';
+import type { CheckConstraintInfo } from '../metadata-types/check-constraint-info';
import { createAggregatedIndexes } from '../metadata-types/index-info';
import {
decodeBase64ToUtf16LE,
@@ -51,6 +52,7 @@ export const createTablesFromMetadata = ({
columns,
indexes,
views: views,
+ check_constraints: checkConstraints,
} = databaseMetadata;
// Pre-compute view names for faster lookup if there are views
@@ -117,6 +119,21 @@ export const createTablesFromMetadata = ({
primaryKeysByTable.get(key)!.push(pk);
});
+ // Group check constraints by table
+ const checkConstraintsByTable = new Map();
+ if (checkConstraints) {
+ checkConstraints.forEach((cc) => {
+ const key = generateTableKey({
+ schemaName: cc.schema,
+ tableName: cc.table,
+ });
+ if (!checkConstraintsByTable.has(key)) {
+ checkConstraintsByTable.set(key, []);
+ }
+ checkConstraintsByTable.get(key)!.push(cc);
+ });
+ }
+
const result = tableInfos.map((tableInfo: TableInfo) => {
const tableSchema = schemaNameToDomainSchemaName(tableInfo.schema);
const tableKey = generateTableKey({
@@ -142,6 +159,7 @@ export const createTablesFromMetadata = ({
tablePrimaryKeys,
tableInfo,
tableSchema,
+ databaseType,
});
// Check for composite primary key and find matching index name
@@ -203,6 +221,17 @@ export const createTablesFromMetadata = ({
const isView = viewNamesSet.has(viewKey);
const isMaterializedView = materializedViewNamesSet.has(viewKey);
+ // Convert check constraints for this table
+ const tableCheckConstraints = checkConstraintsByTable.get(tableKey);
+ const dbCheckConstraints: DBCheckConstraint[] | undefined =
+ tableCheckConstraints && tableCheckConstraints.length > 0
+ ? tableCheckConstraints.map((cc) => ({
+ id: generateId(),
+ expression: cc.expression,
+ createdAt: Date.now(),
+ }))
+ : undefined;
+
// Initial random positions; these will be adjusted later
return {
id: generateId(),
@@ -212,6 +241,7 @@ export const createTablesFromMetadata = ({
y: Math.random() * 800, // Placeholder Y
fields,
indexes: dbIndexes,
+ checkConstraints: dbCheckConstraints,
color: isMaterializedView
? materializedViewColor
: isView
diff --git a/src/lib/data/import-metadata/metadata-types/check-constraint-info.ts b/src/lib/data/import-metadata/metadata-types/check-constraint-info.ts
new file mode 100644
index 000000000..6c4649a71
--- /dev/null
+++ b/src/lib/data/import-metadata/metadata-types/check-constraint-info.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+export interface CheckConstraintInfo {
+ schema: string;
+ table: string;
+ expression: string;
+}
+
+export const CheckConstraintInfoSchema: z.ZodType =
+ z.object({
+ schema: z.string(),
+ table: z.string(),
+ expression: z.string(),
+ });
diff --git a/src/lib/data/import-metadata/metadata-types/column-info.ts b/src/lib/data/import-metadata/metadata-types/column-info.ts
index 4e22a20f7..b434709bc 100644
--- a/src/lib/data/import-metadata/metadata-types/column-info.ts
+++ b/src/lib/data/import-metadata/metadata-types/column-info.ts
@@ -15,6 +15,8 @@ export interface ColumnInfo {
default?: string | null; // Default value for the column, nullable
collation?: string | null;
comment?: string | null;
+ is_identity?: boolean | null; // Indicates if the column is auto-increment/identity
+ is_array?: boolean | null; // Indicates if the column is an array type
}
export const ColumnInfoSchema: z.ZodType = z.object({
@@ -35,4 +37,6 @@ export const ColumnInfoSchema: z.ZodType = z.object({
default: z.string().nullable().optional(),
collation: z.string().nullable().optional(),
comment: z.string().nullable().optional(),
+ is_identity: z.boolean().nullable().optional(),
+ is_array: z.boolean().nullable().optional(),
});
diff --git a/src/lib/data/import-metadata/metadata-types/database-metadata.ts b/src/lib/data/import-metadata/metadata-types/database-metadata.ts
index e4868722e..9cab57aed 100644
--- a/src/lib/data/import-metadata/metadata-types/database-metadata.ts
+++ b/src/lib/data/import-metadata/metadata-types/database-metadata.ts
@@ -1,4 +1,8 @@
import { z } from 'zod';
+import {
+ CheckConstraintInfoSchema,
+ type CheckConstraintInfo,
+} from './check-constraint-info';
import { ForeignKeyInfoSchema, type ForeignKeyInfo } from './foreign-key-info';
import { PrimaryKeyInfoSchema, type PrimaryKeyInfo } from './primary-key-info';
import { ColumnInfoSchema, type ColumnInfo } from './column-info';
@@ -17,6 +21,7 @@ export interface DatabaseMetadata {
indexes: IndexInfo[];
tables: TableInfo[];
views: ViewInfo[];
+ check_constraints?: CheckConstraintInfo[];
custom_types?: DBCustomTypeInfo[];
database_name: string;
version: string;
@@ -29,6 +34,7 @@ export const DatabaseMetadataSchema: z.ZodType = z.object({
indexes: z.array(IndexInfoSchema),
tables: z.array(TableInfoSchema),
views: z.array(ViewInfoSchema),
+ check_constraints: z.array(CheckConstraintInfoSchema).optional(),
custom_types: z.array(DBCustomTypeInfoSchema).optional(),
database_name: z.string(),
version: z.string(),
diff --git a/src/lib/data/import-metadata/scripts/cockroachdb-script.ts b/src/lib/data/import-metadata/scripts/cockroachdb-script.ts
index a220dc6b9..8d34d95d6 100644
--- a/src/lib/data/import-metadata/scripts/cockroachdb-script.ts
+++ b/src/lib/data/import-metadata/scripts/cockroachdb-script.ts
@@ -24,9 +24,9 @@ WITH fk_info AS (
',"table":"', replace(table_name::TEXT, '"', ''), '"',
',"column":"', replace(fk_column::TEXT, '"', ''), '"',
',"foreign_key_name":"', foreign_key_name::TEXT, '"',
- ',"reference_schema":"', COALESCE(reference_schema::TEXT, 'public'), '"',
- ',"reference_table":"', reference_table::TEXT, '"',
- ',"reference_column":"', reference_column::TEXT, '"',
+ ',"reference_schema":"', COALESCE(replace(reference_schema::TEXT, '"', ''), 'public'), '"',
+ ',"reference_table":"', replace(reference_table::TEXT, '"', ''), '"',
+ ',"reference_column":"', replace(reference_column::TEXT, '"', ''), '"',
',"fk_def":"', replace(fk_def::TEXT, '"', ''),
'"}')), ',') as fk_metadata
FROM (
@@ -124,10 +124,16 @@ cols AS (
ELSE 'null'
END,
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN true ELSE false END::TEXT,
- ',"default":"', null,
+ ',"default":"', COALESCE(replace(replace(cols.column_default::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
'","collation":"', COALESCE(cols.COLLATION_NAME::TEXT, ''),
'","comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
- '"}')), ',') AS cols_metadata
+ '","is_identity":', CASE
+ WHEN cols.is_identity = 'YES' THEN 'true'
+ WHEN cols.column_default IS NOT NULL AND cols.column_default LIKE 'nextval(%' THEN 'true'
+ WHEN cols.column_default LIKE 'unique_rowid()%' THEN 'true'
+ ELSE 'false'
+ END,
+ '}')), ',') AS cols_metadata
FROM information_schema.columns cols
LEFT JOIN pg_catalog.pg_class c
ON c.relname = cols.table_name
diff --git a/src/lib/data/import-metadata/scripts/maria-script.ts b/src/lib/data/import-metadata/scripts/maria-script.ts
index 65067c971..4e6932e11 100644
--- a/src/lib/data/import-metadata/scripts/maria-script.ts
+++ b/src/lib/data/import-metadata/scripts/maria-script.ts
@@ -1,5 +1,6 @@
-const withExtras = false;
-const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
+const withDefault = true;
+
+const withDefaultExpr = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
const withoutDefault = `""`;
export const mariaDBQuery = `SET SESSION group_concat_max_len = 10000000;
@@ -68,8 +69,10 @@ SELECT CAST(CONCAT(
',"scale":', IFNULL(cols.numeric_scale, 'null'), '}'), 'null'),
',"ordinal_position":', cols.ordinal_position,
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
- ',"default":"', ${withExtras ? withDefault : withoutDefault},
- '","collation":"', IFNULL(cols.collation_name, ''), '"}')
+ ',"default":"', ${withDefault ? withDefaultExpr : withoutDefault},
+ '","collation":"', IFNULL(cols.collation_name, ''),
+ '","is_identity":', IF(cols.extra LIKE '%auto_increment%', 'true', 'false'),
+ ',"comment":"', REPLACE(REPLACE(IFNULL(cols.column_comment, ''), '"', '\\"'), '\\n', ' '), '"}')
) FROM (
SELECT cols.table_schema,
cols.table_name,
@@ -81,7 +84,9 @@ SELECT CAST(CONCAT(
cols.ordinal_position,
cols.is_nullable,
cols.column_default,
- cols.collation_name
+ cols.collation_name,
+ cols.extra,
+ cols.column_comment
FROM information_schema.columns cols
WHERE cols.table_schema = DATABASE()
) AS cols), ''),
@@ -124,14 +129,16 @@ SELECT CAST(CONCAT(
'","rows":', IFNULL(tbls.TABLE_ROWS, 0),
',"type":"', IFNULL(tbls.TABLE_TYPE, ''),
'","engine":"', IFNULL(tbls.ENGINE, ''),
- '","collation":"', IFNULL(tbls.TABLE_COLLATION, ''), '"}')
+ '","collation":"', IFNULL(tbls.TABLE_COLLATION, ''),
+ '","comment":"', REPLACE(REPLACE(IFNULL(tbls.TABLE_COMMENT, ''), '"', '\\"'), '\\n', ' '), '"}')
) FROM (
SELECT \`TABLE_SCHEMA\`,
\`TABLE_NAME\`,
\`TABLE_ROWS\`,
\`TABLE_TYPE\`,
\`ENGINE\`,
- \`TABLE_COLLATION\`
+ \`TABLE_COLLATION\`,
+ \`TABLE_COMMENT\`
FROM information_schema.tables tbls
WHERE tbls.table_schema = DATABASE()
) AS tbls), ''),
diff --git a/src/lib/data/import-metadata/scripts/mysql-script.ts b/src/lib/data/import-metadata/scripts/mysql-script.ts
index 1967f26ff..f47634552 100644
--- a/src/lib/data/import-metadata/scripts/mysql-script.ts
+++ b/src/lib/data/import-metadata/scripts/mysql-script.ts
@@ -8,9 +8,9 @@ export const getMySQLQuery = (
const databaseEdition: DatabaseEdition | undefined =
options.databaseEdition;
- const withExtras = false;
+ const withDefault = true;
- const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
+ const withDefaultExpr = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
const withoutDefault = `""`;
const newMySQLQuery = `WITH fk_info as (
@@ -91,8 +91,11 @@ export const getMySQLQuery = (
END,
',"ordinal_position":', cols.ordinal_position,
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
- ',"default":"', ${withExtras ? withDefault : withoutDefault},
- '","collation":"', IFNULL(cols.collation_name, ''), '"}'
+ ',"default":"', ${withDefault ? withDefaultExpr : withoutDefault},
+ '","collation":"', IFNULL(cols.collation_name, ''),
+ '","is_identity":', IF(cols.extra LIKE '%auto_increment%', 'true', 'false'),
+ ',"comment":"', REPLACE(REPLACE(IFNULL(cols.column_comment, ''), '"', '\\\\"'), '\\n', ' '),
+ '"}'
)))))
), indexes as (
(SELECT (@indexes:=NULL),
@@ -129,7 +132,8 @@ export const getMySQLQuery = (
'"rows":', IFNULL(\`TABLE_ROWS\`, 0),
', "type":"', IFNULL(\`TABLE_TYPE\`, ''), '",',
'"engine":"', IFNULL(\`ENGINE\`, ''), '",',
- '"collation":"', IFNULL(\`TABLE_COLLATION\`, ''), '"}')))))
+ '"collation":"', IFNULL(\`TABLE_COLLATION\`, ''), '",',
+ '"comment":"', REPLACE(REPLACE(IFNULL(\`TABLE_COMMENT\`, ''), '"', '\\\\"'), '\\\\n', ' '), '"}')))))
), views as (
(SELECT (@views:=NULL),
(SELECT (0)
@@ -216,8 +220,9 @@ export const getMySQLQuery = (
',"scale":', IFNULL(cols.numeric_scale, 'null'), '}'), 'null'),
',"ordinal_position":', cols.ordinal_position,
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
- ',"default":"', ${withExtras ? withDefault : withoutDefault},
- '","collation":"', IFNULL(cols.collation_name, ''), '"}')
+ ',"default":"', ${withDefault ? withDefaultExpr : withoutDefault},
+ '","collation":"', IFNULL(cols.collation_name, ''),
+ '","comment":"', REPLACE(REPLACE(IFNULL(cols.column_comment, ''), '"', '\\"'), '\\n', ' '), '"}')
) FROM (
SELECT cols.table_schema,
cols.table_name,
@@ -229,7 +234,8 @@ export const getMySQLQuery = (
cols.ordinal_position,
cols.is_nullable,
cols.column_default,
- cols.collation_name
+ cols.collation_name,
+ cols.column_comment
FROM information_schema.columns cols
WHERE cols.table_schema = DATABASE()
) AS cols), ''),
@@ -272,14 +278,16 @@ export const getMySQLQuery = (
'","rows":', IFNULL(tbls.TABLE_ROWS, 0),
',"type":"', IFNULL(tbls.TABLE_TYPE, ''),
'","engine":"', IFNULL(tbls.ENGINE, ''),
- '","collation":"', IFNULL(tbls.TABLE_COLLATION, ''), '"}')
+ '","collation":"', IFNULL(tbls.TABLE_COLLATION, ''),
+ '","comment":"', REPLACE(REPLACE(IFNULL(tbls.TABLE_COMMENT, ''), '"', '\\\\"'), '\\\\n', ' '), '"}')
) FROM (
SELECT \`TABLE_SCHEMA\`,
\`TABLE_NAME\`,
\`TABLE_ROWS\`,
\`TABLE_TYPE\`,
\`ENGINE\`,
- \`TABLE_COLLATION\`
+ \`TABLE_COLLATION\`,
+ \`TABLE_COMMENT\`
FROM information_schema.tables tbls
WHERE tbls.table_schema = DATABASE()
) AS tbls), ''),
diff --git a/src/lib/data/import-metadata/scripts/postgres-script.ts b/src/lib/data/import-metadata/scripts/postgres-script.ts
index 0b0924d0b..6a30bce07 100644
--- a/src/lib/data/import-metadata/scripts/postgres-script.ts
+++ b/src/lib/data/import-metadata/scripts/postgres-script.ts
@@ -64,8 +64,9 @@ export const getPostgresQuery = (
`;
const withExtras = false;
+ const withDefault = true;
- const withDefault = `COALESCE(replace(replace(cols.column_default, '"', '\\"'), '\\x', '\\\\x'), '')`;
+ const withDefaultExpr = `CASE WHEN cols.column_default IS NOT NULL AND cols.column_default LIKE 'nextval(%' THEN '' ELSE COALESCE(replace(replace(cols.column_default, '"', '\\"'), '\\x', '\\\\x'), '') END`;
const withoutDefault = `null`;
const withComments = `COALESCE(replace(replace(dsc.description, '"', '\\"'), '\\x', '\\\\x'), '')`;
@@ -78,9 +79,9 @@ WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
',"table":"', replace(table_name::text, '"', ''), '"',
',"column":"', replace(fk_column::text, '"', ''), '"',
',"foreign_key_name":"', foreign_key_name, '"',
- ',"reference_schema":"', COALESCE(reference_schema, 'public'), '"',
- ',"reference_table":"', reference_table, '"',
- ',"reference_column":"', reference_column, '"',
+ ',"reference_schema":"', COALESCE(replace(reference_schema, '"', ''), 'public'), '"',
+ ',"reference_table":"', replace(reference_table, '"', ''), '"',
+ ',"reference_column":"', replace(reference_column, '"', ''), '"',
',"fk_def":"', replace(fk_def, '"', ''),
'"}')), ',') as fk_metadata
FROM (
@@ -181,7 +182,20 @@ cols AS (
'","table":"', cols.table_name,
'","name":"', cols.column_name,
'","ordinal_position":', cols.ordinal_position,
- ',"type":"', case when LOWER(replace(cols.data_type, '"', '')) = 'user-defined' then pg_type.typname else LOWER(replace(cols.data_type, '"', '')) end,
+ ',"type":"', CASE WHEN cols.column_default IS NOT NULL AND cols.column_default LIKE 'nextval(%' THEN
+ CASE
+ WHEN LOWER(replace(cols.data_type, '"', '')) = 'smallint' THEN 'smallserial'
+ WHEN LOWER(replace(cols.data_type, '"', '')) = 'integer' THEN 'serial'
+ WHEN LOWER(replace(cols.data_type, '"', '')) = 'bigint' THEN 'bigserial'
+ ELSE LOWER(replace(cols.data_type, '"', ''))
+ END
+ WHEN cols.data_type = 'ARRAY' THEN
+ format_type(pg_type.typelem, NULL)
+ WHEN LOWER(replace(cols.data_type, '"', '')) = 'user-defined' THEN
+ format_type(pg_type.oid, NULL)
+ ELSE
+ LOWER(replace(cols.data_type, '"', ''))
+ END,
'","character_maximum_length":"', COALESCE(cols.character_maximum_length::text, 'null'),
'","precision":',
CASE
@@ -191,10 +205,19 @@ cols AS (
ELSE 'null'
END,
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN 'true' ELSE 'false' END,
- ',"default":"', ${withExtras ? withDefault : withoutDefault},
+ ',"default":"', ${withDefault ? withDefaultExpr : withoutDefault},
'","collation":"', COALESCE(cols.COLLATION_NAME, ''),
'","comment":"', ${withExtras ? withComments : withoutComments},
- '"}')), ',') AS cols_metadata
+ '","is_identity":', CASE
+ WHEN cols.is_identity = 'YES' THEN 'true'
+ WHEN cols.column_default IS NOT NULL AND cols.column_default LIKE 'nextval(%' THEN 'true'
+ ELSE 'false'
+ END,
+ ',"is_array":', CASE
+ WHEN cols.data_type = 'ARRAY' OR pg_type.typelem > 0 THEN 'true'
+ ELSE 'false'
+ END,
+ '}')), ',') AS cols_metadata
FROM information_schema.columns cols
LEFT JOIN pg_catalog.pg_class c
ON c.relname = cols.table_name
@@ -206,6 +229,8 @@ cols AS (
ON attr.attrelid = c.oid AND attr.attname = cols.column_name
LEFT JOIN pg_catalog.pg_type
ON pg_type.oid = attr.atttypid
+ LEFT JOIN pg_catalog.pg_type AS elem_type
+ ON elem_type.oid = pg_type.typelem
WHERE cols.table_schema NOT IN ('information_schema', 'pg_catalog')${
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
? timescaleColFilter
@@ -319,7 +344,7 @@ cols AS (
JOIN pg_class c ON c.oid = t.typrelid
JOIN pg_attribute a ON a.attrelid = c.oid
WHERE t.typtype = 'c'
- AND c.relkind = 'c' -- ✅ Only user-defined composite types
+ AND c.relkind = 'c' -- Only user-defined composite types
AND a.attnum > 0 AND NOT a.attisdropped
AND n.nspname NOT IN ('pg_catalog', 'information_schema') ${
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
@@ -331,6 +356,32 @@ cols AS (
GROUP BY n.nspname, t.typname
) AS comp
) AS all_types
+), check_constraints AS (
+ SELECT array_to_string(array_agg(CONCAT('{"schema":"', replace(schema_name, '"', ''), '"',
+ ',"table":"', replace(table_name, '"', ''), '"',
+ ',"expression":"', replace(replace(check_expr, '"', '\\"'), E'\\n', ' '),
+ '"}')), ',') AS check_constraints_metadata
+ FROM (
+ SELECT
+ n.nspname AS schema_name,
+ CASE
+ WHEN position('.' in c.conrelid::regclass::text) > 0
+ THEN split_part(c.conrelid::regclass::text, '.', 2)
+ ELSE c.conrelid::regclass::text
+ END AS table_name,
+ substring(pg_get_constraintdef(c.oid) FROM 'CHECK \\((.*)\\)') AS check_expr
+ FROM pg_constraint c
+ JOIN pg_class cl ON cl.oid = c.conrelid
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
+ WHERE c.contype = 'c'
+ AND n.nspname NOT IN ('information_schema', 'pg_catalog')${
+ databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
+ ? timescaleFilters
+ : databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
+ ? supabaseFilters
+ : ''
+ }
+ ) AS chk
)
SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
'], "pk_info": [', COALESCE(pk_metadata, ''),
@@ -338,10 +389,11 @@ SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
'], "indexes": [', COALESCE(indexes_metadata, ''),
'], "tables":[', COALESCE(tbls_metadata, ''),
'], "views":[', COALESCE(views_metadata, ''),
+ '], "check_constraints": [', COALESCE(check_constraints_metadata, ''),
'], "custom_types": [', COALESCE(custom_types_metadata, ''),
'], "database_name": "', CURRENT_DATABASE(), '', '", "version": "', '',
'"}') AS metadata_json_to_import
-FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, indexes_metadata, tbls, config, views, custom_types;
+FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, indexes_metadata, tbls, config, views, check_constraints, custom_types;
`;
const psqlPreCommand = `# *** Remember to change! (HOST_NAME, PORT, USER_NAME, DATABASE_NAME) *** \n`;
diff --git a/src/lib/data/import-metadata/scripts/sqlite-script.ts b/src/lib/data/import-metadata/scripts/sqlite-script.ts
index 3ece3a73d..c1be6cfb4 100644
--- a/src/lib/data/import-metadata/scripts/sqlite-script.ts
+++ b/src/lib/data/import-metadata/scripts/sqlite-script.ts
@@ -119,7 +119,13 @@ WITH fk_info AS (
END
ELSE null
END,
- 'default', ${withExtras ? withDefault : withoutDefault}
+ 'default', ${withExtras ? withDefault : withoutDefault},
+ 'is_identity',
+ CASE
+ WHEN p.pk = 1 AND LOWER(p.type) LIKE '%int%' THEN json('true')
+ WHEN LOWER((SELECT sql FROM sqlite_master WHERE name = m.name)) LIKE '%' || p.name || '%autoincrement%' THEN json('true')
+ ELSE json('false')
+ END
)
) AS cols_metadata
FROM
@@ -292,7 +298,13 @@ WITH fk_info AS (
END
ELSE null
END,
- 'default', ${withExtras ? withDefault : withoutDefault}
+ 'default', ${withExtras ? withDefault : withoutDefault},
+ 'is_identity',
+ CASE
+ WHEN p.pk = 1 AND LOWER(p.type) LIKE '%int%' THEN json('true')
+ WHEN LOWER((SELECT sql FROM sqlite_master WHERE name = m.name)) LIKE '%' || p.name || '%autoincrement%' THEN json('true')
+ ELSE json('false')
+ END
)
) AS cols_metadata
FROM
diff --git a/src/lib/data/import-metadata/scripts/sqlserver-script.ts b/src/lib/data/import-metadata/scripts/sqlserver-script.ts
index b6a6ee04f..5edfb305f 100644
--- a/src/lib/data/import-metadata/scripts/sqlserver-script.ts
+++ b/src/lib/data/import-metadata/scripts/sqlserver-script.ts
@@ -1,8 +1,8 @@
import { DatabaseEdition } from '@/lib/domain/database-edition';
-const withExtras = false;
+const withDefault = true;
-const withDefault = `'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"'`;
+const withDefaultExpr = `'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"'`;
const withoutDefault = `'""'`;
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
@@ -86,11 +86,16 @@ cols AS (
ELSE 'null'
END +
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
- ', "default": ' + ${withExtras ? withDefault : withoutDefault} +
+ ', "default": ' + ${withDefault ? withDefaultExpr : withoutDefault} +
', "collation": ' + CASE
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
END +
+ ', "is_identity": ' + CASE
+ WHEN COLUMNPROPERTY(OBJECT_ID(cols.TABLE_SCHEMA + '.' + cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') = 1
+ THEN 'true'
+ ELSE 'false'
+ END +
N'}') COLLATE DATABASE_DEFAULT
), N','
) +
@@ -179,6 +184,24 @@ views AS (
CROSS APPLY
(SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
WHERE s.name LIKE '%'
+),
+check_constraints AS (
+ SELECT
+ JSON_QUERY(
+ N'[' + STRING_AGG(
+ CONVERT(nvarchar(max),
+ JSON_QUERY(N'{
+ "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
+ '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
+ '", "expression": "' + STRING_ESCAPE(COALESCE(REPLACE(REPLACE(cc.definition, '"', '\\"'), CHAR(10), ' '), ''), 'json') +
+ '"}') COLLATE DATABASE_DEFAULT
+ ), N','
+ ) + N']'
+ ) AS all_check_constraints_json
+ FROM sys.check_constraints cc
+ JOIN sys.tables t ON cc.parent_object_id = t.object_id
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
+ WHERE s.name LIKE '%'
)
SELECT JSON_QUERY(
N'{
@@ -188,6 +211,7 @@ SELECT JSON_QUERY(
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
+ ', "check_constraints": ' + ISNULL((SELECT cast(all_check_constraints_json as nvarchar(max)) FROM check_constraints), N'[]') +
', "database_name": "' + STRING_ESCAPE(DB_NAME(), 'json') +
'", "version": ""
}'
@@ -279,7 +303,7 @@ cols AS (
ELSE 'null'
END +
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
- ', "default": ' + ${withExtras ? withDefault : withoutDefault} +
+ ', "default": ' + ${withDefault ? withDefaultExpr : withoutDefault} +
', "collation": ' +
CASE
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
@@ -391,6 +415,26 @@ views AS (
s.name LIKE '%'
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
) + ']' AS all_views_json
+),
+check_constraints AS (
+ SELECT
+ '[' + ISNULL(
+ STUFF((
+ SELECT ',' +
+ CONVERT(nvarchar(max),
+ JSON_QUERY(N'{
+ "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
+ '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
+ '", "expression": "' + STRING_ESCAPE(COALESCE(REPLACE(REPLACE(cc.definition, '"', '\\"'), CHAR(10), ' '), ''), 'json') +
+ '"}') COLLATE DATABASE_DEFAULT
+ )
+ FROM sys.check_constraints cc
+ JOIN sys.tables t ON cc.parent_object_id = t.object_id
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
+ WHERE s.name LIKE '%'
+ FOR XML PATH('')
+ ), 1, 1, ''), '')
+ + N']' AS all_check_constraints_json
)
SELECT JSON_QUERY(
N'{
@@ -400,6 +444,7 @@ SELECT JSON_QUERY(
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
+ ', "check_constraints": ' + ISNULL((SELECT cast(all_check_constraints_json as nvarchar(max)) FROM check_constraints), N'[]') +
', "database_name": "' + DB_NAME() + '"' +
', "version": ""
}'
diff --git a/src/lib/data/import-metadata/utils.ts b/src/lib/data/import-metadata/utils.ts
index fe6901764..6efed71fb 100644
--- a/src/lib/data/import-metadata/utils.ts
+++ b/src/lib/data/import-metadata/utils.ts
@@ -18,11 +18,15 @@ export const fixMetadataJson = (metadataJson: string): string => {
metadataJson
.trim()
// First unescape the JSON string
+ .replace(/\\n/g, '') // Remove literal \n (backslash + n) from stringified JSON
+ .replace(/\\t/g, '') // Remove literal \t (backslash + t) from stringified JSON
+ .replace(/\\r/g, '') // Remove literal \r (backslash + r) from stringified JSON
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
.replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
- .replace(/:""([^"]+)""/g, ':"$1"') // Convert :""value"" to :"value"
+ .replace(/: ""([^"]*)""/g, ': "$1"') // Convert : ""value"" to : "value" (handles values with any content)
+ .replace(/:""([^"]*)""/g, ':"$1"') // Convert :""value"" to :"value" (no space variant)
.replace(/""(\w+)""/g, '"$1"') // Convert ""key"" to "key"
.replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '')
@@ -53,7 +57,7 @@ export const fixMetadataJson = (metadataJson: string): string => {
/* eslint-disable-next-line no-useless-escape */
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
- .replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
+ .replace(/(:\s*)""(?=\s*[,}])/g, '$1___EMPTY___') // Temporarily replace empty strings (Safari-compatible)
.replace(/""/g, '"') // Replace remaining double quotes
.replace(/___ESCAPED_QUOTE___/g, '"') // Restore empty strings
.replace(/___EMPTY___/g, '""') // Restore empty strings
diff --git a/src/lib/data/sql-export/__tests__/array-fields.test.ts b/src/lib/data/sql-export/__tests__/array-fields.test.ts
new file mode 100644
index 000000000..560af4e23
--- /dev/null
+++ b/src/lib/data/sql-export/__tests__/array-fields.test.ts
@@ -0,0 +1,356 @@
+import { describe, it, expect } from 'vitest';
+import { generateId } from '@/lib/utils';
+import { exportBaseSQL } from '../export-sql-script';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { Diagram } from '@/lib/domain/diagram';
+
+describe('SQL Export - Array Fields (Fantasy RPG Theme)', () => {
+ it('should export array fields for magical spell components', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Magical Spell System',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'spells',
+ schema: '',
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'name',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ createdAt: Date.now(),
+ characterMaximumLength: '200',
+ },
+ {
+ id: generateId(),
+ name: 'components',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ isArray: true,
+ comments: 'Magical components needed for the spell',
+ },
+ {
+ id: generateId(),
+ name: 'elemental_types',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ characterMaximumLength: '50',
+ isArray: true,
+ comments:
+ 'Elements involved: fire, water, earth, air',
+ },
+ ],
+ indexes: [],
+ x: 0,
+ y: 0,
+ color: '#3b82f6',
+ isView: false,
+ createdAt: Date.now(),
+ order: 0,
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true,
+ });
+
+ expect(sql).toContain('CREATE TABLE "spells"');
+ expect(sql).toContain('"components" text[]');
+ expect(sql).toContain('"elemental_types" varchar(50)[]');
+ });
+
+ it('should export array fields for hero inventory system', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'RPG Inventory System',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'heroes',
+ schema: 'game',
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'name',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ createdAt: Date.now(),
+ characterMaximumLength: '100',
+ },
+ {
+ id: generateId(),
+ name: 'abilities',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ characterMaximumLength: '100',
+ isArray: true,
+ comments:
+ 'Special abilities like Stealth, Fireball, etc',
+ },
+ {
+ id: generateId(),
+ name: 'inventory_slots',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ isArray: true,
+ comments: 'Item IDs in inventory',
+ },
+ {
+ id: generateId(),
+ name: 'skill_levels',
+ type: { id: 'numeric', name: 'numeric' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ precision: 5,
+ scale: 2,
+ isArray: true,
+ comments: 'Skill proficiency levels',
+ },
+ ],
+ indexes: [],
+ x: 0,
+ y: 0,
+ color: '#ef4444',
+ isView: false,
+ createdAt: Date.now(),
+ order: 0,
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true,
+ });
+
+ expect(sql).toContain('CREATE TABLE "game"."heroes"');
+ expect(sql).toContain('"abilities" varchar(100)[]');
+ expect(sql).toContain('"inventory_slots" integer[]');
+ expect(sql).toContain('"skill_levels" numeric(5, 2)[]');
+ });
+
+ it('should export non-array fields normally when isArray is false or undefined', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Quest System',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'quests',
+ schema: '',
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'title',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ createdAt: Date.now(),
+ characterMaximumLength: '200',
+ isArray: false,
+ },
+ {
+ id: generateId(),
+ name: 'description',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ // isArray is undefined - should not be treated as array
+ },
+ ],
+ indexes: [],
+ x: 0,
+ y: 0,
+ color: '#8b5cf6',
+ isView: false,
+ createdAt: Date.now(),
+ order: 0,
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true,
+ });
+
+ expect(sql).toContain('"title" varchar(200)');
+ expect(sql).not.toContain('"title" varchar(200)[]');
+ expect(sql).toContain('"description" text');
+ expect(sql).not.toContain('"description" text[]');
+ });
+
+ it('should handle mixed array and non-array fields in magical creatures table', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Bestiary System',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'magical_creatures',
+ schema: 'bestiary',
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'species_name',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ createdAt: Date.now(),
+ characterMaximumLength: '100',
+ },
+ {
+ id: generateId(),
+ name: 'habitats',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ characterMaximumLength: '80',
+ isArray: true,
+ comments:
+ 'Preferred habitats: forest, mountain, swamp',
+ },
+ {
+ id: generateId(),
+ name: 'danger_level',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ createdAt: Date.now(),
+ default: '1',
+ },
+ {
+ id: generateId(),
+ name: 'resistances',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ characterMaximumLength: '50',
+ isArray: true,
+ comments: 'Damage resistances',
+ },
+ {
+ id: generateId(),
+ name: 'is_tameable',
+ type: { id: 'boolean', name: 'boolean' },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ createdAt: Date.now(),
+ default: 'false',
+ },
+ ],
+ indexes: [],
+ x: 0,
+ y: 0,
+ color: '#10b981',
+ isView: false,
+ createdAt: Date.now(),
+ order: 0,
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true,
+ });
+
+ expect(sql).toContain('CREATE TABLE "bestiary"."magical_creatures"');
+ expect(sql).toContain('"species_name" varchar(100)');
+ expect(sql).not.toContain('"species_name" varchar(100)[]');
+ expect(sql).toContain('"habitats" varchar(80)[]');
+ expect(sql).toContain('"danger_level" integer');
+ expect(sql).not.toContain('"danger_level" integer[]');
+ expect(sql).toContain('"resistances" varchar(50)[]');
+ expect(sql).toContain('"is_tameable" boolean');
+ expect(sql).not.toContain('"is_tameable" boolean[]');
+ });
+});
diff --git a/src/lib/data/sql-export/__tests__/cross-dialect-export.test.ts b/src/lib/data/sql-export/__tests__/cross-dialect-export.test.ts
new file mode 100644
index 000000000..9b77659d3
--- /dev/null
+++ b/src/lib/data/sql-export/__tests__/cross-dialect-export.test.ts
@@ -0,0 +1,849 @@
+import { describe, it, expect } from 'vitest';
+import { exportPostgreSQLToMySQL } from '../cross-dialect/postgresql/to-mysql';
+import { exportPostgreSQLToMSSQL } from '../cross-dialect/postgresql/to-mssql';
+import { exportBaseSQL } from '../export-sql-script';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+import {
+ type DBCustomType,
+ DBCustomTypeKind,
+} from '@/lib/domain/db-custom-type';
+
+describe('Cross-Dialect Export Tests', () => {
+ let idCounter = 0;
+ const testId = () => `test-id-${++idCounter}`;
+ const testTime = Date.now();
+
+ const createField = (overrides: Partial): DBField =>
+ ({
+ id: testId(),
+ name: 'field',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ createdAt: testTime,
+ ...overrides,
+ }) as DBField;
+
+ const createTable = (overrides: Partial): DBTable =>
+ ({
+ id: testId(),
+ name: 'table',
+ fields: [],
+ indexes: [],
+ createdAt: testTime,
+ x: 0,
+ y: 0,
+ width: 200,
+ ...overrides,
+ }) as DBTable;
+
+ const createDiagram = (overrides: Partial): Diagram =>
+ ({
+ id: testId(),
+ name: 'diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [],
+ relationships: [],
+ createdAt: testTime,
+ updatedAt: testTime,
+ ...overrides,
+ }) as Diagram;
+
+ describe('PostgreSQL to MySQL Export', () => {
+ describe('Type Conversions', () => {
+ it('should convert basic integer types', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'users',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'int', name: 'int' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ createField({
+ name: 'count',
+ type: { id: 'bigint', name: 'bigint' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('`id` INT NOT NULL');
+ expect(result).toContain('`count` BIGINT');
+ });
+
+ it('should convert boolean to TINYINT(1)', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'flags',
+ fields: [
+ createField({
+ name: 'is_active',
+ type: { id: 'boolean', name: 'boolean' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('TINYINT(1)');
+ });
+
+ it('should convert UUID to CHAR(36) with comment', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'entities',
+ fields: [
+ createField({
+ name: 'external_id',
+ type: { id: 'uuid', name: 'uuid' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('CHAR(36)');
+ expect(result).toContain('-- Was: uuid');
+ });
+
+ it('should convert JSONB to JSON with inline comment', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'documents',
+ fields: [
+ createField({
+ name: 'data',
+ type: { id: 'jsonb', name: 'jsonb' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('JSON');
+ expect(result).toContain('-- Was: jsonb');
+ });
+
+ it('should convert array types to JSON', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'posts',
+ fields: [
+ createField({
+ name: 'tags',
+ type: { id: 'text[]', name: 'text[]' },
+ isArray: true,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('JSON');
+ expect(result).toContain('PostgreSQL array');
+ });
+
+ it('should convert SERIAL to INT AUTO_INCREMENT', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'items',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'serial', name: 'serial' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('INT');
+ expect(result).toContain('AUTO_INCREMENT');
+ });
+
+ it('should convert nextval default to AUTO_INCREMENT', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'items',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'int', name: 'int' },
+ primaryKey: true,
+ nullable: false,
+ default:
+ "nextval('items_id_seq'::regclass)",
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('AUTO_INCREMENT');
+ expect(result).not.toContain('nextval');
+ });
+
+ it('should convert timestamptz to DATETIME with warning', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'events',
+ fields: [
+ createField({
+ name: 'occurred_at',
+ type: {
+ id: 'timestamptz',
+ name: 'timestamptz',
+ },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('DATETIME');
+ expect(result).toContain('-- Was: timestamptz');
+ });
+
+ it('should convert inet to VARCHAR(45)', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'connections',
+ fields: [
+ createField({
+ name: 'ip_address',
+ type: { id: 'inet', name: 'inet' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('VARCHAR(45)');
+ });
+ });
+
+ describe('ENUM Types', () => {
+ it('should convert ENUM to VARCHAR with values comment', () => {
+ const customTypes: DBCustomType[] = [
+ {
+ id: testId(),
+ name: 'status_type',
+ kind: DBCustomTypeKind.enum,
+ values: ['pending', 'active', 'closed'],
+ },
+ ];
+
+ const diagram = createDiagram({
+ customTypes,
+ tables: [
+ createTable({
+ name: 'tickets',
+ fields: [
+ createField({
+ name: 'status',
+ type: {
+ id: 'status_type',
+ name: 'status_type',
+ },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('VARCHAR');
+ expect(result).toContain("'pending', 'active', 'closed'");
+ });
+ });
+
+ describe('Schema Handling', () => {
+ it('should convert PostgreSQL schema to MySQL database', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'users',
+ schema: 'app',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'int', name: 'int' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('CREATE DATABASE IF NOT EXISTS `app`');
+ expect(result).toContain('`app`.`users`');
+ });
+ });
+
+ describe('Default Values', () => {
+ it('should convert now() to CURRENT_TIMESTAMP', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'logs',
+ fields: [
+ createField({
+ name: 'created_at',
+ type: {
+ id: 'timestamp',
+ name: 'timestamp',
+ },
+ default: 'now()',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('DEFAULT CURRENT_TIMESTAMP');
+ });
+
+ it('should convert gen_random_uuid() to UUID()', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'entities',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ default: 'gen_random_uuid()',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('DEFAULT (UUID())');
+ });
+ });
+
+ describe('Warnings Header', () => {
+ it('should include conversion notes header', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'test',
+ fields: [
+ createField({
+ name: 'data',
+ type: { id: 'jsonb', name: 'jsonb' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('-- PostgreSQL to MySQL conversion');
+ expect(result).toContain('-- Generated by ChartDB');
+ });
+ });
+ });
+
+ describe('PostgreSQL to SQL Server Export', () => {
+ describe('Type Conversions', () => {
+ it('should convert boolean to BIT', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'flags',
+ fields: [
+ createField({
+ name: 'is_active',
+ type: { id: 'boolean', name: 'boolean' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('BIT');
+ });
+
+ it('should convert UUID to UNIQUEIDENTIFIER', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'entities',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('UNIQUEIDENTIFIER');
+ });
+
+ it('should convert TEXT to NVARCHAR(MAX)', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'articles',
+ fields: [
+ createField({
+ name: 'content',
+ type: { id: 'text', name: 'text' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('NVARCHAR(MAX)');
+ });
+
+ it('should convert SERIAL to INT IDENTITY', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'items',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'serial', name: 'serial' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('INT');
+ expect(result).toContain('IDENTITY(1,1)');
+ });
+
+ it('should convert timestamptz to DATETIMEOFFSET', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'events',
+ fields: [
+ createField({
+ name: 'occurred_at',
+ type: {
+ id: 'timestamptz',
+ name: 'timestamptz',
+ },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('DATETIMEOFFSET');
+ });
+
+ it('should convert JSON/JSONB to NVARCHAR(MAX)', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'documents',
+ fields: [
+ createField({
+ name: 'data',
+ type: { id: 'jsonb', name: 'jsonb' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('NVARCHAR(MAX)');
+ });
+ });
+
+ describe('Default Values', () => {
+ it('should convert now() to GETDATE()', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'logs',
+ fields: [
+ createField({
+ name: 'created_at',
+ type: {
+ id: 'timestamp',
+ name: 'timestamp',
+ },
+ default: 'now()',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('DEFAULT GETDATE()');
+ });
+
+ it('should convert gen_random_uuid() to NEWID()', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'entities',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ default: 'gen_random_uuid()',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('DEFAULT NEWID()');
+ });
+
+ it('should convert true/false to 1/0', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'settings',
+ fields: [
+ createField({
+ name: 'is_enabled',
+ type: { id: 'boolean', name: 'boolean' },
+ default: 'true',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('DEFAULT 1');
+ });
+ });
+
+ describe('Schema Handling', () => {
+ it('should create SQL Server schema', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'users',
+ schema: 'app',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'int', name: 'int' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain(
+ "SELECT * FROM sys.schemas WHERE name = 'app'"
+ );
+ expect(result).toContain('[app].[users]');
+ });
+ });
+
+ describe('Comments via Extended Properties', () => {
+ it('should add table comments as extended properties', () => {
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'users',
+ comments: 'User accounts table',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'int', name: 'int' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMSSQL({ diagram });
+
+ expect(result).toContain('sp_addextendedproperty');
+ expect(result).toContain('User accounts table');
+ });
+ });
+ });
+
+ describe('Export Routing via exportBaseSQL', () => {
+ it('should route PostgreSQL to MySQL through deterministic exporter', () => {
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ name: 'test',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.MYSQL,
+ });
+
+ // Should use deterministic export (CHAR(36) for UUID)
+ expect(result).toContain('CHAR(36)');
+ expect(result).toContain('-- PostgreSQL to MySQL conversion');
+ });
+
+ it('should route PostgreSQL to SQL Server through deterministic exporter', () => {
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ name: 'test',
+ fields: [
+ createField({
+ name: 'id',
+ type: { id: 'uuid', name: 'uuid' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.SQL_SERVER,
+ });
+
+ // Should use deterministic export (UNIQUEIDENTIFIER for UUID)
+ expect(result).toContain('UNIQUEIDENTIFIER');
+ expect(result).toContain('-- PostgreSQL to SQL Server conversion');
+ });
+
+ it('should route PostgreSQL to MariaDB through MySQL deterministic exporter', () => {
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ name: 'test',
+ fields: [
+ createField({
+ name: 'active',
+ type: { id: 'boolean', name: 'boolean' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.MARIADB,
+ });
+
+ // Should use MySQL-style conversion (TINYINT(1) for boolean)
+ expect(result).toContain('TINYINT(1)');
+ });
+ });
+
+ describe('Index Handling', () => {
+ it('should downgrade GIN index to BTREE for MySQL', () => {
+ const fieldId = testId();
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'documents',
+ fields: [
+ createField({
+ id: fieldId,
+ name: 'data',
+ type: { id: 'jsonb', name: 'jsonb' },
+ }),
+ ],
+ indexes: [
+ {
+ id: testId(),
+ name: 'idx_data',
+ unique: false,
+ fieldIds: [fieldId],
+ createdAt: testTime,
+ type: 'gin',
+ },
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('CREATE INDEX');
+ expect(result).toContain('-- GIN index downgraded to BTREE');
+ });
+
+ it('should add prefix length for JSON indexes in MySQL', () => {
+ const fieldId = testId();
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ name: 'documents',
+ fields: [
+ createField({
+ id: fieldId,
+ name: 'data',
+ type: { id: 'jsonb', name: 'jsonb' },
+ }),
+ ],
+ indexes: [
+ {
+ id: testId(),
+ name: 'idx_data',
+ unique: false,
+ fieldIds: [fieldId],
+ createdAt: testTime,
+ },
+ ],
+ }),
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ // JSON columns in MySQL need prefix length for indexing
+ expect(result).toContain('(255)');
+ });
+ });
+
+ describe('Foreign Key Handling', () => {
+ it('should generate foreign keys with MySQL syntax', () => {
+ const sourceFieldId = testId();
+ const targetFieldId = testId();
+ const sourceTableId = testId();
+ const targetTableId = testId();
+
+ const diagram = createDiagram({
+ tables: [
+ createTable({
+ id: sourceTableId,
+ name: 'orders',
+ fields: [
+ createField({
+ id: sourceFieldId,
+ name: 'user_id',
+ type: { id: 'int', name: 'int' },
+ }),
+ ],
+ }),
+ createTable({
+ id: targetTableId,
+ name: 'users',
+ fields: [
+ createField({
+ id: targetFieldId,
+ name: 'id',
+ type: { id: 'int', name: 'int' },
+ primaryKey: true,
+ }),
+ ],
+ }),
+ ],
+ relationships: [
+ {
+ id: testId(),
+ name: 'fk_orders_users',
+ sourceTableId,
+ targetTableId,
+ sourceFieldId,
+ targetFieldId,
+ sourceCardinality: 'many',
+ targetCardinality: 'one',
+ createdAt: testTime,
+ },
+ ],
+ });
+
+ const result = exportPostgreSQLToMySQL({ diagram });
+
+ expect(result).toContain('ALTER TABLE');
+ expect(result).toContain('ADD CONSTRAINT');
+ expect(result).toContain('FOREIGN KEY');
+ expect(result).toContain('REFERENCES');
+ });
+ });
+});
diff --git a/src/lib/data/sql-export/__tests__/export-sql-dbml.test.ts b/src/lib/data/sql-export/__tests__/export-sql-dbml.test.ts
index a489d9e13..8b617c28d 100644
--- a/src/lib/data/sql-export/__tests__/export-sql-dbml.test.ts
+++ b/src/lib/data/sql-export/__tests__/export-sql-dbml.test.ts
@@ -943,5 +943,77 @@ describe('DBML Export - SQL Generation Tests', () => {
// Should include precision only when scale is not provided
expect(sql).toContain('"interest_rate" numeric(5)');
});
+
+ it('should normalize over-escaped default values', () => {
+ const diagram: Diagram = createDiagram({
+ id: testId(),
+ name: 'Corrupted Defaults Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ id: testId(),
+ name: 'settings',
+ fields: [
+ createField({
+ id: testId(),
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ unique: false,
+ }),
+ createField({
+ id: testId(),
+ name: 'color',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ // Over-escaped: '''#999999'' should become '#999999'
+ default: "'''#999999''",
+ }),
+ createField({
+ id: testId(),
+ name: 'status',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ // Over-escaped: '''open'' should become 'open'
+ default: "'''open''",
+ }),
+ createField({
+ id: testId(),
+ name: 'plan_tier',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ // Double-escaped: ''free'' should become 'free'
+ default: "''free''",
+ }),
+ ],
+ indexes: [],
+ color: '#888888',
+ }),
+ ],
+ relationships: [],
+ });
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true,
+ });
+
+ // Should normalize over-escaped quotes to proper single quotes
+ expect(sql).toContain("DEFAULT '#999999'");
+ expect(sql).toContain("DEFAULT 'open'");
+ expect(sql).toContain("DEFAULT 'free'");
+
+ // Should NOT contain the malformed triple/double quotes
+ expect(sql).not.toContain("'''");
+ expect(sql).not.toContain("''free''");
+ });
});
});
diff --git a/src/lib/data/sql-export/__tests__/export-sql.test.ts b/src/lib/data/sql-export/__tests__/export-sql.test.ts
new file mode 100644
index 000000000..e7077d7ad
--- /dev/null
+++ b/src/lib/data/sql-export/__tests__/export-sql.test.ts
@@ -0,0 +1,589 @@
+import { describe, it, expect } from 'vitest';
+import { exportBaseSQL } from '../export-sql-script';
+import { exportPostgreSQL } from '../export-per-type/postgresql';
+import { exportMySQL } from '../export-per-type/mysql';
+import { exportMSSQL } from '../export-per-type/mssql';
+import { exportSQLite } from '../export-per-type/sqlite';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+
+describe('SQL Export Tests', () => {
+ let idCounter = 0;
+ const testId = () => `test-id-${++idCounter}`;
+ const testTime = Date.now();
+
+ const createField = (overrides: Partial): DBField =>
+ ({
+ id: testId(),
+ name: 'field',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ createdAt: testTime,
+ ...overrides,
+ }) as DBField;
+
+ const createTable = (overrides: Partial): DBTable =>
+ ({
+ id: testId(),
+ name: 'table',
+ fields: [],
+ indexes: [],
+ createdAt: testTime,
+ x: 0,
+ y: 0,
+ width: 200,
+ ...overrides,
+ }) as DBTable;
+
+ const createDiagram = (overrides: Partial): Diagram =>
+ ({
+ id: testId(),
+ name: 'diagram',
+ databaseType: DatabaseType.GENERIC,
+ tables: [],
+ relationships: [],
+ createdAt: testTime,
+ updatedAt: testTime,
+ ...overrides,
+ }) as Diagram;
+
+ const createTestDiagramWithPKIndex = (
+ databaseType: DatabaseType
+ ): { diagram: Diagram; fieldId: string } => {
+ const fieldId = testId();
+ const diagram = createDiagram({
+ id: testId(),
+ name: 'PK Test',
+ databaseType,
+ tables: [
+ createTable({
+ id: testId(),
+ name: 'table_1',
+ schema: 'public',
+ fields: [
+ createField({
+ id: fieldId,
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ unique: false,
+ }),
+ ],
+ indexes: [
+ {
+ id: testId(),
+ name: '', // Empty name indicates auto-generated PK index
+ unique: true,
+ fieldIds: [fieldId],
+ createdAt: testTime,
+ isPrimaryKey: true,
+ },
+ ],
+ }),
+ ],
+ relationships: [],
+ });
+ return { diagram, fieldId };
+ };
+
+ describe('Primary Key Index Export', () => {
+ describe('exportBaseSQL', () => {
+ it('should export PRIMARY KEY without CONSTRAINT for PostgreSQL', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.POSTGRESQL
+ );
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(sql).toContain('PRIMARY KEY ("id")');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+
+ it('should export PRIMARY KEY without CONSTRAINT for MySQL', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.MYSQL
+ );
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.MYSQL,
+ });
+
+ expect(sql).toContain('PRIMARY KEY');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+
+ it('should export PRIMARY KEY without CONSTRAINT for SQL Server', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.SQL_SERVER
+ );
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.SQL_SERVER,
+ });
+
+ expect(sql).toContain('PRIMARY KEY');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+
+ it('should export PRIMARY KEY without CONSTRAINT for SQLite', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.SQLITE
+ );
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.SQLITE,
+ });
+
+ expect(sql).toContain('PRIMARY KEY');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+ });
+
+ describe('Database-specific exporters', () => {
+ it('exportPostgreSQL: should export PRIMARY KEY without CONSTRAINT', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.POSTGRESQL
+ );
+
+ const sql = exportPostgreSQL({ diagram });
+
+ expect(sql).toContain('PRIMARY KEY ("id")');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+
+ it('exportMySQL: should export PRIMARY KEY without CONSTRAINT', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.MYSQL
+ );
+
+ const sql = exportMySQL({ diagram });
+
+ expect(sql).toContain('PRIMARY KEY (`id`)');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+
+ it('exportMSSQL: should export PRIMARY KEY without CONSTRAINT', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.SQL_SERVER
+ );
+
+ const sql = exportMSSQL({ diagram });
+
+ expect(sql).toContain('PRIMARY KEY ([id])');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+
+ it('exportSQLite: should export PRIMARY KEY without CONSTRAINT', () => {
+ const { diagram } = createTestDiagramWithPKIndex(
+ DatabaseType.SQLITE
+ );
+
+ const sql = exportSQLite({ diagram });
+
+ // SQLite uses inline PRIMARY KEY for single integer columns
+ expect(sql).toContain('PRIMARY KEY');
+ expect(sql).not.toContain('CONSTRAINT');
+ });
+ });
+ });
+
+ describe('Unique Constraint Index Export', () => {
+ it('should not generate CREATE UNIQUE INDEX for single-column unique fields in PostgreSQL', () => {
+ const fieldId = testId();
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ name: 'orders_copy',
+ schema: 'public',
+ fields: [
+ createField({
+ id: fieldId,
+ name: 'id',
+ type: { id: 'bigserial', name: 'bigserial' },
+ primaryKey: false,
+ nullable: false,
+ unique: true,
+ }),
+ createField({
+ name: 'created_at',
+ type: {
+ id: 'timestamptz',
+ name: 'timestamptz',
+ },
+ nullable: false,
+ }),
+ ],
+ indexes: [
+ {
+ id: testId(),
+ name: 'orders_copy_id_key',
+ fieldIds: [fieldId],
+ unique: true,
+ isPrimaryKey: false,
+ createdAt: testTime,
+ },
+ ],
+ }),
+ ],
+ });
+
+ const sql = exportPostgreSQL({ diagram });
+
+ // Should have inline UNIQUE
+ expect(sql).toContain('"id" bigserial NOT NULL UNIQUE');
+ // Should NOT have separate CREATE UNIQUE INDEX for the unique field
+ expect(sql).not.toContain('CREATE UNIQUE INDEX');
+ });
+
+ it('should still generate CREATE UNIQUE INDEX for multi-column unique indexes in PostgreSQL', () => {
+ const fieldId1 = testId();
+ const fieldId2 = testId();
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ name: 'test_table',
+ schema: 'public',
+ fields: [
+ createField({
+ id: fieldId1,
+ name: 'col_a',
+ type: { id: 'text', name: 'text' },
+ nullable: false,
+ unique: false,
+ }),
+ createField({
+ id: fieldId2,
+ name: 'col_b',
+ type: { id: 'text', name: 'text' },
+ nullable: false,
+ unique: false,
+ }),
+ ],
+ indexes: [
+ {
+ id: testId(),
+ name: 'test_table_unique_idx',
+ fieldIds: [fieldId1, fieldId2],
+ unique: true,
+ isPrimaryKey: false,
+ createdAt: testTime,
+ },
+ ],
+ }),
+ ],
+ });
+
+ const sql = exportPostgreSQL({ diagram });
+
+ // Should have CREATE UNIQUE INDEX for multi-column unique constraint
+ expect(sql).toContain('CREATE UNIQUE INDEX');
+ expect(sql).toContain('"col_a", "col_b"');
+ });
+
+ it('should generate CREATE UNIQUE INDEX for single-column unique index when field is not marked unique', () => {
+ const fieldId = testId();
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ name: 'test_table',
+ schema: 'public',
+ fields: [
+ createField({
+ id: fieldId,
+ name: 'email',
+ type: { id: 'text', name: 'text' },
+ nullable: false,
+ unique: false, // Field not marked as unique
+ }),
+ ],
+ indexes: [
+ {
+ id: testId(),
+ name: 'test_table_email_key',
+ fieldIds: [fieldId],
+ unique: true,
+ isPrimaryKey: false,
+ createdAt: testTime,
+ },
+ ],
+ }),
+ ],
+ });
+
+ const sql = exportPostgreSQL({ diagram });
+
+ // Should NOT have inline UNIQUE (field.unique is false)
+ expect(sql).not.toContain('UNIQUE,');
+ expect(sql).not.toContain('NOT NULL UNIQUE');
+ // Should have CREATE UNIQUE INDEX since the field doesn't have inline UNIQUE
+ expect(sql).toContain('CREATE UNIQUE INDEX');
+ });
+ });
+
+ describe('exportBaseSQL with foreign key relationships', () => {
+ it('should export PostgreSQL diagram with two tables and a foreign key relationship', () => {
+ const diagram = createDiagram({
+ id: 'ee570f766a15',
+ name: 'Diagram 1',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ id: '10hmfnjq496fk2p0vgl5cooal',
+ name: 'user_profiles',
+ x: 1701,
+ y: -100,
+ schema: 'public',
+ color: '#8eb7ff',
+ isView: false,
+ order: 1,
+ fields: [
+ createField({
+ id: '0moy7ijg3k2azxhsbsjg0n94f',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: true,
+ nullable: false,
+ primaryKey: true,
+ }),
+ createField({
+ id: '3vo5j6c208kox4qt2i6lpx9x8',
+ name: 'user_id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: false,
+ nullable: true,
+ primaryKey: false,
+ }),
+ ],
+ indexes: [
+ {
+ id: '4sjv9srxk3ny3j4aptozbjzg2',
+ name: '',
+ fieldIds: ['0moy7ijg3k2azxhsbsjg0n94f'],
+ unique: true,
+ isPrimaryKey: true,
+ createdAt: testTime,
+ },
+ ],
+ }),
+ createTable({
+ id: 'tooohdxmn7kw9u1sv77o7x0pb',
+ name: 'users',
+ x: 1229,
+ y: -92,
+ schema: 'public',
+ color: '#8eb7ff',
+ isView: false,
+ order: 0,
+ fields: [
+ createField({
+ id: 'ahppb8odc54hqyenfslw37xv1',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: true,
+ nullable: false,
+ primaryKey: true,
+ }),
+ ],
+ indexes: [
+ {
+ id: 'y7rauhiqqwywx7zz1z59pig0r',
+ name: '',
+ fieldIds: ['ahppb8odc54hqyenfslw37xv1'],
+ unique: true,
+ isPrimaryKey: true,
+ createdAt: testTime,
+ },
+ ],
+ }),
+ ],
+ relationships: [
+ {
+ id: 's8leta6fjmm86fcwfpjngq65c',
+ name: 'users_id_fk',
+ sourceSchema: 'public',
+ sourceTableId: 'tooohdxmn7kw9u1sv77o7x0pb',
+ targetSchema: 'public',
+ targetTableId: '10hmfnjq496fk2p0vgl5cooal',
+ sourceFieldId: 'ahppb8odc54hqyenfslw37xv1',
+ targetFieldId: '3vo5j6c208kox4qt2i6lpx9x8',
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ createdAt: testTime,
+ },
+ ],
+ });
+
+ const expectedSql = `CREATE SCHEMA IF NOT EXISTS "public";
+
+CREATE TABLE "public"."user_profiles" (
+ "id" bigint NOT NULL,
+ "user_id" bigint,
+ PRIMARY KEY ("id")
+);
+
+CREATE TABLE "public"."users" (
+ "id" bigint NOT NULL,
+ PRIMARY KEY ("id")
+);
+
+-- Foreign key constraints
+-- Schema: public
+ALTER TABLE "public"."user_profiles" ADD CONSTRAINT "fk_user_profiles_user_id_users_id" FOREIGN KEY("user_id") REFERENCES "public"."users"("id");`;
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(sql.trim()).toBe(expectedSql.trim());
+ });
+
+ it('should place FK on target table for 1:1 relationships in DBML flow', () => {
+ // This tests the generic code path used by DBML export (isDBMLFlow: true)
+ const usersTableId = 'users-table-id';
+ const profilesTableId = 'profiles-table-id';
+ const usersIdFieldId = 'users-id-field';
+ const profilesUserIdFieldId = 'profiles-user-id-field';
+
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ id: usersTableId,
+ name: 'users',
+ schema: 'public',
+ fields: [
+ createField({
+ id: usersIdFieldId,
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ createTable({
+ id: profilesTableId,
+ name: 'profiles',
+ schema: 'public',
+ fields: [
+ createField({
+ id: profilesUserIdFieldId,
+ name: 'user_id',
+ type: { id: 'bigint', name: 'bigint' },
+ nullable: true,
+ }),
+ ],
+ }),
+ ],
+ relationships: [
+ {
+ id: 'rel-1',
+ name: 'profiles_user_fk',
+ sourceSchema: 'public',
+ sourceTableId: usersTableId, // users is source (parent)
+ targetSchema: 'public',
+ targetTableId: profilesTableId, // profiles is target (child with FK)
+ sourceFieldId: usersIdFieldId,
+ targetFieldId: profilesUserIdFieldId,
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ createdAt: testTime,
+ },
+ ],
+ });
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true, // Use the generic code path
+ });
+
+ // For 1:1 relationships, FK should be on target table (profiles)
+ // The ALTER TABLE should be on profiles, referencing users
+ expect(sql).toContain(
+ 'ALTER TABLE "public"."profiles" ADD CONSTRAINT profiles_user_fk FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id")'
+ );
+ });
+
+ it('should place FK on many side for one-to-many relationships', () => {
+ const ordersTableId = 'orders-table-id';
+ const customersTableId = 'customers-table-id';
+ const ordersCustomerIdFieldId = 'orders-customer-id-field';
+ const customersIdFieldId = 'customers-id-field';
+
+ const diagram = createDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createTable({
+ id: customersTableId,
+ name: 'customers',
+ schema: 'public',
+ fields: [
+ createField({
+ id: customersIdFieldId,
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ }),
+ ],
+ }),
+ createTable({
+ id: ordersTableId,
+ name: 'orders',
+ schema: 'public',
+ fields: [
+ createField({
+ id: ordersCustomerIdFieldId,
+ name: 'customer_id',
+ type: { id: 'bigint', name: 'bigint' },
+ nullable: true,
+ }),
+ ],
+ }),
+ ],
+ relationships: [
+ {
+ id: 'rel-2',
+ name: 'orders_customer_fk',
+ sourceSchema: 'public',
+ sourceTableId: customersTableId, // customers is one
+ targetSchema: 'public',
+ targetTableId: ordersTableId, // orders is many
+ sourceFieldId: customersIdFieldId,
+ targetFieldId: ordersCustomerIdFieldId,
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ createdAt: testTime,
+ },
+ ],
+ });
+
+ const sql = exportBaseSQL({
+ diagram,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ isDBMLFlow: true,
+ });
+
+ // For one:many, FK should be on the many side (orders)
+ expect(sql).toContain(
+ 'ALTER TABLE "public"."orders" ADD CONSTRAINT orders_customer_fk FOREIGN KEY ("customer_id") REFERENCES "public"."customers" ("id")'
+ );
+ });
+ });
+});
diff --git a/src/lib/data/sql-export/cross-dialect/common.ts b/src/lib/data/sql-export/cross-dialect/common.ts
new file mode 100644
index 000000000..7a03c6a98
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/common.ts
@@ -0,0 +1,131 @@
+/**
+ * Common utilities for cross-dialect SQL export.
+ * These utilities are shared across all cross-dialect exporters.
+ */
+
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+
+export function isFunction(value: string): boolean {
+ // Common SQL functions
+ const functionPatterns = [
+ /^CURRENT_TIMESTAMP$/i,
+ /^NOW\(\)$/i,
+ /^GETDATE\(\)$/i,
+ /^CURRENT_DATE$/i,
+ /^CURRENT_TIME$/i,
+ /^UUID\(\)$/i,
+ /^NEWID\(\)$/i,
+ /^NEXT VALUE FOR/i,
+ /^IDENTITY\s*\(\d+,\s*\d+\)$/i,
+ ];
+ return functionPatterns.some((pattern) => pattern.test(value.trim()));
+}
+
+export function isKeyword(value: string): boolean {
+ // Common SQL keywords that can be used as default values
+ const keywords = [
+ 'NULL',
+ 'TRUE',
+ 'FALSE',
+ 'CURRENT_TIMESTAMP',
+ 'CURRENT_DATE',
+ 'CURRENT_TIME',
+ 'CURRENT_USER',
+ 'SESSION_USER',
+ 'SYSTEM_USER',
+ ];
+ return keywords.includes(value.trim().toUpperCase());
+}
+
+export function strHasQuotes(value: string): boolean {
+ return /^['"].*['"]$/.test(value.trim());
+}
+
+export function exportFieldComment(comment: string): string {
+ if (!comment) {
+ return '';
+ }
+
+ return comment
+ .split('\n')
+ .map((commentLine) => ` -- ${commentLine}\n`)
+ .join('');
+}
+
+export function escapeSQLComment(comment: string): string {
+ if (!comment) {
+ return '';
+ }
+
+ // Escape single quotes by doubling them
+ let escaped = comment.replace(/'/g, "''");
+
+ // Replace newlines with spaces to prevent breaking SQL syntax
+ // Some databases support multi-line comments with specific syntax,
+ // but for maximum compatibility, we'll replace newlines with spaces
+ escaped = escaped.replace(/[\r\n]+/g, ' ');
+
+ // Trim any excessive whitespace
+ escaped = escaped.replace(/\s+/g, ' ').trim();
+
+ return escaped;
+}
+
+export function formatTableComment(comment: string): string {
+ if (!comment) {
+ return '';
+ }
+
+ // Split by newlines and add -- to each line
+ return (
+ comment
+ .split('\n')
+ .map((line) => `-- ${line}`)
+ .join('\n') + '\n'
+ );
+}
+
+export function formatMSSQLTableComment(comment: string): string {
+ if (!comment) {
+ return '';
+ }
+
+ // For MSSQL, we use multi-line comment syntax
+ // Escape */ to prevent breaking the comment block
+ const escaped = comment.replace(/\*\//g, '* /');
+ return `/**\n${escaped}\n*/\n`;
+}
+
+export function getInlineFK(table: DBTable, diagram: Diagram): string {
+ if (!diagram.relationships) {
+ return '';
+ }
+
+ const fks = diagram.relationships
+ .filter((r) => r.sourceTableId === table.id)
+ .map((r) => {
+ const targetTable = diagram.tables?.find(
+ (t) => t.id === r.targetTableId
+ );
+ const sourceField = table.fields.find(
+ (f) => f.id === r.sourceFieldId
+ );
+ const targetField = targetTable?.fields.find(
+ (f) => f.id === r.targetFieldId
+ );
+
+ if (!targetTable || !sourceField || !targetField) {
+ return '';
+ }
+
+ const targetTableName = targetTable.schema
+ ? `"${targetTable.schema}"."${targetTable.name}"`
+ : `"${targetTable.name}"`;
+
+ return ` FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
+ })
+ .filter(Boolean);
+
+ return fks.join(',\n');
+}
diff --git a/src/lib/data/sql-export/cross-dialect/index.ts b/src/lib/data/sql-export/cross-dialect/index.ts
new file mode 100644
index 000000000..f2e699e6b
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/index.ts
@@ -0,0 +1,90 @@
+/**
+ * Cross-dialect SQL export module.
+ * Provides deterministic conversion between different database dialects.
+ */
+
+import { DatabaseType } from '@/lib/domain/database-type';
+
+// Re-export types
+export type {
+ TypeMapping,
+ TypeMappingTable,
+ IndexTypeMapping,
+ IndexTypeMappingTable,
+} from './types';
+
+// Re-export PostgreSQL exporters
+export { exportPostgreSQLToMySQL } from './postgresql/to-mysql';
+export { exportPostgreSQLToMSSQL } from './postgresql/to-mssql';
+
+// Re-export unsupported features detection
+export {
+ detectUnsupportedFeatures,
+ formatWarningsHeader,
+ getFieldInlineComment,
+ getIndexInlineComment,
+} from './unsupported-features';
+export type {
+ UnsupportedFeature,
+ UnsupportedFeatureType,
+} from './unsupported-features';
+
+/**
+ * Supported cross-dialect conversion paths.
+ * Maps source database type to an array of supported target database types.
+ */
+const CROSS_DIALECT_SUPPORT: Partial> = {
+ [DatabaseType.POSTGRESQL]: [
+ DatabaseType.MYSQL,
+ DatabaseType.MARIADB,
+ DatabaseType.SQL_SERVER,
+ ],
+};
+
+/**
+ * Check if deterministic cross-dialect export is supported from source to target database type.
+ *
+ * @param sourceDatabaseType - The source database type (diagram's original database)
+ * @param targetDatabaseType - The target database type for export
+ * @returns true if deterministic cross-dialect export is available, false otherwise
+ *
+ * @example
+ * ```ts
+ * hasCrossDialectSupport(DatabaseType.POSTGRESQL, DatabaseType.MYSQL) // true
+ * hasCrossDialectSupport(DatabaseType.POSTGRESQL, DatabaseType.SQL_SERVER) // true
+ * hasCrossDialectSupport(DatabaseType.MYSQL, DatabaseType.POSTGRESQL) // false (not yet implemented)
+ * ```
+ */
+export function hasCrossDialectSupport(
+ sourceDatabaseType: DatabaseType,
+ targetDatabaseType: DatabaseType
+): boolean {
+ // Same database type doesn't need cross-dialect conversion
+ if (sourceDatabaseType === targetDatabaseType) {
+ return false;
+ }
+
+ // Generic target doesn't need cross-dialect conversion
+ if (targetDatabaseType === DatabaseType.GENERIC) {
+ return false;
+ }
+
+ const supportedTargets = CROSS_DIALECT_SUPPORT[sourceDatabaseType];
+ if (!supportedTargets) {
+ return false;
+ }
+
+ return supportedTargets.includes(targetDatabaseType);
+}
+
+/**
+ * Get all supported target database types for a given source database type.
+ *
+ * @param sourceDatabaseType - The source database type
+ * @returns Array of supported target database types, or empty array if none
+ */
+export function getSupportedTargetDialects(
+ sourceDatabaseType: DatabaseType
+): DatabaseType[] {
+ return CROSS_DIALECT_SUPPORT[sourceDatabaseType] ?? [];
+}
diff --git a/src/lib/data/sql-export/cross-dialect/postgresql/to-mssql.ts b/src/lib/data/sql-export/cross-dialect/postgresql/to-mssql.ts
new file mode 100644
index 000000000..5d9d12af8
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/postgresql/to-mssql.ts
@@ -0,0 +1,708 @@
+/**
+ * Deterministic exporter for PostgreSQL diagrams to SQL Server DDL.
+ * Converts PostgreSQL-specific types and features to SQL Server equivalents,
+ * with comments for features that cannot be fully converted.
+ */
+
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+import type { DBRelationship } from '@/lib/domain/db-relationship';
+import type { DBCustomType } from '@/lib/domain/db-custom-type';
+import {
+ exportFieldComment,
+ formatMSSQLTableComment,
+ isFunction,
+ isKeyword,
+ strHasQuotes,
+} from '../common';
+import {
+ postgresqlIndexTypeToSQLServer,
+ getTypeMapping,
+ getFallbackTypeMapping,
+} from './type-mappings';
+import {
+ detectUnsupportedFeatures,
+ formatWarningsHeader,
+ getIndexInlineComment,
+} from '../unsupported-features';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+/**
+ * Convert a PostgreSQL default value to SQL Server equivalent
+ */
+function convertPostgresDefaultToMSSQL(field: DBField): string {
+ if (!field.default) {
+ return '';
+ }
+
+ const defaultValue = field.default.trim();
+ const defaultLower = defaultValue.toLowerCase();
+
+ // Handle sequences (nextval) - these become IDENTITY, no default needed
+ if (defaultLower.includes('nextval')) {
+ return '';
+ }
+
+ // Handle PostgreSQL now() -> SQL Server GETDATE()
+ if (defaultLower === 'now()' || defaultLower === 'current_timestamp') {
+ return 'GETDATE()';
+ }
+
+ // Handle UUID generation functions
+ if (
+ defaultLower.includes('gen_random_uuid') ||
+ defaultLower.includes('uuid_generate')
+ ) {
+ return 'NEWID()';
+ }
+
+ // Handle JSONB/JSON functions
+ if (
+ defaultLower.includes('json_build_object') ||
+ defaultLower.includes('jsonb_build_object')
+ ) {
+ return "N'{}'";
+ }
+ if (
+ defaultLower.includes('json_build_array') ||
+ defaultLower.includes('jsonb_build_array')
+ ) {
+ return "N'[]'";
+ }
+
+ // Handle empty array defaults
+ if (
+ defaultLower === "'{}'::text[]" ||
+ defaultLower.match(/'\{\}'::.*\[\]/)
+ ) {
+ return "N'[]'";
+ }
+
+ // Handle array literals
+ if (defaultLower.startsWith('array[')) {
+ const content = defaultValue.match(/ARRAY\[(.*?)\]/i)?.[1] || '';
+ return `N'[${content}]'`;
+ }
+
+ // Handle PostgreSQL true/false -> SQL Server 1/0
+ if (defaultLower === 'true') {
+ return '1';
+ }
+ if (defaultLower === 'false') {
+ return '0';
+ }
+
+ // Strip PostgreSQL type casts
+ const withoutCast = defaultValue.split('::')[0].trim();
+
+ // Handle SQL Server specific syntax for wrapped defaults
+ if (withoutCast.match(/^\(\(.*\)\)$/)) {
+ return withoutCast.replace(/^\(\(|\)\)$/g, '');
+ }
+
+ // If it's a function call, try to map to SQL Server
+ if (isFunction(withoutCast)) {
+ return withoutCast;
+ }
+
+ // If it's a keyword, keep it
+ if (isKeyword(withoutCast)) {
+ return withoutCast;
+ }
+
+ // If already quoted, convert to N'' style
+ if (strHasQuotes(withoutCast)) {
+ // Convert single quotes to N'' style
+ if (withoutCast.startsWith("'") && withoutCast.endsWith("'")) {
+ return `N${withoutCast}`;
+ }
+ return withoutCast;
+ }
+
+ // If it's a number, keep it
+ if (/^-?\d+(\.\d+)?$/.test(withoutCast)) {
+ return withoutCast;
+ }
+
+ // For other cases, wrap in N''
+ return `N'${withoutCast.replace(/'/g, "''")}'`;
+}
+
+/**
+ * Check if a field type matches a custom enum or composite type
+ */
+function findCustomType(
+ fieldTypeName: string,
+ customTypes: DBCustomType[]
+): DBCustomType | undefined {
+ const normalizedName = fieldTypeName.toLowerCase();
+ return customTypes.find((ct) => {
+ const ctName = ct.schema ? `${ct.schema}.${ct.name}` : ct.name;
+ return (
+ ctName.toLowerCase() === normalizedName ||
+ ct.name.toLowerCase() === normalizedName
+ );
+ });
+}
+
+/**
+ * Map a PostgreSQL type to SQL Server type with size/precision handling
+ * @param field - The field to map
+ * @param customTypes - Custom types defined in the schema
+ * @param isIndexed - Whether this field is used in an index (affects MAX type handling)
+ */
+function mapPostgresTypeToMSSQL(
+ field: DBField,
+ customTypes: DBCustomType[],
+ isIndexed: boolean = false
+): {
+ typeName: string;
+ inlineComment: string | null;
+} {
+ const originalType = field.type.name.toLowerCase();
+ let inlineComment: string | null = null;
+
+ // SQL Server has a 900-byte limit for index keys. NVARCHAR uses 2 bytes per char,
+ // so 450 chars is the max for indexed NVARCHAR columns.
+ const indexSafeNvarcharSize = 'NVARCHAR(450)';
+
+ // Handle array types
+ if (field.isArray || originalType.endsWith('[]')) {
+ // Arrays used in indexes need bounded size (though this is unusual)
+ const arrayType = isIndexed ? indexSafeNvarcharSize : 'NVARCHAR(MAX)';
+ return {
+ typeName: arrayType,
+ inlineComment: `Was: ${field.type.name} (PostgreSQL array${isIndexed ? ', size limited for index' : ', stored as JSON'})`,
+ };
+ }
+
+ // Check for custom types (ENUM or composite)
+ const customType = findCustomType(field.type.name, customTypes);
+ if (customType) {
+ if (customType.kind === 'enum') {
+ // ENUMs become NVARCHAR(255)
+ return {
+ typeName: 'NVARCHAR(255)',
+ inlineComment: null, // Inline comment handled separately via getEnumValuesComment
+ };
+ } else if (customType.kind === 'composite') {
+ // Composite types become NVARCHAR(MAX) as JSON (shouldn't be indexed normally)
+ const compositeType = isIndexed
+ ? indexSafeNvarcharSize
+ : 'NVARCHAR(MAX)';
+ return {
+ typeName: compositeType,
+ inlineComment: `Was: ${field.type.name} (PostgreSQL composite type${isIndexed ? ', size limited for index' : ''})`,
+ };
+ }
+ }
+
+ // Look up mapping
+ const mapping = getTypeMapping(originalType, 'sqlserver');
+ const effectiveMapping = mapping || getFallbackTypeMapping('sqlserver');
+
+ let typeName = effectiveMapping.targetType;
+
+ // If indexed and type contains (MAX), replace with bounded size for index compatibility
+ // SQL Server cannot use MAX types as index keys
+ if (isIndexed && typeName.includes('(MAX)')) {
+ typeName = typeName.replace('(MAX)', '(450)');
+ inlineComment = `Was: ${field.type.name} (size limited for index)`;
+ }
+
+ // Handle size/precision
+ if (field.characterMaximumLength) {
+ if (
+ typeName === 'VARCHAR' ||
+ typeName === 'NVARCHAR' ||
+ typeName === 'CHAR' ||
+ typeName === 'NCHAR' ||
+ typeName === 'VARBINARY'
+ ) {
+ typeName = `${typeName}(${field.characterMaximumLength})`;
+ }
+ } else if (effectiveMapping.defaultLength) {
+ if (
+ typeName === 'VARCHAR' ||
+ typeName === 'NVARCHAR' ||
+ typeName === 'CHAR' ||
+ typeName === 'NCHAR'
+ ) {
+ typeName = `${typeName}(${effectiveMapping.defaultLength})`;
+ }
+ }
+
+ if (field.precision !== undefined && field.scale !== undefined) {
+ if (
+ typeName === 'DECIMAL' ||
+ typeName === 'NUMERIC' ||
+ typeName === 'DATETIME2' ||
+ typeName === 'DATETIMEOFFSET'
+ ) {
+ if (typeName === 'DATETIME2' || typeName === 'DATETIMEOFFSET') {
+ // For datetime types, only precision applies (fractional seconds)
+ if (field.precision !== null && field.precision <= 7) {
+ typeName = `${typeName}(${field.precision})`;
+ }
+ } else {
+ typeName = `${typeName}(${field.precision}, ${field.scale})`;
+ }
+ }
+ } else if (field.precision !== undefined) {
+ if (typeName === 'DECIMAL' || typeName === 'NUMERIC') {
+ typeName = `${typeName}(${field.precision})`;
+ }
+ } else if (
+ effectiveMapping.defaultPrecision &&
+ (typeName === 'DECIMAL' || typeName === 'NUMERIC')
+ ) {
+ typeName = `${typeName}(${effectiveMapping.defaultPrecision}, ${effectiveMapping.defaultScale || 0})`;
+ }
+
+ // Set inline comment if conversion note exists (but don't override index-related comment)
+ if (effectiveMapping.includeInlineComment && !inlineComment) {
+ inlineComment = `Was: ${field.type.name}`;
+ }
+
+ return { typeName, inlineComment };
+}
+
+/**
+ * Check if a field should have IDENTITY
+ */
+function isIdentity(field: DBField): boolean {
+ // Check increment flag
+ if (field.increment) {
+ return true;
+ }
+
+ // Check for serial types
+ const typeLower = field.type.name.toLowerCase();
+ if (
+ typeLower === 'serial' ||
+ typeLower === 'smallserial' ||
+ typeLower === 'bigserial'
+ ) {
+ return true;
+ }
+
+ // Check for nextval in default
+ if (field.default?.toLowerCase().includes('nextval')) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Build enum value comment for custom enum types
+ */
+function getEnumValuesComment(
+ fieldTypeName: string,
+ customTypes: DBCustomType[]
+): string | null {
+ const enumType = customTypes.find((ct) => {
+ const ctName = ct.schema ? `${ct.schema}.${ct.name}` : ct.name;
+ return (
+ ctName.toLowerCase() === fieldTypeName.toLowerCase() ||
+ ct.name.toLowerCase() === fieldTypeName.toLowerCase()
+ );
+ });
+
+ if (enumType?.kind === 'enum' && enumType.values?.length) {
+ return `PostgreSQL ENUM: '${enumType.values.join("', '")}'`;
+ }
+
+ return null;
+}
+
+/**
+ * Main export function: PostgreSQL diagram to SQL Server DDL
+ */
+export function exportPostgreSQLToMSSQL({
+ diagram,
+ onlyRelationships = false,
+}: {
+ diagram: Diagram;
+ onlyRelationships?: boolean;
+}): string {
+ if (!diagram.tables || !diagram.relationships) {
+ return '';
+ }
+
+ const tables = diagram.tables;
+ const relationships = diagram.relationships;
+ const customTypes = diagram.customTypes || [];
+
+ // Detect unsupported features for warnings header
+ const unsupportedFeatures = detectUnsupportedFeatures(
+ diagram,
+ DatabaseType.SQL_SERVER
+ );
+
+ // Build output
+ let sqlScript = formatWarningsHeader(
+ unsupportedFeatures,
+ 'PostgreSQL',
+ 'SQL Server'
+ );
+
+ if (!onlyRelationships) {
+ // Create schemas if they don't exist
+ const schemas = new Set();
+ tables.forEach((table) => {
+ if (table.schema) {
+ schemas.add(table.schema);
+ }
+ });
+
+ schemas.forEach((schema) => {
+ sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\nGO\n`;
+ });
+
+ if (schemas.size > 0) {
+ sqlScript += '\n';
+ }
+
+ // Generate table creation SQL
+ sqlScript += tables
+ .map((table: DBTable) => {
+ // Skip views
+ if (table.isView) {
+ return '';
+ }
+
+ const tableName = table.schema
+ ? `[${table.schema}].[${table.name}]`
+ : `[${table.name}]`;
+
+ // Get primary key fields
+ const primaryKeyFields = table.fields.filter(
+ (f) => f.primaryKey
+ );
+
+ // Check if we have following constraints (for comma placement)
+ const validCheckConstraints = (
+ table.checkConstraints ?? []
+ ).filter((c) => c.expression && c.expression.trim());
+ const hasFollowingConstraints =
+ primaryKeyFields.length > 0 ||
+ validCheckConstraints.length > 0;
+
+ // Compute which fields are used in indexes (for type size limiting)
+ // SQL Server has a 900-byte limit for index keys
+ const indexedFieldIds = new Set();
+ table.indexes.forEach((idx) => {
+ idx.fieldIds.forEach((fieldId) => {
+ indexedFieldIds.add(fieldId);
+ });
+ });
+ // Also add primary key fields as they are indexed
+ primaryKeyFields.forEach((f) => {
+ indexedFieldIds.add(f.id);
+ });
+ // Also add unique fields as they create implicit indexes
+ table.fields.forEach((f) => {
+ if (f.unique) {
+ indexedFieldIds.add(f.id);
+ }
+ });
+
+ const fieldDefinitions = table.fields.map(
+ (field: DBField, index: number, allFields: DBField[]) => {
+ const fieldName = `[${field.name}]`;
+
+ // Check if this field is used in an index
+ const isIndexed = indexedFieldIds.has(field.id);
+
+ // Map type to SQL Server
+ const { typeName, inlineComment } =
+ mapPostgresTypeToMSSQL(
+ field,
+ customTypes,
+ isIndexed
+ );
+
+ // Check for enum type and get values
+ const enumComment = getEnumValuesComment(
+ field.type.name,
+ customTypes
+ );
+
+ // Combine inline comments
+ const fullInlineComment = enumComment || inlineComment;
+
+ const notNull = field.nullable ? '' : ' NOT NULL';
+
+ // Handle IDENTITY
+ const identity = isIdentity(field)
+ ? ' IDENTITY(1,1)'
+ : '';
+
+ // Only add UNIQUE constraint if not primary key
+ const unique =
+ !field.primaryKey && field.unique ? ' UNIQUE' : '';
+
+ // Handle default value
+ const convertedDefault =
+ convertPostgresDefaultToMSSQL(field);
+ const defaultValue =
+ convertedDefault && !identity
+ ? ` DEFAULT ${convertedDefault}`
+ : '';
+
+ // Build inline SQL comment for conversion notes
+ const sqlInlineComment = fullInlineComment
+ ? ` -- ${fullInlineComment}`
+ : '';
+
+ // Determine if this field needs a trailing comma
+ const isLastField = index === allFields.length - 1;
+ const needsComma =
+ !isLastField || hasFollowingConstraints;
+
+ return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeName}${notNull}${identity}${unique}${defaultValue}${needsComma ? ',' : ''}${sqlInlineComment}`;
+ }
+ );
+
+ return `${
+ table.comments
+ ? formatMSSQLTableComment(table.comments)
+ : ''
+ }CREATE TABLE ${tableName} (\n${fieldDefinitions.join('\n')}${
+ // Add PRIMARY KEY as table constraint
+ primaryKeyFields.length > 0
+ ? `\n PRIMARY KEY (${primaryKeyFields
+ .map((f) => `[${f.name}]`)
+ .join(
+ ', '
+ )})${validCheckConstraints.length > 0 ? ',' : ''}`
+ : ''
+ }${
+ // Add check constraints (already computed above as validCheckConstraints)
+ validCheckConstraints.length > 0
+ ? validCheckConstraints
+ .map(
+ (constraint, index) =>
+ `${index > 0 ? ',' : ''}\n CHECK (${constraint.expression})`
+ )
+ .join('')
+ : ''
+ }\n);\nGO${
+ // Add indexes
+ (() => {
+ const validIndexes = table.indexes
+ .map((index) => {
+ // Skip primary key indexes
+ if (index.isPrimaryKey) {
+ return '';
+ }
+
+ // Get the list of fields for this index
+ const indexFields = index.fieldIds
+ .map((fieldId) => {
+ const field = table.fields.find(
+ (f) => f.id === fieldId
+ );
+ return field ? field : null;
+ })
+ .filter(Boolean);
+
+ // Skip if matches primary key
+ if (
+ primaryKeyFields.length ===
+ indexFields.length &&
+ primaryKeyFields.every((pk) =>
+ indexFields.some(
+ (field) =>
+ field && field.id === pk.id
+ )
+ )
+ ) {
+ return '';
+ }
+
+ // Get index type conversion
+ const indexType = (
+ index.type || 'btree'
+ ).toLowerCase();
+ const indexTypeMapping =
+ postgresqlIndexTypeToSQLServer[indexType];
+ const indexInlineComment =
+ getIndexInlineComment(index, 'sqlserver');
+
+ const indexName = table.schema
+ ? `[${table.schema}_${index.name}]`
+ : `[${index.name}]`;
+
+ const indexFieldNames = indexFields
+ .map((field) =>
+ field ? `[${field.name}]` : ''
+ )
+ .filter(Boolean);
+
+ // SQL Server has 32 column limit
+ if (indexFieldNames.length > 32) {
+ console.warn(
+ `Warning: Index ${indexName} has ${indexFieldNames.length} columns. Truncating to 32.`
+ );
+ indexFieldNames.length = 32;
+ }
+
+ const commentStr = indexInlineComment
+ ? ` -- ${indexInlineComment}`
+ : '';
+
+ return indexFieldNames.length > 0
+ ? `CREATE ${index.unique ? 'UNIQUE ' : ''}${indexTypeMapping?.targetType === 'CLUSTERED' ? 'CLUSTERED ' : 'NONCLUSTERED '}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});${commentStr}`
+ : '';
+ })
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b));
+
+ return validIndexes.length > 0
+ ? `\n-- Indexes\n${validIndexes.join('\nGO\n')}\nGO`
+ : '';
+ })()
+ }`;
+ })
+ .filter(Boolean)
+ .join('\n');
+
+ // Add extended properties for table/column comments
+ const commentStatements: string[] = [];
+ for (const table of tables) {
+ if (table.isView) continue;
+
+ const schemaName = table.schema || 'dbo';
+
+ if (table.comments) {
+ commentStatements.push(
+ `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${table.comments.replace(/'/g, "''")}', @level0type=N'SCHEMA', @level0name=N'${schemaName}', @level1type=N'TABLE', @level1name=N'${table.name}';`
+ );
+ }
+
+ for (const field of table.fields) {
+ if (field.comments) {
+ commentStatements.push(
+ `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${field.comments.replace(/'/g, "''")}', @level0type=N'SCHEMA', @level0name=N'${schemaName}', @level1type=N'TABLE', @level1name=N'${table.name}', @level2type=N'COLUMN', @level2name=N'${field.name}';`
+ );
+ }
+ }
+ }
+
+ if (commentStatements.length > 0) {
+ sqlScript += '\n-- Table and column descriptions\n';
+ sqlScript += commentStatements.join('\nGO\n');
+ sqlScript += '\nGO\n';
+ }
+ }
+
+ // Generate foreign keys
+ if (relationships.length > 0) {
+ sqlScript += '\n-- Foreign key constraints\n';
+
+ // Process relationships and group by schema
+ const foreignKeys = relationships
+ .map((r: DBRelationship) => {
+ const sourceTable = tables.find(
+ (t) => t.id === r.sourceTableId
+ );
+ const targetTable = tables.find(
+ (t) => t.id === r.targetTableId
+ );
+
+ if (
+ !sourceTable ||
+ !targetTable ||
+ sourceTable.isView ||
+ targetTable.isView
+ ) {
+ return null;
+ }
+
+ const sourceField = sourceTable.fields.find(
+ (f) => f.id === r.sourceFieldId
+ );
+ const targetField = targetTable.fields.find(
+ (f) => f.id === r.targetFieldId
+ );
+
+ if (!sourceField || !targetField) {
+ return null;
+ }
+
+ // Determine FK placement based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
+ let fkTable, fkField, refTable, refField;
+
+ if (
+ r.sourceCardinality === 'many' &&
+ r.targetCardinality === 'many'
+ ) {
+ // Many-to-many relationships need a junction table, skip
+ return null;
+ } else if (
+ r.sourceCardinality === 'many' &&
+ r.targetCardinality === 'one'
+ ) {
+ // FK goes on source table (the many side)
+ fkTable = sourceTable;
+ fkField = sourceField;
+ refTable = targetTable;
+ refField = targetField;
+ } else {
+ // All other cases: FK goes on target table
+ fkTable = targetTable;
+ fkField = targetField;
+ refTable = sourceTable;
+ refField = sourceField;
+ }
+
+ const fkTableName = fkTable.schema
+ ? `[${fkTable.schema}].[${fkTable.name}]`
+ : `[${fkTable.name}]`;
+ const refTableName = refTable.schema
+ ? `[${refTable.schema}].[${refTable.name}]`
+ : `[${refTable.name}]`;
+
+ return {
+ schema: fkTable.schema || 'dbo',
+ sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name || `fk_${fkTable.name}_${fkField.name}`}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`,
+ };
+ })
+ .filter(Boolean) as { schema: string; sql: string }[];
+
+ // Group by schema
+ const fksBySchema = foreignKeys.reduce(
+ (acc, fk) => {
+ if (!acc[fk.schema]) {
+ acc[fk.schema] = [];
+ }
+ acc[fk.schema].push(fk.sql);
+ return acc;
+ },
+ {} as Record
+ );
+
+ // Sort schemas and output
+ const sortedSchemas = Object.keys(fksBySchema).sort();
+ const fkSql = sortedSchemas
+ .map((schema, index) => {
+ const schemaFks = fksBySchema[schema].join('\nGO\n');
+ if (index === 0) {
+ return `-- Schema: ${schema}\n${schemaFks}`;
+ } else {
+ return `\n-- Schema: ${schema}\n${schemaFks}`;
+ }
+ })
+ .join('\n');
+
+ sqlScript += fkSql;
+ sqlScript += '\nGO\n';
+ }
+
+ return sqlScript;
+}
diff --git a/src/lib/data/sql-export/cross-dialect/postgresql/to-mysql.ts b/src/lib/data/sql-export/cross-dialect/postgresql/to-mysql.ts
new file mode 100644
index 000000000..7284b4ac8
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/postgresql/to-mysql.ts
@@ -0,0 +1,616 @@
+/**
+ * Deterministic exporter for PostgreSQL diagrams to MySQL DDL.
+ * Converts PostgreSQL-specific types and features to MySQL equivalents,
+ * with comments for features that cannot be fully converted.
+ */
+
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+import type { DBRelationship } from '@/lib/domain/db-relationship';
+import type { DBCustomType } from '@/lib/domain/db-custom-type';
+import {
+ exportFieldComment,
+ escapeSQLComment,
+ formatTableComment,
+ isFunction,
+ isKeyword,
+ strHasQuotes,
+} from '../common';
+import {
+ postgresqlIndexTypeToMySQL,
+ getTypeMapping,
+ getFallbackTypeMapping,
+} from './type-mappings';
+import {
+ detectUnsupportedFeatures,
+ formatWarningsHeader,
+ getIndexInlineComment,
+} from '../unsupported-features';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+/**
+ * Convert a PostgreSQL default value to MySQL equivalent
+ */
+function convertPostgresDefaultToMySQL(field: DBField): string {
+ if (!field.default) {
+ return '';
+ }
+
+ const defaultValue = field.default.trim();
+ const defaultLower = defaultValue.toLowerCase();
+
+ // Handle sequences (nextval) - these become AUTO_INCREMENT, no default needed
+ if (defaultLower.includes('nextval')) {
+ return '';
+ }
+
+ // Handle PostgreSQL now() -> MySQL CURRENT_TIMESTAMP
+ if (defaultLower === 'now()' || defaultLower === 'current_timestamp') {
+ return 'CURRENT_TIMESTAMP';
+ }
+
+ // Handle UUID generation functions
+ if (
+ defaultLower.includes('gen_random_uuid') ||
+ defaultLower.includes('uuid_generate')
+ ) {
+ return '(UUID())';
+ }
+
+ // Handle JSONB/JSON functions
+ if (
+ defaultLower.includes('json_build_object') ||
+ defaultLower.includes('jsonb_build_object')
+ ) {
+ return "'{}'";
+ }
+ if (
+ defaultLower.includes('json_build_array') ||
+ defaultLower.includes('jsonb_build_array')
+ ) {
+ return "'[]'";
+ }
+
+ // Handle empty array defaults
+ if (
+ defaultLower === "'{}'::text[]" ||
+ defaultLower.match(/'\{\}'::.*\[\]/)
+ ) {
+ return "'[]'";
+ }
+
+ // Handle array literals like ARRAY[1,2,3]
+ if (defaultLower.startsWith('array[')) {
+ const content = defaultValue.match(/ARRAY\[(.*?)\]/i)?.[1] || '';
+ return `'[${content}]'`;
+ }
+
+ // Strip PostgreSQL type casts
+ const withoutCast = defaultValue.split('::')[0].trim();
+
+ // If it's a function call, keep it (MySQL might support it)
+ if (isFunction(withoutCast)) {
+ return withoutCast;
+ }
+
+ // If it's a keyword, keep it
+ if (isKeyword(withoutCast)) {
+ return withoutCast;
+ }
+
+ // If already quoted, keep it
+ if (strHasQuotes(withoutCast)) {
+ return withoutCast;
+ }
+
+ // If it's a number, keep it
+ if (/^-?\d+(\.\d+)?$/.test(withoutCast)) {
+ return withoutCast;
+ }
+
+ // For other cases, add quotes
+ return `'${withoutCast.replace(/'/g, "''")}'`;
+}
+
+/**
+ * Check if a field type matches a custom enum or composite type
+ */
+function findCustomType(
+ fieldTypeName: string,
+ customTypes: DBCustomType[]
+): DBCustomType | undefined {
+ const normalizedName = fieldTypeName.toLowerCase();
+ return customTypes.find((ct) => {
+ const ctName = ct.schema ? `${ct.schema}.${ct.name}` : ct.name;
+ return (
+ ctName.toLowerCase() === normalizedName ||
+ ct.name.toLowerCase() === normalizedName
+ );
+ });
+}
+
+/**
+ * Map a PostgreSQL type to MySQL type with size/precision handling
+ */
+function mapPostgresTypeToMySQL(
+ field: DBField,
+ customTypes: DBCustomType[]
+): {
+ typeName: string;
+ inlineComment: string | null;
+} {
+ const originalType = field.type.name.toLowerCase();
+ let inlineComment: string | null = null;
+
+ // Handle array types
+ if (field.isArray || originalType.endsWith('[]')) {
+ return {
+ typeName: 'JSON',
+ inlineComment: `Was: ${field.type.name} (PostgreSQL array)`,
+ };
+ }
+
+ // Check for custom types (ENUM or composite)
+ const customType = findCustomType(field.type.name, customTypes);
+ if (customType) {
+ if (customType.kind === 'enum') {
+ // ENUMs become VARCHAR(255)
+ return {
+ typeName: 'VARCHAR(255)',
+ inlineComment: null, // Inline comment handled separately via getEnumValuesComment
+ };
+ } else if (customType.kind === 'composite') {
+ // Composite types become JSON
+ return {
+ typeName: 'JSON',
+ inlineComment: `Was: ${field.type.name} (PostgreSQL composite type)`,
+ };
+ }
+ }
+
+ // Look up mapping
+ const mapping = getTypeMapping(originalType, 'mysql');
+ const effectiveMapping = mapping || getFallbackTypeMapping('mysql');
+
+ let typeName = effectiveMapping.targetType;
+
+ // Handle size/precision
+ if (field.characterMaximumLength) {
+ if (
+ typeName === 'VARCHAR' ||
+ typeName === 'CHAR' ||
+ typeName === 'VARBINARY'
+ ) {
+ typeName = `${typeName}(${field.characterMaximumLength})`;
+ }
+ } else if (effectiveMapping.defaultLength) {
+ if (typeName === 'VARCHAR' || typeName === 'CHAR') {
+ typeName = `${typeName}(${effectiveMapping.defaultLength})`;
+ }
+ }
+
+ if (field.precision !== undefined && field.scale !== undefined) {
+ if (typeName === 'DECIMAL' || typeName === 'NUMERIC') {
+ typeName = `${typeName}(${field.precision}, ${field.scale})`;
+ }
+ } else if (field.precision !== undefined) {
+ if (typeName === 'DECIMAL' || typeName === 'NUMERIC') {
+ typeName = `${typeName}(${field.precision})`;
+ }
+ } else if (
+ effectiveMapping.defaultPrecision &&
+ (typeName === 'DECIMAL' || typeName === 'NUMERIC')
+ ) {
+ typeName = `${typeName}(${effectiveMapping.defaultPrecision}, ${effectiveMapping.defaultScale || 0})`;
+ }
+
+ // Set inline comment if conversion note exists
+ if (effectiveMapping.includeInlineComment) {
+ inlineComment = `Was: ${field.type.name}`;
+ }
+
+ return { typeName, inlineComment };
+}
+
+/**
+ * Check if a field should have AUTO_INCREMENT
+ */
+function isAutoIncrement(field: DBField): boolean {
+ // Check increment flag
+ if (field.increment) {
+ return true;
+ }
+
+ // Check for serial types
+ const typeLower = field.type.name.toLowerCase();
+ if (
+ typeLower === 'serial' ||
+ typeLower === 'smallserial' ||
+ typeLower === 'bigserial'
+ ) {
+ return true;
+ }
+
+ // Check for nextval in default
+ if (field.default?.toLowerCase().includes('nextval')) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Build enum value comment for custom enum types
+ */
+function getEnumValuesComment(
+ fieldTypeName: string,
+ customTypes: DBCustomType[]
+): string | null {
+ // Find matching enum type
+ const enumType = customTypes.find((ct) => {
+ const ctName = ct.schema ? `${ct.schema}.${ct.name}` : ct.name;
+ return (
+ ctName.toLowerCase() === fieldTypeName.toLowerCase() ||
+ ct.name.toLowerCase() === fieldTypeName.toLowerCase()
+ );
+ });
+
+ if (enumType?.kind === 'enum' && enumType.values?.length) {
+ return `PostgreSQL ENUM: '${enumType.values.join("', '")}'`;
+ }
+
+ return null;
+}
+
+/**
+ * Main export function: PostgreSQL diagram to MySQL DDL
+ */
+export function exportPostgreSQLToMySQL({
+ diagram,
+ onlyRelationships = false,
+}: {
+ diagram: Diagram;
+ onlyRelationships?: boolean;
+}): string {
+ if (!diagram.tables || !diagram.relationships) {
+ return '';
+ }
+
+ const tables = diagram.tables;
+ const relationships = diagram.relationships;
+ const customTypes = diagram.customTypes || [];
+
+ // Detect unsupported features for warnings header
+ const unsupportedFeatures = detectUnsupportedFeatures(
+ diagram,
+ DatabaseType.MYSQL
+ );
+
+ // Build output
+ let sqlScript = formatWarningsHeader(
+ unsupportedFeatures,
+ 'PostgreSQL',
+ 'MySQL'
+ );
+
+ if (!onlyRelationships) {
+ // Create databases (schemas) if they don't exist
+ const schemas = new Set();
+ tables.forEach((table) => {
+ if (table.schema) {
+ schemas.add(table.schema);
+ }
+ });
+
+ schemas.forEach((schema) => {
+ sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
+ });
+
+ if (schemas.size > 0) {
+ sqlScript += '\n';
+ }
+
+ // Generate table creation SQL
+ sqlScript += tables
+ .map((table: DBTable) => {
+ // Skip views
+ if (table.isView) {
+ return '';
+ }
+
+ // Use schema prefix if available
+ const tableName = table.schema
+ ? `\`${table.schema}\`.\`${table.name}\``
+ : `\`${table.name}\``;
+
+ // Get primary key fields
+ const primaryKeyFields = table.fields.filter(
+ (f) => f.primaryKey
+ );
+
+ // Check if we have following constraints (for comma placement)
+ const validCheckConstraints = (
+ table.checkConstraints ?? []
+ ).filter((c) => c.expression && c.expression.trim());
+ const hasFollowingConstraints =
+ primaryKeyFields.length > 0 ||
+ validCheckConstraints.length > 0;
+
+ const fieldDefinitions = table.fields.map(
+ (field: DBField, index: number, allFields: DBField[]) => {
+ const fieldName = `\`${field.name}\``;
+
+ // Map type to MySQL
+ const { typeName, inlineComment } =
+ mapPostgresTypeToMySQL(field, customTypes);
+
+ // Check for enum type and get values
+ const enumComment = getEnumValuesComment(
+ field.type.name,
+ customTypes
+ );
+
+ // Combine inline comments
+ const fullInlineComment = enumComment || inlineComment;
+
+ const notNull = field.nullable ? '' : ' NOT NULL';
+
+ // Handle auto_increment
+ const autoIncrement = isAutoIncrement(field)
+ ? ' AUTO_INCREMENT'
+ : '';
+
+ // Only add UNIQUE constraint if the field is not part of the primary key
+ const unique =
+ !field.primaryKey && field.unique ? ' UNIQUE' : '';
+
+ // Handle default value
+ const convertedDefault =
+ convertPostgresDefaultToMySQL(field);
+ const defaultValue =
+ convertedDefault && !autoIncrement
+ ? ` DEFAULT ${convertedDefault}`
+ : '';
+
+ // MySQL supports inline column comments
+ const comment = field.comments
+ ? ` COMMENT '${escapeSQLComment(field.comments)}'`
+ : '';
+
+ // Build inline SQL comment for conversion notes
+ const sqlInlineComment = fullInlineComment
+ ? ` -- ${fullInlineComment}`
+ : '';
+
+ // Determine if this field needs a trailing comma
+ const isLastField = index === allFields.length - 1;
+ const needsComma =
+ !isLastField || hasFollowingConstraints;
+
+ return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeName}${notNull}${autoIncrement}${unique}${defaultValue}${comment}${needsComma ? ',' : ''}${sqlInlineComment}`;
+ }
+ );
+
+ return `${
+ table.comments ? formatTableComment(table.comments) : ''
+ }\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${fieldDefinitions.join('\n')}${
+ // Add PRIMARY KEY as table constraint
+ primaryKeyFields.length > 0
+ ? `\n PRIMARY KEY (${primaryKeyFields
+ .map((f) => `\`${f.name}\``)
+ .join(
+ ', '
+ )})${validCheckConstraints.length > 0 ? ',' : ''}`
+ : ''
+ }${
+ // Add CHECK constraints (already computed above as validCheckConstraints)
+ validCheckConstraints.length > 0
+ ? validCheckConstraints
+ .map(
+ (constraint, index) =>
+ `${index > 0 ? ',' : ''}\n CHECK (${constraint.expression})`
+ )
+ .join('')
+ : ''
+ }\n)${
+ // MySQL supports table comments
+ table.comments
+ ? ` COMMENT='${escapeSQLComment(table.comments)}'`
+ : ''
+ };${
+ // Add indexes
+ (() => {
+ const validIndexes = table.indexes
+ .map((index) => {
+ // Skip primary key indexes
+ if (index.isPrimaryKey) {
+ return '';
+ }
+
+ // Get the list of fields for this index
+ const indexFields = index.fieldIds
+ .map((fieldId) => {
+ const field = table.fields.find(
+ (f) => f.id === fieldId
+ );
+ return field ? field : null;
+ })
+ .filter(Boolean);
+
+ // Skip if this index exactly matches the primary key fields
+ if (
+ primaryKeyFields.length ===
+ indexFields.length &&
+ primaryKeyFields.every((pk) =>
+ indexFields.some(
+ (field) =>
+ field && field.id === pk.id
+ )
+ )
+ ) {
+ return '';
+ }
+
+ // Get index type conversion info
+ const indexType = (
+ index.type || 'btree'
+ ).toLowerCase();
+ const indexTypeMapping =
+ postgresqlIndexTypeToMySQL[indexType];
+ const indexInlineComment =
+ getIndexInlineComment(index, 'mysql');
+
+ // Create index name
+ const fieldNamesForIndex = indexFields
+ .map((field) => field?.name || '')
+ .join('_');
+ const uniqueIndicator = index.unique
+ ? '_unique'
+ : '';
+ const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
+
+ // Get the properly quoted field names
+ const indexFieldNames = indexFields
+ .map((field) =>
+ field ? `\`${field.name}\`` : ''
+ )
+ .filter(Boolean);
+
+ // Check for text/blob fields that need prefix length
+ const indexFieldsWithPrefix =
+ indexFieldNames.map((name) => {
+ const field = indexFields.find(
+ (f) => `\`${f?.name}\`` === name
+ );
+ if (!field) return name;
+
+ const typeName =
+ field.type.name.toLowerCase();
+ // Check if it maps to TEXT, JSON, or BLOB in MySQL
+ const mapping = getTypeMapping(
+ typeName,
+ 'mysql'
+ );
+ const targetType = (
+ mapping?.targetType || ''
+ ).toUpperCase();
+ if (
+ targetType === 'TEXT' ||
+ targetType === 'LONGTEXT' ||
+ targetType === 'MEDIUMTEXT' ||
+ targetType === 'JSON' ||
+ targetType === 'BLOB' ||
+ targetType === 'LONGBLOB'
+ ) {
+ return `${name}(255)`;
+ }
+ return name;
+ });
+
+ const indexTypeStr =
+ indexTypeMapping?.targetType &&
+ indexTypeMapping.targetType !== 'BTREE'
+ ? ` USING ${indexTypeMapping.targetType}`
+ : '';
+
+ const commentStr = indexInlineComment
+ ? ` -- ${indexInlineComment}`
+ : '';
+
+ return indexFieldNames.length > 0
+ ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${indexTypeStr} (${indexFieldsWithPrefix.join(', ')});${commentStr}`
+ : '';
+ })
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b));
+
+ return validIndexes.length > 0
+ ? `\n-- Indexes\n${validIndexes.join('\n')}`
+ : '';
+ })()
+ }`;
+ })
+ .filter(Boolean)
+ .join('\n');
+ }
+
+ // Generate foreign keys
+ if (relationships.length > 0) {
+ sqlScript += '\n-- Foreign key constraints\n';
+
+ const foreignKeys = relationships
+ .map((r: DBRelationship) => {
+ const sourceTable = tables.find(
+ (t) => t.id === r.sourceTableId
+ );
+ const targetTable = tables.find(
+ (t) => t.id === r.targetTableId
+ );
+
+ if (
+ !sourceTable ||
+ !targetTable ||
+ sourceTable.isView ||
+ targetTable.isView
+ ) {
+ return '';
+ }
+
+ const sourceField = sourceTable.fields.find(
+ (f) => f.id === r.sourceFieldId
+ );
+ const targetField = targetTable.fields.find(
+ (f) => f.id === r.targetFieldId
+ );
+
+ if (!sourceField || !targetField) {
+ return '';
+ }
+
+ // Determine which table should have the foreign key based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
+ let fkTable, fkField, refTable, refField;
+
+ if (
+ r.sourceCardinality === 'many' &&
+ r.targetCardinality === 'many'
+ ) {
+ // Many-to-many relationships need a junction table, skip
+ return '';
+ } else if (
+ r.sourceCardinality === 'many' &&
+ r.targetCardinality === 'one'
+ ) {
+ // FK goes on source table (the many side)
+ fkTable = sourceTable;
+ fkField = sourceField;
+ refTable = targetTable;
+ refField = targetField;
+ } else {
+ // All other cases: FK goes on target table
+ fkTable = targetTable;
+ fkField = targetField;
+ refTable = sourceTable;
+ refField = sourceField;
+ }
+
+ const fkTableName = fkTable.schema
+ ? `\`${fkTable.schema}\`.\`${fkTable.name}\``
+ : `\`${fkTable.name}\``;
+ const refTableName = refTable.schema
+ ? `\`${refTable.schema}\`.\`${refTable.name}\``
+ : `\`${refTable.name}\``;
+
+ const constraintName = `\`fk_${fkTable.name}_${fkField.name}\``;
+
+ return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
+ })
+ .filter(Boolean);
+
+ sqlScript += foreignKeys.join('\n');
+ }
+
+ return sqlScript;
+}
diff --git a/src/lib/data/sql-export/cross-dialect/postgresql/type-mappings.ts b/src/lib/data/sql-export/cross-dialect/postgresql/type-mappings.ts
new file mode 100644
index 000000000..378abe87c
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/postgresql/type-mappings.ts
@@ -0,0 +1,576 @@
+/**
+ * Type mappings for PostgreSQL as the source dialect.
+ * Maps PostgreSQL types to MySQL and SQL Server equivalents.
+ */
+
+import type {
+ TypeMapping,
+ TypeMappingTable,
+ IndexTypeMappingTable,
+} from '../types';
+
+/**
+ * PostgreSQL to MySQL type mappings
+ */
+export const postgresqlToMySQL: TypeMappingTable = {
+ // Integer types
+ int: { targetType: 'INT' },
+ int4: { targetType: 'INT' },
+ integer: { targetType: 'INT' },
+ smallint: { targetType: 'SMALLINT' },
+ int2: { targetType: 'SMALLINT' },
+ bigint: { targetType: 'BIGINT' },
+ int8: { targetType: 'BIGINT' },
+
+ // Serial types (auto-increment) - handled specially in exporter
+ serial: { targetType: 'INT' },
+ smallserial: { targetType: 'SMALLINT' },
+ bigserial: { targetType: 'BIGINT' },
+
+ // Floating point types
+ real: { targetType: 'FLOAT' },
+ float4: { targetType: 'FLOAT' },
+ 'double precision': { targetType: 'DOUBLE' },
+ float8: { targetType: 'DOUBLE' },
+ float: { targetType: 'DOUBLE' },
+
+ // Decimal/Numeric types
+ decimal: { targetType: 'DECIMAL', defaultPrecision: 10, defaultScale: 2 },
+ numeric: { targetType: 'DECIMAL', defaultPrecision: 10, defaultScale: 2 },
+ money: {
+ targetType: 'DECIMAL',
+ defaultPrecision: 19,
+ defaultScale: 4,
+ conversionNote: 'PostgreSQL money type converted to DECIMAL(19,4)',
+ includeInlineComment: true,
+ },
+
+ // Character types
+ char: { targetType: 'CHAR', defaultLength: 1 },
+ character: { targetType: 'CHAR', defaultLength: 1 },
+ varchar: { targetType: 'VARCHAR', defaultLength: 255 },
+ 'character varying': { targetType: 'VARCHAR', defaultLength: 255 },
+ text: { targetType: 'TEXT' },
+ name: { targetType: 'VARCHAR', defaultLength: 63 },
+
+ // Binary types
+ bytea: {
+ targetType: 'LONGBLOB',
+ conversionNote: 'PostgreSQL bytea converted to LONGBLOB',
+ includeInlineComment: true,
+ },
+
+ // Boolean type
+ boolean: { targetType: 'TINYINT(1)' },
+ bool: { targetType: 'TINYINT(1)' },
+
+ // Date/Time types
+ date: { targetType: 'DATE' },
+ time: { targetType: 'TIME' },
+ timetz: {
+ targetType: 'TIME',
+ conversionNote: 'Time zone information lost in conversion',
+ includeInlineComment: true,
+ },
+ 'time with time zone': {
+ targetType: 'TIME',
+ conversionNote: 'Time zone information lost in conversion',
+ includeInlineComment: true,
+ },
+ 'time without time zone': { targetType: 'TIME' },
+ timestamp: { targetType: 'DATETIME' },
+ timestamptz: {
+ targetType: 'DATETIME',
+ conversionNote: 'Time zone information lost in conversion',
+ includeInlineComment: true,
+ },
+ 'timestamp with time zone': {
+ targetType: 'DATETIME',
+ conversionNote: 'Time zone information lost in conversion',
+ includeInlineComment: true,
+ },
+ 'timestamp without time zone': { targetType: 'DATETIME' },
+ interval: {
+ targetType: 'VARCHAR',
+ defaultLength: 100,
+ conversionNote:
+ 'PostgreSQL interval type has no MySQL equivalent, stored as string',
+ includeInlineComment: true,
+ },
+
+ // JSON types
+ json: { targetType: 'JSON' },
+ jsonb: {
+ targetType: 'JSON',
+ conversionNote:
+ 'JSONB binary optimizations not available in MySQL JSON',
+ includeInlineComment: true,
+ },
+
+ // UUID type
+ uuid: {
+ targetType: 'CHAR',
+ defaultLength: 36,
+ conversionNote: 'UUID stored as CHAR(36)',
+ includeInlineComment: true,
+ },
+
+ // Network types
+ inet: {
+ targetType: 'VARCHAR',
+ defaultLength: 45,
+ conversionNote: 'PostgreSQL inet type converted to VARCHAR',
+ includeInlineComment: true,
+ },
+ cidr: {
+ targetType: 'VARCHAR',
+ defaultLength: 45,
+ conversionNote: 'PostgreSQL cidr type converted to VARCHAR',
+ includeInlineComment: true,
+ },
+ macaddr: {
+ targetType: 'VARCHAR',
+ defaultLength: 17,
+ conversionNote: 'PostgreSQL macaddr type converted to VARCHAR',
+ includeInlineComment: true,
+ },
+ macaddr8: {
+ targetType: 'VARCHAR',
+ defaultLength: 23,
+ conversionNote: 'PostgreSQL macaddr8 type converted to VARCHAR',
+ includeInlineComment: true,
+ },
+
+ // Bit string types
+ bit: { targetType: 'BIT', defaultLength: 1 },
+ varbit: { targetType: 'BIT', defaultLength: 64 },
+ 'bit varying': { targetType: 'BIT', defaultLength: 64 },
+
+ // Geometric types (MySQL has partial support)
+ point: { targetType: 'POINT' },
+ line: {
+ targetType: 'LINESTRING',
+ conversionNote: 'PostgreSQL infinite line converted to LINESTRING',
+ includeInlineComment: true,
+ },
+ lseg: { targetType: 'LINESTRING' },
+ box: { targetType: 'POLYGON' },
+ path: { targetType: 'LINESTRING' },
+ polygon: { targetType: 'POLYGON' },
+ circle: {
+ targetType: 'POLYGON',
+ conversionNote: 'PostgreSQL circle approximated as POLYGON',
+ includeInlineComment: true,
+ },
+ geometry: { targetType: 'GEOMETRY' },
+ geography: { targetType: 'GEOMETRY' },
+
+ // Text search types (no MySQL equivalent)
+ tsvector: {
+ targetType: 'TEXT',
+ conversionNote:
+ 'PostgreSQL full-text search type has no MySQL equivalent',
+ includeInlineComment: true,
+ },
+ tsquery: {
+ targetType: 'TEXT',
+ conversionNote:
+ 'PostgreSQL full-text search type has no MySQL equivalent',
+ includeInlineComment: true,
+ },
+
+ // Range types (no MySQL equivalent)
+ int4range: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL range type stored as JSON [lower, upper]',
+ includeInlineComment: true,
+ },
+ int8range: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL range type stored as JSON [lower, upper]',
+ includeInlineComment: true,
+ },
+ numrange: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL range type stored as JSON [lower, upper]',
+ includeInlineComment: true,
+ },
+ tsrange: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL range type stored as JSON [lower, upper]',
+ includeInlineComment: true,
+ },
+ tstzrange: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL range type stored as JSON [lower, upper]',
+ includeInlineComment: true,
+ },
+ daterange: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL range type stored as JSON [lower, upper]',
+ includeInlineComment: true,
+ },
+
+ // OID and system types
+ oid: { targetType: 'INT UNSIGNED' },
+ regproc: { targetType: 'VARCHAR', defaultLength: 255 },
+ regprocedure: { targetType: 'VARCHAR', defaultLength: 255 },
+ regoper: { targetType: 'VARCHAR', defaultLength: 255 },
+ regoperator: { targetType: 'VARCHAR', defaultLength: 255 },
+ regclass: { targetType: 'VARCHAR', defaultLength: 255 },
+ regtype: { targetType: 'VARCHAR', defaultLength: 255 },
+ regrole: { targetType: 'VARCHAR', defaultLength: 255 },
+ regnamespace: { targetType: 'VARCHAR', defaultLength: 255 },
+ regconfig: { targetType: 'VARCHAR', defaultLength: 255 },
+ regdictionary: { targetType: 'VARCHAR', defaultLength: 255 },
+
+ // XML type
+ xml: {
+ targetType: 'TEXT',
+ conversionNote: 'PostgreSQL XML type converted to TEXT',
+ includeInlineComment: true,
+ },
+
+ // User-defined and array types (handled specially)
+ 'user-defined': {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL custom type converted to JSON',
+ includeInlineComment: true,
+ },
+ array: {
+ targetType: 'JSON',
+ conversionNote: 'PostgreSQL array type converted to JSON',
+ includeInlineComment: true,
+ },
+
+ // Enum type (handled specially, but fallback here)
+ enum: {
+ targetType: 'VARCHAR',
+ defaultLength: 255,
+ conversionNote: 'PostgreSQL ENUM converted to VARCHAR',
+ includeInlineComment: true,
+ },
+};
+
+/**
+ * PostgreSQL to SQL Server type mappings
+ */
+export const postgresqlToSQLServer: TypeMappingTable = {
+ // Integer types
+ int: { targetType: 'INT' },
+ int4: { targetType: 'INT' },
+ integer: { targetType: 'INT' },
+ smallint: { targetType: 'SMALLINT' },
+ int2: { targetType: 'SMALLINT' },
+ bigint: { targetType: 'BIGINT' },
+ int8: { targetType: 'BIGINT' },
+
+ // Serial types - handled specially with IDENTITY
+ serial: { targetType: 'INT' },
+ smallserial: { targetType: 'SMALLINT' },
+ bigserial: { targetType: 'BIGINT' },
+
+ // Floating point types
+ real: { targetType: 'REAL' },
+ float4: { targetType: 'REAL' },
+ 'double precision': { targetType: 'FLOAT' },
+ float8: { targetType: 'FLOAT' },
+ float: { targetType: 'FLOAT' },
+
+ // Decimal/Numeric types
+ decimal: { targetType: 'DECIMAL', defaultPrecision: 18, defaultScale: 2 },
+ numeric: { targetType: 'NUMERIC', defaultPrecision: 18, defaultScale: 2 },
+ money: { targetType: 'MONEY' },
+
+ // Character types
+ char: { targetType: 'CHAR', defaultLength: 1 },
+ character: { targetType: 'CHAR', defaultLength: 1 },
+ varchar: { targetType: 'VARCHAR', defaultLength: 255 },
+ 'character varying': { targetType: 'VARCHAR', defaultLength: 255 },
+ text: { targetType: 'NVARCHAR(MAX)' },
+ name: { targetType: 'NVARCHAR', defaultLength: 128 },
+
+ // Binary types
+ bytea: { targetType: 'VARBINARY(MAX)' },
+
+ // Boolean type
+ boolean: { targetType: 'BIT' },
+ bool: { targetType: 'BIT' },
+
+ // Date/Time types
+ date: { targetType: 'DATE' },
+ time: { targetType: 'TIME' },
+ timetz: {
+ targetType: 'TIME',
+ conversionNote: 'Time zone offset not preserved',
+ includeInlineComment: true,
+ },
+ 'time with time zone': {
+ targetType: 'TIME',
+ conversionNote: 'Time zone offset not preserved',
+ includeInlineComment: true,
+ },
+ 'time without time zone': { targetType: 'TIME' },
+ timestamp: { targetType: 'DATETIME2' },
+ timestamptz: { targetType: 'DATETIMEOFFSET' },
+ 'timestamp with time zone': { targetType: 'DATETIMEOFFSET' },
+ 'timestamp without time zone': { targetType: 'DATETIME2' },
+ interval: {
+ targetType: 'NVARCHAR',
+ defaultLength: 100,
+ conversionNote:
+ 'PostgreSQL interval type has no SQL Server equivalent, stored as string',
+ includeInlineComment: true,
+ },
+
+ // JSON types
+ json: { targetType: 'NVARCHAR(MAX)' },
+ jsonb: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'JSON stored as NVARCHAR(MAX). Use ISJSON() for validation, JSON functions for querying.',
+ includeInlineComment: true,
+ },
+
+ // UUID type
+ uuid: { targetType: 'UNIQUEIDENTIFIER' },
+
+ // Network types
+ inet: {
+ targetType: 'NVARCHAR',
+ defaultLength: 45,
+ conversionNote: 'PostgreSQL inet type converted to NVARCHAR',
+ includeInlineComment: true,
+ },
+ cidr: {
+ targetType: 'NVARCHAR',
+ defaultLength: 45,
+ conversionNote: 'PostgreSQL cidr type converted to NVARCHAR',
+ includeInlineComment: true,
+ },
+ macaddr: {
+ targetType: 'NVARCHAR',
+ defaultLength: 17,
+ conversionNote: 'PostgreSQL macaddr type converted to NVARCHAR',
+ includeInlineComment: true,
+ },
+ macaddr8: {
+ targetType: 'NVARCHAR',
+ defaultLength: 23,
+ conversionNote: 'PostgreSQL macaddr8 type converted to NVARCHAR',
+ includeInlineComment: true,
+ },
+
+ // Bit string types
+ bit: { targetType: 'BIT' },
+ varbit: {
+ targetType: 'VARBINARY',
+ defaultLength: 64,
+ conversionNote: 'Variable bit string converted to VARBINARY',
+ includeInlineComment: true,
+ },
+ 'bit varying': {
+ targetType: 'VARBINARY',
+ defaultLength: 64,
+ conversionNote: 'Variable bit string converted to VARBINARY',
+ includeInlineComment: true,
+ },
+
+ // Geometric types
+ point: { targetType: 'GEOMETRY' },
+ line: { targetType: 'GEOMETRY' },
+ lseg: { targetType: 'GEOMETRY' },
+ box: { targetType: 'GEOMETRY' },
+ path: { targetType: 'GEOMETRY' },
+ polygon: { targetType: 'GEOMETRY' },
+ circle: {
+ targetType: 'GEOMETRY',
+ conversionNote: 'Circle represented as geometry point with radius',
+ includeInlineComment: true,
+ },
+ geometry: { targetType: 'GEOMETRY' },
+ geography: { targetType: 'GEOGRAPHY' },
+
+ // Text search types (no direct equivalent)
+ tsvector: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL full-text search. Use SQL Server Full-Text Search instead.',
+ includeInlineComment: true,
+ },
+ tsquery: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL full-text search. Use SQL Server Full-Text Search instead.',
+ includeInlineComment: true,
+ },
+
+ // Range types (no SQL Server equivalent)
+ int4range: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL range type. Consider using two columns for lower/upper bounds.',
+ includeInlineComment: true,
+ },
+ int8range: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL range type. Consider using two columns for lower/upper bounds.',
+ includeInlineComment: true,
+ },
+ numrange: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL range type. Consider using two columns for lower/upper bounds.',
+ includeInlineComment: true,
+ },
+ tsrange: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL range type. Consider using two columns for lower/upper bounds.',
+ includeInlineComment: true,
+ },
+ tstzrange: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL range type. Consider using two columns for lower/upper bounds.',
+ includeInlineComment: true,
+ },
+ daterange: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL range type. Consider using two columns for lower/upper bounds.',
+ includeInlineComment: true,
+ },
+
+ // OID and system types
+ oid: { targetType: 'INT' },
+ regproc: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regprocedure: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regoper: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regoperator: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regclass: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regtype: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regrole: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regnamespace: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regconfig: { targetType: 'NVARCHAR', defaultLength: 255 },
+ regdictionary: { targetType: 'NVARCHAR', defaultLength: 255 },
+
+ // XML type
+ xml: { targetType: 'XML' },
+
+ // User-defined and array types
+ 'user-defined': {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote: 'PostgreSQL custom type converted to NVARCHAR(MAX)',
+ includeInlineComment: true,
+ },
+ array: {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'PostgreSQL array converted to NVARCHAR(MAX) as JSON array',
+ includeInlineComment: true,
+ },
+
+ // Enum type (handled specially)
+ enum: {
+ targetType: 'NVARCHAR',
+ defaultLength: 255,
+ conversionNote: 'PostgreSQL ENUM converted to NVARCHAR',
+ includeInlineComment: true,
+ },
+};
+
+/**
+ * Index type mappings from PostgreSQL to MySQL
+ */
+export const postgresqlIndexTypeToMySQL: IndexTypeMappingTable = {
+ btree: { targetType: 'BTREE' },
+ hash: { targetType: 'HASH' },
+ gin: {
+ targetType: 'BTREE',
+ note: 'GIN index downgraded to BTREE (MySQL does not support GIN)',
+ },
+ gist: {
+ targetType: 'BTREE',
+ note: 'GiST index downgraded to BTREE (MySQL does not support GiST)',
+ },
+ spgist: {
+ targetType: 'BTREE',
+ note: 'SP-GiST index downgraded to BTREE (MySQL does not support SP-GiST)',
+ },
+ brin: {
+ targetType: 'BTREE',
+ note: 'BRIN index downgraded to BTREE (MySQL does not support BRIN)',
+ },
+};
+
+/**
+ * Index type mappings from PostgreSQL to SQL Server
+ */
+export const postgresqlIndexTypeToSQLServer: IndexTypeMappingTable = {
+ btree: { targetType: 'NONCLUSTERED' },
+ hash: {
+ targetType: 'NONCLUSTERED',
+ note: 'Hash index converted to NONCLUSTERED',
+ },
+ gin: {
+ targetType: 'NONCLUSTERED',
+ note: 'GIN index downgraded to NONCLUSTERED. Consider using Full-Text Index.',
+ },
+ gist: {
+ targetType: 'SPATIAL',
+ note: 'GiST index converted to SPATIAL (for geometry types) or NONCLUSTERED',
+ },
+ spgist: {
+ targetType: 'NONCLUSTERED',
+ note: 'SP-GiST index converted to NONCLUSTERED',
+ },
+ brin: {
+ targetType: 'NONCLUSTERED',
+ note: 'BRIN index converted to NONCLUSTERED',
+ },
+ clustered: { targetType: 'CLUSTERED' },
+ nonclustered: { targetType: 'NONCLUSTERED' },
+};
+
+/**
+ * Get the type mapping for a PostgreSQL type to a target dialect
+ */
+export function getTypeMapping(
+ postgresType: string,
+ targetDialect: 'mysql' | 'sqlserver'
+): TypeMapping | undefined {
+ const normalizedType = postgresType.toLowerCase().trim();
+
+ // Check for array types
+ if (normalizedType.endsWith('[]')) {
+ return targetDialect === 'mysql'
+ ? postgresqlToMySQL['array']
+ : postgresqlToSQLServer['array'];
+ }
+
+ const mappingTable =
+ targetDialect === 'mysql' ? postgresqlToMySQL : postgresqlToSQLServer;
+ return mappingTable[normalizedType];
+}
+
+/**
+ * Get fallback type mapping when no explicit mapping exists
+ */
+export function getFallbackTypeMapping(
+ targetDialect: 'mysql' | 'sqlserver'
+): TypeMapping {
+ return targetDialect === 'mysql'
+ ? {
+ targetType: 'TEXT',
+ conversionNote: 'Unknown PostgreSQL type converted to TEXT',
+ includeInlineComment: true,
+ }
+ : {
+ targetType: 'NVARCHAR(MAX)',
+ conversionNote:
+ 'Unknown PostgreSQL type converted to NVARCHAR(MAX)',
+ includeInlineComment: true,
+ };
+}
diff --git a/src/lib/data/sql-export/cross-dialect/types.ts b/src/lib/data/sql-export/cross-dialect/types.ts
new file mode 100644
index 000000000..138d5423a
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/types.ts
@@ -0,0 +1,42 @@
+/**
+ * Shared type definitions for cross-dialect SQL export.
+ * These types are used across all source→target dialect mappings.
+ */
+
+/**
+ * Represents a type mapping from a source database type to a target type.
+ */
+export interface TypeMapping {
+ /** The target database type name */
+ targetType: string;
+ /** Optional comment/warning about the conversion */
+ conversionNote?: string;
+ /** Whether the original type info should be included as inline comment */
+ includeInlineComment?: boolean;
+ /** For types that need length specification */
+ defaultLength?: number;
+ /** For types that need precision */
+ defaultPrecision?: number;
+ /** For types that need scale */
+ defaultScale?: number;
+}
+
+/**
+ * A table of type mappings keyed by source type name.
+ */
+export type TypeMappingTable = Record;
+
+/**
+ * Represents an index type mapping from source to target database.
+ */
+export interface IndexTypeMapping {
+ /** The target index type name */
+ targetType: string;
+ /** Optional note about the conversion */
+ note?: string;
+}
+
+/**
+ * A table of index type mappings keyed by source index type.
+ */
+export type IndexTypeMappingTable = Record;
diff --git a/src/lib/data/sql-export/cross-dialect/unsupported-features.ts b/src/lib/data/sql-export/cross-dialect/unsupported-features.ts
new file mode 100644
index 000000000..21d025819
--- /dev/null
+++ b/src/lib/data/sql-export/cross-dialect/unsupported-features.ts
@@ -0,0 +1,368 @@
+/**
+ * Detects PostgreSQL features that cannot be fully converted to target dialects.
+ * Used to generate warning comments in cross-dialect exports.
+ */
+
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+import type { DBIndex } from '@/lib/domain/db-index';
+import type { DBCustomType } from '@/lib/domain/db-custom-type';
+import { DatabaseType } from '@/lib/domain/database-type';
+import {
+ getTypeMapping,
+ postgresqlIndexTypeToMySQL,
+ postgresqlIndexTypeToSQLServer,
+} from './postgresql/type-mappings';
+
+export type UnsupportedFeatureType =
+ | 'type'
+ | 'index'
+ | 'constraint'
+ | 'default'
+ | 'custom_type'
+ | 'array'
+ | 'schema';
+
+export interface UnsupportedFeature {
+ type: UnsupportedFeatureType;
+ tableName?: string;
+ objectName: string;
+ feature: string;
+ recommendation: string;
+}
+
+/**
+ * Detect all unsupported PostgreSQL features when converting to a target dialect
+ */
+export function detectUnsupportedFeatures(
+ diagram: Diagram,
+ targetDialect: DatabaseType
+): UnsupportedFeature[] {
+ const features: UnsupportedFeature[] = [];
+ const dialectKey =
+ targetDialect === DatabaseType.SQL_SERVER ? 'sqlserver' : 'mysql';
+
+ // Check custom types (ENUMs and composites)
+ if (diagram.customTypes && diagram.customTypes.length > 0) {
+ features.push(
+ ...detectCustomTypeIssues(diagram.customTypes, dialectKey)
+ );
+ }
+
+ // Check each table
+ if (diagram.tables) {
+ for (const table of diagram.tables) {
+ if (table.isView) continue;
+
+ // Check fields
+ features.push(...detectFieldIssues(table, dialectKey));
+
+ // Check indexes
+ features.push(...detectIndexIssues(table, dialectKey));
+ }
+ }
+
+ return features;
+}
+
+/**
+ * Detect issues with custom types (ENUMs, composites)
+ */
+function detectCustomTypeIssues(
+ customTypes: DBCustomType[],
+ dialectKey: 'mysql' | 'sqlserver'
+): UnsupportedFeature[] {
+ const features: UnsupportedFeature[] = [];
+
+ for (const customType of customTypes) {
+ const typeName = customType.schema
+ ? `${customType.schema}.${customType.name}`
+ : customType.name;
+
+ if (customType.kind === 'enum') {
+ const values = customType.values?.join("', '") || '';
+ features.push({
+ type: 'custom_type',
+ objectName: typeName,
+ feature: `ENUM type with values: '${values}'`,
+ recommendation:
+ dialectKey === 'mysql'
+ ? `Converted to VARCHAR(255). Consider using MySQL ENUM or CHECK constraint.`
+ : `Converted to NVARCHAR(255). Consider using CHECK constraint.`,
+ });
+ } else if (customType.kind === 'composite') {
+ const fields =
+ customType.fields?.map((f) => `${f.field}: ${f.type}`) || [];
+ features.push({
+ type: 'custom_type',
+ objectName: typeName,
+ feature: `Composite type with fields: ${fields.join(', ')}`,
+ recommendation:
+ dialectKey === 'mysql'
+ ? `Converted to JSON. Consider restructuring as separate columns or JSON.`
+ : `Converted to NVARCHAR(MAX) as JSON. Consider restructuring as separate columns.`,
+ });
+ }
+ }
+
+ return features;
+}
+
+/**
+ * Detect issues with field types and defaults
+ */
+function detectFieldIssues(
+ table: DBTable,
+ dialectKey: 'mysql' | 'sqlserver'
+): UnsupportedFeature[] {
+ const features: UnsupportedFeature[] = [];
+ const tableName = table.schema
+ ? `${table.schema}.${table.name}`
+ : table.name;
+
+ for (const field of table.fields) {
+ const typeName = field.type.name.toLowerCase();
+
+ // Check for array types
+ if (field.isArray || typeName.endsWith('[]')) {
+ features.push({
+ type: 'array',
+ tableName,
+ objectName: field.name,
+ feature: `Array type: ${typeName}`,
+ recommendation:
+ dialectKey === 'mysql'
+ ? `Converted to JSON. Use JSON_ARRAY() for inserts.`
+ : `Converted to NVARCHAR(MAX) as JSON array.`,
+ });
+ }
+
+ // Check type mapping for conversion notes
+ const mapping = getTypeMapping(typeName, dialectKey);
+ if (mapping?.conversionNote) {
+ features.push({
+ type: 'type',
+ tableName,
+ objectName: field.name,
+ feature: `Type: ${typeName}`,
+ recommendation: mapping.conversionNote,
+ });
+ }
+
+ // Check for PostgreSQL-specific defaults
+ if (field.default) {
+ const defaultLower = field.default.toLowerCase();
+
+ // Sequences
+ if (defaultLower.includes('nextval')) {
+ const match = field.default.match(/nextval\('([^']+)'/);
+ const seqName = match ? match[1] : 'unknown';
+ features.push({
+ type: 'default',
+ tableName,
+ objectName: field.name,
+ feature: `Sequence: ${seqName}`,
+ recommendation:
+ dialectKey === 'mysql'
+ ? `Converted to AUTO_INCREMENT.`
+ : `Converted to IDENTITY(1,1).`,
+ });
+ }
+
+ // PostgreSQL-specific functions
+ if (
+ defaultLower.includes('gen_random_uuid') ||
+ defaultLower.includes('uuid_generate')
+ ) {
+ features.push({
+ type: 'default',
+ tableName,
+ objectName: field.name,
+ feature: `UUID generation function`,
+ recommendation:
+ dialectKey === 'mysql'
+ ? `Use UUID() function in MySQL.`
+ : `Use NEWID() function in SQL Server.`,
+ });
+ }
+
+ // Array constructors
+ if (
+ defaultLower.includes('array[') ||
+ defaultLower.includes("'{}")
+ ) {
+ features.push({
+ type: 'default',
+ tableName,
+ objectName: field.name,
+ feature: `Array default value`,
+ recommendation:
+ dialectKey === 'mysql'
+ ? `Converted to JSON array literal.`
+ : `Converted to JSON array string.`,
+ });
+ }
+ }
+ }
+
+ return features;
+}
+
+/**
+ * Detect issues with index types
+ */
+function detectIndexIssues(
+ table: DBTable,
+ dialectKey: 'mysql' | 'sqlserver'
+): UnsupportedFeature[] {
+ const features: UnsupportedFeature[] = [];
+ const tableName = table.schema
+ ? `${table.schema}.${table.name}`
+ : table.name;
+
+ const indexTypeMap =
+ dialectKey === 'mysql'
+ ? postgresqlIndexTypeToMySQL
+ : postgresqlIndexTypeToSQLServer;
+
+ for (const index of table.indexes) {
+ if (index.isPrimaryKey) continue;
+
+ const indexType = (index.type || 'btree').toLowerCase();
+ const mapping = indexTypeMap[indexType];
+
+ if (mapping?.note) {
+ features.push({
+ type: 'index',
+ tableName,
+ objectName: index.name,
+ feature: `${indexType.toUpperCase()} index`,
+ recommendation: mapping.note,
+ });
+ }
+ }
+
+ return features;
+}
+
+/**
+ * Format unsupported features as a warning comment block for SQL output
+ */
+export function formatWarningsHeader(
+ features: UnsupportedFeature[],
+ sourceDialect: string,
+ targetDialect: string
+): string {
+ if (features.length === 0) {
+ return `-- ${sourceDialect} to ${targetDialect} conversion\n-- Generated by ChartDB\n`;
+ }
+
+ let header = `-- ${sourceDialect} to ${targetDialect} conversion\n`;
+ header += `-- Generated by ChartDB\n`;
+ header += `--\n`;
+ header += `-- CONVERSION NOTES (${features.length} items):\n`;
+
+ // Group by type
+ const grouped = groupFeaturesByType(features);
+
+ for (const [type, items] of Object.entries(grouped)) {
+ header += `--\n`;
+ header += `-- ${formatTypeLabel(type as UnsupportedFeatureType)}:\n`;
+ for (const item of items) {
+ const location = item.tableName
+ ? `${item.tableName}.${item.objectName}`
+ : item.objectName;
+ header += `-- - ${location}: ${item.feature}\n`;
+ }
+ }
+
+ header += `--\n\n`;
+ return header;
+}
+
+/**
+ * Group features by their type for organized output
+ */
+function groupFeaturesByType(
+ features: UnsupportedFeature[]
+): Record {
+ const grouped: Record = {};
+
+ for (const feature of features) {
+ if (!grouped[feature.type]) {
+ grouped[feature.type] = [];
+ }
+ grouped[feature.type].push(feature);
+ }
+
+ return grouped;
+}
+
+/**
+ * Format type label for display
+ */
+function formatTypeLabel(type: UnsupportedFeatureType): string {
+ switch (type) {
+ case 'custom_type':
+ return 'Custom Types (ENUM/Composite)';
+ case 'array':
+ return 'Array Fields';
+ case 'type':
+ return 'Type Conversions';
+ case 'index':
+ return 'Index Type Changes';
+ case 'default':
+ return 'Default Value Conversions';
+ case 'constraint':
+ return 'Constraint Changes';
+ case 'schema':
+ return 'Schema Changes';
+ default:
+ return type;
+ }
+}
+
+/**
+ * Get inline comment for a specific field conversion
+ */
+export function getFieldInlineComment(
+ field: DBField,
+ dialectKey: 'mysql' | 'sqlserver'
+): string | null {
+ const typeName = field.type.name.toLowerCase();
+
+ // Array types
+ if (field.isArray || typeName.endsWith('[]')) {
+ return `Was: ${field.type.name} (PostgreSQL array)`;
+ }
+
+ // Check type mapping
+ const mapping = getTypeMapping(typeName, dialectKey);
+ if (mapping?.includeInlineComment && mapping.conversionNote) {
+ return `Was: ${field.type.name}`;
+ }
+
+ return null;
+}
+
+/**
+ * Get inline comment for an index conversion
+ */
+export function getIndexInlineComment(
+ index: DBIndex,
+ dialectKey: 'mysql' | 'sqlserver'
+): string | null {
+ const indexType = (index.type || 'btree').toLowerCase();
+ const indexTypeMap =
+ dialectKey === 'mysql'
+ ? postgresqlIndexTypeToMySQL
+ : postgresqlIndexTypeToSQLServer;
+ const mapping = indexTypeMap[indexType];
+
+ if (mapping?.note) {
+ return mapping.note;
+ }
+
+ return null;
+}
diff --git a/src/lib/data/sql-export/export-per-type/mssql.ts b/src/lib/data/sql-export/export-per-type/mssql.ts
index 071a6ab20..7b4c93b88 100644
--- a/src/lib/data/sql-export/export-per-type/mssql.ts
+++ b/src/lib/data/sql-export/export-per-type/mssql.ts
@@ -132,9 +132,15 @@ export function exportMSSQL({
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
- typeName.toLowerCase() === 'nchar'
+ typeName.toLowerCase() === 'nchar' ||
+ typeName.toLowerCase() === 'varbinary'
) {
- typeWithSize = `${typeName}(${field.characterMaximumLength})`;
+ // SQL Server uses -1 to represent MAX length
+ const lengthSpec =
+ field.characterMaximumLength === '-1'
+ ? 'MAX'
+ : field.characterMaximumLength;
+ typeWithSize = `${typeName}(${lengthSpec})`;
}
}
if (field.precision && field.scale) {
@@ -178,22 +184,34 @@ export function exportMSSQL({
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
- ? `,\n ${(() => {
- // Find PK index to get the constraint name
- const pkIndex = table.indexes.find(
- (idx) => idx.isPrimaryKey
- );
- return pkIndex?.name
- ? `CONSTRAINT [${pkIndex.name}] `
- : '';
- })()}PRIMARY KEY (${table.fields
+ ? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
+ }${
+ // Add check constraints (filter out empty expressions)
+ (() => {
+ const validChecks = (
+ table.checkConstraints ?? []
+ ).filter((c) => c.expression && c.expression.trim());
+ return validChecks.length > 0
+ ? validChecks
+ .map(
+ (constraint) =>
+ `,\n CHECK (${constraint.expression})`
+ )
+ .join('')
+ : '';
+ })()
}\n);\n${(() => {
const validIndexes = table.indexes
.map((index) => {
+ // Skip primary key indexes - they're already handled as constraints
+ if (index.isPrimaryKey) {
+ return '';
+ }
+
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
@@ -214,15 +232,16 @@ export function exportMSSQL({
);
indexFields.length = 32;
return indexFields.length > 0
- ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
+ ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFields.join(', ')});`
: '';
}
return indexFields.length > 0
- ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
+ ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFields.join(', ')});`
: '';
})
- .filter(Boolean);
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b)); // Sort for consistent output
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
@@ -268,39 +287,31 @@ export function exportMSSQL({
}
// Determine which table should have the foreign key based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
let fkTable, fkField, refTable, refField;
if (
- r.sourceCardinality === 'one' &&
+ r.sourceCardinality === 'many' &&
r.targetCardinality === 'many'
) {
- // FK goes on target table
- fkTable = targetTable;
- fkField = targetField;
- refTable = sourceTable;
- refField = sourceField;
+ // Many-to-many relationships need a junction table, skip
+ return '';
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
- // FK goes on source table
- fkTable = sourceTable;
- fkField = sourceField;
- refTable = targetTable;
- refField = targetField;
- } else if (
- r.sourceCardinality === 'one' &&
- r.targetCardinality === 'one'
- ) {
- // For 1:1, FK can go on either side, but typically goes on the table that references the other
- // We'll keep the current behavior for 1:1
+ // FK goes on source table (the many side)
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
- // Many-to-many relationships need a junction table, skip for now
- return '';
+ // All other cases: FK goes on target table
+ fkTable = targetTable;
+ fkField = targetField;
+ refTable = sourceTable;
+ refField = sourceField;
}
const fkTableName = fkTable.schema
diff --git a/src/lib/data/sql-export/export-per-type/mysql.ts b/src/lib/data/sql-export/export-per-type/mysql.ts
index c8a75ab63..8be10230b 100644
--- a/src/lib/data/sql-export/export-per-type/mysql.ts
+++ b/src/lib/data/sql-export/export-per-type/mysql.ts
@@ -313,18 +313,25 @@ export function exportMySQL({
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
- ? `,\n ${(() => {
- // Find PK index to get the constraint name
- const pkIndex = table.indexes.find(
- (idx) => idx.isPrimaryKey
- );
- return pkIndex?.name
- ? `CONSTRAINT \`${pkIndex.name}\` `
- : '';
- })()}PRIMARY KEY (${primaryKeyFields
+ ? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''
+ }${
+ // Add check constraints (filter out empty expressions)
+ (() => {
+ const validChecks = (
+ table.checkConstraints ?? []
+ ).filter((c) => c.expression && c.expression.trim());
+ return validChecks.length > 0
+ ? validChecks
+ .map(
+ (constraint) =>
+ `,\n CHECK (${constraint.expression})`
+ )
+ .join('')
+ : '';
+ })()
}\n)${
// MySQL supports table comments
table.comments
@@ -417,7 +424,8 @@ export function exportMySQL({
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
: '';
})
- .filter(Boolean);
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b)); // Sort for consistent output
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
@@ -463,39 +471,31 @@ export function exportMySQL({
}
// Determine which table should have the foreign key based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
let fkTable, fkField, refTable, refField;
if (
- r.sourceCardinality === 'one' &&
+ r.sourceCardinality === 'many' &&
r.targetCardinality === 'many'
) {
- // FK goes on target table
- fkTable = targetTable;
- fkField = targetField;
- refTable = sourceTable;
- refField = sourceField;
+ // Many-to-many relationships need a junction table, skip
+ return '';
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
- // FK goes on source table
- fkTable = sourceTable;
- fkField = sourceField;
- refTable = targetTable;
- refField = targetField;
- } else if (
- r.sourceCardinality === 'one' &&
- r.targetCardinality === 'one'
- ) {
- // For 1:1, FK can go on either side, but typically goes on the table that references the other
- // We'll keep the current behavior for 1:1
+ // FK goes on source table (the many side)
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
- // Many-to-many relationships need a junction table, skip for now
- return '';
+ // All other cases: FK goes on target table
+ fkTable = targetTable;
+ fkField = targetField;
+ refTable = sourceTable;
+ refField = sourceField;
}
const fkTableName = fkTable.schema
diff --git a/src/lib/data/sql-export/export-per-type/postgresql.ts b/src/lib/data/sql-export/export-per-type/postgresql.ts
index dbe1ffc5e..bf22dcb53 100644
--- a/src/lib/data/sql-export/export-per-type/postgresql.ts
+++ b/src/lib/data/sql-export/export-per-type/postgresql.ts
@@ -251,11 +251,11 @@ export function exportPostgreSQL({
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
- serialType = 'SERIAL';
+ serialType = 'serial';
} else if (typeName.toLowerCase() === 'bigint') {
- serialType = 'BIGSERIAL';
+ serialType = 'bigserial';
} else if (typeName.toLowerCase() === 'smallint') {
- serialType = 'SMALLSERIAL';
+ serialType = 'smallserial';
}
}
@@ -286,10 +286,14 @@ export function exportPostgreSQL({
}
}
- // Handle array types (check if the type name ends with '[]')
- if (typeName.endsWith('[]')) {
- typeWithSize =
- typeWithSize.replace('[]', '') + '[]';
+ // Handle array types (check if isArray flag or if type name ends with '[]')
+ if (field.isArray || typeName.endsWith('[]')) {
+ // Remove any existing [] notation
+ const baseTypeWithoutArray = typeWithSize.replace(
+ /\[\]$/,
+ ''
+ );
+ typeWithSize = baseTypeWithoutArray + '[]';
}
const notNull = field.nullable ? '' : ' NOT NULL';
@@ -321,22 +325,29 @@ export function exportPostgreSQL({
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
- return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
+ return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
- ? `,\n ${(() => {
- // Find PK index to get the constraint name
- const pkIndex = table.indexes.find(
- (idx) => idx.isPrimaryKey
- );
- return pkIndex?.name
- ? `CONSTRAINT "${pkIndex.name}" `
- : '';
- })()}PRIMARY KEY (${primaryKeyFields
+ ? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
+ }${
+ // Add check constraints (filter out empty expressions)
+ (() => {
+ const validChecks = (
+ table.checkConstraints ?? []
+ ).filter((c) => c.expression && c.expression.trim());
+ return validChecks.length > 0
+ ? validChecks
+ .map(
+ (constraint) =>
+ `,\n CHECK (${constraint.expression})`
+ )
+ .join('')
+ : '';
+ })()
}\n);${
// Add table comments
table.comments
@@ -382,6 +393,16 @@ export function exportPostgreSQL({
return '';
}
+ // Skip unique indexes on single columns that already have inline UNIQUE
+ // PostgreSQL automatically creates an index for UNIQUE constraints
+ if (
+ index.unique &&
+ indexFields.length === 1 &&
+ indexFields[0]?.unique
+ ) {
+ return '';
+ }
+
// Create unique index name using table name and index name
// This ensures index names are unique across the database
const safeTableName = table.name.replace(
@@ -416,7 +437,8 @@ export function exportPostgreSQL({
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${index.type && index.type !== 'btree' ? ` USING ${index.type.toUpperCase()}` : ''} (${indexFieldNames.join(', ')});`
: '';
})
- .filter(Boolean);
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b)); // Sort for consistent output
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
@@ -463,39 +485,31 @@ export function exportPostgreSQL({
}
// Determine which table should have the foreign key based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
let fkTable, fkField, refTable, refField;
if (
- r.sourceCardinality === 'one' &&
+ r.sourceCardinality === 'many' &&
r.targetCardinality === 'many'
) {
- // FK goes on target table
- fkTable = targetTable;
- fkField = targetField;
- refTable = sourceTable;
- refField = sourceField;
+ // Many-to-many relationships need a junction table, skip
+ return '';
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
- // FK goes on source table
- fkTable = sourceTable;
- fkField = sourceField;
- refTable = targetTable;
- refField = targetField;
- } else if (
- r.sourceCardinality === 'one' &&
- r.targetCardinality === 'one'
- ) {
- // For 1:1, FK can go on either side, but typically goes on the table that references the other
- // We'll keep the current behavior for 1:1
+ // FK goes on source table (the many side)
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
- // Many-to-many relationships need a junction table, skip for now
- return '';
+ // All other cases: FK goes on target table
+ fkTable = targetTable;
+ fkField = targetField;
+ refTable = sourceTable;
+ refField = sourceField;
}
const fkTableName = fkTable.schema
diff --git a/src/lib/data/sql-export/export-per-type/sqlite.ts b/src/lib/data/sql-export/export-per-type/sqlite.ts
index 96e69727f..9c563609d 100644
--- a/src/lib/data/sql-export/export-per-type/sqlite.ts
+++ b/src/lib/data/sql-export/export-per-type/sqlite.ts
@@ -251,39 +251,31 @@ export function exportSQLite({
}
// Determine which table should have the foreign key based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
let fkTable, fkField, refTable, refField;
if (
- r.sourceCardinality === 'one' &&
+ r.sourceCardinality === 'many' &&
r.targetCardinality === 'many'
) {
- // FK goes on target table
- fkTable = targetTable;
- fkField = targetField;
- refTable = sourceTable;
- refField = sourceField;
+ // Many-to-many relationships need a junction table, skip
+ return;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
- // FK goes on source table
- fkTable = sourceTable;
- fkField = sourceField;
- refTable = targetTable;
- refField = targetField;
- } else if (
- r.sourceCardinality === 'one' &&
- r.targetCardinality === 'one'
- ) {
- // For 1:1, FK can go on either side, but typically goes on the table that references the other
- // We'll keep the current behavior for 1:1
+ // FK goes on source table (the many side)
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
- // Many-to-many relationships need a junction table, skip for now
- return;
+ // All other cases: FK goes on target table
+ fkTable = targetTable;
+ fkField = targetField;
+ refTable = sourceTable;
+ refField = sourceField;
}
// If this foreign key belongs to the current table, add it
@@ -393,6 +385,21 @@ export function exportSQLite({
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
+ }${
+ // Add check constraints (filter out empty expressions)
+ (() => {
+ const validChecks = (
+ table.checkConstraints ?? []
+ ).filter((c) => c.expression && c.expression.trim());
+ return validChecks.length > 0
+ ? validChecks
+ .map(
+ (constraint) =>
+ `,\n CHECK (${constraint.expression})`
+ )
+ .join('')
+ : '';
+ })()
}${
// Add foreign key constraints
tableForeignKeys.length > 0
@@ -444,7 +451,8 @@ export function exportSQLite({
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});`
: '';
})
- .filter(Boolean);
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b)); // Sort for consistent output
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
diff --git a/src/lib/data/sql-export/export-sql-script.ts b/src/lib/data/sql-export/export-sql-script.ts
index d69f3b4bb..f3202934d 100644
--- a/src/lib/data/sql-export/export-sql-script.ts
+++ b/src/lib/data/sql-export/export-sql-script.ts
@@ -1,17 +1,93 @@
import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
-import {
- DatabaseType,
- databaseTypesWithCommentSupport,
-} from '@/lib/domain/database-type';
+import { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
-import type { DataType } from '../data-types/data-types';
+import { dataTypeMap, type DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
import { exportMSSQL } from './export-per-type/mssql';
import { exportPostgreSQL } from './export-per-type/postgresql';
import { exportSQLite } from './export-per-type/sqlite';
import { exportMySQL } from './export-per-type/mysql';
+import {
+ exportPostgreSQLToMySQL,
+ exportPostgreSQLToMSSQL,
+} from './cross-dialect';
import { escapeSQLComment } from './export-per-type/common';
+import {
+ databaseTypesWithCommentSupport,
+ supportsCustomTypes,
+ supportsCheckConstraints,
+} from '@/lib/domain/database-capabilities';
+
+// Function to normalize over-escaped default values
+// Handles cases like '''value'' which should be 'value'
+const normalizeQuotedDefault = (value: string): string => {
+ // Check for over-escaped patterns: '''value'', ''value'', etc.
+ // These happen when a quoted string gets re-quoted during import/export cycles
+ const overEscapedMatch = value.match(/^('{2,})(.*?)('{1,})$/);
+ if (overEscapedMatch) {
+ const [, leadingQuotes, innerValue, trailingQuotes] = overEscapedMatch;
+ // If we have more than one leading/trailing quote, it's over-escaped
+ if (leadingQuotes.length > 1 || trailingQuotes.length > 1) {
+ // Extract the actual value and re-quote properly
+ // First, unescape any doubled quotes in the inner value
+ const unescaped = innerValue.replace(/''/g, "'");
+ // Return properly quoted string
+ return `'${unescaped.replace(/'/g, "''")}'`;
+ }
+ }
+ return value;
+};
+
+// Function to format default values with proper quoting
+const formatDefaultValue = (value: string): string => {
+ const trimmed = value.trim();
+
+ // SQL keywords and function-like keywords that don't need quotes
+ const keywords = [
+ 'TRUE',
+ 'FALSE',
+ 'NULL',
+ 'CURRENT_TIMESTAMP',
+ 'CURRENT_DATE',
+ 'CURRENT_TIME',
+ 'NOW',
+ 'GETDATE',
+ 'NEWID',
+ 'UUID',
+ ];
+ if (keywords.includes(trimmed.toUpperCase())) {
+ return trimmed;
+ }
+
+ // Function calls (contain parentheses) don't need quotes
+ if (trimmed.includes('(') && trimmed.includes(')')) {
+ return trimmed;
+ }
+
+ // Numbers don't need quotes
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
+ return trimmed;
+ }
+
+ // Already quoted strings - normalize and return
+ if (
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
+ ) {
+ // Normalize over-escaped quotes (e.g., '''value'' -> 'value')
+ return normalizeQuotedDefault(trimmed);
+ }
+
+ // Check if it's a simple identifier (alphanumeric, no spaces) that might be a currency or enum
+ // These typically don't have spaces and are short (< 10 chars)
+ if (/^[A-Z][A-Z0-9_]*$/i.test(trimmed) && trimmed.length <= 10) {
+ return trimmed; // Treat as unquoted identifier (e.g., EUR, USD)
+ }
+
+ // Everything else needs to be quoted and escaped
+ return `'${trimmed.replace(/'/g, "''")}'`;
+};
// Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => {
@@ -80,11 +156,13 @@ export const exportBaseSQL = ({
targetDatabaseType,
isDBMLFlow = false,
onlyRelationships = false,
+ skipFKGeneration = false,
}: {
diagram: Diagram;
targetDatabaseType: DatabaseType;
isDBMLFlow?: boolean;
onlyRelationships?: boolean;
+ skipFKGeneration?: boolean;
}): string => {
const { tables, relationships } = diagram;
@@ -108,6 +186,20 @@ export const exportBaseSQL = ({
}
}
+ // Deterministic cross-dialect exports (PostgreSQL to MySQL/SQL Server)
+ // These do not use LLM and provide consistent, predictable output
+ if (!isDBMLFlow && diagram.databaseType === DatabaseType.POSTGRESQL) {
+ if (
+ targetDatabaseType === DatabaseType.MYSQL ||
+ targetDatabaseType === DatabaseType.MARIADB
+ ) {
+ return exportPostgreSQLToMySQL({ diagram, onlyRelationships });
+ }
+ if (targetDatabaseType === DatabaseType.SQL_SERVER) {
+ return exportPostgreSQLToMSSQL({ diagram, onlyRelationships });
+ }
+ }
+
// Filter out the tables that are views
const nonViewTables = tables.filter((table) => !table.isView);
@@ -151,10 +243,7 @@ export const exportBaseSQL = ({
// or if we rely on the DBML generator to create Enums separately (as currently done)
// For now, let's assume PostgreSQL-style for demonstration if isDBMLFlow is false.
// If isDBMLFlow is true, we let TableDBML.tsx handle Enum syntax directly.
- if (
- targetDatabaseType === DatabaseType.POSTGRESQL &&
- !isDBMLFlow
- ) {
+ if (supportsCustomTypes(targetDatabaseType) && !isDBMLFlow) {
const enumValues = customType.values
.map((v) => `'${v.replace(/'/g, "''")}'`)
.join(', ');
@@ -167,10 +256,7 @@ export const exportBaseSQL = ({
) {
// For PostgreSQL, generate CREATE TYPE ... AS (...)
// This is crucial for composite types to be recognized by the DBML importer
- if (
- targetDatabaseType === DatabaseType.POSTGRESQL ||
- isDBMLFlow
- ) {
+ if (supportsCustomTypes(targetDatabaseType) || isDBMLFlow) {
// Assume other DBs might not support this or DBML flow needs it
const compositeFields = customType.fields
.map((f) => `${f.field} ${simplifyDataType(f.type)}`)
@@ -185,13 +271,12 @@ export const exportBaseSQL = ({
(ct.kind === 'enum' &&
ct.values &&
ct.values.length > 0 &&
- targetDatabaseType === DatabaseType.POSTGRESQL &&
+ supportsCustomTypes(targetDatabaseType) &&
!isDBMLFlow) ||
(ct.kind === 'composite' &&
ct.fields &&
ct.fields.length > 0 &&
- (targetDatabaseType === DatabaseType.POSTGRESQL ||
- isDBMLFlow))
+ (supportsCustomTypes(targetDatabaseType) || isDBMLFlow))
)
) {
sqlScript += '\n';
@@ -251,7 +336,7 @@ export const exportBaseSQL = ({
if (
customEnumType &&
- targetDatabaseType === DatabaseType.POSTGRESQL &&
+ supportsCustomTypes(targetDatabaseType) &&
!isDBMLFlow
) {
typeName = customEnumType.schema
@@ -294,7 +379,14 @@ export const exportBaseSQL = ({
}
const quotedFieldName = getQuotedFieldName(field.name, isDBMLFlow);
- sqlScript += ` ${quotedFieldName} ${typeName}`;
+
+ // Quote multi-word type names for DBML flow to prevent @dbml/core parser issues
+ const quotedTypeName =
+ isDBMLFlow && typeName.includes(' ')
+ ? `"${typeName}"`
+ : typeName;
+
+ sqlScript += ` ${quotedFieldName} ${quotedTypeName}`;
// Add size for character types
if (
@@ -314,11 +406,31 @@ export const exportBaseSQL = ({
sqlScript += `(1)`;
}
- // Add precision and scale for numeric types
- if (field.precision && field.scale) {
- sqlScript += `(${field.precision}, ${field.scale})`;
- } else if (field.precision) {
- sqlScript += `(${field.precision})`;
+ // Add precision and scale for numeric types only
+ const precisionAndScaleTypes = dataTypeMap[targetDatabaseType]
+ .filter(
+ (t) =>
+ t.fieldAttributes?.precision && t.fieldAttributes?.scale
+ )
+ .map((t) => t.name);
+
+ const isNumericType = precisionAndScaleTypes.some(
+ (t) =>
+ field.type.name.toLowerCase().includes(t) ||
+ typeName.toLowerCase().includes(t)
+ );
+
+ if (isNumericType) {
+ if (field.precision && field.scale) {
+ sqlScript += `(${field.precision}, ${field.scale})`;
+ } else if (field.precision) {
+ sqlScript += `(${field.precision})`;
+ }
+ }
+
+ // Add array suffix if field is an array (after type size and precision)
+ if (field.isArray) {
+ sqlScript += '[]';
}
// Handle NOT NULL constraint
@@ -331,9 +443,26 @@ export const exportBaseSQL = ({
sqlScript += ` UNIQUE`;
}
- // Handle AUTO INCREMENT - add as a comment for AI to process
+ // Handle AUTO INCREMENT
if (field.increment) {
- sqlScript += ` /* AUTO_INCREMENT */`;
+ if (isDBMLFlow) {
+ // For DBML flow, generate proper database-specific syntax
+ if (
+ targetDatabaseType === DatabaseType.MYSQL ||
+ targetDatabaseType === DatabaseType.MARIADB
+ ) {
+ sqlScript += ` AUTO_INCREMENT`;
+ } else if (targetDatabaseType === DatabaseType.SQL_SERVER) {
+ sqlScript += ` IDENTITY(1,1)`;
+ } else if (targetDatabaseType === DatabaseType.SQLITE) {
+ // SQLite AUTOINCREMENT only works with INTEGER PRIMARY KEY
+ // Will be handled when PRIMARY KEY is added
+ }
+ // PostgreSQL/CockroachDB: increment attribute added by restoreIncrementAttribute in DBML export
+ } else {
+ // For non-DBML flow, add as a comment for AI to process
+ sqlScript += ` /* AUTO_INCREMENT */`;
+ }
}
// Handle DEFAULT value
@@ -366,39 +495,72 @@ export const exportBaseSQL = ({
fieldDefault = `now()`;
}
- sqlScript += ` DEFAULT ${fieldDefault}`;
+ // Fix CURRENT_DATE() for PostgreSQL in DBML flow - PostgreSQL uses CURRENT_DATE without parentheses
+ if (
+ isDBMLFlow &&
+ targetDatabaseType === DatabaseType.POSTGRESQL
+ ) {
+ if (fieldDefault.toUpperCase() === 'CURRENT_DATE()') {
+ fieldDefault = 'CURRENT_DATE';
+ }
+ }
+
+ // Format default value with proper quoting
+ const formattedDefault = formatDefaultValue(fieldDefault);
+ sqlScript += ` DEFAULT ${formattedDefault}`;
}
}
- // Handle PRIMARY KEY constraint - only add inline if no PK index with custom name
- const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
- if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
+ // Handle PRIMARY KEY constraint - add inline for single PK fields
+ // Never use named constraints to avoid duplicate constraint name issues
+ if (field.primaryKey && !hasCompositePrimaryKey) {
sqlScript += ' PRIMARY KEY';
+
+ // For SQLite with DBML flow, add AUTOINCREMENT after PRIMARY KEY
+ if (
+ isDBMLFlow &&
+ field.increment &&
+ targetDatabaseType === DatabaseType.SQLITE &&
+ (typeName.toLowerCase() === 'integer' ||
+ typeName.toLowerCase() === 'int')
+ ) {
+ sqlScript += ' AUTOINCREMENT';
+ }
}
- // Add a comma after each field except the last one (or before PK constraint)
- const needsPKConstraint =
- hasCompositePrimaryKey ||
- (primaryKeyFields.length === 1 && pkIndex?.name);
- if (index < table.fields.length - 1 || needsPKConstraint) {
+ // Add a comma after each field except the last one (or before composite PK constraint)
+ if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
sqlScript += ',\n';
}
});
- // Add primary key constraint if needed (for composite PKs or single PK with custom name)
- const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
- if (
- hasCompositePrimaryKey ||
- (primaryKeyFields.length === 1 && pkIndex?.name)
- ) {
+ // Add primary key constraint for composite PKs only (single PKs are inline)
+ // Never use named constraints to avoid duplicate constraint name issues
+ if (hasCompositePrimaryKey) {
const pkFieldNames = primaryKeyFields
.map((f) => getQuotedFieldName(f.name, isDBMLFlow))
.join(', ');
- if (pkIndex?.name) {
- sqlScript += `\n CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`;
- } else {
- sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
- }
+ sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
+ }
+
+ // Add CHECK constraints (only for databases that support them, filter out empty)
+ const dbSupportsChecks = supportsCheckConstraints(targetDatabaseType);
+ const validCheckConstraints = (table.checkConstraints ?? []).filter(
+ (c) => c.expression && c.expression.trim()
+ );
+ if (validCheckConstraints.length > 0 && dbSupportsChecks) {
+ validCheckConstraints.forEach((checkConstraint, idx) => {
+ // Add comma if needed (after fields or composite PK constraint)
+ if (
+ idx === 0 &&
+ (table.fields.length > 0 || hasCompositePrimaryKey)
+ ) {
+ sqlScript += ',';
+ } else if (idx > 0) {
+ sqlScript += ',';
+ }
+ sqlScript += `\n CHECK (${checkConstraint.expression})`;
+ });
}
sqlScript += '\n);\n';
@@ -422,7 +584,9 @@ export const exportBaseSQL = ({
}
});
- // Generate SQL for indexes
+ // Generate SQL for indexes - collect, then sort by the full CREATE INDEX statement
+ const indexStatements: string[] = [];
+
table.indexes.forEach((index) => {
// Skip the primary key index (it's already handled as a constraint)
if (index.isPrimaryKey) {
@@ -454,94 +618,104 @@ export const exportBaseSQL = ({
.join(', ');
if (fieldNames) {
- const indexName =
+ const rawIndexName =
table.schema && !isDBMLFlow
? `${table.schema}_${index.name}`
: index.name;
- sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
+ // Quote index name if it contains special characters
+ // For DBML flow, also quote if contains special characters
+ const needsQuoting = /[^a-zA-Z0-9_]/.test(rawIndexName);
+ const indexName = needsQuoting
+ ? `"${rawIndexName}"`
+ : rawIndexName;
+ indexStatements.push(
+ `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});`
+ );
}
});
- });
- if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
- sqlScript += '\n';
- }
+ // Sort index statements alphabetically for consistent, deterministic output
+ indexStatements.sort((a, b) => a.localeCompare(b));
+ sqlScript += indexStatements.join('\n');
+ if (indexStatements.length > 0) {
+ sqlScript += '\n';
+ }
+ });
- // Handle relationships (foreign keys)
- relationships?.forEach((relationship) => {
- const sourceTable = nonViewTables.find(
- (table) => table.id === relationship.sourceTableId
- );
- const targetTable = nonViewTables.find(
- (table) => table.id === relationship.targetTableId
- );
+ // Skip FK generation when requested (e.g., for DBML export which generates Refs directly)
+ if (!skipFKGeneration) {
+ if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
+ sqlScript += '\n';
+ }
- const sourceTableField = sourceTable?.fields.find(
- (field) => field.id === relationship.sourceFieldId
- );
- const targetTableField = targetTable?.fields.find(
- (field) => field.id === relationship.targetFieldId
- );
+ // Handle relationships (foreign keys)
+ relationships?.forEach((relationship) => {
+ const sourceTable = nonViewTables.find(
+ (table) => table.id === relationship.sourceTableId
+ );
+ const targetTable = nonViewTables.find(
+ (table) => table.id === relationship.targetTableId
+ );
- if (
- sourceTable &&
- targetTable &&
- sourceTableField &&
- targetTableField
- ) {
- // Determine which table should have the foreign key based on cardinality
- // In a 1:many relationship, the foreign key goes on the "many" side
- // If source is "one" and target is "many", FK goes on target table
- // If source is "many" and target is "one", FK goes on source table
- let fkTable, fkField, refTable, refField;
+ const sourceTableField = sourceTable?.fields.find(
+ (field) => field.id === relationship.sourceFieldId
+ );
+ const targetTableField = targetTable?.fields.find(
+ (field) => field.id === relationship.targetFieldId
+ );
if (
- relationship.sourceCardinality === 'one' &&
- relationship.targetCardinality === 'many'
- ) {
- // FK goes on target table
- fkTable = targetTable;
- fkField = targetTableField;
- refTable = sourceTable;
- refField = sourceTableField;
- } else if (
- relationship.sourceCardinality === 'many' &&
- relationship.targetCardinality === 'one'
- ) {
- // FK goes on source table
- fkTable = sourceTable;
- fkField = sourceTableField;
- refTable = targetTable;
- refField = targetTableField;
- } else if (
- relationship.sourceCardinality === 'one' &&
- relationship.targetCardinality === 'one'
+ sourceTable &&
+ targetTable &&
+ sourceTableField &&
+ targetTableField
) {
- // For 1:1, FK can go on either side, but typically goes on the table that references the other
- // We'll keep the current behavior for 1:1
- fkTable = sourceTable;
- fkField = sourceTableField;
- refTable = targetTable;
- refField = targetTableField;
- } else {
- // Many-to-many relationships need a junction table, skip for now
- return;
- }
+ // Determine which table should have the foreign key based on cardinality
+ // - FK goes on the "many" side when cardinalities differ
+ // - FK goes on target when cardinalities are the same (one:one, many:many)
+ // - Many-to-many needs a junction table, skip for SQL export
+ let fkTable, fkField, refTable, refField;
- const fkTableName = getQuotedTableName(fkTable, isDBMLFlow);
- const refTableName = getQuotedTableName(refTable, isDBMLFlow);
- const quotedFkFieldName = getQuotedFieldName(
- fkField.name,
- isDBMLFlow
- );
- const quotedRefFieldName = getQuotedFieldName(
- refField.name,
- isDBMLFlow
- );
+ if (
+ relationship.sourceCardinality === 'many' &&
+ relationship.targetCardinality === 'many'
+ ) {
+ // Many-to-many relationships need a junction table, skip
+ return;
+ } else if (
+ relationship.sourceCardinality === 'many' &&
+ relationship.targetCardinality === 'one'
+ ) {
+ // FK goes on source table (the many side)
+ fkTable = sourceTable;
+ fkField = sourceTableField;
+ refTable = targetTable;
+ refField = targetTableField;
+ } else {
+ // All other cases: FK goes on target table
+ // - one:one (same cardinality → target)
+ // - one:many (target is many side → target)
+ fkTable = targetTable;
+ fkField = targetTableField;
+ refTable = sourceTable;
+ refField = sourceTableField;
+ }
- sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${quotedFkFieldName}) REFERENCES ${refTableName} (${quotedRefFieldName});\n`;
- }
- });
+ const fkTableName = getQuotedTableName(fkTable, isDBMLFlow);
+ const refTableName = getQuotedTableName(refTable, isDBMLFlow);
+ const quotedFkFieldName = getQuotedFieldName(
+ fkField.name,
+ isDBMLFlow
+ );
+ const quotedRefFieldName = getQuotedFieldName(
+ refField.name,
+ isDBMLFlow
+ );
+
+ sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${quotedFkFieldName}) REFERENCES ${refTableName} (${quotedRefFieldName});\n`;
+ }
+ });
+ }
return sqlScript;
};
diff --git a/src/lib/data/sql-import/__tests__/sql-import.test.ts b/src/lib/data/sql-import/__tests__/sql-import.test.ts
new file mode 100644
index 000000000..83d063256
--- /dev/null
+++ b/src/lib/data/sql-import/__tests__/sql-import.test.ts
@@ -0,0 +1,561 @@
+import { describe, it, expect } from 'vitest';
+import { sqlImportToDiagram } from '../index';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('sqlImportToDiagram', () => {
+ it('should parse a simple PostgreSQL table and return a valid diagram', async () => {
+ const sql = `
+ CREATE TABLE users (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) UNIQUE
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.POSTGRESQL,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+ expect(diagram.id).toBeDefined();
+ expect(diagram.databaseType).toBe(DatabaseType.POSTGRESQL);
+
+ // Verify table was parsed
+ expect(diagram.tables).toHaveLength(1);
+ expect(diagram.tables?.[0].name).toBe('users');
+
+ // Verify fields were parsed
+ const fields = diagram.tables?.[0].fields;
+ expect(fields).toHaveLength(3);
+
+ const fieldNames = fields?.map((f) => f.name);
+ expect(fieldNames).toContain('id');
+ expect(fieldNames).toContain('name');
+ expect(fieldNames).toContain('email');
+
+ // Verify primary key
+ const idField = fields?.find((f) => f.name === 'id');
+ expect(idField?.primaryKey).toBe(true);
+
+ // Verify nullable constraints
+ const nameField = fields?.find((f) => f.name === 'name');
+ expect(nameField?.nullable).toBe(false);
+
+ // Verify unique constraint
+ const emailField = fields?.find((f) => f.name === 'email');
+ expect(emailField?.unique).toBe(true);
+ });
+
+ it('should parse foreign key constraints properly', async () => {
+ const sql = `
+ CREATE SCHEMA IF NOT EXISTS "public";
+
+CREATE TABLE "public"."playlists" (
+ "playlist_id" SERIAL,
+ "user_id" int NOT NULL,
+ PRIMARY KEY ("playlist_id")
+);
+
+CREATE TABLE "public"."users" (
+ "user_id" SERIAL,
+ PRIMARY KEY ("user_id")
+);
+
+-- Foreign key constraints
+-- Schema: public
+ALTER TABLE "public"."playlists" ADD CONSTRAINT "fk_playlists_user_id_users_user_id" FOREIGN KEY("user_id") REFERENCES "public"."users"("user_id");
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.POSTGRESQL,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+
+ const playlistTable = diagram.tables?.find(
+ (t) => t.name === 'playlists'
+ );
+ expect(playlistTable).toBeDefined();
+
+ const playlistUserIdField = playlistTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(playlistUserIdField).toBeDefined();
+
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const usersUserIdField = usersTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(usersUserIdField).toBeDefined();
+
+ // verify relationships
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships).toHaveLength(1);
+
+ const relationship = diagram.relationships?.[0];
+ expect(relationship?.sourceSchema).toBe('public');
+ expect(relationship?.sourceTableId).toBe(usersTable?.id);
+ expect(relationship?.sourceFieldId).toBe(usersUserIdField?.id);
+ expect(relationship?.sourceCardinality).toBe('one');
+
+ expect(relationship?.targetSchema).toBe('public');
+ expect(relationship?.targetTableId).toBe(playlistTable?.id);
+ expect(relationship?.targetFieldId).toBe(playlistUserIdField?.id);
+ expect(relationship?.targetCardinality).toBe('many');
+ });
+
+ it('should parse foreign key constraints properly - MySQL', async () => {
+ const sql = `
+CREATE TABLE \`users\` (
+ \`user_id\` INT AUTO_INCREMENT,
+ PRIMARY KEY (\`user_id\`)
+) ENGINE=InnoDB;
+
+CREATE TABLE \`playlists\` (
+ \`playlist_id\` INT AUTO_INCREMENT,
+ \`user_id\` INT NOT NULL,
+ PRIMARY KEY (\`playlist_id\`),
+ CONSTRAINT \`fk_playlists_user_id\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`user_id\`)
+) ENGINE=InnoDB;
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.MYSQL,
+ targetDatabaseType: DatabaseType.MYSQL,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+
+ const playlistTable = diagram.tables?.find(
+ (t) => t.name === 'playlists'
+ );
+ expect(playlistTable).toBeDefined();
+
+ const playlistUserIdField = playlistTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(playlistUserIdField).toBeDefined();
+
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const usersUserIdField = usersTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(usersUserIdField).toBeDefined();
+
+ // verify relationships
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships).toHaveLength(1);
+
+ const relationship = diagram.relationships?.[0];
+ expect(relationship?.sourceTableId).toBe(usersTable?.id);
+ expect(relationship?.sourceFieldId).toBe(usersUserIdField?.id);
+ expect(relationship?.sourceCardinality).toBe('one');
+
+ expect(relationship?.targetTableId).toBe(playlistTable?.id);
+ expect(relationship?.targetFieldId).toBe(playlistUserIdField?.id);
+ expect(relationship?.targetCardinality).toBe('many');
+ });
+
+ it('should parse foreign key constraints properly - MariaDB', async () => {
+ const sql = `
+CREATE TABLE \`users\` (
+ \`user_id\` INT AUTO_INCREMENT,
+ PRIMARY KEY (\`user_id\`)
+) ENGINE=InnoDB;
+
+CREATE TABLE \`playlists\` (
+ \`playlist_id\` INT AUTO_INCREMENT,
+ \`user_id\` INT NOT NULL,
+ PRIMARY KEY (\`playlist_id\`),
+ CONSTRAINT \`fk_playlists_user_id\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`user_id\`)
+) ENGINE=InnoDB;
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.MARIADB,
+ targetDatabaseType: DatabaseType.MARIADB,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+
+ const playlistTable = diagram.tables?.find(
+ (t) => t.name === 'playlists'
+ );
+ expect(playlistTable).toBeDefined();
+
+ const playlistUserIdField = playlistTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(playlistUserIdField).toBeDefined();
+
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const usersUserIdField = usersTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(usersUserIdField).toBeDefined();
+
+ // verify relationships
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships).toHaveLength(1);
+
+ const relationship = diagram.relationships?.[0];
+ expect(relationship?.sourceTableId).toBe(usersTable?.id);
+ expect(relationship?.sourceFieldId).toBe(usersUserIdField?.id);
+ expect(relationship?.sourceCardinality).toBe('one');
+
+ expect(relationship?.targetTableId).toBe(playlistTable?.id);
+ expect(relationship?.targetFieldId).toBe(playlistUserIdField?.id);
+ expect(relationship?.targetCardinality).toBe('many');
+ });
+
+ it('should parse foreign key constraints properly - SQL Server', async () => {
+ const sql = `
+CREATE TABLE [dbo].[users] (
+ [user_id] INT IDENTITY(1,1) NOT NULL,
+ PRIMARY KEY ([user_id])
+);
+
+CREATE TABLE [dbo].[playlists] (
+ [playlist_id] INT IDENTITY(1,1) NOT NULL,
+ [user_id] INT NOT NULL,
+ PRIMARY KEY ([playlist_id]),
+ CONSTRAINT [fk_playlists_user_id] FOREIGN KEY ([user_id]) REFERENCES [dbo].[users]([user_id])
+);
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.SQL_SERVER,
+ targetDatabaseType: DatabaseType.SQL_SERVER,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+
+ const playlistTable = diagram.tables?.find(
+ (t) => t.name === 'playlists'
+ );
+ expect(playlistTable).toBeDefined();
+
+ const playlistUserIdField = playlistTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(playlistUserIdField).toBeDefined();
+
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const usersUserIdField = usersTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(usersUserIdField).toBeDefined();
+
+ // verify relationships
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships).toHaveLength(1);
+
+ const relationship = diagram.relationships?.[0];
+ expect(relationship?.sourceSchema).toBe('dbo');
+ expect(relationship?.sourceTableId).toBe(usersTable?.id);
+ expect(relationship?.sourceFieldId).toBe(usersUserIdField?.id);
+ expect(relationship?.sourceCardinality).toBe('one');
+
+ expect(relationship?.targetSchema).toBe('dbo');
+ expect(relationship?.targetTableId).toBe(playlistTable?.id);
+ expect(relationship?.targetFieldId).toBe(playlistUserIdField?.id);
+ expect(relationship?.targetCardinality).toBe('many');
+ });
+
+ it('should parse foreign key constraints properly - SQLite', async () => {
+ const sql = `
+CREATE TABLE users (
+ user_id INTEGER PRIMARY KEY AUTOINCREMENT
+);
+
+CREATE TABLE playlists (
+ playlist_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users(user_id)
+);
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.SQLITE,
+ targetDatabaseType: DatabaseType.SQLITE,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+
+ const playlistTable = diagram.tables?.find(
+ (t) => t.name === 'playlists'
+ );
+ expect(playlistTable).toBeDefined();
+
+ const playlistUserIdField = playlistTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(playlistUserIdField).toBeDefined();
+
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const usersUserIdField = usersTable?.fields?.find(
+ (f) => f.name === 'user_id'
+ );
+ expect(usersUserIdField).toBeDefined();
+
+ // verify relationships
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships).toHaveLength(1);
+
+ const relationship = diagram.relationships?.[0];
+ expect(relationship?.sourceTableId).toBe(usersTable?.id);
+ expect(relationship?.sourceFieldId).toBe(usersUserIdField?.id);
+ expect(relationship?.sourceCardinality).toBe('one');
+
+ expect(relationship?.targetTableId).toBe(playlistTable?.id);
+ expect(relationship?.targetFieldId).toBe(playlistUserIdField?.id);
+ expect(relationship?.targetCardinality).toBe('many');
+ });
+
+ it('should parse PostgreSQL table with schema, decimal types, and various column types', async () => {
+ const sql = `
+ CREATE TABLE "inventory"."order_summary" (
+ "order_id" SERIAL PRIMARY KEY,
+ "customer_id" int,
+ "product_id" int,
+ "batch_id" int,
+ "order_date" date,
+ "total_amount" decimal(15,2),
+ "discount_amount" decimal(15,2),
+ "items_count" int,
+ "units_sold" int
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.POSTGRESQL,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+ expect(diagram.databaseType).toBe(DatabaseType.POSTGRESQL);
+
+ // Verify table was parsed
+ expect(diagram.tables).toHaveLength(1);
+ const table = diagram.tables?.[0];
+ expect(table?.name).toBe('order_summary');
+ expect(table?.schema).toBe('inventory');
+
+ // Verify all fields were parsed
+ const fields = table?.fields;
+ expect(fields).toHaveLength(9);
+
+ const fieldNames = fields?.map((f) => f.name);
+ expect(fieldNames).toContain('order_id');
+ expect(fieldNames).toContain('customer_id');
+ expect(fieldNames).toContain('product_id');
+ expect(fieldNames).toContain('batch_id');
+ expect(fieldNames).toContain('order_date');
+ expect(fieldNames).toContain('total_amount');
+ expect(fieldNames).toContain('discount_amount');
+ expect(fieldNames).toContain('items_count');
+ expect(fieldNames).toContain('units_sold');
+
+ // Verify primary key - serial is preserved (not converted to int)
+ const pkField = fields?.find((f) => f.name === 'order_id');
+ expect(pkField?.primaryKey).toBe(true);
+ expect(pkField?.type.name).toBe('serial');
+
+ // Verify decimal fields (decimal is normalized to numeric in PostgreSQL)
+ const totalAmountField = fields?.find((f) => f.name === 'total_amount');
+ expect(totalAmountField?.type.name).toBe('numeric');
+ expect(totalAmountField?.type.id).toBe('numeric');
+ expect(totalAmountField?.precision).toBe(15);
+ expect(totalAmountField?.scale).toBe(2);
+
+ const discountAmountField = fields?.find(
+ (f) => f.name === 'discount_amount'
+ );
+ expect(discountAmountField?.type.name).toBe('numeric');
+ expect(discountAmountField?.type.id).toBe('numeric');
+ expect(discountAmountField?.precision).toBe(15);
+ expect(discountAmountField?.scale).toBe(2);
+
+ // Verify date field
+ const orderDateField = fields?.find((f) => f.name === 'order_date');
+ expect(orderDateField?.type.name).toBe('date');
+
+ // Verify int fields
+ const customerIdField = fields?.find((f) => f.name === 'customer_id');
+ expect(customerIdField?.type.name).toBe('int');
+ });
+
+ it('should parse PostgreSQL table with GENERATED BY DEFAULT AS IDENTITY, decimal precision/scale, and various constraints', async () => {
+ const sql = `
+ CREATE TABLE "accounting"."invoices" (
+ "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ "code" varchar(5) NOT NULL,
+ "number" int NOT NULL,
+ "fk_customer" int NOT NULL,
+ "fk_operation" int NOT NULL,
+ "fk_provider" int NOT NULL,
+ "issue_date" date NOT NULL,
+ "reference" varchar(128),
+ "suggested_amount" decimal(15,2),
+ "gross_amount" decimal(15,2) NOT NULL,
+ "fk_type" int NOT NULL,
+ "tax_rate_1" decimal(13,4),
+ "tax_rate_2" decimal(13,4),
+ "tax_rate_3" decimal(13,4),
+ "net_amount" decimal(15,2),
+ "paid_amount" decimal(15,2),
+ "payment_date" date,
+ "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" timestamp,
+ "fk_created_by" int NOT NULL,
+ "fk_updated_by" int,
+ "fk_status" int NOT NULL DEFAULT 1
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.POSTGRESQL,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify diagram structure
+ expect(diagram).toBeDefined();
+ expect(diagram.databaseType).toBe(DatabaseType.POSTGRESQL);
+
+ // Verify table was parsed
+ expect(diagram.tables).toHaveLength(1);
+ const table = diagram.tables?.[0];
+ expect(table?.name).toBe('invoices');
+ expect(table?.schema).toBe('accounting');
+
+ // Verify all fields were parsed (22 columns)
+ const fields = table?.fields;
+ expect(fields).toHaveLength(22);
+
+ // Verify id - GENERATED BY DEFAULT AS IDENTITY should be:
+ // - type: integer (INT is normalized to integer)
+ // - primaryKey: true
+ // - nullable: false (primary keys are not nullable)
+ // - increment: true (GENERATED BY DEFAULT AS IDENTITY marks it as auto-increment)
+ const pkField = fields?.find((f) => f.name === 'id');
+ expect(pkField).toBeDefined();
+ expect(pkField?.type.name).toBe('int');
+ expect(pkField?.primaryKey).toBe(true);
+ expect(pkField?.nullable).toBe(false);
+ expect(pkField?.increment).toBe(true);
+
+ // Verify number - int NOT NULL (should NOT be auto-increment)
+ const numberField = fields?.find((f) => f.name === 'number');
+ expect(numberField).toBeDefined();
+ expect(numberField?.type.name).toBe('int');
+ expect(numberField?.nullable).toBe(false);
+ expect(numberField?.increment).toBeFalsy();
+
+ // Verify code - varchar(5) NOT NULL
+ const codeField = fields?.find((f) => f.name === 'code');
+ expect(codeField).toBeDefined();
+ expect(codeField?.type.name).toBe('varchar');
+ expect(codeField?.nullable).toBe(false);
+ expect(codeField?.characterMaximumLength).toBe('5');
+
+ // Verify reference - varchar(128) nullable
+ const referenceField = fields?.find((f) => f.name === 'reference');
+ expect(referenceField).toBeDefined();
+ expect(referenceField?.type.name).toBe('varchar');
+ expect(referenceField?.nullable).toBe(true);
+ expect(referenceField?.characterMaximumLength).toBe('128');
+
+ // Verify gross_amount - decimal(15,2) NOT NULL
+ // decimal is normalized to numeric in PostgreSQL
+ const grossAmountField = fields?.find((f) => f.name === 'gross_amount');
+ expect(grossAmountField).toBeDefined();
+ expect(grossAmountField?.type.name).toBe('numeric');
+ expect(grossAmountField?.type.id).toBe('numeric');
+ expect(grossAmountField?.nullable).toBe(false);
+ expect(grossAmountField?.precision).toBe(15);
+ expect(grossAmountField?.scale).toBe(2);
+
+ // Verify tax_rate_1 - decimal(13,4) nullable
+ const taxRate1Field = fields?.find((f) => f.name === 'tax_rate_1');
+ expect(taxRate1Field).toBeDefined();
+ expect(taxRate1Field?.type.name).toBe('numeric');
+ expect(taxRate1Field?.nullable).toBe(true);
+ expect(taxRate1Field?.precision).toBe(13);
+ expect(taxRate1Field?.scale).toBe(4);
+
+ // Verify issue_date - date NOT NULL
+ const issueDateField = fields?.find((f) => f.name === 'issue_date');
+ expect(issueDateField).toBeDefined();
+ expect(issueDateField?.type.name).toBe('date');
+ expect(issueDateField?.nullable).toBe(false);
+
+ // Verify payment_date - date nullable
+ const paymentDateField = fields?.find((f) => f.name === 'payment_date');
+ expect(paymentDateField).toBeDefined();
+ expect(paymentDateField?.type.name).toBe('date');
+ expect(paymentDateField?.nullable).toBe(true);
+
+ // Verify created_at - timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
+ const createdAtField = fields?.find((f) => f.name === 'created_at');
+ expect(createdAtField).toBeDefined();
+ expect(createdAtField?.type.name).toBe('timestamp');
+ expect(createdAtField?.nullable).toBe(false);
+ expect(createdAtField?.default).toBe('CURRENT_TIMESTAMP');
+
+ // Verify updated_at - timestamp nullable (no default)
+ const updatedAtField = fields?.find((f) => f.name === 'updated_at');
+ expect(updatedAtField).toBeDefined();
+ expect(updatedAtField?.type.name).toBe('timestamp');
+ expect(updatedAtField?.nullable).toBe(true);
+
+ // Verify fk_status - int NOT NULL DEFAULT 1
+ const fkStatusField = fields?.find((f) => f.name === 'fk_status');
+ expect(fkStatusField).toBeDefined();
+ expect(fkStatusField?.type.name).toBe('int');
+ expect(fkStatusField?.nullable).toBe(false);
+ expect(fkStatusField?.default).toBe('1');
+
+ // Verify fk_updated_by - int nullable (no NOT NULL constraint)
+ const fkUpdatedByField = fields?.find(
+ (f) => f.name === 'fk_updated_by'
+ );
+ expect(fkUpdatedByField).toBeDefined();
+ expect(fkUpdatedByField?.type.name).toBe('int');
+ expect(fkUpdatedByField?.nullable).toBe(true);
+
+ // Verify fk_created_by - int NOT NULL
+ const fkCreatedByField = fields?.find(
+ (f) => f.name === 'fk_created_by'
+ );
+ expect(fkCreatedByField).toBeDefined();
+ expect(fkCreatedByField?.type.name).toBe('int');
+ expect(fkCreatedByField?.nullable).toBe(false);
+ });
+});
diff --git a/src/lib/data/sql-import/common.ts b/src/lib/data/sql-import/common.ts
index 1b8047083..74653664e 100644
--- a/src/lib/data/sql-import/common.ts
+++ b/src/lib/data/sql-import/common.ts
@@ -4,12 +4,17 @@ import type { DBTable } from '@/lib/domain/db-table';
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
-import type { DataType } from '@/lib/data/data-types/data-types';
+import type { DBCheckConstraint } from '@/lib/domain/db-check-constraint';
+import {
+ getPreferredSynonym,
+ type DataType,
+} from '@/lib/data/data-types/data-types';
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
-import { defaultTableColor } from '@/lib/colors';
+import { defaultTableColor, viewColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
+import { supportsCustomTypes } from '@/lib/domain/database-capabilities';
// Common interfaces for SQL entities
export interface SQLColumn {
@@ -31,20 +36,27 @@ export interface SQLColumn {
increment?: boolean;
}
+export interface SQLCheckConstraint {
+ expression: string;
+}
+
export interface SQLTable {
id: string;
name: string;
schema?: string;
columns: SQLColumn[];
indexes: SQLIndex[];
+ checkConstraints?: SQLCheckConstraint[];
comment?: string;
order: number;
+ isView?: boolean;
}
export interface SQLIndex {
name: string;
columns: string[];
unique: boolean;
+ type?: string; // Index type (btree, hash, gin, gist, etc.)
}
export interface SQLForeignKey {
@@ -419,6 +431,10 @@ export const typeAffinity: Record> = {
int2: 'smallint',
bigint: 'bigint',
int8: 'bigint',
+ // Serial types - map to themselves (they're valid PostgreSQL types)
+ serial: 'serial',
+ smallserial: 'smallserial',
+ bigserial: 'bigserial',
decimal: 'decimal',
numeric: 'numeric',
real: 'real',
@@ -536,13 +552,40 @@ export const typeAffinity: Record> = {
},
[DatabaseType.ORACLE]: {
// Oracle data types (all lowercase for consistency)
+ // Character types
varchar2: 'varchar',
nvarchar2: 'varchar',
+ char: 'char',
+ nchar: 'char',
+ clob: 'text',
+ nclob: 'text',
+ long: 'text',
+ // Numeric types
number: 'numeric',
+ integer: 'integer',
+ int: 'integer',
+ smallint: 'smallint',
+ float: 'float',
+ real: 'real',
+ binary_float: 'float',
+ binary_double: 'double',
+ // Date/Time types
date: 'date',
timestamp: 'timestamp',
- clob: 'text',
+ 'timestamp with time zone': 'timestamp',
+ 'timestamp with local time zone': 'timestamp',
+ interval: 'interval',
+ // Binary types
blob: 'blob',
+ raw: 'blob',
+ 'long raw': 'blob',
+ bfile: 'blob',
+ // Other types
+ rowid: 'varchar',
+ urowid: 'varchar',
+ xmltype: 'text',
+ json: 'json',
+ boolean: 'boolean',
},
[DatabaseType.GENERIC]: {
// Generic fallback types (all lowercase for consistency)
@@ -572,6 +615,9 @@ export const typeAffinity: Record> = {
},
};
+// CockroachDB uses PostgreSQL-compatible types - reference dynamically
+typeAffinity[DatabaseType.COCKROACHDB] = typeAffinity[DatabaseType.POSTGRESQL];
+
// Convert SQLParserResult to ChartDB Diagram structure
export function convertToChartDBDiagram(
parserResult: SQLParserResult,
@@ -594,9 +640,18 @@ export function convertToChartDBDiagram(
// Use special case handling for specific database types to ensure correct mapping
let mappedType: DataType;
+ // Detect and handle array types (e.g., int[], text[], varchar[])
+ const isArrayType = column.type.endsWith('[]');
+ const baseColumnType = isArrayType
+ ? column.type.slice(0, -2)
+ : column.type;
+
+ // Create a modified column object with the base type for mapping
+ const columnForMapping = { ...column, type: baseColumnType };
+
// SQLite-specific handling for numeric types
if (sourceDatabaseType === DatabaseType.SQLITE) {
- const normalizedType = column.type.toLowerCase();
+ const normalizedType = columnForMapping.type.toLowerCase();
if (normalizedType === 'integer' || normalizedType === 'int') {
// Ensure integer types are preserved
@@ -616,7 +671,7 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
- column.type,
+ columnForMapping.type,
sourceDatabaseType
);
}
@@ -626,7 +681,7 @@ export function convertToChartDBDiagram(
sourceDatabaseType === DatabaseType.MYSQL ||
sourceDatabaseType === DatabaseType.MARIADB
) {
- const normalizedType = column.type
+ const normalizedType = columnForMapping.type
.toLowerCase()
.replace(/\(\d+\)/, '')
.trim();
@@ -648,39 +703,70 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
- column.type,
+ columnForMapping.type,
sourceDatabaseType
);
}
}
- // Handle PostgreSQL integer type specifically
+ // Handle PostgreSQL/CockroachDB integer type specifically
else if (
- sourceDatabaseType === DatabaseType.POSTGRESQL &&
- (column.type.toLowerCase() === 'integer' ||
- column.type.toLowerCase() === 'int' ||
- column.type.toLowerCase() === 'int4')
+ (sourceDatabaseType === DatabaseType.POSTGRESQL ||
+ sourceDatabaseType === DatabaseType.COCKROACHDB) &&
+ (columnForMapping.type.toLowerCase() === 'integer' ||
+ columnForMapping.type.toLowerCase() === 'int' ||
+ columnForMapping.type.toLowerCase() === 'int4')
) {
// Ensure integer types are preserved
mappedType = { id: 'integer', name: 'integer' };
} else if (
- sourceDatabaseType === DatabaseType.POSTGRESQL &&
+ supportsCustomTypes(sourceDatabaseType) &&
parserResult.enums &&
parserResult.enums.some(
- (e) => e.name.toLowerCase() === column.type.toLowerCase()
+ (e) =>
+ e.name.toLowerCase() ===
+ columnForMapping.type.toLowerCase()
)
) {
// If the column type matches a custom enum type, preserve it
mappedType = {
- id: column.type.toLowerCase(),
- name: column.type,
+ id: columnForMapping.type.toLowerCase(),
+ name: columnForMapping.type,
};
}
+ // Handle PostgreSQL/CockroachDB-specific types (not in genericDataTypes)
+ else if (
+ (sourceDatabaseType === DatabaseType.POSTGRESQL ||
+ sourceDatabaseType === DatabaseType.COCKROACHDB) &&
+ (targetDatabaseType === DatabaseType.POSTGRESQL ||
+ targetDatabaseType === DatabaseType.COCKROACHDB)
+ ) {
+ const normalizedType = columnForMapping.type.toLowerCase();
+
+ // Preserve PostgreSQL-specific types that don't exist in genericDataTypes
+ // Serial types are PostgreSQL-specific syntax (not true data types)
+ if (
+ normalizedType === 'serial' ||
+ normalizedType === 'smallserial' ||
+ normalizedType === 'bigserial' ||
+ normalizedType === 'jsonb' ||
+ normalizedType === 'timestamptz' ||
+ normalizedType === 'timetz'
+ ) {
+ mappedType = { id: normalizedType, name: normalizedType };
+ } else {
+ // Use the standard mapping for other types
+ mappedType = mapSQLTypeToGenericType(
+ columnForMapping.type,
+ sourceDatabaseType
+ );
+ }
+ }
// Handle SQL Server types specifically
else if (
sourceDatabaseType === DatabaseType.SQL_SERVER &&
targetDatabaseType === DatabaseType.SQL_SERVER
) {
- const normalizedType = column.type.toLowerCase();
+ const normalizedType = columnForMapping.type.toLowerCase();
// Preserve SQL Server specific types when target is also SQL Server
if (
@@ -702,28 +788,40 @@ export function convertToChartDBDiagram(
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
- column.type,
+ columnForMapping.type,
sourceDatabaseType
);
}
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
- column.type,
+ columnForMapping.type,
sourceDatabaseType
);
}
+ // Check if there's a preferred synonym for this type
+ const preferredType = getPreferredSynonym(
+ mappedType.name,
+ targetDatabaseType
+ );
+
+ // Use the preferred synonym if it exists, otherwise use the mapped type
+ const finalType = preferredType
+ ? { id: preferredType.id, name: preferredType.name }
+ : mappedType;
+
const field: DBField = {
id: generateId(),
name: column.name,
- type: mappedType,
+ type: finalType,
nullable: column.nullable,
primaryKey: column.primaryKey,
unique: column.unique,
default: column.default || '',
createdAt: Date.now(),
increment: column.increment,
+ isArray: isArrayType || undefined,
};
// Add type arguments if present
@@ -820,16 +918,33 @@ export function convertToChartDBDiagram(
return null;
}
- return {
+ const index: DBIndex = {
id: generateId(),
name: sqlIndex.name,
fieldIds,
unique: sqlIndex.unique,
createdAt: Date.now(),
};
+
+ // Add type if specified (for GIN, HASH, etc.)
+ if (sqlIndex.type) {
+ index.type = sqlIndex.type as DBIndex['type'];
+ }
+
+ return index;
})
.filter((idx): idx is DBIndex => idx !== null);
+ // Convert check constraints
+ const checkConstraints: DBCheckConstraint[] | undefined =
+ table.checkConstraints && table.checkConstraints.length > 0
+ ? table.checkConstraints.map((c) => ({
+ id: generateId(),
+ expression: c.expression,
+ createdAt: Date.now(),
+ }))
+ : undefined;
+
return {
id: newId,
name: table.name,
@@ -837,10 +952,11 @@ export function convertToChartDBDiagram(
order: index,
fields,
indexes,
+ checkConstraints,
x: col * tableSpacing,
y: row * tableSpacing,
- color: defaultTableColor,
- isView: false,
+ color: table.isView ? viewColor : defaultTableColor,
+ isView: table.isView ?? false,
createdAt: Date.now(),
} satisfies DBTable;
});
@@ -902,22 +1018,25 @@ export function convertToChartDBDiagram(
}
// Use the cardinality from the SQL parser if available, otherwise determine it
+ // Note: In SQLForeignKey, source = table with FK, target = referenced table
+ // In DBRelationship, we want source = referenced table (PK), target = FK table
+ // So we swap them here
const sourceCardinality =
- rel.sourceCardinality ||
- (sourceField.unique || sourceField.primaryKey ? 'one' : 'many');
- const targetCardinality =
rel.targetCardinality ||
(targetField.unique || targetField.primaryKey ? 'one' : 'many');
+ const targetCardinality =
+ rel.sourceCardinality ||
+ (sourceField.unique || sourceField.primaryKey ? 'one' : 'many');
relationships.push({
id: generateId(),
name: rel.name,
- sourceSchema: sourceTable.schema,
- targetSchema: targetTable.schema,
- sourceTableId: sourceTableId,
- targetTableId: targetTableId,
- sourceFieldId: sourceField.id,
- targetFieldId: targetField.id,
+ sourceSchema: targetTable.schema,
+ targetSchema: sourceTable.schema,
+ sourceTableId: targetTableId,
+ targetTableId: sourceTableId,
+ sourceFieldId: targetField.id,
+ targetFieldId: sourceField.id,
sourceCardinality,
targetCardinality,
createdAt: Date.now(),
diff --git a/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-core.test.ts b/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-core.test.ts
new file mode 100644
index 000000000..1b79c63c0
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-core.test.ts
@@ -0,0 +1,54 @@
+import { describe, it, expect } from 'vitest';
+import { fromMySQL } from '../mysql';
+
+describe('MySQL Core Parser Tests', () => {
+ describe('Primary Key Uniqueness', () => {
+ it('should mark single-column primary key field as unique', async () => {
+ const sql = `
+CREATE TABLE \`table_1\` (
+ \`id\` BIGINT NOT NULL,
+ CONSTRAINT \`pk_table_1_id\` PRIMARY KEY (\`id\`)
+) ENGINE=InnoDB;
+ `;
+
+ const result = await fromMySQL(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(true);
+ });
+
+ it('should not mark composite primary key fields as unique individually', async () => {
+ const sql = `
+CREATE TABLE \`table_1\` (
+ \`id\` BIGINT NOT NULL,
+ \`field_2\` BIGINT NOT NULL,
+ CONSTRAINT \`pk_table_1_id\` PRIMARY KEY (\`id\`, \`field_2\`)
+) ENGINE=InnoDB;
+ `;
+
+ const result = await fromMySQL(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(false);
+
+ const field2Column = table.columns.find(
+ (c) => c.name === 'field_2'
+ );
+ expect(field2Column).toBeDefined();
+ expect(field2Column?.primaryKey).toBe(true);
+ expect(field2Column?.unique).toBe(false);
+ });
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-default-values.test.ts b/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-default-values.test.ts
index 0241f8f9a..f6588f9ed 100644
--- a/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-default-values.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-default-values.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import { fromMySQL } from '../mysql';
describe('MySQL Default Value Import', () => {
@@ -173,6 +173,10 @@ describe('MySQL Default Value Import', () => {
describe('Complex Real-World Example', () => {
it('should handle complex table with multiple default types', async () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
const sql = `
CREATE TABLE adventurer_profiles (
adventurer_id BIGINT NOT NULL AUTO_INCREMENT,
@@ -223,6 +227,8 @@ describe('MySQL Default Value Import', () => {
(c) => c.name === 'inventory_data'
);
expect(inventoryColumn?.default).toBe('NULL');
+
+ consoleErrorSpy.mockRestore();
});
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-views.test.ts b/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-views.test.ts
new file mode 100644
index 000000000..39b1f081d
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/mysql/__tests__/mysql-views.test.ts
@@ -0,0 +1,575 @@
+import { describe, it, expect } from 'vitest';
+import { fromMySQL } from '../mysql';
+
+const SQL_WITH_VIEWS = `
+-- MySQL 8+ example schema: 12 tables + 6 views
+-- Domain: small SaaS app (orgs, users, projects, issues, billing, events)
+
+CREATE DATABASE IF NOT EXISTS demo;
+USE demo;
+
+-- ----------------
+-- Tables (12)
+-- ----------------
+
+CREATE TABLE organizations (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ name VARCHAR(200) NOT NULL,
+ slug VARCHAR(80) NOT NULL,
+ plan_tier VARCHAR(30) NOT NULL DEFAULT 'free',
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_organizations_slug (slug)
+) ENGINE=InnoDB;
+
+CREATE TABLE users (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ email VARCHAR(320) NOT NULL,
+ full_name VARCHAR(200) NULL,
+ is_active TINYINT(1) NOT NULL DEFAULT 1,
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_users_email (email)
+) ENGINE=InnoDB;
+
+CREATE TABLE org_memberships (
+ org_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ role VARCHAR(30) NOT NULL DEFAULT 'member',
+ joined_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (org_id, user_id),
+ CONSTRAINT fk_org_memberships_org
+ FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE,
+ CONSTRAINT fk_org_memberships_user
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE projects (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ org_id BIGINT UNSIGNED NOT NULL,
+ name VARCHAR(200) NOT NULL,
+ project_key VARCHAR(20) NOT NULL,
+ is_archived TINYINT(1) NOT NULL DEFAULT 0,
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_projects_org_key (org_id, project_key),
+ KEY ix_projects_org_id (org_id),
+ CONSTRAINT fk_projects_org
+ FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE labels (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ org_id BIGINT UNSIGNED NOT NULL,
+ name VARCHAR(80) NOT NULL,
+ color VARCHAR(20) NOT NULL DEFAULT '#999999',
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_labels_org_name (org_id, name),
+ KEY ix_labels_org_id (org_id),
+ CONSTRAINT fk_labels_org
+ FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE issues (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ project_id BIGINT UNSIGNED NOT NULL,
+ title VARCHAR(300) NOT NULL,
+ description TEXT NULL,
+ status VARCHAR(30) NOT NULL DEFAULT 'open',
+ priority INT NOT NULL DEFAULT 3,
+ created_by BIGINT UNSIGNED NULL,
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ closed_at TIMESTAMP(3) NULL,
+ PRIMARY KEY (id),
+ KEY ix_issues_project_status (project_id, status),
+ KEY ix_issues_created_by (created_by),
+ CONSTRAINT fk_issues_project
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ CONSTRAINT fk_issues_created_by
+ FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
+ CONSTRAINT ck_issues_status CHECK (status IN ('open','in_progress','closed')),
+ CONSTRAINT ck_issues_priority CHECK (priority BETWEEN 1 AND 5)
+) ENGINE=InnoDB;
+
+CREATE TABLE issue_labels (
+ issue_id BIGINT UNSIGNED NOT NULL,
+ label_id BIGINT UNSIGNED NOT NULL,
+ PRIMARY KEY (issue_id, label_id),
+ KEY ix_issue_labels_label (label_id),
+ CONSTRAINT fk_issue_labels_issue
+ FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
+ CONSTRAINT fk_issue_labels_label
+ FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE comments (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ issue_id BIGINT UNSIGNED NOT NULL,
+ author_id BIGINT UNSIGNED NULL,
+ body TEXT NOT NULL,
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ KEY ix_comments_issue_created_at (issue_id, created_at),
+ KEY ix_comments_author_id (author_id),
+ CONSTRAINT fk_comments_issue
+ FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
+ CONSTRAINT fk_comments_author
+ FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
+) ENGINE=InnoDB;
+
+CREATE TABLE api_keys (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ org_id BIGINT UNSIGNED NOT NULL,
+ name VARCHAR(120) NOT NULL,
+ key_hash VARCHAR(200) NOT NULL,
+ last_used_at TIMESTAMP(3) NULL,
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_api_keys_org_name (org_id, name),
+ KEY ix_api_keys_org_id (org_id),
+ CONSTRAINT fk_api_keys_org
+ FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE invoices (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ org_id BIGINT UNSIGNED NOT NULL,
+ period_start DATE NOT NULL,
+ period_end DATE NOT NULL,
+ status VARCHAR(30) NOT NULL DEFAULT 'open',
+ total_cents INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ KEY ix_invoices_org_status (org_id, status),
+ CONSTRAINT fk_invoices_org
+ FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE,
+ CONSTRAINT ck_invoices_period CHECK (period_end >= period_start),
+ CONSTRAINT ck_invoices_status CHECK (status IN ('open','paid','void')),
+ CONSTRAINT ck_invoices_total_cents CHECK (total_cents >= 0)
+) ENGINE=InnoDB;
+
+CREATE TABLE payments (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ invoice_id BIGINT UNSIGNED NOT NULL,
+ provider VARCHAR(30) NOT NULL,
+ amount_cents INT NOT NULL,
+ paid_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ KEY ix_payments_invoice_id (invoice_id),
+ CONSTRAINT fk_payments_invoice
+ FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
+ CONSTRAINT ck_payments_amount CHECK (amount_cents > 0)
+) ENGINE=InnoDB;
+
+CREATE TABLE events (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ org_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NULL,
+ event_type VARCHAR(80) NOT NULL,
+ metadata JSON NOT NULL,
+ occurred_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ PRIMARY KEY (id),
+ KEY ix_events_org_occurred_at (org_id, occurred_at),
+ KEY ix_events_user_id (user_id),
+ CONSTRAINT fk_events_org
+ FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE,
+ CONSTRAINT fk_events_user
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
+) ENGINE=InnoDB;
+
+-- ----------------
+-- Views (6)
+-- ----------------
+
+CREATE OR REPLACE VIEW v_active_org_members AS
+SELECT
+ o.id AS org_id,
+ o.slug AS org_slug,
+ u.id AS user_id,
+ u.email,
+ u.full_name,
+ m.role,
+ m.joined_at
+FROM org_memberships m
+JOIN organizations o ON o.id = m.org_id
+JOIN users u ON u.id = m.user_id
+WHERE u.is_active = 1;
+
+CREATE OR REPLACE VIEW v_project_issue_summary AS
+SELECT
+ p.id AS project_id,
+ p.org_id,
+ p.name AS project_name,
+ p.project_key,
+ SUM(CASE WHEN i.status = 'open' THEN 1 ELSE 0 END) AS open_issues,
+ SUM(CASE WHEN i.status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_issues,
+ SUM(CASE WHEN i.status = 'closed' THEN 1 ELSE 0 END) AS closed_issues,
+ COUNT(i.id) AS total_issues,
+ MAX(i.created_at) AS last_issue_created_at
+FROM projects p
+LEFT JOIN issues i ON i.project_id = p.id
+GROUP BY p.id, p.org_id, p.name, p.project_key;
+
+CREATE OR REPLACE VIEW v_issue_details AS
+SELECT
+ i.id AS issue_id,
+ i.project_id,
+ p.org_id,
+ i.title,
+ i.status,
+ i.priority,
+ i.created_at,
+ i.closed_at,
+ i.created_by,
+ u.email AS created_by_email,
+ (SELECT COUNT(*) FROM comments c WHERE c.issue_id = i.id) AS comment_count
+FROM issues i
+JOIN projects p ON p.id = i.project_id
+LEFT JOIN users u ON u.id = i.created_by;
+
+CREATE OR REPLACE VIEW v_invoice_balances AS
+SELECT
+ inv.id AS invoice_id,
+ inv.org_id,
+ inv.period_start,
+ inv.period_end,
+ inv.status,
+ inv.total_cents,
+ COALESCE(SUM(pay.amount_cents), 0) AS paid_cents,
+ GREATEST(inv.total_cents - COALESCE(SUM(pay.amount_cents), 0), 0) AS due_cents,
+ MAX(pay.paid_at) AS last_payment_at
+FROM invoices inv
+LEFT JOIN payments pay ON pay.invoice_id = inv.id
+GROUP BY inv.id, inv.org_id, inv.period_start, inv.period_end, inv.status, inv.total_cents;
+
+CREATE OR REPLACE VIEW v_recent_events AS
+SELECT
+ e.id,
+ e.org_id,
+ o.slug AS org_slug,
+ e.user_id,
+ u.email AS user_email,
+ e.event_type,
+ e.metadata,
+ e.occurred_at
+FROM events e
+JOIN organizations o ON o.id = e.org_id
+LEFT JOIN users u ON u.id = e.user_id
+WHERE e.occurred_at >= (UTC_TIMESTAMP(3) - INTERVAL 7 DAY);
+
+CREATE OR REPLACE VIEW v_org_activity_daily AS
+SELECT
+ e.org_id,
+ DATE(e.occurred_at) AS day,
+ COUNT(*) AS events_count,
+ SUM(CASE WHEN e.event_type = 'login' THEN 1 ELSE 0 END) AS logins
+FROM events e
+WHERE e.occurred_at >= (UTC_TIMESTAMP(3) - INTERVAL 30 DAY)
+GROUP BY e.org_id, DATE(e.occurred_at);
+`;
+
+describe('MySQL View Import', () => {
+ it('should import 12 tables and 6 views', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ // Count tables and views
+ const tables = result.tables.filter((t) => !t.isView);
+ const views = result.tables.filter((t) => t.isView);
+
+ expect(tables.length).toBe(12);
+ expect(views.length).toBe(6);
+ });
+
+ it('should correctly parse view names', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ const views = result.tables.filter((t) => t.isView);
+ const viewNames = views.map((v) => v.name).sort();
+
+ expect(viewNames).toEqual([
+ 'v_active_org_members',
+ 'v_invoice_balances',
+ 'v_issue_details',
+ 'v_org_activity_daily',
+ 'v_project_issue_summary',
+ 'v_recent_events',
+ ]);
+ });
+
+ it('should correctly parse table names', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ const tables = result.tables.filter((t) => !t.isView);
+ const tableNames = tables.map((t) => t.name).sort();
+
+ expect(tableNames).toEqual([
+ 'api_keys',
+ 'comments',
+ 'events',
+ 'invoices',
+ 'issue_labels',
+ 'issues',
+ 'labels',
+ 'org_memberships',
+ 'organizations',
+ 'payments',
+ 'projects',
+ 'users',
+ ]);
+ });
+
+ it('should parse relationships correctly', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ // Expected foreign key relationships (15 total):
+ // 1. org_memberships.org_id -> organizations.id
+ // 2. org_memberships.user_id -> users.id
+ // 3. projects.org_id -> organizations.id
+ // 4. labels.org_id -> organizations.id
+ // 5. issues.project_id -> projects.id
+ // 6. issues.created_by -> users.id
+ // 7. issue_labels.issue_id -> issues.id
+ // 8. issue_labels.label_id -> labels.id
+ // 9. comments.issue_id -> issues.id
+ // 10. comments.author_id -> users.id
+ // 11. api_keys.org_id -> organizations.id
+ // 12. invoices.org_id -> organizations.id
+ // 13. payments.invoice_id -> invoices.id
+ // 14. events.org_id -> organizations.id
+ // 15. events.user_id -> users.id
+
+ // Should have exactly 15 relationships (no duplicates)
+ expect(result.relationships.length).toBe(15);
+
+ // Create a map for easier lookup
+ const relationshipMap = new Map<
+ string,
+ (typeof result.relationships)[0]
+ >();
+ for (const rel of result.relationships) {
+ const key = `${rel.sourceTable}.${rel.sourceColumn}->${rel.targetTable}.${rel.targetColumn}`;
+ relationshipMap.set(key, rel);
+ }
+
+ // Validate each relationship exists and is correctly defined
+ const expectedRelationships = [
+ {
+ sourceTable: 'org_memberships',
+ sourceColumn: 'org_id',
+ targetTable: 'organizations',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'org_memberships',
+ sourceColumn: 'user_id',
+ targetTable: 'users',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'projects',
+ sourceColumn: 'org_id',
+ targetTable: 'organizations',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'labels',
+ sourceColumn: 'org_id',
+ targetTable: 'organizations',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'issues',
+ sourceColumn: 'project_id',
+ targetTable: 'projects',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'issues',
+ sourceColumn: 'created_by',
+ targetTable: 'users',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'issue_labels',
+ sourceColumn: 'issue_id',
+ targetTable: 'issues',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'issue_labels',
+ sourceColumn: 'label_id',
+ targetTable: 'labels',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'comments',
+ sourceColumn: 'issue_id',
+ targetTable: 'issues',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'comments',
+ sourceColumn: 'author_id',
+ targetTable: 'users',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'api_keys',
+ sourceColumn: 'org_id',
+ targetTable: 'organizations',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'invoices',
+ sourceColumn: 'org_id',
+ targetTable: 'organizations',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'payments',
+ sourceColumn: 'invoice_id',
+ targetTable: 'invoices',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'events',
+ sourceColumn: 'org_id',
+ targetTable: 'organizations',
+ targetColumn: 'id',
+ },
+ {
+ sourceTable: 'events',
+ sourceColumn: 'user_id',
+ targetTable: 'users',
+ targetColumn: 'id',
+ },
+ ];
+
+ for (const expected of expectedRelationships) {
+ const key = `${expected.sourceTable}.${expected.sourceColumn}->${expected.targetTable}.${expected.targetColumn}`;
+ expect(
+ relationshipMap.has(key),
+ `Missing relationship: ${key}`
+ ).toBe(true);
+ }
+
+ // Verify all relationships have valid sourceTableId and targetTableId
+ for (const rel of result.relationships) {
+ expect(
+ rel.sourceTableId,
+ `Relationship ${rel.sourceTable}.${rel.sourceColumn} missing sourceTableId`
+ ).toBeTruthy();
+ expect(
+ rel.targetTableId,
+ `Relationship ${rel.sourceTable}.${rel.sourceColumn} missing targetTableId`
+ ).toBeTruthy();
+ }
+ });
+
+ it('should correctly parse the organizations table', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ const orgsTable = result.tables.find((t) => t.name === 'organizations');
+
+ expect(orgsTable).toBeDefined();
+ expect(orgsTable?.isView).toBeFalsy();
+
+ // Check columns
+ const columnNames = orgsTable?.columns.map((c) => c.name);
+ expect(columnNames).toContain('id');
+ expect(columnNames).toContain('name');
+ expect(columnNames).toContain('slug');
+ expect(columnNames).toContain('plan_tier');
+ expect(columnNames).toContain('created_at');
+
+ // Check id column properties
+ const idColumn = orgsTable?.columns.find((c) => c.name === 'id');
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.increment).toBe(true);
+
+ // Check name column properties
+ const nameColumn = orgsTable?.columns.find((c) => c.name === 'name');
+ expect(nameColumn?.nullable).toBe(false);
+ expect(nameColumn?.type.toLowerCase()).toContain('varchar');
+ });
+
+ it('should correctly parse the issues table', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ const issuesTable = result.tables.find((t) => t.name === 'issues');
+
+ expect(issuesTable).toBeDefined();
+ expect(issuesTable?.isView).toBeFalsy();
+
+ // Check columns
+ const columnNames = issuesTable?.columns.map((c) => c.name);
+ expect(columnNames).toContain('id');
+ expect(columnNames).toContain('project_id');
+ expect(columnNames).toContain('title');
+ expect(columnNames).toContain('description');
+ expect(columnNames).toContain('status');
+ expect(columnNames).toContain('priority');
+ expect(columnNames).toContain('created_by');
+ expect(columnNames).toContain('created_at');
+ expect(columnNames).toContain('closed_at');
+
+ // Check id column properties
+ const idColumn = issuesTable?.columns.find((c) => c.name === 'id');
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.increment).toBe(true);
+
+ // Check nullable columns
+ const descColumn = issuesTable?.columns.find(
+ (c) => c.name === 'description'
+ );
+ expect(descColumn?.nullable).toBe(true);
+
+ const createdByColumn = issuesTable?.columns.find(
+ (c) => c.name === 'created_by'
+ );
+ expect(createdByColumn?.nullable).toBe(true);
+
+ const closedAtColumn = issuesTable?.columns.find(
+ (c) => c.name === 'closed_at'
+ );
+ expect(closedAtColumn?.nullable).toBe(true);
+ });
+
+ it('should extract columns from views', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ const activeOrgMembersView = result.tables.find(
+ (t) => t.name === 'v_active_org_members'
+ );
+
+ expect(activeOrgMembersView).toBeDefined();
+ expect(activeOrgMembersView?.isView).toBe(true);
+
+ // Check that columns were extracted from the SELECT clause
+ const columnNames = activeOrgMembersView?.columns.map((c) => c.name);
+ expect(columnNames).toContain('org_id');
+ expect(columnNames).toContain('org_slug');
+ expect(columnNames).toContain('user_id');
+ expect(columnNames).toContain('email');
+ expect(columnNames).toContain('full_name');
+ expect(columnNames).toContain('role');
+ expect(columnNames).toContain('joined_at');
+ });
+
+ it('should mark views with isView=true and tables with isView falsy', async () => {
+ const result = await fromMySQL(SQL_WITH_VIEWS);
+
+ const tables = result.tables.filter((t) => !t.isView);
+ const views = result.tables.filter((t) => t.isView);
+
+ // All tables should NOT have isView set
+ tables.forEach((table) => {
+ expect(table.isView).toBeFalsy();
+ });
+
+ // All views should have isView=true
+ views.forEach((view) => {
+ expect(view.isView).toBe(true);
+ });
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/mysql/mysql.ts b/src/lib/data/sql-import/dialect-importers/mysql/mysql.ts
index cabd1cae9..5d7c5475b 100644
--- a/src/lib/data/sql-import/dialect-importers/mysql/mysql.ts
+++ b/src/lib/data/sql-import/dialect-importers/mysql/mysql.ts
@@ -5,8 +5,51 @@ import type {
SQLColumn,
SQLIndex,
SQLForeignKey,
+ SQLCheckConstraint,
} from '../../common';
import { buildSQLFromAST } from '../../common';
+
+/**
+ * Extract CHECK constraints from CREATE TABLE statements
+ */
+function extractCheckConstraintsFromCreateTable(
+ sql: string
+): SQLCheckConstraint[] {
+ const constraints: SQLCheckConstraint[] = [];
+
+ // Extract the table body
+ const tableBodyMatch = sql.match(/\(([\s\S]+)\)/);
+ if (!tableBodyMatch) return constraints;
+
+ const tableBody = tableBodyMatch[1];
+
+ // Pattern for CHECK constraints:
+ // CHECK (expression) or CONSTRAINT name CHECK (expression)
+ const checkPattern = /(?:CONSTRAINT\s+(?:`[^`]+`|[^\s]+)\s+)?CHECK\s*\(/gi;
+ let match;
+
+ while ((match = checkPattern.exec(tableBody)) !== null) {
+ const startIdx = match.index + match[0].length;
+ let depth = 1;
+ let endIdx = startIdx;
+
+ // Find the matching closing parenthesis
+ for (let i = startIdx; i < tableBody.length && depth > 0; i++) {
+ if (tableBody[i] === '(') depth++;
+ else if (tableBody[i] === ')') depth--;
+ endIdx = i;
+ }
+
+ if (depth === 0) {
+ const expression = tableBody.substring(startIdx, endIdx).trim();
+ if (expression) {
+ constraints.push({ expression });
+ }
+ }
+ }
+
+ return constraints;
+}
import type {
ColumnDefinition,
ConstraintDefinition,
@@ -59,6 +102,102 @@ function extractStatements(sqlContent: string): string[] {
return statements;
}
+/**
+ * Extract columns from a CREATE VIEW statement
+ * Views can have explicit column names or derive them from the SELECT
+ */
+function extractColumnsFromView(sql: string): SQLColumn[] {
+ const columns: SQLColumn[] = [];
+
+ // First, try to extract explicit column list from CREATE VIEW viewname (col1, col2, ...) AS
+ const explicitColumnsMatch = sql.match(
+ /CREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM\s*=\s*\w+\s+)?(?:DEFINER\s*=\s*[^\s]+\s+)?(?:SQL\s+SECURITY\s+\w+\s+)?VIEW\s+(?:`?[^`\s.]+`?\.)?`?[^`\s.(]+`?\s*\(([^)]+)\)\s*AS/i
+ );
+
+ if (explicitColumnsMatch) {
+ // Parse explicit column list
+ const columnList = explicitColumnsMatch[1];
+ const columnNames = columnList
+ .split(',')
+ .map((col) => col.trim().replace(/^[`'"]+|[`'"]+$/g, ''));
+
+ for (const colName of columnNames) {
+ if (colName) {
+ columns.push({
+ name: colName,
+ type: 'text', // Default type for views since we don't know the actual type
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ });
+ }
+ }
+
+ return columns;
+ }
+
+ // If no explicit columns, try to extract from SELECT clause
+ const selectMatch = sql.match(/\bAS\s+SELECT\s+([\s\S]+?)\s+FROM\s+/i);
+
+ if (selectMatch) {
+ const selectClause = selectMatch[1];
+
+ // Handle SELECT * - we can't determine columns
+ if (selectClause.trim() === '*') {
+ return columns;
+ }
+
+ // Split by comma, but be careful of nested functions/expressions
+ let depth = 0;
+ let currentCol = '';
+ const selectParts: string[] = [];
+
+ for (const char of selectClause) {
+ if (char === '(' || char === '[') depth++;
+ else if (char === ')' || char === ']') depth--;
+ else if (char === ',' && depth === 0) {
+ selectParts.push(currentCol.trim());
+ currentCol = '';
+ continue;
+ }
+ currentCol += char;
+ }
+ if (currentCol.trim()) {
+ selectParts.push(currentCol.trim());
+ }
+
+ for (const part of selectParts) {
+ let columnName = '';
+
+ // Check for alias: ... AS `name` or ... AS name
+ const aliasMatch = part.match(/\s+AS\s+[`'"']?(\w+)[`'"']?\s*$/i);
+ if (aliasMatch) {
+ columnName = aliasMatch[1];
+ } else {
+ // Try to extract the column reference
+ const colRefMatch = part.match(
+ /(?:[\w`"]+\.)?[`'"']?(\w+)[`'"']?\s*$/
+ );
+ if (colRefMatch) {
+ columnName = colRefMatch[1];
+ }
+ }
+
+ if (columnName && columnName !== '*') {
+ columns.push({
+ name: columnName,
+ type: 'text',
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ });
+ }
+ }
+ }
+
+ return columns;
+}
+
// Function to extract columns from a CREATE TABLE statement using regex
function extractColumnsFromCreateTable(statement: string): SQLColumn[] {
const columns: SQLColumn[] = [];
@@ -424,6 +563,9 @@ export async function fromMySQL(sqlContent: string): Promise {
)
);
+ const isSingleColumnPK =
+ pkColumns.length === 1;
+
// Mark columns as PK
for (const colName of pkColumns) {
const col =
@@ -433,8 +575,13 @@ export async function fromMySQL(sqlContent: string): Promise {
colName
);
if (col) {
- col.primaryKey =
- true;
+ col.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (
+ isSingleColumnPK
+ ) {
+ col.unique = true;
+ }
}
}
@@ -703,6 +850,10 @@ export async function fromMySQL(sqlContent: string): Promise {
relationships.push(
fk
);
+ // Track this relationship to avoid duplicates from regex fallback
+ addedRelationships.add(
+ `${fk.sourceTable}.${fk.sourceColumn}-${fk.targetTable}.${fk.targetColumn}`
+ );
}
}
}
@@ -720,6 +871,12 @@ export async function fromMySQL(sqlContent: string): Promise {
}
}
+ // Extract check constraints
+ const checkConstraints =
+ extractCheckConstraintsFromCreateTable(
+ trimmedStmt
+ );
+
// Create and store the table
tables.push({
id: tableId,
@@ -727,6 +884,10 @@ export async function fromMySQL(sqlContent: string): Promise {
schema: database || undefined,
columns,
indexes,
+ checkConstraints:
+ checkConstraints.length > 0
+ ? checkConstraints
+ : undefined,
order: tables.length,
});
}
@@ -749,12 +910,20 @@ export async function fromMySQL(sqlContent: string): Promise {
const extractedColumns =
extractColumnsFromCreateTable(trimmedStmt);
if (extractedColumns.length > 0) {
+ const checkConstraints =
+ extractCheckConstraintsFromCreateTable(
+ trimmedStmt
+ );
tables.push({
id: tableId,
name: tableName,
schema: undefined,
columns: extractedColumns,
indexes: [],
+ checkConstraints:
+ checkConstraints.length > 0
+ ? checkConstraints
+ : undefined,
order: tables.length,
});
}
@@ -763,7 +932,52 @@ export async function fromMySQL(sqlContent: string): Promise {
}
}
- // Second pass: process CREATE INDEX statements
+ // Second pass: process CREATE VIEW statements
+ for (const statement of statements) {
+ const trimmedStmt = statement.trim();
+ const upperStmt = trimmedStmt.toUpperCase();
+
+ if (
+ upperStmt.startsWith('CREATE VIEW') ||
+ upperStmt.startsWith('CREATE OR REPLACE VIEW') ||
+ upperStmt.includes('CREATE VIEW') ||
+ upperStmt.includes('CREATE OR REPLACE VIEW')
+ ) {
+ // Extract view name - handle MySQL syntax with ALGORITHM, DEFINER, etc.
+ const viewMatch = trimmedStmt.match(
+ /CREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM\s*=\s*\w+\s+)?(?:DEFINER\s*=\s*[^\s]+\s+)?(?:SQL\s+SECURITY\s+\w+\s+)?VIEW\s+(?:`?([^`\s.]+)`?\.)?`?([^`\s.(]+)`?/i
+ );
+
+ if (viewMatch) {
+ const database = viewMatch[1] || '';
+ const viewName = viewMatch[2].replace(/`/g, '');
+
+ if (viewName) {
+ const viewId = generateId();
+ tableMap[viewName] = viewId;
+ if (database) {
+ tableMap[`${database}.${viewName}`] = viewId;
+ }
+
+ // Extract columns from the view definition
+ const columns = extractColumnsFromView(trimmedStmt);
+
+ // Create view object (as a table with isView: true)
+ tables.push({
+ id: viewId,
+ name: viewName,
+ schema: database || undefined,
+ columns,
+ indexes: [], // Views don't have indexes
+ order: tables.length,
+ isView: true,
+ });
+ }
+ }
+ }
+ }
+
+ // Third pass: process CREATE INDEX statements
for (const statement of statements) {
const trimmedStmt = statement.trim();
if (
@@ -774,7 +988,7 @@ export async function fromMySQL(sqlContent: string): Promise {
}
}
- // Third pass: process ALTER TABLE statements for foreign keys
+ // Fourth pass: process ALTER TABLE statements for foreign keys
for (const statement of statements) {
const trimmedStmt = statement.trim();
if (
@@ -884,6 +1098,10 @@ export async function fromMySQL(sqlContent: string): Promise {
};
relationships.push(fk);
+ // Track this relationship to avoid duplicates from regex fallback
+ addedRelationships.add(
+ `${fk.sourceTable}.${fk.sourceColumn}-${fk.targetTable}.${fk.targetColumn}`
+ );
}
} catch (fkError) {
console.error(
@@ -938,6 +1156,10 @@ export async function fromMySQL(sqlContent: string): Promise {
};
relationships.push(fk);
+ // Track this relationship to avoid duplicates from regex fallback
+ addedRelationships.add(
+ `${fk.sourceTable}.${fk.sourceColumn}-${fk.targetTable}.${fk.targetColumn}`
+ );
}
}
}
@@ -1101,12 +1323,14 @@ function findForeignKeysUsingRegex(
continue;
}
- // Get table IDs
- const sourceTableKey = `${sourceSchema}.${sourceTable}`;
- const targetTableKey = `${sourceSchema}.${targetTable}`;
-
- const sourceTableId = tableMap[sourceTableKey];
- const targetTableId = tableMap[targetTableKey];
+ // Get table IDs - try multiple key formats
+ // Tables might be stored as just "tableName" or "schema.tableName"
+ const sourceTableId =
+ tableMap[sourceTable] ||
+ tableMap[`${sourceSchema}.${sourceTable}`];
+ const targetTableId =
+ tableMap[targetTable] ||
+ tableMap[`${sourceSchema}.${targetTable}`];
// Skip if either table ID is missing
if (!sourceTableId || !targetTableId) {
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-core.test.ts b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-core.test.ts
new file mode 100644
index 000000000..f07d431d1
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-core.test.ts
@@ -0,0 +1,355 @@
+import { describe, it, expect } from 'vitest';
+import { fromOracle } from '../oracle';
+
+describe('Oracle Core Parser Tests', () => {
+ it('should parse basic tables', async () => {
+ const sql = `
+ CREATE TABLE employees (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(255) NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ expect(result.tables[0].name).toBe('employees');
+ expect(result.tables[0].columns).toHaveLength(2);
+ });
+
+ it('should parse tables with schemas', async () => {
+ const sql = `
+ CREATE TABLE hr.departments (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ location VARCHAR2(200)
+ );
+
+ CREATE TABLE sales.orders (
+ id NUMBER(10) PRIMARY KEY,
+ order_date DATE NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(2);
+ expect(
+ result.tables.find((t) => t.name === 'departments')
+ ).toBeDefined();
+ expect(
+ result.tables.find((t) => t.name === 'departments')?.schema
+ ).toBe('hr');
+ expect(result.tables.find((t) => t.name === 'orders')?.schema).toBe(
+ 'sales'
+ );
+ });
+
+ it('should parse foreign key relationships', async () => {
+ const sql = `
+ CREATE TABLE departments (id NUMBER(10) PRIMARY KEY);
+ CREATE TABLE employees (
+ id NUMBER(10) PRIMARY KEY,
+ department_id NUMBER(10) REFERENCES departments(id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(2);
+ expect(result.relationships).toHaveLength(1);
+ expect(result.relationships[0].sourceTable).toBe('employees');
+ expect(result.relationships[0].targetTable).toBe('departments');
+ expect(result.relationships[0].sourceColumn).toBe('department_id');
+ expect(result.relationships[0].targetColumn).toBe('id');
+ });
+
+ it('should parse foreign keys with schema references', async () => {
+ const sql = `
+ CREATE TABLE hr.departments (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL
+ );
+
+ CREATE TABLE hr.employees (
+ id NUMBER(10) PRIMARY KEY,
+ department_id NUMBER(10) NOT NULL,
+ name VARCHAR2(100) NOT NULL,
+ CONSTRAINT FK_emp_dept FOREIGN KEY (department_id) REFERENCES hr.departments(id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(2);
+ expect(result.relationships).toHaveLength(1);
+ expect(result.relationships[0].sourceTable).toBe('employees');
+ expect(result.relationships[0].targetTable).toBe('departments');
+ expect(result.relationships[0].sourceSchema).toBe('hr');
+ expect(result.relationships[0].targetSchema).toBe('hr');
+ });
+
+ it('should handle Oracle-specific data types correctly', async () => {
+ const sql = `
+ CREATE TABLE data_types_test (
+ id NUMBER(10) NOT NULL,
+ name VARCHAR2(255) NOT NULL,
+ description CLOB,
+ amount NUMBER(18, 2) NOT NULL,
+ is_active NUMBER(1) NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ birth_date DATE,
+ photo BLOB,
+ small_number INTEGER,
+ precise_value BINARY_DOUBLE,
+ xml_data XMLTYPE,
+ PRIMARY KEY (id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'id')?.type).toBe('number');
+ expect(columns.find((c) => c.name === 'name')?.type).toBe('varchar2');
+ expect(columns.find((c) => c.name === 'description')?.type).toBe(
+ 'clob'
+ );
+ expect(columns.find((c) => c.name === 'amount')?.type).toBe('number');
+ expect(columns.find((c) => c.name === 'is_active')?.type).toBe(
+ 'number'
+ );
+ expect(columns.find((c) => c.name === 'created_at')?.type).toBe(
+ 'timestamp'
+ );
+ expect(columns.find((c) => c.name === 'birth_date')?.type).toBe('date');
+ expect(columns.find((c) => c.name === 'photo')?.type).toBe('blob');
+ expect(columns.find((c) => c.name === 'small_number')?.type).toBe(
+ 'integer'
+ );
+ expect(columns.find((c) => c.name === 'precise_value')?.type).toBe(
+ 'binary_double'
+ );
+ expect(columns.find((c) => c.name === 'xml_data')?.type).toBe(
+ 'xmltype'
+ );
+ });
+
+ it('should handle GENERATED AS IDENTITY columns', async () => {
+ const sql = `
+ CREATE TABLE products (
+ id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ price NUMBER(10, 2) NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const idColumn = result.tables[0].columns.find((c) => c.name === 'id');
+ expect(idColumn?.increment).toBe(true);
+ });
+
+ it('should parse composite primary keys', async () => {
+ const sql = `
+ CREATE TABLE order_items (
+ order_id NUMBER(10) NOT NULL,
+ product_id NUMBER(10) NOT NULL,
+ quantity NUMBER(10) NOT NULL,
+ CONSTRAINT pk_order_items PRIMARY KEY (order_id, product_id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.columns.filter((c) => c.primaryKey)).toHaveLength(2);
+ expect(
+ table.columns.find((c) => c.name === 'order_id')?.primaryKey
+ ).toBe(true);
+ expect(
+ table.columns.find((c) => c.name === 'product_id')?.primaryKey
+ ).toBe(true);
+ });
+
+ it('should handle unique constraints', async () => {
+ const sql = `
+ CREATE TABLE users (
+ id NUMBER(10) NOT NULL PRIMARY KEY,
+ email VARCHAR2(255) NOT NULL,
+ username VARCHAR2(50) NOT NULL,
+ CONSTRAINT uq_users_email UNIQUE (email)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ expect(result.tables[0].indexes).toHaveLength(1);
+ expect(result.tables[0].indexes[0].name).toBe('uq_users_email');
+ expect(result.tables[0].indexes[0].unique).toBe(true);
+ expect(result.tables[0].indexes[0].columns).toContain('email');
+ });
+
+ it('should handle default values', async () => {
+ const sql = `
+ CREATE TABLE audit_log (
+ id NUMBER(10) NOT NULL,
+ action VARCHAR2(100) NOT NULL,
+ is_active NUMBER(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
+ updated_at DATE DEFAULT SYSDATE,
+ status VARCHAR2(20) DEFAULT 'pending',
+ PRIMARY KEY (id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'is_active')?.default).toBe('1');
+ expect(
+ columns.find((c) => c.name === 'created_at')?.default
+ ).toBeDefined();
+ expect(
+ columns.find((c) => c.name === 'updated_at')?.default
+ ).toBeDefined();
+ expect(columns.find((c) => c.name === 'status')?.default).toBe(
+ "'pending'"
+ );
+ });
+
+ it('should parse indexes created separately', async () => {
+ const sql = `
+ CREATE TABLE customers (
+ id NUMBER(10) NOT NULL PRIMARY KEY,
+ first_name VARCHAR2(100) NOT NULL,
+ last_name VARCHAR2(100) NOT NULL,
+ email VARCHAR2(255) NOT NULL
+ );
+
+ CREATE INDEX idx_customers_name ON customers (last_name, first_name);
+ CREATE UNIQUE INDEX idx_customers_email ON customers (email);
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ expect(result.tables[0].indexes).toHaveLength(2);
+
+ const nameIndex = result.tables[0].indexes.find(
+ (i) => i.name === 'idx_customers_name'
+ );
+ expect(nameIndex?.unique).toBe(false);
+ expect(nameIndex?.columns).toContain('last_name');
+ expect(nameIndex?.columns).toContain('first_name');
+
+ const emailIndex = result.tables[0].indexes.find(
+ (i) => i.name === 'idx_customers_email'
+ );
+ expect(emailIndex?.unique).toBe(true);
+ expect(emailIndex?.columns).toContain('email');
+ });
+
+ it('should handle nullable and not null columns correctly', async () => {
+ const sql = `
+ CREATE TABLE test_nullability (
+ id NUMBER(10) NOT NULL PRIMARY KEY,
+ required_field VARCHAR2(100) NOT NULL,
+ optional_field VARCHAR2(100),
+ another_optional CLOB
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'id')?.nullable).toBe(false);
+ expect(columns.find((c) => c.name === 'required_field')?.nullable).toBe(
+ false
+ );
+ expect(columns.find((c) => c.name === 'optional_field')?.nullable).toBe(
+ true
+ );
+ expect(
+ columns.find((c) => c.name === 'another_optional')?.nullable
+ ).toBe(true);
+ });
+
+ it('should handle quoted identifiers', async () => {
+ const sql = `
+ CREATE TABLE "Order" (
+ "Id" NUMBER(10) NOT NULL PRIMARY KEY,
+ "Order Date" DATE NOT NULL,
+ "Customer ID" NUMBER(10) NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ expect(result.tables[0].name).toBe('Order');
+ expect(result.tables[0].columns).toHaveLength(3);
+ expect(
+ result.tables[0].columns.find((c) => c.name === 'Id')
+ ).toBeDefined();
+ });
+
+ describe('Primary Key Uniqueness', () => {
+ it('should mark single-column primary key field as unique', async () => {
+ const sql = `
+CREATE TABLE table_1 (
+ id NUMBER(19) NOT NULL,
+ CONSTRAINT pk_table_1_id PRIMARY KEY (id)
+);
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(true);
+ });
+
+ it('should not mark composite primary key fields as unique individually', async () => {
+ const sql = `
+CREATE TABLE table_1 (
+ id NUMBER(19) NOT NULL,
+ field_2 NUMBER(19) NOT NULL,
+ CONSTRAINT pk_table_1_id PRIMARY KEY (id, field_2)
+);
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(false);
+
+ const field2Column = table.columns.find(
+ (c) => c.name === 'field_2'
+ );
+ expect(field2Column).toBeDefined();
+ expect(field2Column?.primaryKey).toBe(true);
+ expect(field2Column?.unique).toBe(false);
+ });
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-data-types.test.ts b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-data-types.test.ts
new file mode 100644
index 000000000..05e741f94
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-data-types.test.ts
@@ -0,0 +1,227 @@
+import { describe, it, expect } from 'vitest';
+import { fromOracle } from '../oracle';
+
+describe('Oracle Data Types Tests', () => {
+ it('should parse character data types', async () => {
+ const sql = `
+ CREATE TABLE char_types (
+ id NUMBER(10) PRIMARY KEY,
+ var_char VARCHAR2(100),
+ n_var_char NVARCHAR2(100),
+ fixed_char CHAR(10),
+ n_fixed_char NCHAR(10),
+ large_text CLOB,
+ n_large_text NCLOB,
+ legacy_text LONG
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'var_char')?.type).toBe(
+ 'varchar2'
+ );
+ expect(columns.find((c) => c.name === 'n_var_char')?.type).toBe(
+ 'nvarchar2'
+ );
+ expect(columns.find((c) => c.name === 'fixed_char')?.type).toBe('char');
+ expect(columns.find((c) => c.name === 'n_fixed_char')?.type).toBe(
+ 'nchar'
+ );
+ expect(columns.find((c) => c.name === 'large_text')?.type).toBe('clob');
+ expect(columns.find((c) => c.name === 'n_large_text')?.type).toBe(
+ 'nclob'
+ );
+ expect(columns.find((c) => c.name === 'legacy_text')?.type).toBe(
+ 'long'
+ );
+ });
+
+ it('should parse numeric data types', async () => {
+ const sql = `
+ CREATE TABLE numeric_types (
+ id NUMBER(10) PRIMARY KEY,
+ plain_number NUMBER,
+ precise_number NUMBER(18, 2),
+ int_val INTEGER,
+ small_int SMALLINT,
+ float_val FLOAT,
+ real_val REAL,
+ bin_float BINARY_FLOAT,
+ bin_double BINARY_DOUBLE
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'plain_number')?.type).toBe(
+ 'number'
+ );
+ expect(columns.find((c) => c.name === 'precise_number')?.type).toBe(
+ 'number'
+ );
+ expect(columns.find((c) => c.name === 'int_val')?.type).toBe('integer');
+ expect(columns.find((c) => c.name === 'small_int')?.type).toBe(
+ 'smallint'
+ );
+ expect(columns.find((c) => c.name === 'float_val')?.type).toBe('float');
+ expect(columns.find((c) => c.name === 'real_val')?.type).toBe('real');
+ expect(columns.find((c) => c.name === 'bin_float')?.type).toBe(
+ 'binary_float'
+ );
+ expect(columns.find((c) => c.name === 'bin_double')?.type).toBe(
+ 'binary_double'
+ );
+ });
+
+ it('should parse date and time data types', async () => {
+ const sql = `
+ CREATE TABLE datetime_types (
+ id NUMBER(10) PRIMARY KEY,
+ simple_date DATE,
+ time_stamp TIMESTAMP,
+ time_stamp_tz TIMESTAMP,
+ created_at TIMESTAMP NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'simple_date')?.type).toBe(
+ 'date'
+ );
+ expect(columns.find((c) => c.name === 'time_stamp')?.type).toBe(
+ 'timestamp'
+ );
+ expect(columns.find((c) => c.name === 'created_at')?.type).toBe(
+ 'timestamp'
+ );
+ });
+
+ it('should parse binary data types', async () => {
+ const sql = `
+ CREATE TABLE binary_types (
+ id NUMBER(10) PRIMARY KEY,
+ binary_data BLOB,
+ raw_data RAW(100),
+ file_ref BFILE
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'binary_data')?.type).toBe(
+ 'blob'
+ );
+ expect(columns.find((c) => c.name === 'raw_data')?.type).toBe('raw');
+ expect(columns.find((c) => c.name === 'file_ref')?.type).toBe('bfile');
+ });
+
+ it('should parse special Oracle data types', async () => {
+ const sql = `
+ CREATE TABLE special_types (
+ id NUMBER(10) PRIMARY KEY,
+ row_identifier ROWID,
+ xml_content XMLTYPE,
+ json_content JSON
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ expect(columns.find((c) => c.name === 'row_identifier')?.type).toBe(
+ 'rowid'
+ );
+ expect(columns.find((c) => c.name === 'xml_content')?.type).toBe(
+ 'xmltype'
+ );
+ expect(columns.find((c) => c.name === 'json_content')?.type).toBe(
+ 'json'
+ );
+ });
+
+ it('should preserve type arguments for NUMBER types', async () => {
+ const sql = `
+ CREATE TABLE number_precision (
+ id NUMBER(10) PRIMARY KEY,
+ amount NUMBER(18, 2) NOT NULL,
+ percentage NUMBER(5, 4) NOT NULL,
+ quantity NUMBER(10) NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ const amountCol = columns.find((c) => c.name === 'amount');
+ expect(amountCol?.typeArgs).toBeDefined();
+ if (
+ typeof amountCol?.typeArgs === 'object' &&
+ !Array.isArray(amountCol.typeArgs)
+ ) {
+ expect(amountCol.typeArgs.precision).toBe(18);
+ expect(amountCol.typeArgs.scale).toBe(2);
+ }
+
+ const percentCol = columns.find((c) => c.name === 'percentage');
+ expect(percentCol?.typeArgs).toBeDefined();
+ if (
+ typeof percentCol?.typeArgs === 'object' &&
+ !Array.isArray(percentCol.typeArgs)
+ ) {
+ expect(percentCol.typeArgs.precision).toBe(5);
+ expect(percentCol.typeArgs.scale).toBe(4);
+ }
+ });
+
+ it('should preserve type arguments for VARCHAR2 types', async () => {
+ const sql = `
+ CREATE TABLE varchar_lengths (
+ id NUMBER(10) PRIMARY KEY,
+ short_text VARCHAR2(50) NOT NULL,
+ medium_text VARCHAR2(255) NOT NULL,
+ long_text VARCHAR2(4000)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const columns = result.tables[0].columns;
+
+ const shortCol = columns.find((c) => c.name === 'short_text');
+ expect(shortCol?.typeArgs).toBeDefined();
+ if (
+ typeof shortCol?.typeArgs === 'object' &&
+ !Array.isArray(shortCol.typeArgs)
+ ) {
+ expect(shortCol.typeArgs.length).toBe(50);
+ }
+
+ const longCol = columns.find((c) => c.name === 'long_text');
+ expect(longCol?.typeArgs).toBeDefined();
+ if (
+ typeof longCol?.typeArgs === 'object' &&
+ !Array.isArray(longCol.typeArgs)
+ ) {
+ expect(longCol.typeArgs.length).toBe(4000);
+ }
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-examples.test.ts b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-examples.test.ts
new file mode 100644
index 000000000..08590e9fd
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-examples.test.ts
@@ -0,0 +1,324 @@
+import { describe, it, expect } from 'vitest';
+import { fromOracle } from '../oracle';
+
+describe('Oracle Real-World Examples Tests', () => {
+ it('should parse a typical HR schema', async () => {
+ const sql = `
+ -- Regions table
+ CREATE TABLE hr.regions (
+ region_id NUMBER(10) PRIMARY KEY,
+ region_name VARCHAR2(25)
+ );
+
+ -- Countries table
+ CREATE TABLE hr.countries (
+ country_id CHAR(2) PRIMARY KEY,
+ country_name VARCHAR2(40),
+ region_id NUMBER(10),
+ CONSTRAINT fk_country_region FOREIGN KEY (region_id) REFERENCES hr.regions(region_id)
+ );
+
+ -- Locations table
+ CREATE TABLE hr.locations (
+ location_id NUMBER(10) PRIMARY KEY,
+ street_address VARCHAR2(40),
+ postal_code VARCHAR2(12),
+ city VARCHAR2(30) NOT NULL,
+ state_province VARCHAR2(25),
+ country_id CHAR(2),
+ CONSTRAINT fk_loc_country FOREIGN KEY (country_id) REFERENCES hr.countries(country_id)
+ );
+
+ -- Departments table
+ CREATE TABLE hr.departments (
+ department_id NUMBER(10) PRIMARY KEY,
+ department_name VARCHAR2(30) NOT NULL,
+ manager_id NUMBER(10),
+ location_id NUMBER(10),
+ CONSTRAINT fk_dept_loc FOREIGN KEY (location_id) REFERENCES hr.locations(location_id)
+ );
+
+ -- Jobs table
+ CREATE TABLE hr.jobs (
+ job_id VARCHAR2(10) PRIMARY KEY,
+ job_title VARCHAR2(35) NOT NULL,
+ min_salary NUMBER(10),
+ max_salary NUMBER(10)
+ );
+
+ -- Employees table
+ CREATE TABLE hr.employees (
+ employee_id NUMBER(10) PRIMARY KEY,
+ first_name VARCHAR2(20),
+ last_name VARCHAR2(25) NOT NULL,
+ email VARCHAR2(25) NOT NULL,
+ phone_number VARCHAR2(20),
+ hire_date DATE NOT NULL,
+ job_id VARCHAR2(10) NOT NULL,
+ salary NUMBER(10, 2),
+ commission_pct NUMBER(2, 2),
+ manager_id NUMBER(10),
+ department_id NUMBER(10),
+ CONSTRAINT fk_emp_job FOREIGN KEY (job_id) REFERENCES hr.jobs(job_id),
+ CONSTRAINT fk_emp_dept FOREIGN KEY (department_id) REFERENCES hr.departments(department_id),
+ CONSTRAINT fk_emp_mgr FOREIGN KEY (manager_id) REFERENCES hr.employees(employee_id)
+ );
+
+ -- Job history table
+ CREATE TABLE hr.job_history (
+ employee_id NUMBER(10) NOT NULL,
+ start_date DATE NOT NULL,
+ end_date DATE NOT NULL,
+ job_id VARCHAR2(10) NOT NULL,
+ department_id NUMBER(10),
+ CONSTRAINT pk_job_history PRIMARY KEY (employee_id, start_date),
+ CONSTRAINT fk_jh_emp FOREIGN KEY (employee_id) REFERENCES hr.employees(employee_id),
+ CONSTRAINT fk_jh_job FOREIGN KEY (job_id) REFERENCES hr.jobs(job_id),
+ CONSTRAINT fk_jh_dept FOREIGN KEY (department_id) REFERENCES hr.departments(department_id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(7);
+ expect(result.relationships.length).toBeGreaterThanOrEqual(8);
+
+ // Verify all tables exist
+ expect(result.tables.find((t) => t.name === 'regions')).toBeDefined();
+ expect(result.tables.find((t) => t.name === 'countries')).toBeDefined();
+ expect(result.tables.find((t) => t.name === 'locations')).toBeDefined();
+ expect(
+ result.tables.find((t) => t.name === 'departments')
+ ).toBeDefined();
+ expect(result.tables.find((t) => t.name === 'jobs')).toBeDefined();
+ expect(result.tables.find((t) => t.name === 'employees')).toBeDefined();
+ expect(
+ result.tables.find((t) => t.name === 'job_history')
+ ).toBeDefined();
+
+ // Verify schema is set
+ result.tables.forEach((t) => {
+ expect(t.schema).toBe('hr');
+ });
+
+ // Verify self-referencing relationship exists
+ const selfRefRel = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'employees' && r.targetTable === 'employees'
+ );
+ expect(selfRefRel).toBeDefined();
+ });
+
+ it('should parse an e-commerce schema', async () => {
+ const sql = `
+ -- Customers
+ CREATE TABLE ecom.customers (
+ customer_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ email VARCHAR2(255) NOT NULL UNIQUE,
+ password_hash VARCHAR2(255) NOT NULL,
+ first_name VARCHAR2(100) NOT NULL,
+ last_name VARCHAR2(100) NOT NULL,
+ phone VARCHAR2(20),
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
+ updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
+ );
+
+ -- Addresses
+ CREATE TABLE ecom.addresses (
+ address_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ customer_id NUMBER(10) NOT NULL,
+ address_type VARCHAR2(20) NOT NULL,
+ street_address VARCHAR2(255) NOT NULL,
+ city VARCHAR2(100) NOT NULL,
+ state VARCHAR2(100),
+ postal_code VARCHAR2(20),
+ country VARCHAR2(100) NOT NULL,
+ is_default NUMBER(1) DEFAULT 0,
+ CONSTRAINT fk_addr_customer FOREIGN KEY (customer_id) REFERENCES ecom.customers(customer_id)
+ );
+
+ -- Categories with self-reference for hierarchy
+ CREATE TABLE ecom.categories (
+ category_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ parent_category_id NUMBER(10),
+ name VARCHAR2(100) NOT NULL,
+ description CLOB,
+ image_url VARCHAR2(500),
+ is_active NUMBER(1) DEFAULT 1,
+ CONSTRAINT fk_cat_parent FOREIGN KEY (parent_category_id) REFERENCES ecom.categories(category_id)
+ );
+
+ -- Products
+ CREATE TABLE ecom.products (
+ product_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ category_id NUMBER(10) NOT NULL,
+ sku VARCHAR2(50) NOT NULL UNIQUE,
+ name VARCHAR2(255) NOT NULL,
+ description CLOB,
+ price NUMBER(10, 2) NOT NULL,
+ cost_price NUMBER(10, 2),
+ stock_quantity NUMBER(10) DEFAULT 0,
+ weight NUMBER(10, 3),
+ is_active NUMBER(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
+ CONSTRAINT fk_prod_category FOREIGN KEY (category_id) REFERENCES ecom.categories(category_id)
+ );
+
+ -- Orders
+ CREATE TABLE ecom.orders (
+ order_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ customer_id NUMBER(10) NOT NULL,
+ shipping_address_id NUMBER(10) NOT NULL,
+ billing_address_id NUMBER(10) NOT NULL,
+ order_status VARCHAR2(20) DEFAULT 'pending',
+ order_date TIMESTAMP DEFAULT SYSTIMESTAMP,
+ shipped_date TIMESTAMP,
+ total_amount NUMBER(12, 2) NOT NULL,
+ notes CLOB,
+ CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES ecom.customers(customer_id),
+ CONSTRAINT fk_order_ship_addr FOREIGN KEY (shipping_address_id) REFERENCES ecom.addresses(address_id),
+ CONSTRAINT fk_order_bill_addr FOREIGN KEY (billing_address_id) REFERENCES ecom.addresses(address_id)
+ );
+
+ -- Order items
+ CREATE TABLE ecom.order_items (
+ order_item_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ order_id NUMBER(10) NOT NULL,
+ product_id NUMBER(10) NOT NULL,
+ quantity NUMBER(10) NOT NULL,
+ unit_price NUMBER(10, 2) NOT NULL,
+ discount_amount NUMBER(10, 2) DEFAULT 0,
+ CONSTRAINT fk_oi_order FOREIGN KEY (order_id) REFERENCES ecom.orders(order_id),
+ CONSTRAINT fk_oi_product FOREIGN KEY (product_id) REFERENCES ecom.products(product_id)
+ );
+
+ -- Reviews
+ CREATE TABLE ecom.reviews (
+ review_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ product_id NUMBER(10) NOT NULL,
+ customer_id NUMBER(10) NOT NULL,
+ rating NUMBER(1) NOT NULL,
+ title VARCHAR2(200),
+ content CLOB,
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
+ is_verified NUMBER(1) DEFAULT 0,
+ CONSTRAINT fk_review_product FOREIGN KEY (product_id) REFERENCES ecom.products(product_id),
+ CONSTRAINT fk_review_customer FOREIGN KEY (customer_id) REFERENCES ecom.customers(customer_id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(7);
+ expect(result.relationships.length).toBeGreaterThanOrEqual(9);
+
+ // Verify customers table structure
+ const customersTable = result.tables.find(
+ (t) => t.name === 'customers'
+ );
+ expect(customersTable).toBeDefined();
+ expect(customersTable!.columns.length).toBe(8);
+
+ // Verify identity columns
+ const customerIdCol = customersTable!.columns.find(
+ (c) => c.name === 'customer_id'
+ );
+ expect(customerIdCol?.increment).toBe(true);
+
+ // Verify category self-reference
+ const catSelfRef = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'categories' && r.targetTable === 'categories'
+ );
+ expect(catSelfRef).toBeDefined();
+
+ // Verify order has multiple address references
+ const orderAddressRels = result.relationships.filter(
+ (r) => r.sourceTable === 'orders' && r.targetTable === 'addresses'
+ );
+ expect(orderAddressRels.length).toBe(2);
+ });
+
+ it('should parse a financial services schema with complex relationships', async () => {
+ const sql = `
+ -- Account types
+ CREATE TABLE fin.account_types (
+ type_id NUMBER(10) PRIMARY KEY,
+ type_name VARCHAR2(50) NOT NULL,
+ description VARCHAR2(500),
+ min_balance NUMBER(12, 2) DEFAULT 0,
+ interest_rate NUMBER(5, 4) DEFAULT 0
+ );
+
+ -- Accounts
+ CREATE TABLE fin.accounts (
+ account_id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ account_number VARCHAR2(20) NOT NULL UNIQUE,
+ account_type_id NUMBER(10) NOT NULL,
+ customer_id NUMBER(10) NOT NULL,
+ balance NUMBER(15, 2) DEFAULT 0,
+ status VARCHAR2(20) DEFAULT 'active',
+ opened_date DATE DEFAULT SYSDATE,
+ closed_date DATE,
+ CONSTRAINT fk_acc_type FOREIGN KEY (account_type_id) REFERENCES fin.account_types(type_id)
+ );
+
+ -- Transactions
+ CREATE TABLE fin.transactions (
+ transaction_id NUMBER(15) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ from_account_id NUMBER(10),
+ to_account_id NUMBER(10),
+ transaction_type VARCHAR2(20) NOT NULL,
+ amount NUMBER(15, 2) NOT NULL,
+ currency CHAR(3) DEFAULT 'USD',
+ transaction_date TIMESTAMP DEFAULT SYSTIMESTAMP,
+ description VARCHAR2(500),
+ reference_number VARCHAR2(50),
+ status VARCHAR2(20) DEFAULT 'completed',
+ CONSTRAINT fk_trans_from FOREIGN KEY (from_account_id) REFERENCES fin.accounts(account_id),
+ CONSTRAINT fk_trans_to FOREIGN KEY (to_account_id) REFERENCES fin.accounts(account_id)
+ );
+
+ -- Audit log
+ CREATE TABLE fin.audit_log (
+ log_id NUMBER(15) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ table_name VARCHAR2(100) NOT NULL,
+ record_id NUMBER(15) NOT NULL,
+ action VARCHAR2(20) NOT NULL,
+ old_values CLOB,
+ new_values CLOB,
+ changed_by VARCHAR2(100) NOT NULL,
+ changed_at TIMESTAMP DEFAULT SYSTIMESTAMP
+ );
+
+ -- Indexes
+ CREATE INDEX fin.idx_acc_customer ON fin.accounts(customer_id);
+ CREATE INDEX fin.idx_acc_type ON fin.accounts(account_type_id);
+ CREATE INDEX fin.idx_trans_from ON fin.transactions(from_account_id);
+ CREATE INDEX fin.idx_trans_to ON fin.transactions(to_account_id);
+ CREATE INDEX fin.idx_trans_date ON fin.transactions(transaction_date);
+ CREATE INDEX fin.idx_audit_table ON fin.audit_log(table_name, record_id);
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(4);
+ expect(result.relationships).toHaveLength(3);
+
+ // Verify transaction has two account references
+ const transAccountRels = result.relationships.filter(
+ (r) =>
+ r.sourceTable === 'transactions' && r.targetTable === 'accounts'
+ );
+ expect(transAccountRels.length).toBe(2);
+
+ // Verify indexes were parsed
+ const accountsTable = result.tables.find((t) => t.name === 'accounts');
+ expect(accountsTable?.indexes.length).toBeGreaterThanOrEqual(1);
+
+ const transactionsTable = result.tables.find(
+ (t) => t.name === 'transactions'
+ );
+ expect(transactionsTable?.indexes.length).toBeGreaterThanOrEqual(1);
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-full-flow.test.ts b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-full-flow.test.ts
new file mode 100644
index 000000000..9079e4438
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-full-flow.test.ts
@@ -0,0 +1,278 @@
+import { describe, it, expect } from 'vitest';
+import { sqlImportToDiagram, detectDatabaseType } from '../../../index';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('Oracle Full Flow Tests', () => {
+ it('should detect Oracle database type from SQL content', () => {
+ const sql = `
+ CREATE TABLE employees (
+ id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name VARCHAR2(255) NOT NULL,
+ hire_date DATE DEFAULT SYSDATE
+ );
+ `;
+
+ const detectedType = detectDatabaseType(sql);
+ expect(detectedType).toBe(DatabaseType.ORACLE);
+ });
+
+ it('should detect Oracle from VARCHAR2 type', () => {
+ const sql = `
+ CREATE TABLE test (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100)
+ );
+ `;
+
+ const detectedType = detectDatabaseType(sql);
+ expect(detectedType).toBe(DatabaseType.ORACLE);
+ });
+
+ it('should detect Oracle from SYSDATE usage', () => {
+ const sql = `
+ CREATE TABLE audit_log (
+ id INTEGER PRIMARY KEY,
+ created_at DATE DEFAULT SYSDATE
+ );
+ `;
+
+ const detectedType = detectDatabaseType(sql);
+ expect(detectedType).toBe(DatabaseType.ORACLE);
+ });
+
+ it('should detect Oracle from GENERATED AS IDENTITY', () => {
+ const sql = `
+ CREATE TABLE products (
+ id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ name VARCHAR(100)
+ );
+ `;
+
+ const detectedType = detectDatabaseType(sql);
+ expect(detectedType).toBe(DatabaseType.ORACLE);
+ });
+
+ it('should import Oracle SQL to diagram with correct table structure', async () => {
+ const sql = `
+ CREATE TABLE departments (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ budget NUMBER(12, 2)
+ );
+
+ CREATE TABLE employees (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ department_id NUMBER(10),
+ hire_date DATE DEFAULT SYSDATE,
+ CONSTRAINT fk_emp_dept FOREIGN KEY (department_id) REFERENCES departments(id)
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.ORACLE,
+ targetDatabaseType: DatabaseType.ORACLE,
+ });
+
+ expect(diagram).toBeDefined();
+ expect(diagram.tables).toBeDefined();
+ expect(diagram.tables!).toHaveLength(2);
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships!).toHaveLength(1);
+
+ // Check table names
+ const deptTable = diagram.tables!.find((t) => t.name === 'departments');
+ const empTable = diagram.tables!.find((t) => t.name === 'employees');
+
+ expect(deptTable).toBeDefined();
+ expect(empTable).toBeDefined();
+
+ // Check fields
+ expect(deptTable!.fields).toHaveLength(3);
+ expect(empTable!.fields).toHaveLength(4);
+
+ // Check relationship
+ expect(diagram.relationships![0].sourceTableId).toBe(deptTable!.id);
+ expect(diagram.relationships![0].targetTableId).toBe(empTable!.id);
+ });
+
+ it('should handle Oracle with auto-detection when source type is GENERIC', async () => {
+ const sql = `
+ CREATE TABLE products (
+ id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name VARCHAR2(200) NOT NULL,
+ price NUMBER(10, 2) NOT NULL,
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.GENERIC,
+ targetDatabaseType: DatabaseType.GENERIC,
+ });
+
+ expect(diagram).toBeDefined();
+ expect(diagram.tables).toBeDefined();
+ expect(diagram.tables!).toHaveLength(1);
+ expect(diagram.tables![0].name).toBe('products');
+ expect(diagram.tables![0].fields).toHaveLength(4);
+ });
+
+ it('should import complex Oracle schema with multiple tables and relationships', async () => {
+ const sql = `
+ -- Categories table
+ CREATE TABLE categories (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ description CLOB
+ );
+
+ -- Products table
+ CREATE TABLE products (
+ id NUMBER(10) PRIMARY KEY,
+ category_id NUMBER(10) NOT NULL,
+ name VARCHAR2(200) NOT NULL,
+ price NUMBER(10, 2) NOT NULL,
+ stock_quantity NUMBER(10) DEFAULT 0
+ );
+
+ -- Customers table
+ CREATE TABLE customers (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ email VARCHAR2(255) UNIQUE,
+ phone VARCHAR2(20)
+ );
+
+ -- Orders table
+ CREATE TABLE orders (
+ id NUMBER(10) PRIMARY KEY,
+ customer_id NUMBER(10) NOT NULL,
+ order_date DATE DEFAULT SYSDATE,
+ total_amount NUMBER(12, 2)
+ );
+
+ -- Order items table
+ CREATE TABLE order_items (
+ id NUMBER(10) PRIMARY KEY,
+ order_id NUMBER(10) NOT NULL,
+ product_id NUMBER(10) NOT NULL,
+ quantity NUMBER(10) NOT NULL,
+ unit_price NUMBER(10, 2) NOT NULL
+ );
+
+ -- Foreign key constraints
+ ALTER TABLE products ADD CONSTRAINT fk_prod_cat FOREIGN KEY (category_id) REFERENCES categories(id);
+ ALTER TABLE orders ADD CONSTRAINT fk_ord_cust FOREIGN KEY (customer_id) REFERENCES customers(id);
+ ALTER TABLE order_items ADD CONSTRAINT fk_oi_order FOREIGN KEY (order_id) REFERENCES orders(id);
+ ALTER TABLE order_items ADD CONSTRAINT fk_oi_product FOREIGN KEY (product_id) REFERENCES products(id);
+
+ -- Indexes
+ CREATE INDEX idx_products_category ON products(category_id);
+ CREATE INDEX idx_orders_customer ON orders(customer_id);
+ CREATE INDEX idx_order_items_order ON order_items(order_id);
+ CREATE INDEX idx_order_items_product ON order_items(product_id);
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.ORACLE,
+ targetDatabaseType: DatabaseType.ORACLE,
+ });
+
+ expect(diagram).toBeDefined();
+ expect(diagram.tables).toBeDefined();
+ expect(diagram.tables!).toHaveLength(5);
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships!).toHaveLength(4);
+
+ // Verify all tables exist
+ expect(
+ diagram.tables!.find((t) => t.name === 'categories')
+ ).toBeDefined();
+ expect(
+ diagram.tables!.find((t) => t.name === 'products')
+ ).toBeDefined();
+ expect(
+ diagram.tables!.find((t) => t.name === 'customers')
+ ).toBeDefined();
+ expect(diagram.tables!.find((t) => t.name === 'orders')).toBeDefined();
+ expect(
+ diagram.tables!.find((t) => t.name === 'order_items')
+ ).toBeDefined();
+
+ // Verify indexes were parsed
+ const productsTable = diagram.tables!.find(
+ (t) => t.name === 'products'
+ );
+ expect(productsTable?.indexes?.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it('should handle Oracle-specific syntax without errors', async () => {
+ const sql = `
+ SET DEFINE OFF;
+ SPOOL output.log
+
+ CREATE TABLE test_table (
+ id NUMBER(10) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL,
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
+ updated_at DATE DEFAULT SYSDATE
+ )
+ TABLESPACE users
+ STORAGE (INITIAL 64K NEXT 64K);
+
+ CREATE INDEX idx_test_name ON test_table(name)
+ TABLESPACE users_idx;
+
+ SPOOL OFF;
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.ORACLE,
+ targetDatabaseType: DatabaseType.ORACLE,
+ });
+
+ expect(diagram).toBeDefined();
+ expect(diagram.tables).toBeDefined();
+ expect(diagram.tables!).toHaveLength(1);
+ expect(diagram.tables![0].name).toBe('test_table');
+ });
+
+ it('should preserve field types correctly when target is Oracle', async () => {
+ const sql = `
+ CREATE TABLE data_preservation_test (
+ id NUMBER(10) PRIMARY KEY,
+ amount NUMBER(18, 4) NOT NULL,
+ description VARCHAR2(500),
+ created_at TIMESTAMP,
+ is_active NUMBER(1),
+ xml_data XMLTYPE,
+ binary_data BLOB
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.ORACLE,
+ targetDatabaseType: DatabaseType.ORACLE,
+ });
+
+ expect(diagram).toBeDefined();
+ expect(diagram.tables).toBeDefined();
+ const fields = diagram.tables![0].fields;
+
+ // Verify types are preserved or mapped appropriately
+ expect(fields.find((f) => f.name === 'id')?.type.id).toBeDefined();
+ expect(fields.find((f) => f.name === 'amount')?.type.id).toBeDefined();
+ expect(
+ fields.find((f) => f.name === 'description')?.type.id
+ ).toBeDefined();
+ expect(
+ fields.find((f) => f.name === 'created_at')?.type.id
+ ).toBeDefined();
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-relationships.test.ts b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-relationships.test.ts
new file mode 100644
index 000000000..ee8edff25
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/__tests__/oracle-relationships.test.ts
@@ -0,0 +1,323 @@
+import { describe, it, expect } from 'vitest';
+import { fromOracle } from '../oracle';
+
+describe('Oracle Foreign Key Relationship Tests', () => {
+ it('should properly link foreign key relationships with correct table IDs', async () => {
+ const sql = `
+ CREATE TABLE hr.departments (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL
+ );
+
+ CREATE TABLE hr.employees (
+ id NUMBER(10) PRIMARY KEY,
+ department_id NUMBER(10) NOT NULL,
+ name VARCHAR2(100) NOT NULL
+ );
+
+ ALTER TABLE hr.employees ADD CONSTRAINT fk_emp_dept
+ FOREIGN KEY (department_id) REFERENCES hr.departments(id);
+ `;
+
+ const result = await fromOracle(sql);
+
+ // Check tables are parsed
+ expect(result.tables).toHaveLength(2);
+ const deptTable = result.tables.find((t) => t.name === 'departments');
+ const empTable = result.tables.find((t) => t.name === 'employees');
+ expect(deptTable).toBeDefined();
+ expect(empTable).toBeDefined();
+
+ // Check relationship is parsed
+ expect(result.relationships).toHaveLength(1);
+ const rel = result.relationships[0];
+
+ // Verify the relationship has proper table IDs
+ expect(rel.sourceTableId).toBe(empTable!.id);
+ expect(rel.targetTableId).toBe(deptTable!.id);
+
+ // Verify other relationship properties
+ expect(rel.sourceTable).toBe('employees');
+ expect(rel.targetTable).toBe('departments');
+ expect(rel.sourceColumn).toBe('department_id');
+ expect(rel.targetColumn).toBe('id');
+ expect(rel.sourceSchema).toBe('hr');
+ expect(rel.targetSchema).toBe('hr');
+ });
+
+ it('should handle cross-schema foreign key relationships', async () => {
+ const sql = `
+ CREATE TABLE finance.accounts (
+ id NUMBER(10) PRIMARY KEY,
+ account_number VARCHAR2(50) NOT NULL
+ );
+
+ CREATE TABLE sales.transactions (
+ id NUMBER(10) PRIMARY KEY,
+ account_id NUMBER(10) NOT NULL
+ );
+
+ ALTER TABLE sales.transactions ADD CONSTRAINT fk_trans_account
+ FOREIGN KEY (account_id) REFERENCES finance.accounts(id);
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(2);
+ expect(result.relationships).toHaveLength(1);
+
+ const rel = result.relationships[0];
+ const accountsTable = result.tables.find(
+ (t) => t.name === 'accounts' && t.schema === 'finance'
+ );
+ const transactionsTable = result.tables.find(
+ (t) => t.name === 'transactions' && t.schema === 'sales'
+ );
+
+ // Verify cross-schema relationship IDs are properly linked
+ expect(rel.sourceTableId).toBe(transactionsTable!.id);
+ expect(rel.targetTableId).toBe(accountsTable!.id);
+ });
+
+ it('should parse complex foreign keys from enterprise database with proper table IDs', async () => {
+ const sql = `
+ -- Inventory schema
+ CREATE TABLE inventory.products (
+ id NUMBER(10) NOT NULL,
+ name VARCHAR2(255) NOT NULL,
+ category VARCHAR2(100) NOT NULL,
+ price NUMBER(10, 2) NOT NULL,
+ description CLOB,
+ CONSTRAINT pk_products PRIMARY KEY (id)
+ );
+
+ -- Purchase orders
+ CREATE TABLE inventory.purchase_orders (
+ id NUMBER(10) NOT NULL,
+ product_id NUMBER(10) NOT NULL,
+ supplier_id NUMBER(10) NOT NULL,
+ order_date DATE NOT NULL,
+ quantity NUMBER(10) NOT NULL,
+ total_amount NUMBER(12, 2) NOT NULL,
+ CONSTRAINT pk_purchase_orders PRIMARY KEY (id)
+ );
+
+ -- Suppliers schema
+ CREATE TABLE suppliers.suppliers (
+ id NUMBER(10) NOT NULL,
+ name VARCHAR2(255) NOT NULL,
+ contact_name VARCHAR2(100),
+ email VARCHAR2(255),
+ CONSTRAINT pk_suppliers PRIMARY KEY (id)
+ );
+
+ -- Employee purchases
+ CREATE TABLE hr.employee_purchases (
+ id NUMBER(10) NOT NULL,
+ employee_id NUMBER(10) NOT NULL,
+ manager_id NUMBER(10) NOT NULL,
+ purchase_date DATE NOT NULL,
+ CONSTRAINT pk_employee_purchases PRIMARY KEY (id)
+ );
+
+ -- HR employees
+ CREATE TABLE hr.employees (
+ id NUMBER(10) NOT NULL,
+ name VARCHAR2(255) NOT NULL,
+ title VARCHAR2(100),
+ hire_date DATE NOT NULL,
+ CONSTRAINT pk_employees PRIMARY KEY (id)
+ );
+
+ -- Add foreign key constraints
+ ALTER TABLE inventory.purchase_orders
+ ADD CONSTRAINT fk_po_product
+ FOREIGN KEY (product_id)
+ REFERENCES inventory.products(id);
+
+ ALTER TABLE inventory.purchase_orders
+ ADD CONSTRAINT fk_po_supplier
+ FOREIGN KEY (supplier_id)
+ REFERENCES suppliers.suppliers(id);
+
+ ALTER TABLE hr.employee_purchases
+ ADD CONSTRAINT fk_ep_employee
+ FOREIGN KEY (employee_id)
+ REFERENCES hr.employees(id);
+
+ ALTER TABLE hr.employee_purchases
+ ADD CONSTRAINT fk_ep_manager
+ FOREIGN KEY (manager_id)
+ REFERENCES hr.employees(id);
+ `;
+
+ const result = await fromOracle(sql);
+
+ // Check if we have the expected number of tables and relationships
+ expect(result.tables).toHaveLength(5);
+ expect(result.relationships).toHaveLength(4);
+
+ // Check a specific relationship we know should exist
+ const poProductRel = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'purchase_orders' &&
+ r.targetTable === 'products' &&
+ r.sourceColumn === 'product_id'
+ );
+
+ expect(poProductRel).toBeDefined();
+
+ // Find the corresponding tables
+ const productsTable = result.tables.find(
+ (t) => t.name === 'products' && t.schema === 'inventory'
+ );
+ const poTable = result.tables.find(
+ (t) => t.name === 'purchase_orders' && t.schema === 'inventory'
+ );
+
+ // Verify the IDs are properly linked
+ expect(poProductRel!.sourceTableId).toBeTruthy();
+ expect(poProductRel!.targetTableId).toBeTruthy();
+ expect(poProductRel!.sourceTableId).toBe(poTable!.id);
+ expect(poProductRel!.targetTableId).toBe(productsTable!.id);
+
+ // Check the employee self-referencing relationships
+ const epEmployeeRel = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'employee_purchases' &&
+ r.targetTable === 'employees' &&
+ r.sourceColumn === 'employee_id'
+ );
+
+ const epManagerRel = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'employee_purchases' &&
+ r.targetTable === 'employees' &&
+ r.sourceColumn === 'manager_id'
+ );
+
+ expect(epEmployeeRel).toBeDefined();
+ expect(epManagerRel).toBeDefined();
+
+ // Check that all relationships have valid table IDs
+ const relationshipsWithMissingIds = result.relationships.filter(
+ (r) =>
+ !r.sourceTableId ||
+ !r.targetTableId ||
+ r.sourceTableId === '' ||
+ r.targetTableId === ''
+ );
+
+ expect(relationshipsWithMissingIds).toHaveLength(0);
+ });
+
+ it('should handle inline foreign key references', async () => {
+ const sql = `
+ CREATE TABLE categories (
+ id NUMBER(10) PRIMARY KEY,
+ name VARCHAR2(100) NOT NULL
+ );
+
+ CREATE TABLE products (
+ id NUMBER(10) PRIMARY KEY,
+ category_id NUMBER(10) REFERENCES categories(id),
+ name VARCHAR2(100) NOT NULL
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(2);
+ expect(result.relationships).toHaveLength(1);
+
+ const rel = result.relationships[0];
+ expect(rel.sourceTable).toBe('products');
+ expect(rel.targetTable).toBe('categories');
+ expect(rel.sourceColumn).toBe('category_id');
+ expect(rel.targetColumn).toBe('id');
+ });
+
+ it('should handle multiple foreign keys in a single table', async () => {
+ const sql = `
+ CREATE TABLE users (id NUMBER(10) PRIMARY KEY, name VARCHAR2(100));
+ CREATE TABLE products (id NUMBER(10) PRIMARY KEY, name VARCHAR2(100));
+
+ CREATE TABLE reviews (
+ id NUMBER(10) PRIMARY KEY,
+ user_id NUMBER(10) NOT NULL,
+ product_id NUMBER(10) NOT NULL,
+ rating NUMBER(1) NOT NULL,
+ CONSTRAINT fk_review_user FOREIGN KEY (user_id) REFERENCES users(id),
+ CONSTRAINT fk_review_product FOREIGN KEY (product_id) REFERENCES products(id)
+ );
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(3);
+ expect(result.relationships).toHaveLength(2);
+
+ const userRel = result.relationships.find(
+ (r) => r.targetTable === 'users'
+ );
+ const productRel = result.relationships.find(
+ (r) => r.targetTable === 'products'
+ );
+
+ expect(userRel).toBeDefined();
+ expect(productRel).toBeDefined();
+ expect(userRel!.sourceTable).toBe('reviews');
+ expect(productRel!.sourceTable).toBe('reviews');
+ });
+
+ it('should handle ALTER TABLE with quoted identifiers', async () => {
+ const sql = `
+ CREATE TABLE "table_1" (
+ "id" number NOT NULL,
+ "field_2" number,
+ "field_3" number,
+ CONSTRAINT "pk_table_1_id" PRIMARY KEY ("id")
+ );
+
+ CREATE TABLE "table_2" (
+ "id" number NOT NULL,
+ "field_2" number,
+ "field_3" number,
+ CONSTRAINT "pk_table_2_id" PRIMARY KEY ("id")
+ );
+
+ ALTER TABLE "table_1" ADD CONSTRAINT "fk_table_1_id_table_2_field_3" FOREIGN KEY("id") REFERENCES "table_2"("field_3");
+ `;
+
+ const result = await fromOracle(sql);
+
+ expect(result.tables).toHaveLength(2);
+ const table1 = result.tables.find((t) => t.name === 'table_1');
+ const table2 = result.tables.find((t) => t.name === 'table_2');
+ expect(table1).toBeDefined();
+ expect(table2).toBeDefined();
+
+ // Check primary key columns for table_1
+ const table1IdCol = table1!.columns.find((c) => c.name === 'id');
+ expect(table1IdCol).toBeDefined();
+ expect(table1IdCol!.primaryKey).toBe(true);
+ expect(table1IdCol!.nullable).toBe(false);
+
+ // Check primary key columns for table_2
+ const table2IdCol = table2!.columns.find((c) => c.name === 'id');
+ expect(table2IdCol).toBeDefined();
+ expect(table2IdCol!.primaryKey).toBe(true);
+ expect(table2IdCol!.nullable).toBe(false);
+
+ // Check relationship
+ expect(result.relationships).toHaveLength(1);
+ const rel = result.relationships[0];
+
+ expect(rel.sourceTable).toBe('table_1');
+ expect(rel.targetTable).toBe('table_2');
+ expect(rel.sourceColumn).toBe('id');
+ expect(rel.targetColumn).toBe('field_3');
+ expect(rel.sourceTableId).toBe(table1!.id);
+ expect(rel.targetTableId).toBe(table2!.id);
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/oracle-common.ts b/src/lib/data/sql-import/dialect-importers/oracle/oracle-common.ts
new file mode 100644
index 000000000..94c1475f4
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/oracle-common.ts
@@ -0,0 +1,186 @@
+import type { SQLASTNode } from '../../common';
+
+// Set up the SQL parser with Oracle/PL-SQL dialect
+export const parserOpts = {
+ database: 'plsql',
+};
+
+// Type definitions for Oracle AST
+export interface TableReference {
+ db?: string;
+ schema?: string;
+ table: string;
+ as?: string;
+}
+
+export interface ColumnReference {
+ type: 'column_ref';
+ table?: string;
+ column: string;
+}
+
+export interface CreateTableStatement extends SQLASTNode {
+ type: 'create';
+ keyword: 'table';
+ table: TableReference | TableReference[];
+ create_definitions: (ColumnDefinition | ConstraintDefinition)[];
+ table_options?: Record[];
+ if_not_exists?: boolean;
+}
+
+export interface CreateIndexStatement extends SQLASTNode {
+ type: 'create';
+ keyword: 'index';
+ index: string;
+ table: TableReference | TableReference[];
+ columns: ColumnReference[];
+ constraint?: string;
+ index_using?: string;
+ index_options?: Record[];
+}
+
+export interface AlterTableStatement extends SQLASTNode {
+ type: 'alter';
+ keyword: 'table';
+ table: TableReference | TableReference[];
+ expr: AlterTableExprItem[];
+}
+
+export interface AlterTableExprItem {
+ action: string;
+ column?: ColumnReference | string;
+ definition?: Record;
+ resource?: string;
+ [key: string]: unknown;
+}
+
+export interface ColumnDefinition {
+ column: ColumnReference | string;
+ definition?: {
+ dataType: string;
+ length?: number | string;
+ width?: number | string;
+ scale?: number;
+ precision?: number;
+ parentheses?: boolean;
+ suffix?: string[];
+ constraint?: string;
+ [key: string]: unknown;
+ };
+ nullable?: { type: string };
+ primary_key?: string;
+ unique?: string;
+ default_val?: unknown;
+ auto_increment?: string;
+ comment?: string;
+ reference?: Record;
+ resource: string;
+ [key: string]: unknown;
+}
+
+export interface ConstraintDefinition {
+ constraint_type: string;
+ constraint?: string;
+ definition?: Array | Record;
+ resource: string;
+ reference?: {
+ table: TableReference;
+ columns: ColumnReference[];
+ on_delete?: string;
+ on_update?: string;
+ };
+ [key: string]: unknown;
+}
+
+/**
+ * Extract column name from a column reference
+ */
+export function extractColumnName(columnRef: ColumnReference | string): string {
+ if (typeof columnRef === 'string') {
+ // Remove double quotes if present (Oracle quoted identifiers)
+ return columnRef.replace(/^"|"$/g, '');
+ }
+
+ if (columnRef.type === 'column_ref') {
+ return columnRef.column.replace(/^"|"$/g, '');
+ }
+
+ return '';
+}
+
+/**
+ * Extract type arguments such as length, precision, scale
+ */
+export function getTypeArgs(
+ definition?: ColumnDefinition['definition']
+): { length?: number; precision?: number; scale?: number } | undefined {
+ if (!definition) return undefined;
+
+ const result: { length?: number; precision?: number; scale?: number } = {};
+
+ // Check if length/width is present
+ if (definition.length !== undefined) {
+ result.length = Number(definition.length);
+ } else if (definition.width !== undefined) {
+ result.length = Number(definition.width);
+ }
+
+ // Check if precision is present
+ if (definition.precision !== undefined) {
+ result.precision = Number(definition.precision);
+ }
+
+ // Check if scale is present
+ if (definition.scale !== undefined) {
+ result.scale = Number(definition.scale);
+ }
+
+ return Object.keys(result).length > 0 ? result : undefined;
+}
+
+/**
+ * Find a table in the tables array with schema support
+ */
+export function findTableWithSchemaSupport(
+ tables: Array<{ id: string; name: string; schema?: string }>,
+ tableName: string,
+ schemaName?: string
+): { id: string; name: string; schema?: string } | undefined {
+ // Normalize names (Oracle is case-insensitive by default, uppercase)
+ const normalizedTableName = tableName.toUpperCase();
+ const normalizedSchemaName = schemaName?.toUpperCase();
+
+ // If schema is provided, search for exact match
+ if (normalizedSchemaName) {
+ return tables.find(
+ (t) =>
+ t.name.toUpperCase() === normalizedTableName &&
+ t.schema?.toUpperCase() === normalizedSchemaName
+ );
+ }
+
+ // No schema provided, first try to find exact match without schema
+ const exactMatch = tables.find(
+ (t) => t.name.toUpperCase() === normalizedTableName && !t.schema
+ );
+ if (exactMatch) return exactMatch;
+
+ // Finally, look for any table with matching name regardless of schema
+ return tables.find((t) => t.name.toUpperCase() === normalizedTableName);
+}
+
+/**
+ * Normalize Oracle identifier (remove quotes, handle case)
+ */
+export function normalizeOracleIdentifier(identifier: string): string {
+ if (!identifier) return '';
+
+ // If quoted with double quotes, preserve case and remove quotes
+ if (identifier.startsWith('"') && identifier.endsWith('"')) {
+ return identifier.slice(1, -1);
+ }
+
+ // Unquoted identifiers in Oracle are case-insensitive
+ // We preserve the original case for display purposes
+ return identifier;
+}
diff --git a/src/lib/data/sql-import/dialect-importers/oracle/oracle.ts b/src/lib/data/sql-import/dialect-importers/oracle/oracle.ts
new file mode 100644
index 000000000..8b421c026
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/oracle/oracle.ts
@@ -0,0 +1,1093 @@
+import { generateId } from '@/lib/utils';
+import type {
+ SQLParserResult,
+ SQLTable,
+ SQLColumn,
+ SQLIndex,
+ SQLForeignKey,
+ SQLASTNode,
+} from '../../common';
+import type {
+ TableReference,
+ ColumnReference,
+ ConstraintDefinition,
+ CreateIndexStatement,
+ AlterTableStatement,
+} from './oracle-common';
+import {
+ parserOpts,
+ extractColumnName,
+ findTableWithSchemaSupport,
+ normalizeOracleIdentifier,
+} from './oracle-common';
+
+/**
+ * Preprocess Oracle SQL script to remove or modify parts that the parser can't handle
+ */
+function preprocessOracleScript(sqlContent: string): string {
+ // 1. Remove Oracle-specific SET commands
+ sqlContent = sqlContent.replace(
+ /SET\s+(DEFINE|ECHO|FEEDBACK|HEADING|LINESIZE|PAGESIZE|SERVEROUTPUT|TERMOUT|TIMING|VERIFY)\s+\w+\s*;?/gi,
+ ''
+ );
+
+ // 2. Remove WHENEVER commands
+ sqlContent = sqlContent.replace(/WHENEVER\s+\w+\s+[^;]*;?/gi, '');
+
+ // 3. Remove PROMPT commands
+ sqlContent = sqlContent.replace(/PROMPT\s+[^;]*;?/gi, '');
+
+ // 4. Remove SPOOL commands
+ sqlContent = sqlContent.replace(/SPOOL\s+[^;]*;?/gi, '');
+
+ // 5. Remove EXIT/QUIT commands
+ sqlContent = sqlContent.replace(/\b(EXIT|QUIT)\b\s*;?/gi, '');
+
+ // 6. Remove REM (remark) comments
+ sqlContent = sqlContent.replace(/^REM\s+.*$/gim, '');
+
+ // 7. Remove slash command (Oracle script delimiter)
+ sqlContent = sqlContent.replace(/^\s*\/\s*$/gm, '');
+
+ // 8. Remove PL/SQL blocks (CREATE OR REPLACE PROCEDURE/FUNCTION/PACKAGE/TRIGGER)
+ sqlContent = sqlContent.replace(
+ /CREATE\s+(?:OR\s+REPLACE\s+)?(?:PROCEDURE|FUNCTION|PACKAGE(?:\s+BODY)?|TRIGGER)\s+[\s\S]*?(?:END\s*;|END\s+\w+\s*;)/gi,
+ ''
+ );
+
+ // 9. Remove EXECUTE IMMEDIATE and dynamic SQL
+ sqlContent = sqlContent.replace(/EXECUTE\s+IMMEDIATE\s+[^;]+;/gi, '');
+
+ // 10. Handle Oracle GENERATED ALWAYS AS IDENTITY
+ sqlContent = sqlContent.replace(
+ /GENERATED\s+(?:ALWAYS|BY\s+DEFAULT)\s+AS\s+IDENTITY(?:\s*\([^)]*\))?/gi,
+ 'AUTO_INCREMENT'
+ );
+
+ // 11. Handle Oracle sequences with NEXTVAL for default values
+ sqlContent = sqlContent.replace(
+ /DEFAULT\s+(\w+)\.NEXTVAL/gi,
+ "DEFAULT 'sequence'"
+ );
+
+ // 12. Remove STORAGE clauses
+ sqlContent = sqlContent.replace(/STORAGE\s*\([^)]*\)/gi, '');
+
+ // 13. Remove TABLESPACE clauses
+ sqlContent = sqlContent.replace(/TABLESPACE\s+\w+/gi, '');
+
+ // 14. Remove PCTFREE, PCTUSED, INITRANS, MAXTRANS clauses
+ sqlContent = sqlContent.replace(
+ /\b(PCTFREE|PCTUSED|INITRANS|MAXTRANS)\s+\d+/gi,
+ ''
+ );
+
+ // 15. Remove LOGGING/NOLOGGING
+ sqlContent = sqlContent.replace(/\b(LOGGING|NOLOGGING)\b/gi, '');
+
+ // 16. Remove COMPRESS/NOCOMPRESS
+ sqlContent = sqlContent.replace(/\b(COMPRESS|NOCOMPRESS)(?:\s+\d+)?/gi, '');
+
+ // 17. Remove PARALLEL clause
+ sqlContent = sqlContent.replace(
+ /PARALLEL\s*(?:\(\s*DEGREE\s+\d+\s*\)|\d+)?/gi,
+ ''
+ );
+
+ // 18. Remove ENABLE/DISABLE keywords for constraints
+ sqlContent = sqlContent.replace(
+ /\b(ENABLE|DISABLE)(?:\s+VALIDATE|\s+NOVALIDATE)?\b/gi,
+ ''
+ );
+
+ // 19. Remove USING INDEX clause
+ sqlContent = sqlContent.replace(
+ /USING\s+INDEX\s*(?:\([^)]*\)|[\w."]+)?/gi,
+ ''
+ );
+
+ // 20. Handle CHECK constraints - simplify them
+ sqlContent = sqlContent.replace(
+ /CHECK\s*\([^)]+\)/gi,
+ '/* CHECK CONSTRAINT */'
+ );
+
+ // 21. Handle Oracle-specific default functions
+ sqlContent = sqlContent.replace(/DEFAULT\s+SYSDATE/gi, "DEFAULT 'sysdate'");
+ sqlContent = sqlContent.replace(
+ /DEFAULT\s+SYSTIMESTAMP/gi,
+ "DEFAULT 'systimestamp'"
+ );
+ sqlContent = sqlContent.replace(
+ /DEFAULT\s+SYS_GUID\(\)/gi,
+ "DEFAULT 'sys_guid'"
+ );
+ sqlContent = sqlContent.replace(/DEFAULT\s+USER/gi, "DEFAULT 'user'");
+ sqlContent = sqlContent.replace(
+ /DEFAULT\s+CURRENT_TIMESTAMP/gi,
+ "DEFAULT 'current_timestamp'"
+ );
+ sqlContent = sqlContent.replace(
+ /DEFAULT\s+CURRENT_DATE/gi,
+ "DEFAULT 'current_date'"
+ );
+
+ // 22. Remove LOB storage clauses
+ sqlContent = sqlContent.replace(
+ /LOB\s*\([^)]+\)\s*STORE\s+AS\s*(?:\([^)]*\)|[^,)]+)/gi,
+ ''
+ );
+
+ // 23. Remove SUPPLEMENTAL LOG clauses
+ sqlContent = sqlContent.replace(/SUPPLEMENTAL\s+LOG\s+[^;]+;?/gi, '');
+
+ // 24. Split into individual statements
+ const statements = sqlContent
+ .split(';')
+ .filter((stmt) => stmt.trim().length > 0);
+
+ // Filter to keep only CREATE TABLE, CREATE INDEX, and ALTER TABLE statements
+ const filteredStatements = statements.filter((stmt) => {
+ const trimmedStmt = stmt.trim().toUpperCase();
+ return (
+ trimmedStmt.includes('CREATE TABLE') ||
+ trimmedStmt.includes('CREATE UNIQUE INDEX') ||
+ trimmedStmt.includes('CREATE INDEX') ||
+ trimmedStmt.includes('ALTER TABLE')
+ );
+ });
+
+ return filteredStatements.join(';\n') + ';';
+}
+
+/**
+ * Manual parsing of ALTER TABLE ADD CONSTRAINT statements for Oracle
+ */
+function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
+ const fkData: SQLForeignKey[] = [];
+
+ // Oracle ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY pattern
+ const alterTableRegex =
+ /ALTER\s+TABLE\s+"?(\w+)"?(?:\."?(\w+)"?)?\s+ADD\s+CONSTRAINT\s+"?(\w+)"?\s+FOREIGN\s+KEY\s*\("?([^")]+)"?\)\s*REFERENCES\s+"?(\w+)"?(?:\."?(\w+)"?)?\s*\("?([^")]+)"?\)/i;
+
+ for (const stmt of statements) {
+ const match = stmt.match(alterTableRegex);
+ if (match) {
+ const [
+ ,
+ sourceSchemaOrTable,
+ sourceTableIfSchema,
+ constraintName,
+ sourceColumn,
+ targetSchemaOrTable,
+ targetTableIfSchema,
+ targetColumn,
+ ] = match;
+
+ // Handle both schema.table and just table formats
+ let sourceSchema = '';
+ let sourceTable = '';
+ let targetSchema = '';
+ let targetTable = '';
+
+ // If second group is empty, first group is the table name
+ if (!sourceTableIfSchema) {
+ sourceTable = sourceSchemaOrTable;
+ } else {
+ sourceSchema = sourceSchemaOrTable;
+ sourceTable = sourceTableIfSchema;
+ }
+
+ if (!targetTableIfSchema) {
+ targetTable = targetSchemaOrTable;
+ } else {
+ targetSchema = targetSchemaOrTable;
+ targetTable = targetTableIfSchema;
+ }
+
+ fkData.push({
+ name: normalizeOracleIdentifier(constraintName),
+ sourceTable: normalizeOracleIdentifier(sourceTable),
+ sourceSchema: normalizeOracleIdentifier(sourceSchema),
+ sourceColumn: normalizeOracleIdentifier(sourceColumn),
+ targetTable: normalizeOracleIdentifier(targetTable),
+ targetSchema: normalizeOracleIdentifier(targetSchema),
+ targetColumn: normalizeOracleIdentifier(targetColumn),
+ sourceTableId: '', // Will be filled by linkRelationships
+ targetTableId: '', // Will be filled by linkRelationships
+ });
+ }
+ }
+
+ return fkData;
+}
+
+/**
+ * Map Oracle data type strings to normalized types
+ */
+function normalizeOracleDataType(dataType: string): string {
+ const lowerType = dataType.toLowerCase().trim();
+
+ switch (lowerType) {
+ // Character types
+ case 'varchar2':
+ case 'varchar':
+ return 'varchar2';
+ case 'nvarchar2':
+ return 'nvarchar2';
+ case 'char':
+ return 'char';
+ case 'nchar':
+ return 'nchar';
+ case 'clob':
+ return 'clob';
+ case 'nclob':
+ return 'nclob';
+ case 'long':
+ return 'long';
+
+ // Numeric types
+ case 'number':
+ case 'numeric':
+ case 'decimal':
+ return 'number';
+ case 'integer':
+ case 'int':
+ return 'integer';
+ case 'smallint':
+ return 'smallint';
+ case 'float':
+ return 'float';
+ case 'binary_float':
+ return 'binary_float';
+ case 'binary_double':
+ return 'binary_double';
+ case 'real':
+ return 'real';
+
+ // Date/Time types
+ case 'date':
+ return 'date';
+ case 'timestamp':
+ return 'timestamp';
+ case 'timestamp with time zone':
+ case 'timestamp with local time zone':
+ return 'timestamp';
+ case 'interval year to month':
+ case 'interval day to second':
+ return 'interval';
+
+ // Binary types
+ case 'blob':
+ return 'blob';
+ case 'raw':
+ return 'raw';
+ case 'long raw':
+ return 'long raw';
+ case 'bfile':
+ return 'bfile';
+
+ // Other types
+ case 'rowid':
+ return 'rowid';
+ case 'urowid':
+ return 'urowid';
+ case 'xmltype':
+ return 'xmltype';
+ case 'json':
+ return 'json';
+ case 'boolean':
+ return 'boolean';
+
+ default:
+ return dataType;
+ }
+}
+
+/**
+ * Manual parsing of CREATE TABLE statements when node-sql-parser fails
+ */
+function parseCreateTableManually(
+ statement: string,
+ tables: SQLTable[],
+ tableMap: Record,
+ relationships: SQLForeignKey[]
+): void {
+ // Extract table name and schema (handling quoted identifiers)
+ const tableMatch = statement.match(
+ /CREATE\s+TABLE\s+"?(\w+)"?(?:\."?(\w+)"?)?\s*\(/i
+ );
+ if (!tableMatch) return;
+
+ let schema = '';
+ let tableName = '';
+
+ // If we have two captures, first is schema, second is table
+ if (tableMatch[2]) {
+ schema = normalizeOracleIdentifier(tableMatch[1]);
+ tableName = normalizeOracleIdentifier(tableMatch[2]);
+ } else {
+ tableName = normalizeOracleIdentifier(tableMatch[1]);
+ }
+
+ // Generate table ID
+ const tableId = generateId();
+ const tableKey = schema ? `${schema}.${tableName}` : tableName;
+ tableMap[tableKey] = tableId;
+ tableMap[tableName] = tableId; // Also map by table name only
+
+ // Extract column definitions
+ const columns: SQLColumn[] = [];
+ const indexes: SQLIndex[] = [];
+
+ // Find the content between the parentheses
+ const tableContentMatch = statement.match(
+ /CREATE\s+TABLE\s+[^(]+\(([\s\S]*)\)\s*(?:TABLESPACE|STORAGE|PCTFREE|$)/i
+ );
+ if (!tableContentMatch) return;
+
+ const tableContent = tableContentMatch[1];
+
+ // Split table content by commas but not within parentheses
+ const parts = [];
+ let current = '';
+ let parenDepth = 0;
+
+ for (let i = 0; i < tableContent.length; i++) {
+ const char = tableContent[i];
+ if (char === '(') parenDepth++;
+ else if (char === ')') parenDepth--;
+ else if (char === ',' && parenDepth === 0) {
+ parts.push(current.trim());
+ current = '';
+ continue;
+ }
+ current += char;
+ }
+ if (current.trim()) parts.push(current.trim());
+
+ // Process each part (column or constraint)
+ for (const part of parts) {
+ // Handle standalone FOREIGN KEY definitions (without CONSTRAINT keyword)
+ if (part.match(/^\s*FOREIGN\s+KEY/i)) {
+ const fkMatch = part.match(
+ /FOREIGN\s+KEY\s*\("?([^")]+)"?\)\s+REFERENCES\s+"?(\w+)"?(?:\."?(\w+)"?)?\s*\("?([^")]+)"?\)/i
+ );
+ if (fkMatch) {
+ const [
+ ,
+ sourceCol,
+ targetSchemaOrTable,
+ targetTableIfSchema,
+ targetCol,
+ ] = fkMatch;
+ relationships.push({
+ name: `FK_${tableName}_${normalizeOracleIdentifier(sourceCol)}`,
+ sourceTable: tableName,
+ sourceSchema: schema,
+ sourceColumn: normalizeOracleIdentifier(sourceCol),
+ targetTable: normalizeOracleIdentifier(
+ targetTableIfSchema || targetSchemaOrTable
+ ),
+ targetSchema: targetTableIfSchema
+ ? normalizeOracleIdentifier(targetSchemaOrTable)
+ : '',
+ targetColumn: normalizeOracleIdentifier(targetCol),
+ sourceTableId: tableId,
+ targetTableId: '', // Will be filled later
+ });
+ }
+ continue;
+ }
+
+ // Handle standalone PRIMARY KEY definitions
+ if (part.match(/^\s*PRIMARY\s+KEY/i)) {
+ const pkColumnsMatch = part.match(
+ /PRIMARY\s+KEY\s*\(([\s\S]+?)\)/i
+ );
+ if (pkColumnsMatch) {
+ const pkColumns = pkColumnsMatch[1]
+ .split(',')
+ .map((c) => normalizeOracleIdentifier(c.trim()));
+ const isSingleColumnPK = pkColumns.length === 1;
+ pkColumns.forEach((col) => {
+ const column = columns.find(
+ (c) => c.name.toUpperCase() === col.toUpperCase()
+ );
+ if (column) {
+ column.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (isSingleColumnPK) {
+ column.unique = true;
+ }
+ }
+ });
+ }
+ continue;
+ }
+
+ // Handle constraint definitions
+ if (part.match(/^\s*CONSTRAINT/i)) {
+ const constraintMatch = part.match(
+ /CONSTRAINT\s+"?(\w+)"?\s+(PRIMARY\s+KEY|UNIQUE|FOREIGN\s+KEY)/i
+ );
+ if (constraintMatch) {
+ const [, constraintName, constraintType] = constraintMatch;
+
+ if (constraintType.match(/PRIMARY\s+KEY/i)) {
+ // Extract columns from PRIMARY KEY constraint
+ const pkColumnsMatch = part.match(
+ /PRIMARY\s+KEY\s*\(([\s\S]+?)\)/i
+ );
+ if (pkColumnsMatch) {
+ const pkColumns = pkColumnsMatch[1]
+ .split(',')
+ .map((c) => normalizeOracleIdentifier(c.trim()));
+ const isSingleColumnPK = pkColumns.length === 1;
+ pkColumns.forEach((col) => {
+ const column = columns.find(
+ (c) =>
+ c.name.toUpperCase() === col.toUpperCase()
+ );
+ if (column) {
+ column.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (isSingleColumnPK) {
+ column.unique = true;
+ }
+ }
+ });
+ }
+ } else if (constraintType === 'UNIQUE') {
+ // Extract columns from UNIQUE constraint
+ const uniqueColumnsMatch = part.match(
+ /UNIQUE\s*\(([\s\S]+?)\)/i
+ );
+ if (uniqueColumnsMatch) {
+ const uniqueColumns = uniqueColumnsMatch[1]
+ .split(',')
+ .map((c) => normalizeOracleIdentifier(c.trim()));
+ indexes.push({
+ name: normalizeOracleIdentifier(constraintName),
+ columns: uniqueColumns,
+ unique: true,
+ });
+ }
+ } else if (constraintType.match(/FOREIGN\s+KEY/i)) {
+ // Parse foreign key constraint
+ const fkMatch = part.match(
+ /FOREIGN\s+KEY\s*\("?([^")]+)"?\)\s+REFERENCES\s+"?(\w+)"?(?:\."?(\w+)"?)?\s*\("?([^")]+)"?\)/i
+ );
+ if (fkMatch) {
+ const [
+ ,
+ sourceCol,
+ targetSchemaOrTable,
+ targetTableIfSchema,
+ targetCol,
+ ] = fkMatch;
+ relationships.push({
+ name: normalizeOracleIdentifier(constraintName),
+ sourceTable: tableName,
+ sourceSchema: schema,
+ sourceColumn: normalizeOracleIdentifier(sourceCol),
+ targetTable: normalizeOracleIdentifier(
+ targetTableIfSchema || targetSchemaOrTable
+ ),
+ targetSchema: targetTableIfSchema
+ ? normalizeOracleIdentifier(targetSchemaOrTable)
+ : '',
+ targetColumn: normalizeOracleIdentifier(targetCol),
+ sourceTableId: tableId,
+ targetTableId: '', // Will be filled later
+ });
+ }
+ }
+ }
+ continue;
+ }
+
+ // Parse column definition
+ // Handle Oracle format: column_name datatype(args) [constraints]
+ const columnMatch = part.match(
+ /^\s*"?(\w+)"?\s+(\w+)(?:\s*\(\s*(\d+(?:\s*,\s*\d+)?)\s*\))?(.*)$/i
+ );
+
+ if (columnMatch) {
+ const [, colName, baseType, typeArgs, rest] = columnMatch;
+
+ if (
+ colName &&
+ !colName.match(/^(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT)$/i)
+ ) {
+ // Check for inline foreign key
+ const inlineFkMatch = rest?.match(
+ /REFERENCES\s+"?(\w+)"?(?:\."?(\w+)"?)?\s*\("?([^")]+)"?\)/i
+ );
+
+ if (inlineFkMatch) {
+ const [
+ ,
+ targetSchemaOrTable,
+ targetTableIfSchema,
+ targetCol,
+ ] = inlineFkMatch;
+ relationships.push({
+ name: `FK_${tableName}_${normalizeOracleIdentifier(colName)}`,
+ sourceTable: tableName,
+ sourceSchema: schema,
+ sourceColumn: normalizeOracleIdentifier(colName),
+ targetTable: normalizeOracleIdentifier(
+ targetTableIfSchema || targetSchemaOrTable
+ ),
+ targetSchema: targetTableIfSchema
+ ? normalizeOracleIdentifier(targetSchemaOrTable)
+ : '',
+ targetColumn: normalizeOracleIdentifier(targetCol),
+ sourceTableId: tableId,
+ targetTableId: '', // Will be filled later
+ });
+ }
+
+ const isPrimaryKey = !!rest?.match(/PRIMARY\s+KEY/i);
+ const isNotNull = !!rest?.match(/NOT\s+NULL/i);
+ const isIdentity =
+ !!rest?.match(
+ /GENERATED\s+(?:ALWAYS|BY\s+DEFAULT)\s+AS\s+IDENTITY/i
+ ) || !!rest?.match(/AUTO_INCREMENT/i);
+ const isUnique = !!rest?.match(/UNIQUE/i);
+ const defaultMatch = rest?.match(/DEFAULT\s+([^,\s]+)/i);
+
+ // Parse type arguments
+ let parsedTypeArgs:
+ | { length?: number; precision?: number; scale?: number }
+ | undefined;
+ if (typeArgs) {
+ const args = typeArgs
+ .split(',')
+ .map((a) => parseInt(a.trim()));
+ if (args.length === 1) {
+ parsedTypeArgs = { length: args[0] };
+ } else if (args.length >= 2) {
+ parsedTypeArgs = { precision: args[0], scale: args[1] };
+ }
+ }
+
+ const column: SQLColumn = {
+ name: normalizeOracleIdentifier(colName),
+ type: normalizeOracleDataType(baseType.trim()),
+ nullable: !isNotNull && !isPrimaryKey,
+ primaryKey: isPrimaryKey,
+ unique: isUnique,
+ increment: isIdentity,
+ default: defaultMatch ? defaultMatch[1].trim() : undefined,
+ };
+
+ if (parsedTypeArgs) {
+ column.typeArgs = parsedTypeArgs;
+ }
+
+ columns.push(column);
+ }
+ }
+ }
+
+ // Add the table
+ tables.push({
+ id: tableId,
+ name: tableName,
+ schema: schema || undefined,
+ columns,
+ indexes,
+ order: tables.length,
+ });
+}
+
+/**
+ * Parse Oracle DDL scripts and extract database structure
+ * @param sqlContent Oracle DDL content as string
+ * @returns Parsed structure including tables, columns, and relationships
+ */
+export async function fromOracle(sqlContent: string): Promise {
+ const tables: SQLTable[] = [];
+ const relationships: SQLForeignKey[] = [];
+ const tableMap: Record = {}; // Maps table name to its ID
+
+ try {
+ // First, split by semicolon for Oracle
+ const statements = sqlContent
+ .split(';')
+ .filter((stmt) => stmt.trim().length > 0);
+
+ // Handle ALTER TABLE statements for foreign keys
+ const alterTableStatements = statements.filter(
+ (stmt) =>
+ stmt.trim().toUpperCase().includes('ALTER TABLE') &&
+ stmt.toUpperCase().includes('FOREIGN KEY')
+ );
+
+ if (alterTableStatements.length > 0) {
+ const fkData = parseAlterTableAddConstraint(alterTableStatements);
+ relationships.push(...fkData);
+ }
+
+ // Parse CREATE TABLE statements manually first
+ const createTableStatements = statements.filter((stmt) =>
+ stmt.trim().toUpperCase().includes('CREATE TABLE')
+ );
+
+ for (const stmt of createTableStatements) {
+ parseCreateTableManually(stmt, tables, tableMap, relationships);
+ }
+
+ // Preprocess the SQL content for node-sql-parser
+ const preprocessedSQL = preprocessOracleScript(sqlContent);
+
+ // Try to use node-sql-parser for additional parsing
+ try {
+ const { Parser } = await import('node-sql-parser');
+ const parser = new Parser();
+ let ast;
+ try {
+ ast = parser.astify(preprocessedSQL, parserOpts);
+ } catch {
+ // Fallback: Try to parse each statement individually
+ const stmts = preprocessedSQL
+ .split(';')
+ .filter((stmt) => stmt.trim().length > 0);
+ ast = [];
+
+ for (const stmt of stmts) {
+ try {
+ const stmtAst = parser.astify(stmt + ';', parserOpts);
+ if (Array.isArray(stmtAst)) {
+ ast.push(...stmtAst);
+ } else if (stmtAst) {
+ ast.push(stmtAst);
+ }
+ } catch {
+ // Skip statements that can't be parsed
+ }
+ }
+ }
+
+ if (Array.isArray(ast) && ast.length > 0) {
+ // Process each statement
+ (ast as unknown as SQLASTNode[]).forEach((stmt) => {
+ // Process CREATE INDEX statements
+ if (stmt.type === 'create' && stmt.keyword === 'index') {
+ processCreateIndex(
+ stmt as CreateIndexStatement,
+ tables
+ );
+ }
+ // Process ALTER TABLE statements for non-FK constraints
+ else if (
+ stmt.type === 'alter' &&
+ stmt.keyword === 'table'
+ ) {
+ processAlterTable(
+ stmt as AlterTableStatement,
+ tables,
+ relationships
+ );
+ }
+ });
+ }
+ } catch (parserError) {
+ // If parser fails completely, continue with manual parsing results
+ console.warn(
+ 'node-sql-parser failed, using manual parsing only:',
+ parserError
+ );
+ }
+
+ // Parse CREATE INDEX statements manually
+ const createIndexStatements = statements.filter(
+ (stmt) =>
+ stmt.trim().toUpperCase().includes('CREATE') &&
+ stmt.trim().toUpperCase().includes('INDEX') &&
+ !stmt.trim().toUpperCase().includes('CREATE TABLE')
+ );
+
+ for (const stmt of createIndexStatements) {
+ // Handle both schema-qualified and non-qualified index names
+ // Format: CREATE [UNIQUE] INDEX [schema.]index_name ON [schema.]table_name(columns)
+ const indexMatch = stmt.match(
+ /CREATE\s+(UNIQUE\s+)?INDEX\s+(?:"?(\w+)"?\.)?"?(\w+)"?\s+ON\s+"?(\w+)"?(?:\."?(\w+)"?)?\s*\(([^)]+)\)/i
+ );
+ if (indexMatch) {
+ const [
+ ,
+ unique,
+ ,
+ indexName,
+ schemaOrTable,
+ tableIfSchema,
+ columnsStr,
+ ] = indexMatch;
+
+ const targetTable = tableIfSchema || schemaOrTable;
+ const targetSchema = tableIfSchema ? schemaOrTable : '';
+
+ const table = tables.find(
+ (t) =>
+ t.name.toUpperCase() === targetTable.toUpperCase() &&
+ (!targetSchema ||
+ t.schema?.toUpperCase() ===
+ targetSchema.toUpperCase())
+ );
+
+ if (table) {
+ const columns = columnsStr
+ .split(',')
+ .map((c) => normalizeOracleIdentifier(c.trim()));
+
+ // Check if this index already exists
+ const existingIndex = table.indexes.find(
+ (i) =>
+ i.name.toUpperCase() ===
+ normalizeOracleIdentifier(indexName).toUpperCase()
+ );
+ if (!existingIndex) {
+ table.indexes.push({
+ name: normalizeOracleIdentifier(indexName),
+ columns,
+ unique: !!unique,
+ });
+ }
+ }
+ }
+ }
+
+ // Link relationships to ensure all targetTableId and sourceTableId fields are filled
+ const validRelationships = linkRelationships(
+ tables,
+ relationships,
+ tableMap
+ );
+
+ // Sort tables by order
+ const sortedTables = [...tables];
+ sortedTables.sort((a, b) => a.order - b.order);
+
+ return {
+ tables: sortedTables,
+ relationships: validRelationships,
+ };
+ } catch (error) {
+ console.error('Error parsing Oracle DDL:', error);
+ throw new Error(`Error parsing Oracle DDL: ${error}`);
+ }
+}
+
+/**
+ * Process a CREATE INDEX statement
+ */
+function processCreateIndex(
+ stmt: CreateIndexStatement,
+ tables: SQLTable[]
+): void {
+ if (!stmt.table || !stmt.columns || stmt.columns.length === 0) {
+ return;
+ }
+
+ // Extract table name and schema
+ let tableName = '';
+ let schemaName = '';
+
+ if (typeof stmt.table === 'object') {
+ if (Array.isArray(stmt.table) && stmt.table.length > 0) {
+ const tableObj = stmt.table[0];
+ tableName = tableObj.table || '';
+ schemaName = tableObj.schema || tableObj.db || '';
+ } else {
+ const tableObj = stmt.table as TableReference;
+ tableName = tableObj.table || '';
+ schemaName = tableObj.schema || tableObj.db || '';
+ }
+ }
+
+ if (!tableName) {
+ return;
+ }
+
+ // Find the table
+ const table = findTableWithSchemaSupport(tables, tableName, schemaName);
+ if (!table) {
+ return;
+ }
+
+ // Extract column names from the index definition
+ const indexColumns = stmt.columns.map((col) => extractColumnName(col));
+ if (indexColumns.length === 0) {
+ return;
+ }
+
+ // Create the index
+ const indexName =
+ stmt.index || `idx_${tableName}_${indexColumns.join('_')}`;
+ const isUnique = stmt.constraint === 'unique';
+
+ // Add index to the table if it doesn't already exist
+ const tableObj = tables.find((t) => t.id === table.id);
+ if (tableObj) {
+ const existingIndex = tableObj.indexes.find(
+ (i) => i.name.toUpperCase() === indexName.toUpperCase()
+ );
+ if (!existingIndex) {
+ tableObj.indexes.push({
+ name: indexName,
+ columns: indexColumns,
+ unique: isUnique,
+ });
+ }
+ }
+}
+
+/**
+ * Process an ALTER TABLE statement
+ */
+function processAlterTable(
+ stmt: AlterTableStatement,
+ tables: SQLTable[],
+ relationships: SQLForeignKey[]
+): void {
+ if (!stmt.table || !stmt.expr || !Array.isArray(stmt.expr)) {
+ return;
+ }
+
+ // Extract table name and schema
+ let tableName = '';
+ let schemaName = '';
+
+ if (typeof stmt.table === 'object') {
+ if (Array.isArray(stmt.table) && stmt.table.length > 0) {
+ const tableObj = stmt.table[0];
+ tableName = tableObj.table || '';
+ schemaName = tableObj.schema || tableObj.db || '';
+ } else {
+ const tableObj = stmt.table as TableReference;
+ tableName = tableObj.table || '';
+ schemaName = tableObj.schema || tableObj.db || '';
+ }
+ }
+
+ if (!tableName) {
+ return;
+ }
+
+ // Find the table
+ const table = findTableWithSchemaSupport(tables, tableName, schemaName);
+ if (!table) {
+ return;
+ }
+
+ // Process each expression in the ALTER TABLE statement
+ for (const expr of stmt.expr) {
+ const action = expr.action;
+
+ // Handle ADD CONSTRAINT for foreign keys
+ if (action === 'add' && expr.resource === 'constraint') {
+ const constraintDef = expr as unknown as ConstraintDefinition;
+
+ if (
+ constraintDef.constraint_type === 'foreign key' &&
+ constraintDef.reference
+ ) {
+ const reference = constraintDef.reference;
+ if (
+ reference &&
+ reference.table &&
+ reference.columns &&
+ reference.columns.length > 0
+ ) {
+ // Extract target table info
+ const targetTable = reference.table as TableReference;
+ const targetTableName = targetTable.table;
+ const targetSchemaName =
+ targetTable.schema || targetTable.db || '';
+
+ // Extract source column
+ let sourceColumnName = '';
+ if (
+ Array.isArray(constraintDef.definition) &&
+ constraintDef.definition.length > 0
+ ) {
+ const sourceColDef = constraintDef.definition[0];
+ if (
+ sourceColDef &&
+ typeof sourceColDef === 'object' &&
+ 'type' in sourceColDef &&
+ (sourceColDef as { type: string }).type ===
+ 'column_ref'
+ ) {
+ sourceColumnName = extractColumnName(
+ sourceColDef as ColumnReference
+ );
+ }
+ }
+
+ // Extract target column
+ const targetColumnName = extractColumnName(
+ reference.columns[0]
+ );
+
+ if (
+ sourceColumnName &&
+ targetTableName &&
+ targetColumnName
+ ) {
+ // Check if this relationship already exists
+ const existingRel = relationships.find(
+ (r) =>
+ r.sourceTable.toUpperCase() ===
+ tableName.toUpperCase() &&
+ r.sourceColumn.toUpperCase() ===
+ sourceColumnName.toUpperCase() &&
+ r.targetTable.toUpperCase() ===
+ targetTableName.toUpperCase()
+ );
+
+ if (!existingRel) {
+ relationships.push({
+ name:
+ constraintDef.constraint ||
+ `fk_${tableName}_${sourceColumnName}`,
+ sourceTable: tableName,
+ sourceSchema: schemaName,
+ sourceColumn: sourceColumnName,
+ targetTable: targetTableName,
+ targetSchema: targetSchemaName,
+ targetColumn: targetColumnName,
+ sourceTableId: table.id,
+ targetTableId: '', // Will be filled later
+ updateAction: reference.on_update,
+ deleteAction: reference.on_delete,
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Post-process the tables and relationships to ensure all targetTableId and sourceTableId fields are filled
+ */
+function linkRelationships(
+ tables: SQLTable[],
+ relationships: SQLForeignKey[],
+ tableMap: Record
+): SQLForeignKey[] {
+ // First, ensure all table keys are normalized
+ const normalizedTableMap: Record = {};
+ for (const [key, id] of Object.entries(tableMap)) {
+ normalizedTableMap[key.toUpperCase()] = id;
+
+ // Also add without schema for fallback
+ if (key.includes('.')) {
+ const tableName = key.split('.')[1];
+ normalizedTableMap[tableName.toUpperCase()] = id;
+ }
+ }
+
+ // Add all tables to the normalized map
+ for (const table of tables) {
+ if (table.schema) {
+ const tableKey = `${table.schema}.${table.name}`;
+ normalizedTableMap[tableKey.toUpperCase()] = table.id;
+ }
+ normalizedTableMap[table.name.toUpperCase()] = table.id;
+ }
+
+ // Process all relationships
+ const validRelationships = relationships.filter((rel) => {
+ // Normalize keys for lookup
+ const sourceTableKey = rel.sourceSchema
+ ? `${rel.sourceSchema}.${rel.sourceTable}`
+ : rel.sourceTable;
+ const targetTableKey = rel.targetSchema
+ ? `${rel.targetSchema}.${rel.targetTable}`
+ : rel.targetTable;
+
+ // Get the source table ID if it's not already set
+ if (!rel.sourceTableId || rel.sourceTableId === '') {
+ const sourceId =
+ normalizedTableMap[sourceTableKey.toUpperCase()] ||
+ normalizedTableMap[rel.sourceTable.toUpperCase()];
+
+ if (sourceId) {
+ rel.sourceTableId = sourceId;
+ } else {
+ return false;
+ }
+ }
+
+ // Get the target table ID
+ if (!rel.targetTableId || rel.targetTableId === '') {
+ const targetId =
+ normalizedTableMap[targetTableKey.toUpperCase()] ||
+ normalizedTableMap[rel.targetTable.toUpperCase()];
+
+ if (targetId) {
+ rel.targetTableId = targetId;
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ return validRelationships;
+}
+
+/**
+ * Detect if SQL content is from Oracle format
+ * @param sqlContent SQL content as string
+ * @returns boolean indicating if the SQL is likely from Oracle
+ */
+export function isOracleFormat(sqlContent: string): boolean {
+ const oracleMarkers = [
+ 'VARCHAR2',
+ 'NUMBER(',
+ 'SYSDATE',
+ 'SYSTIMESTAMP',
+ 'SYS_GUID',
+ 'GENERATED ALWAYS AS IDENTITY',
+ 'GENERATED BY DEFAULT AS IDENTITY',
+ '.NEXTVAL',
+ 'TABLESPACE',
+ 'PCTFREE',
+ 'STORAGE (',
+ 'NVARCHAR2',
+ 'CLOB',
+ 'NCLOB',
+ 'BLOB',
+ 'BFILE',
+ 'BINARY_FLOAT',
+ 'BINARY_DOUBLE',
+ 'ROWID',
+ 'XMLTYPE',
+ 'CREATE SEQUENCE',
+ 'CREATE OR REPLACE',
+ 'CONSTRAINT .* PRIMARY KEY.*ENABLE',
+ ];
+
+ // Check for specific Oracle patterns
+ for (const marker of oracleMarkers) {
+ if (marker.includes('.*')) {
+ // Handle regex patterns
+ const regex = new RegExp(marker, 'i');
+ if (regex.test(sqlContent)) {
+ return true;
+ }
+ } else if (sqlContent.toUpperCase().includes(marker.toUpperCase())) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts
index 8f6c898ef..5a7073228 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts
@@ -43,7 +43,7 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
// Check that the id column is present
const idColumn = locationTable.columns.find((col) => col.name === 'id');
expect(idColumn).toBeDefined();
- expect(idColumn?.type).toBe('BIGINT');
+ expect(idColumn?.type).toBe('bigint');
expect(idColumn?.primaryKey).toBe(true);
// Check some of the added columns
@@ -51,19 +51,19 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
(col) => col.name === 'country_id'
);
expect(countryIdColumn).toBeDefined();
- expect(countryIdColumn?.type).toBe('INTEGER');
+ expect(countryIdColumn?.type).toBe('integer');
const streetColumn = locationTable.columns.find(
(col) => col.name === 'street'
);
expect(streetColumn).toBeDefined();
- expect(streetColumn?.type).toBe('TEXT');
+ expect(streetColumn?.type).toBe('text');
const remarksColumn = locationTable.columns.find(
(col) => col.name === 'remarks'
);
expect(remarksColumn).toBeDefined();
- expect(remarksColumn?.type).toBe('TEXT');
+ expect(remarksColumn?.type).toBe('text');
});
it('should handle ALTER TABLE ADD COLUMN with schema qualification', async () => {
@@ -87,13 +87,13 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
(col) => col.name === 'email'
);
expect(emailColumn).toBeDefined();
- expect(emailColumn?.type).toBe('VARCHAR(255)');
+ expect(emailColumn?.type).toBe('varchar(255)');
const createdAtColumn = usersTable.columns.find(
(col) => col.name === 'created_at'
);
expect(createdAtColumn).toBeDefined();
- expect(createdAtColumn?.type).toBe('TIMESTAMP');
+ expect(createdAtColumn?.type).toBe('timestamp');
});
it('should handle ALTER TABLE ADD COLUMN with constraints', async () => {
@@ -130,7 +130,7 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
(col) => col.name === 'price'
);
expect(priceColumn).toBeDefined();
- expect(priceColumn?.default).toBe('0');
+ expect(priceColumn?.default).toBe('0.00');
});
it('should not add duplicate columns', async () => {
@@ -156,7 +156,7 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
(col) => col.name === 'name'
);
expect(nameColumns).toHaveLength(1);
- expect(nameColumns[0].type).toBe('VARCHAR(100)'); // Should keep original type
+ expect(nameColumns[0].type).toBe('varchar(100)'); // Should keep original type
});
it('should use default schema when not specified', async () => {
@@ -204,12 +204,12 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
(col) => col.name === 'my-column'
);
expect(myColumn).toBeDefined();
- expect(myColumn?.type).toBe('VARCHAR(50)');
+ expect(myColumn?.type).toBe('varchar(50)');
const anotherColumn = myTable.columns.find(
(col) => col.name === 'another-column'
);
expect(anotherColumn).toBeDefined();
- expect(anotherColumn?.type).toBe('INTEGER');
+ expect(anotherColumn?.type).toBe('integer');
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts
index 2296b4cb9..e097f5fb4 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts
@@ -30,15 +30,15 @@ ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254);
// Check that the columns have the updated type
const field1 = table.columns.find((col) => col.name === 'field1');
expect(field1).toBeDefined();
- expect(field1?.type).toBe('VARCHAR(254)'); // Should be updated from 200 to 254
+ expect(field1?.type).toBe('varchar(254)'); // Should be updated from 200 to 254
const field2 = table.columns.find((col) => col.name === 'field2');
expect(field2).toBeDefined();
- expect(field2?.type).toBe('VARCHAR(254)');
+ expect(field2?.type).toBe('varchar(254)');
const field3 = table.columns.find((col) => col.name === 'field3');
expect(field3).toBeDefined();
- expect(field3?.type).toBe('VARCHAR(254)');
+ expect(field3?.type).toBe('varchar(254)');
});
it('should handle various ALTER COLUMN TYPE scenarios', async () => {
@@ -65,13 +65,13 @@ ALTER TABLE test_table ALTER COLUMN score TYPE NUMERIC(10,4);
const table = result.tables[0];
const nameCol = table.columns.find((col) => col.name === 'name');
- expect(nameCol?.type).toBe('VARCHAR(100)');
+ expect(nameCol?.type).toBe('varchar(100)');
const ageCol = table.columns.find((col) => col.name === 'age');
- expect(ageCol?.type).toBe('INTEGER');
+ expect(ageCol?.type).toBe('integer');
const scoreCol = table.columns.find((col) => col.name === 'score');
- expect(scoreCol?.type).toBe('NUMERIC(10,4)');
+ expect(scoreCol?.type).toBe('numeric(10,4)');
});
it('should handle multiple type changes on the same column', async () => {
@@ -101,18 +101,18 @@ ALTER TABLE table_12 ALTER COLUMN field1 TYPE BIGINT;
expect(table.schema).toBe('public');
expect(table.columns).toHaveLength(4);
- // Check that field1 has the final type (BIGINT), not the intermediate VARCHAR(254)
+ // Check that field1 has the final type (bigint), not the intermediate varchar(254)
const field1 = table.columns.find((col) => col.name === 'field1');
expect(field1).toBeDefined();
- expect(field1?.type).toBe('BIGINT'); // Should be BIGINT, not VARCHAR(254)
+ expect(field1?.type).toBe('bigint'); // Should be bigint, not varchar(254)
- // Check that field2 and field3 still have VARCHAR(254)
+ // Check that field2 and field3 still have varchar(254)
const field2 = table.columns.find((col) => col.name === 'field2');
expect(field2).toBeDefined();
- expect(field2?.type).toBe('VARCHAR(254)');
+ expect(field2?.type).toBe('varchar(254)');
const field3 = table.columns.find((col) => col.name === 'field3');
expect(field3).toBeDefined();
- expect(field3?.type).toBe('VARCHAR(254)');
+ expect(field3?.type).toBe('varchar(254)');
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-core.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-core.test.ts
index 21ba583d4..8fe2c4374 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-core.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-core.test.ts
@@ -1,5 +1,10 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import { fromPostgres } from '../postgresql';
+import { sqlImportToDiagram } from '@/lib/data/sql-import';
+import { DatabaseType } from '@/lib/domain/database-type';
+import * as dataTypes from '@/lib/data/data-types/data-types';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
describe('PostgreSQL Core Parser Tests', () => {
it('should parse basic tables', async () => {
@@ -455,4 +460,148 @@ CREATE TRIGGER arcane_audit_apprentices AFTER INSERT OR UPDATE OR DELETE ON appr
expect(fks.length).toBeGreaterThan(0);
expect(fks[0].targetTable).toBe('base');
});
+
+ describe('Type Synonym Resolution', () => {
+ it('should call getPreferredSynonym for PostgreSQL types and use resolved types', async () => {
+ // Spy on getPreferredSynonym
+ const getPreferredSynonymSpy = vi.spyOn(
+ dataTypes,
+ 'getPreferredSynonym'
+ );
+
+ // Mock return value for 'character varying' -> 'varchar'
+ getPreferredSynonymSpy.mockImplementation(
+ (typeName, databaseType) => {
+ if (
+ typeName === 'character varying' &&
+ databaseType === DatabaseType.POSTGRESQL
+ ) {
+ return {
+ id: 'varchar',
+ name: 'varchar',
+ fieldAttributes: { hasCharMaxLength: true },
+ usageLevel: 1,
+ } as const;
+ }
+ if (
+ typeName === 'integer' &&
+ databaseType === DatabaseType.POSTGRESQL
+ ) {
+ return {
+ id: 'int',
+ name: 'int',
+ usageLevel: 1,
+ } as const;
+ }
+ return null;
+ }
+ );
+
+ const sql = `
+ CREATE TABLE users (
+ id INTEGER PRIMARY KEY,
+ name CHARACTER VARYING(255),
+ email CHARACTER VARYING(100)
+ );
+ `;
+
+ const diagram = await sqlImportToDiagram({
+ sqlContent: sql,
+ sourceDatabaseType: DatabaseType.POSTGRESQL,
+ targetDatabaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify getPreferredSynonym was called
+ expect(getPreferredSynonymSpy).toHaveBeenCalled();
+ expect(getPreferredSynonymSpy).toHaveBeenCalledWith(
+ 'integer',
+ DatabaseType.POSTGRESQL
+ );
+ expect(getPreferredSynonymSpy).toHaveBeenCalledWith(
+ 'varchar',
+ DatabaseType.POSTGRESQL
+ );
+
+ // Verify the resolved types were used in the diagram
+ const usersTable = diagram.tables?.find(
+ (t: DBTable) => t.name === 'users'
+ );
+ expect(usersTable).toBeDefined();
+
+ const idField = usersTable?.fields.find(
+ (f: DBField) => f.name === 'id'
+ );
+ expect(idField?.type.id).toBe('int');
+ expect(idField?.type.name).toBe('int');
+
+ const nameField = usersTable?.fields.find(
+ (f: DBField) => f.name === 'name'
+ );
+ expect(nameField?.type.id).toBe('varchar');
+ expect(nameField?.type.name).toBe('varchar');
+
+ const emailField = usersTable?.fields.find(
+ (f: DBField) => f.name === 'email'
+ );
+ expect(emailField?.type.id).toBe('varchar');
+ expect(emailField?.type.name).toBe('varchar');
+
+ // Restore the original implementation
+ getPreferredSynonymSpy.mockRestore();
+ });
+ });
+
+ describe('Primary Key Uniqueness', () => {
+ it('should mark single-column primary key field as unique', async () => {
+ const sql = `
+CREATE SCHEMA IF NOT EXISTS "public";
+
+CREATE TABLE "public"."table_1" (
+ "id" bigint NOT NULL,
+ CONSTRAINT "pk_table_1_id" PRIMARY KEY ("id")
+);
+ `;
+
+ const result = await fromPostgres(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(true);
+ });
+
+ it('should not mark composite primary key fields as unique individually', async () => {
+ const sql = `
+CREATE SCHEMA IF NOT EXISTS "public";
+
+CREATE TABLE "public"."table_1" (
+ "id" bigint NOT NULL,
+ "field_2" bigint NOT NULL,
+ CONSTRAINT "pk_table_1_id" PRIMARY KEY ("id", "field_2")
+);
+ `;
+
+ const result = await fromPostgres(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(false);
+
+ const field2Column = table.columns.find(
+ (c) => c.name === 'field_2'
+ );
+ expect(field2Column).toBeDefined();
+ expect(field2Column?.primaryKey).toBe(true);
+ expect(field2Column?.unique).toBe(false);
+ });
+ });
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts
index 91034b792..eddddc901 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts
@@ -19,7 +19,7 @@ describe('PostgreSQL Parser', () => {
expect(result.tables[0].name).toBe('wizards');
expect(result.tables[0].columns).toHaveLength(4);
expect(result.tables[0].columns[0].name).toBe('id');
- expect(result.tables[0].columns[0].type).toBe('INTEGER');
+ expect(result.tables[0].columns[0].type).toBe('integer');
expect(result.tables[0].columns[0].primaryKey).toBe(true);
});
@@ -81,9 +81,9 @@ describe('PostgreSQL Parser', () => {
expect(result.tables).toHaveLength(1);
const columns = result.tables[0].columns;
- expect(columns.find((c) => c.name === 'id')?.type).toBe('UUID');
- expect(columns.find((c) => c.name === 'data')?.type).toBe('JSONB');
- expect(columns.find((c) => c.name === 'tags')?.type).toBe('TEXT[]');
+ expect(columns.find((c) => c.name === 'id')?.type).toBe('uuid');
+ expect(columns.find((c) => c.name === 'data')?.type).toBe('jsonb');
+ expect(columns.find((c) => c.name === 'tags')?.type).toBe('text[]');
});
it('should handle numeric with precision', async () => {
@@ -102,7 +102,7 @@ describe('PostgreSQL Parser', () => {
const columns = result.tables[0].columns;
// Parser limitation: scale on separate line is not captured
const amountType = columns.find((c) => c.name === 'amount')?.type;
- expect(amountType).toMatch(/^NUMERIC/);
+ expect(amountType).toMatch(/^numeric/);
});
it('should handle multi-line numeric definitions', async () => {
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts
index cc54b3a06..22a1efa17 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts
@@ -196,4 +196,55 @@ CREATE TABLE patients(
expect(result.relationships).toHaveLength(14);
});
+
+ it('should preserve UNIQUE constraint and correct PRIMARY KEY on import', async () => {
+ // Regression test: When importing a table with:
+ // 1. A column with inline UNIQUE (not part of PK)
+ // 2. A composite PRIMARY KEY on different columns
+ // The import should correctly identify which fields are PK vs just UNIQUE
+
+ const inputSql = `
+CREATE TABLE "public"."orders_copy" (
+ "id" bigserial NOT NULL UNIQUE,
+ "created_at" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "user_id" bigint NOT NULL,
+ "customer_id" bigint,
+ CONSTRAINT "orders_pkey" PRIMARY KEY ("user_id", "customer_id")
+);`;
+
+ const result = await fromPostgres(inputSql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+
+ expect(table.name).toBe('orders_copy');
+ expect(table.columns).toBeDefined();
+
+ // Verify field properties
+ const idField = table.columns.find((f) => f.name === 'id');
+ const userIdField = table.columns.find((f) => f.name === 'user_id');
+ const customerIdField = table.columns.find(
+ (f) => f.name === 'customer_id'
+ );
+
+ expect(idField).toBeDefined();
+ expect(userIdField).toBeDefined();
+ expect(customerIdField).toBeDefined();
+
+ // id should be UNIQUE but NOT a primary key
+ expect(idField!.unique).toBe(true);
+ expect(idField!.primaryKey).toBe(false);
+
+ // user_id and customer_id should be primary keys
+ expect(userIdField!.primaryKey).toBe(true);
+ expect(customerIdField!.primaryKey).toBe(true);
+
+ // Verify no field other than user_id and customer_id is marked as primary key
+ const pkFields = table.columns.filter((f) => f.primaryKey);
+ expect(pkFields).toHaveLength(2);
+ expect(pkFields.map((f) => f.name).sort()).toEqual([
+ 'customer_id',
+ 'user_id',
+ ]);
+ });
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts
index 000a73528..f1623714e 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts
@@ -27,55 +27,55 @@ CREATE TABLE public.activities (
// Check each column
const columns = table.columns;
- // id column - serial4 should become INTEGER with auto-increment
+ // id column - serial4 is preserved as serial with auto-increment
const idCol = columns.find((c) => c.name === 'id');
expect(idCol).toBeDefined();
- expect(idCol?.type).toBe('INTEGER');
+ expect(idCol?.type).toBe('serial');
expect(idCol?.primaryKey).toBe(true);
expect(idCol?.increment).toBe(true);
expect(idCol?.nullable).toBe(false);
- // user_id column - int4 should become INTEGER
+ // user_id column - int4 becomes integer
const userIdCol = columns.find((c) => c.name === 'user_id');
expect(userIdCol).toBeDefined();
- expect(userIdCol?.type).toBe('INTEGER');
+ expect(userIdCol?.type).toBe('integer');
expect(userIdCol?.nullable).toBe(false);
// workflow_id column - int4 NULL
const workflowIdCol = columns.find((c) => c.name === 'workflow_id');
expect(workflowIdCol).toBeDefined();
- expect(workflowIdCol?.type).toBe('INTEGER');
+ expect(workflowIdCol?.type).toBe('integer');
expect(workflowIdCol?.nullable).toBe(true);
// task_id column - int4 NULL
const taskIdCol = columns.find((c) => c.name === 'task_id');
expect(taskIdCol).toBeDefined();
- expect(taskIdCol?.type).toBe('INTEGER');
+ expect(taskIdCol?.type).toBe('integer');
expect(taskIdCol?.nullable).toBe(true);
- // action column - character varying(50)
+ // action column - character varying(50) becomes varchar(50)
const actionCol = columns.find((c) => c.name === 'action');
expect(actionCol).toBeDefined();
- expect(actionCol?.type).toBe('VARCHAR(50)');
+ expect(actionCol?.type).toBe('varchar(50)');
expect(actionCol?.nullable).toBe(false);
// description column - text
const descriptionCol = columns.find((c) => c.name === 'description');
expect(descriptionCol).toBeDefined();
- expect(descriptionCol?.type).toBe('TEXT');
+ expect(descriptionCol?.type).toBe('text');
expect(descriptionCol?.nullable).toBe(false);
// created_at column - timestamp with default
const createdAtCol = columns.find((c) => c.name === 'created_at');
expect(createdAtCol).toBeDefined();
- expect(createdAtCol?.type).toBe('TIMESTAMP');
+ expect(createdAtCol?.type).toBe('timestamp');
expect(createdAtCol?.nullable).toBe(false);
expect(createdAtCol?.default).toContain('NOW');
- // is_read column - bool with default
+ // is_read column - bool becomes boolean with default
const isReadCol = columns.find((c) => c.name === 'is_read');
expect(isReadCol).toBeDefined();
- expect(isReadCol?.type).toBe('BOOLEAN');
+ expect(isReadCol?.type).toBe('boolean');
expect(isReadCol?.nullable).toBe(false);
expect(isReadCol?.default).toBe('FALSE');
});
@@ -106,44 +106,46 @@ CREATE TABLE type_test (
const table = result.tables[0];
const cols = table.columns;
- // Check serial types
- expect(cols.find((c) => c.name === 'id')?.type).toBe('INTEGER');
+ // Check serial types - preserved as serial, smallserial, bigserial
+ expect(cols.find((c) => c.name === 'id')?.type).toBe('serial');
expect(cols.find((c) => c.name === 'id')?.increment).toBe(true);
- expect(cols.find((c) => c.name === 'small_id')?.type).toBe('SMALLINT');
+ expect(cols.find((c) => c.name === 'small_id')?.type).toBe(
+ 'smallserial'
+ );
expect(cols.find((c) => c.name === 'small_id')?.increment).toBe(true);
- expect(cols.find((c) => c.name === 'big_id')?.type).toBe('BIGINT');
+ expect(cols.find((c) => c.name === 'big_id')?.type).toBe('bigserial');
expect(cols.find((c) => c.name === 'big_id')?.increment).toBe(true);
- // Check integer types
- expect(cols.find((c) => c.name === 'int_col')?.type).toBe('INTEGER');
- expect(cols.find((c) => c.name === 'small_int')?.type).toBe('SMALLINT');
- expect(cols.find((c) => c.name === 'big_int')?.type).toBe('BIGINT');
+ // Check integer types - normalized to lowercase
+ expect(cols.find((c) => c.name === 'int_col')?.type).toBe('integer');
+ expect(cols.find((c) => c.name === 'small_int')?.type).toBe('smallint');
+ expect(cols.find((c) => c.name === 'big_int')?.type).toBe('bigint');
- // Check boolean types
- expect(cols.find((c) => c.name === 'bool_col')?.type).toBe('BOOLEAN');
+ // Check boolean types - normalized to lowercase
+ expect(cols.find((c) => c.name === 'bool_col')?.type).toBe('boolean');
expect(cols.find((c) => c.name === 'boolean_col')?.type).toBe(
- 'BOOLEAN'
+ 'boolean'
);
- // Check string types
+ // Check string types - normalized to lowercase
expect(cols.find((c) => c.name === 'varchar_col')?.type).toBe(
- 'VARCHAR(100)'
+ 'varchar(100)'
);
- expect(cols.find((c) => c.name === 'char_col')?.type).toBe('CHAR(10)');
- expect(cols.find((c) => c.name === 'text_col')?.type).toBe('TEXT');
+ expect(cols.find((c) => c.name === 'char_col')?.type).toBe('char(10)');
+ expect(cols.find((c) => c.name === 'text_col')?.type).toBe('text');
- // Check timestamp types
+ // Check timestamp types - normalized to lowercase
expect(cols.find((c) => c.name === 'timestamp_col')?.type).toBe(
- 'TIMESTAMP'
+ 'timestamp'
);
expect(cols.find((c) => c.name === 'timestamptz_col')?.type).toBe(
- 'TIMESTAMPTZ'
+ 'timestamptz'
);
- // Check other types
- expect(cols.find((c) => c.name === 'date_col')?.type).toBe('DATE');
- expect(cols.find((c) => c.name === 'time_col')?.type).toBe('TIME');
- expect(cols.find((c) => c.name === 'json_col')?.type).toBe('JSON');
- expect(cols.find((c) => c.name === 'jsonb_col')?.type).toBe('JSONB');
+ // Check other types - normalized to lowercase
+ expect(cols.find((c) => c.name === 'date_col')?.type).toBe('date');
+ expect(cols.find((c) => c.name === 'time_col')?.type).toBe('time');
+ expect(cols.find((c) => c.name === 'json_col')?.type).toBe('json');
+ expect(cols.find((c) => c.name === 'jsonb_col')?.type).toBe('jsonb');
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-alter-table-foreign-keys.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-alter-table-foreign-keys.test.ts
index a231d55c2..c0c444a5a 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-alter-table-foreign-keys.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-alter-table-foreign-keys.test.ts
@@ -163,14 +163,6 @@ ALTER TABLE ONLY "wizard_resident" ADD CONSTRAINT "wizard_tower_fk2" FOREIGN KEY
const result = await fromPostgres(sql);
- console.log('Relationships found:', result.relationships.length);
- result.relationships.forEach((rel, i) => {
- console.log(
- `FK ${i + 1}: ${rel.sourceTable}.${rel.sourceColumn} -> ${rel.targetTable}.${rel.targetColumn}`
- );
- });
- console.log('Warnings:', result.warnings);
-
expect(result.tables).toHaveLength(2);
// At least one relationship should be found (the regex fallback should catch at least one)
@@ -205,9 +197,6 @@ ALTER TABLE ONLY "public"."account_emailaddress" ADD CONSTRAINT "account_emailad
const result = await fromPostgres(sql);
- console.log('Warnings:', result.warnings);
- console.log('Relationships:', result.relationships);
-
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-array-type-conversion.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-array-type-conversion.test.ts
new file mode 100644
index 000000000..c6435a1a3
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-array-type-conversion.test.ts
@@ -0,0 +1,171 @@
+import { describe, it, expect } from 'vitest';
+import { fromPostgres } from '../postgresql';
+import { convertToChartDBDiagram } from '../../../common';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('Array Type Conversion', () => {
+ it('should correctly parse and convert array types with isArray flag', async () => {
+ const sql = `
+CREATE TABLE test_arrays (
+ id bigint NOT NULL PRIMARY KEY,
+ int_array int[],
+ text_array text[],
+ varchar_array varchar(255)[],
+ jsonb_data jsonb,
+ regular_int int
+);
+
+CREATE INDEX idx_int_array ON test_arrays USING GIN (int_array);
+`;
+
+ // Parse SQL
+ const parserResult = await fromPostgres(sql);
+
+ // Verify parser correctly captures array notation in type string
+ const intArrayCol = parserResult.tables[0].columns.find(
+ (c) => c.name === 'int_array'
+ );
+ // The parser normalizes int to integer, but should preserve []
+ expect(intArrayCol?.type).toMatch(/\[\]$/);
+
+ const textArrayCol = parserResult.tables[0].columns.find(
+ (c) => c.name === 'text_array'
+ );
+ expect(textArrayCol?.type).toBe('text[]');
+
+ const varcharArrayCol = parserResult.tables[0].columns.find(
+ (c) => c.name === 'varchar_array'
+ );
+ expect(varcharArrayCol?.type).toBe('varchar(255)[]');
+
+ // Convert to diagram
+ const diagram = convertToChartDBDiagram(
+ parserResult,
+ DatabaseType.POSTGRESQL,
+ DatabaseType.POSTGRESQL
+ );
+
+ const table = diagram.tables?.find((t) => t.name === 'test_arrays');
+ expect(table).toBeDefined();
+
+ // Check int[] field - should have isArray=true and base type (PostgreSQL uses 'int' as the canonical form)
+ const intArrayField = table!.fields.find((f) => f.name === 'int_array');
+ expect(intArrayField).toBeDefined();
+ expect(intArrayField!.isArray).toBe(true);
+ expect(intArrayField!.type.id).toBe('int');
+
+ // Check text[] field - should have isArray=true and type=text
+ const textArrayField = table!.fields.find(
+ (f) => f.name === 'text_array'
+ );
+ expect(textArrayField).toBeDefined();
+ expect(textArrayField!.isArray).toBe(true);
+ expect(textArrayField!.type.id).toBe('text');
+
+ // Check varchar[] field - should have isArray=true and type=varchar
+ const varcharArrayField = table!.fields.find(
+ (f) => f.name === 'varchar_array'
+ );
+ expect(varcharArrayField).toBeDefined();
+ expect(varcharArrayField!.isArray).toBe(true);
+ expect(varcharArrayField!.type.id).toBe('varchar');
+
+ // Check regular jsonb field - should NOT have isArray
+ const jsonbField = table!.fields.find((f) => f.name === 'jsonb_data');
+ expect(jsonbField).toBeDefined();
+ expect(jsonbField!.isArray).toBeUndefined();
+ expect(jsonbField!.type.id).toBe('jsonb');
+
+ // Check regular int field - should NOT have isArray (PostgreSQL uses 'int' as the canonical form)
+ const regularIntField = table!.fields.find(
+ (f) => f.name === 'regular_int'
+ );
+ expect(regularIntField).toBeDefined();
+ expect(regularIntField!.isArray).toBeUndefined();
+ expect(regularIntField!.type.id).toBe('int');
+ });
+
+ it('should handle multi-dimensional arrays', async () => {
+ const sql = `
+CREATE TABLE matrix_data (
+ id serial PRIMARY KEY,
+ matrix int[][]
+);
+`;
+
+ const parserResult = await fromPostgres(sql);
+
+ // Check parser captures the type with array notation
+ const matrixCol = parserResult.tables[0].columns.find(
+ (c) => c.name === 'matrix'
+ );
+ // Multi-dimensional arrays should still have array notation
+ expect(matrixCol?.type).toMatch(/\[\]/);
+ });
+
+ it('should correctly parse GIN index type from USING clause', async () => {
+ const sql = `
+CREATE TABLE test_gin_index (
+ id bigint NOT NULL PRIMARY KEY,
+ tags text[]
+);
+
+CREATE INDEX idx_tags ON test_gin_index USING GIN (tags);
+CREATE INDEX idx_tags_btree ON test_gin_index USING BTREE (id);
+CREATE INDEX idx_tags_hash ON test_gin_index USING HASH (id);
+`;
+
+ // Parse SQL
+ const parserResult = await fromPostgres(sql);
+
+ // Check that the parser captures the index type
+ const table = parserResult.tables[0];
+ expect(table.indexes).toHaveLength(3);
+
+ const ginIndex = table.indexes.find((idx) => idx.name === 'idx_tags');
+ expect(ginIndex).toBeDefined();
+ expect(ginIndex!.type).toBe('gin');
+
+ const btreeIndex = table.indexes.find(
+ (idx) => idx.name === 'idx_tags_btree'
+ );
+ expect(btreeIndex).toBeDefined();
+ expect(btreeIndex!.type).toBe('btree');
+
+ const hashIndex = table.indexes.find(
+ (idx) => idx.name === 'idx_tags_hash'
+ );
+ expect(hashIndex).toBeDefined();
+ expect(hashIndex!.type).toBe('hash');
+
+ // Convert to diagram and verify index types are preserved
+ const diagram = convertToChartDBDiagram(
+ parserResult,
+ DatabaseType.POSTGRESQL,
+ DatabaseType.POSTGRESQL
+ );
+
+ const diagramTable = diagram.tables?.find(
+ (t) => t.name === 'test_gin_index'
+ );
+ expect(diagramTable).toBeDefined();
+
+ const diagramGinIndex = diagramTable!.indexes.find(
+ (idx) => idx.name === 'idx_tags'
+ );
+ expect(diagramGinIndex).toBeDefined();
+ expect(diagramGinIndex!.type).toBe('gin');
+
+ const diagramBtreeIndex = diagramTable!.indexes.find(
+ (idx) => idx.name === 'idx_tags_btree'
+ );
+ expect(diagramBtreeIndex).toBeDefined();
+ expect(diagramBtreeIndex!.type).toBe('btree');
+
+ const diagramHashIndex = diagramTable!.indexes.find(
+ (idx) => idx.name === 'idx_tags_hash'
+ );
+ expect(diagramHashIndex).toBeDefined();
+ expect(diagramHashIndex!.type).toBe('hash');
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-comment-before-table.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-comment-before-table.test.ts
index 8bc6a5c68..7d52670a9 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-comment-before-table.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-comment-before-table.test.ts
@@ -13,13 +13,6 @@ CREATE TABLE crystal_enchantments (
const result = await fromPostgres(sql);
- console.log('\nDebug info:');
- console.log('Tables found:', result.tables.length);
- console.log(
- 'Table names:',
- result.tables.map((t) => t.name)
- );
-
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('crystal_enchantments');
expect(result.tables[0].columns).toHaveLength(2);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complete-database-import.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complete-database-import.test.ts
index 1824392bb..34473b29d 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complete-database-import.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complete-database-import.test.ts
@@ -180,36 +180,26 @@ CREATE TABLE rewards (
);`;
const result = await fromPostgres(sql);
- console.log('\nParsing results:');
- console.log(`- Tables found: ${result.tables.length}`);
- console.log(`- Enums found: ${result.enums?.length || 0}`);
- console.log(`- Warnings: ${result.warnings?.length || 0}`);
-
// List all table names
const tableNames = result.tables.map((t) => t.name).sort();
- console.log('\nTable names:');
- tableNames.forEach((name, i) => {
- console.log(` ${i + 1}. ${name}`);
- });
// Should have all 20 tables
expect(result.tables).toHaveLength(20);
+ // Verify parsing metadata
+ expect(result.enums).toHaveLength(5);
+
// Check for quest_sample_rewards specifically
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
expect(questSampleRewards).toBeDefined();
+ expect(questSampleRewards!.columns).toHaveLength(2);
- if (questSampleRewards) {
- console.log('\nquest_sample_rewards table details:');
- console.log(`- Columns: ${questSampleRewards.columns.length}`);
- questSampleRewards.columns.forEach((col) => {
- console.log(
- ` - ${col.name}: ${col.type} (nullable: ${col.nullable})`
- );
- });
- }
+ // Verify quest_sample_rewards columns
+ const qsrColumnNames = questSampleRewards!.columns.map((c) => c.name);
+ expect(qsrColumnNames).toContain('quest_template_id');
+ expect(qsrColumnNames).toContain('reward_id');
// Expected tables
const expectedTables = [
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complex-enum-scenarios.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complex-enum-scenarios.test.ts
index 0ac3387a0..5d5fd62fb 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complex-enum-scenarios.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-complex-enum-scenarios.test.ts
@@ -62,15 +62,6 @@ ALTER TABLE "spells" ADD CONSTRAINT "spells_wizard_id_wizard_id_fk"
const result = await fromPostgres(sql);
- // Check enum parsing
- console.log('\n=== ENUMS FOUND ===');
- console.log('Count:', result.enums?.length || 0);
- if (result.enums) {
- result.enums.forEach((e) => {
- console.log(` - ${e.name}: ${e.values.length} values`);
- });
- }
-
// Should find all 7 enums
expect(result.enums).toHaveLength(7);
@@ -97,11 +88,6 @@ ALTER TABLE "spells" ADD CONSTRAINT "spells_wizard_id_wizard_id_fk"
'mythic',
]);
- // Check table parsing
- console.log('\n=== TABLES FOUND ===');
- console.log('Count:', result.tables.length);
- console.log('Names:', result.tables.map((t) => t.name).join(', '));
-
// Should find all 4 tables
expect(result.tables).toHaveLength(4);
expect(result.tables.map((t) => t.name).sort()).toEqual([
@@ -111,15 +97,6 @@ ALTER TABLE "spells" ADD CONSTRAINT "spells_wizard_id_wizard_id_fk"
'wizard_account',
]);
- // Check warnings for syntax issues
- console.log('\n=== WARNINGS ===');
- console.log('Count:', result.warnings?.length || 0);
- if (result.warnings) {
- result.warnings.forEach((w) => {
- console.log(` - ${w}`);
- });
- }
-
// Should have warnings about custom types and parsing failures
expect(result.warnings).toBeDefined();
expect(result.warnings!.length).toBeGreaterThan(0);
@@ -150,8 +127,7 @@ CREATE TABLE "dragons" (
expect(result.enums).toHaveLength(1);
expect(result.enums?.[0].name).toBe('dragon_element');
- // Table might have issues due to missing space
- console.log('Tables:', result.tables.length);
- console.log('Warnings:', result.warnings);
+ // Table might succeed or fail due to missing space syntax
+ // The important thing is the enum was still parsed correctly
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-bonds-junction-table.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-bonds-junction-table.test.ts
index 4c973fa59..eff69841f 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-bonds-junction-table.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-bonds-junction-table.test.ts
@@ -11,19 +11,8 @@ CREATE TABLE dragon_bonds (
PRIMARY KEY (dragon_master_id, dragon_id)
);`;
- console.log('Testing with SQL:', sql);
-
const result = await fromPostgres(sql);
- console.log('Result:', {
- tableCount: result.tables.length,
- tables: result.tables.map((t) => ({
- name: t.name,
- columns: t.columns.length,
- })),
- warnings: result.warnings,
- });
-
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('dragon_bonds');
});
@@ -60,11 +49,6 @@ CREATE TABLE dragon_bonds (
const result = await fromPostgres(sql);
- console.log('With dependencies:', {
- tableCount: result.tables.length,
- tableNames: result.tables.map((t) => t.name),
- });
-
expect(result.tables).toHaveLength(3);
const dragonBonds = result.tables.find(
(t) => t.name === 'dragon_bonds'
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-status-enum.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-status-enum.test.ts
index 6ed744064..367431d56 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-status-enum.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-dragon-status-enum.test.ts
@@ -49,11 +49,6 @@ CREATE TABLE dragons (
const result = await fromPostgres(sql);
- console.log(
- 'Parsed enums:',
- result.enums?.map((e) => e.name)
- );
-
expect(result.enums).toHaveLength(3);
// Specifically check for dragon_status
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-complete.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-complete.test.ts
index e7f85799d..1bd4dda9a 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-complete.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-complete.test.ts
@@ -36,17 +36,19 @@ CREATE TABLE dragon_quests (
// Parse the SQL
const result = await fromPostgres(sql);
- // Check enums
- console.log('\nEnum parsing results:');
- console.log(`Found ${result.enums?.length || 0} enum types`);
+ // Convert to diagram
+ const diagram = convertToChartDBDiagram(
+ result,
+ DatabaseType.POSTGRESQL,
+ DatabaseType.POSTGRESQL
+ );
- if (result.enums) {
- result.enums.forEach((e) => {
- console.log(` - ${e.name}: ${e.values.length} values`);
- });
- }
+ // Assertions
+ expect(result.enums).toBeDefined();
+ expect(result.enums).toHaveLength(5);
+ expect(diagram.customTypes).toHaveLength(5);
- // Expected enums
+ // Verify all expected enums are present
const expectedEnums = [
'wizard_rank',
'spell_frequency',
@@ -54,107 +56,41 @@ CREATE TABLE dragon_quests (
'quest_status',
'dragon_mood',
];
+ const foundEnumNames = result.enums!.map((e) => e.name);
+ expectedEnums.forEach((enumName) => {
+ expect(foundEnumNames).toContain(enumName);
+ });
- // Check which are missing
- const foundEnumNames = result.enums?.map((e) => e.name) || [];
- const missingEnums = expectedEnums.filter(
- (e) => !foundEnumNames.includes(e)
- );
-
- if (missingEnums.length > 0) {
- console.log('\nMissing enums:', missingEnums);
-
- // Let's check if they're in the SQL at all
- missingEnums.forEach((enumName) => {
- const regex = new RegExp(`CREATE\\s+TYPE\\s+${enumName}`, 'i');
- if (regex.test(sql)) {
- console.log(
- ` ${enumName} exists in SQL but wasn't parsed`
- );
-
- // Find the line
- const lines = sql.split('\n');
- const lineIndex = lines.findIndex((line) =>
- regex.test(line)
- );
- if (lineIndex !== -1) {
- console.log(
- ` Line ${lineIndex + 1}: ${lines[lineIndex].trim()}`
- );
- }
- }
- });
- }
-
- // Convert to diagram
- const diagram = convertToChartDBDiagram(
- result,
- DatabaseType.POSTGRESQL,
- DatabaseType.POSTGRESQL
- );
-
- // Check custom types in diagram
- console.log(
- '\nCustom types in diagram:',
- diagram.customTypes?.length || 0
+ // Check that wizard_rank is present with correct values
+ const wizardRankEnum = result.enums!.find(
+ (e) => e.name === 'wizard_rank'
);
+ expect(wizardRankEnum).toBeDefined();
+ expect(wizardRankEnum!.values).toHaveLength(5);
- // Check wizards table
+ // Check that the rank field uses wizard_rank type
const wizardsTable = diagram.tables?.find((t) => t.name === 'wizards');
- if (wizardsTable) {
- console.log('\nWizards table:');
- const rankField = wizardsTable.fields.find(
- (f) => f.name === 'rank'
- );
- if (rankField) {
- console.log(
- ` rank field type: ${rankField.type.name} (id: ${rankField.type.id})`
- );
- }
- }
+ expect(wizardsTable).toBeDefined();
+ const rankField = wizardsTable!.fields.find((f) => f.name === 'rank');
+ expect(rankField).toBeDefined();
+ expect(rankField!.type.name.toLowerCase()).toBe('wizard_rank');
- // Check spellbooks table
+ // Check spellbooks table enum fields
const spellbooksTable = diagram.tables?.find(
(t) => t.name === 'spellbooks'
);
- if (spellbooksTable) {
- console.log('\nSpellbooks table:');
- const frequencyField = spellbooksTable.fields.find(
- (f) => f.name === 'cast_frequency'
- );
- if (frequencyField) {
- console.log(
- ` cast_frequency field type: ${frequencyField.type.name}`
- );
- }
-
- const schoolField = spellbooksTable.fields.find(
- (f) => f.name === 'primary_school'
- );
- if (schoolField) {
- console.log(
- ` primary_school field type: ${schoolField.type.name}`
- );
- }
- }
-
- // Assertions
- expect(result.enums).toBeDefined();
- expect(result.enums).toHaveLength(5);
- expect(diagram.customTypes).toHaveLength(5);
+ expect(spellbooksTable).toBeDefined();
- // Check that wizard_rank is present
- const wizardRankEnum = result.enums!.find(
- (e) => e.name === 'wizard_rank'
+ const frequencyField = spellbooksTable!.fields.find(
+ (f) => f.name === 'cast_frequency'
);
- expect(wizardRankEnum).toBeDefined();
+ expect(frequencyField).toBeDefined();
+ expect(frequencyField!.type.name.toLowerCase()).toBe('spell_frequency');
- // Check that the rank field uses wizard_rank type
- if (wizardsTable) {
- const rankField = wizardsTable.fields.find(
- (f) => f.name === 'rank'
- );
- expect(rankField?.type.name.toLowerCase()).toBe('wizard_rank');
- }
+ const schoolField = spellbooksTable!.fields.find(
+ (f) => f.name === 'primary_school'
+ );
+ expect(schoolField).toBeDefined();
+ expect(schoolField!.type.name.toLowerCase()).toBe('magic_school');
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-with-mixed-quotes.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-with-mixed-quotes.test.ts
index b0b8083ce..73e6b4433 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-with-mixed-quotes.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enum-with-mixed-quotes.test.ts
@@ -12,25 +12,16 @@ CREATE TABLE spells (
const result = await fromPostgres(sql);
- console.log('Spells table result:', {
- tableCount: result.tables.length,
- columns: result.tables[0]?.columns.map((c) => ({
- name: c.name,
- type: c.type,
- })),
- });
-
expect(result.tables).toHaveLength(1);
const spellsTable = result.tables[0];
expect(spellsTable.name).toBe('spells');
-
- // Debug: list all columns found
- console.log('Columns found:', spellsTable.columns.length);
- spellsTable.columns.forEach((col, idx) => {
- console.log(` ${idx + 1}. ${col.name}: ${col.type}`);
- });
-
expect(spellsTable.columns).toHaveLength(3);
+
+ // Verify all columns are present
+ const columnNames = spellsTable.columns.map((c) => c.name);
+ expect(columnNames).toContain('id');
+ expect(columnNames).toContain('description');
+ expect(columnNames).toContain('category');
});
it('should handle magical enum types with mixed quotes', async () => {
@@ -38,11 +29,6 @@ CREATE TABLE spells (
const result = await fromPostgres(sql);
- console.log('Enum result:', {
- enumCount: result.enums?.length || 0,
- values: result.enums?.[0]?.values,
- });
-
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(1);
expect(result.enums![0].values).toEqual([
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enums-with-table-usage.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enums-with-table-usage.test.ts
index b4575cbc5..aa2dc3535 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enums-with-table-usage.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-enums-with-table-usage.test.ts
@@ -22,14 +22,6 @@ CREATE TABLE spellbooks (
const result = await fromPostgres(sql);
- // Debug output
- console.log('Enums found:', result.enums?.length || 0);
- if (result.enums) {
- result.enums.forEach((e) => {
- console.log(` - ${e.name}`);
- });
- }
-
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-extension-type.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-extension-type.test.ts
index 7ef406347..5507ff1df 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-extension-type.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-extension-type.test.ts
@@ -32,48 +32,19 @@ CREATE TABLE creature_abilities (
);
`;
- console.log(
- 'Testing PostgreSQL parser with CREATE EXTENSION and CREATE TYPE...\n'
- );
-
- try {
- const result = await fromPostgres(testSQL);
-
- console.log('Parse successful!');
- console.log('\nTables found:', result.tables.length);
- result.tables.forEach((table) => {
- console.log(`\n- Table: ${table.name}`);
- console.log(' Columns:');
- table.columns.forEach((col) => {
- console.log(
- ` - ${col.name}: ${col.type}${col.nullable ? '' : ' NOT NULL'}${col.primaryKey ? ' PRIMARY KEY' : ''}`
- );
- });
- });
-
- console.log('\nRelationships found:', result.relationships.length);
- result.relationships.forEach((rel) => {
- console.log(
- `- ${rel.sourceTable}.${rel.sourceColumn} -> ${rel.targetTable}.${rel.targetColumn}`
- );
- });
-
- if (result.warnings && result.warnings.length > 0) {
- console.log('\nWarnings:');
- result.warnings.forEach((warning) => {
- console.log(`- ${warning}`);
- });
- }
-
- // Basic assertions
- expect(result.tables.length).toBe(2);
- expect(result.tables[0].name).toBe('mystical_creatures');
- expect(result.tables[1].name).toBe('creature_abilities');
- expect(result.relationships.length).toBe(1);
- } catch (error) {
- console.error('Error parsing SQL:', (error as Error).message);
- console.error('\nStack trace:', (error as Error).stack);
- throw error;
- }
+ const result = await fromPostgres(testSQL);
+
+ // Basic assertions
+ expect(result.tables.length).toBe(2);
+ expect(result.tables[0].name).toBe('mystical_creatures');
+ expect(result.tables[1].name).toBe('creature_abilities');
+ expect(result.relationships.length).toBe(1);
+
+ // Verify enums are parsed
+ expect(result.enums).toHaveLength(2);
+ expect(result.enums?.map((e) => e.name).sort()).toEqual([
+ 'creature_alignment',
+ 'magic_school',
+ ]);
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-find-junction-table-in-file.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-find-junction-table-in-file.test.ts
index 971df2bb6..79abab93f 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-find-junction-table-in-file.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-find-junction-table-in-file.test.ts
@@ -140,64 +140,30 @@ CREATE TABLE guild_master_actions (
// First, verify the table exists in the SQL
const tableExists = sql.includes('CREATE TABLE quest_sample_rewards');
- console.log('\nDebugging quest_sample_rewards:');
- console.log('- Table exists in SQL:', tableExists);
-
- // Extract the specific table definition
- const tableMatch = sql.match(
- /-- Junction table[\s\S]*?CREATE TABLE quest_sample_rewards[\s\S]*?;/
- );
- if (tableMatch) {
- console.log('- Table definition found, first 200 chars:');
- console.log(tableMatch[0].substring(0, 200) + '...');
- }
// Now parse
const result = await fromPostgres(sql);
- console.log('\nParsing results:');
- console.log('- Total tables:', result.tables.length);
- console.log(
- '- Table names:',
- result.tables.map((t) => t.name).join(', ')
- );
-
// Look for quest_sample_rewards
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
- console.log('- quest_sample_rewards found:', !!questSampleRewards);
-
- if (!questSampleRewards) {
- // Check warnings for clues
- console.log('\nWarnings that might be relevant:');
- result.warnings?.forEach((w, i) => {
- if (
- w.includes('quest_sample_rewards') ||
- w.includes('Failed to parse')
- ) {
- console.log(` ${i}: ${w}`);
- }
- });
-
- // List all tables to see what's missing
- console.log('\nAll parsed tables:');
- result.tables.forEach((t, i) => {
- console.log(
- ` ${i + 1}. ${t.name} (${t.columns.length} columns)`
- );
- });
- } else {
- console.log('\nquest_sample_rewards details:');
- console.log('- Columns:', questSampleRewards.columns.length);
- questSampleRewards.columns.forEach((c) => {
- console.log(` - ${c.name}: ${c.type}`);
- });
- }
// The test expectation
expect(tableExists).toBe(true);
expect(result.tables.length).toBeGreaterThanOrEqual(19); // At least 19 tables
expect(questSampleRewards).toBeDefined();
+
+ // Verify quest_sample_rewards has correct columns
+ expect(questSampleRewards!.columns).toHaveLength(2);
+ const columnNames = questSampleRewards!.columns.map((c) => c.name);
+ expect(columnNames).toContain('quest_template_id');
+ expect(columnNames).toContain('reward_id');
+
+ // Verify no parsing warnings for this table
+ const questSampleRewardsWarnings = result.warnings?.filter((w) =>
+ w.includes('quest_sample_rewards')
+ );
+ expect(questSampleRewardsWarnings?.length ?? 0).toBe(0);
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-foreign-key-relationships.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-foreign-key-relationships.test.ts
index 5a79b8653..a11a57c81 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-foreign-key-relationships.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-foreign-key-relationships.test.ts
@@ -15,12 +15,6 @@ CREATE TABLE towers (
const result = await fromPostgres(sql);
- console.log(
- 'Tables:',
- result.tables.map((t) => t.name)
- );
- console.log('Relationships:', result.relationships);
-
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceTable).toBe('towers');
@@ -43,13 +37,6 @@ CREATE TABLE quests (
const result = await fromPostgres(sql);
- console.log(
- 'Tables:',
- result.tables.map((t) => t.name)
- );
- console.log('Relationships:', result.relationships);
- console.log('Warnings:', result.warnings);
-
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-forth-example-external-file.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-forth-example-external-file.test.ts
index dacd96db5..fb95cbb77 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-forth-example-external-file.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-forth-example-external-file.test.ts
@@ -40,19 +40,13 @@ CREATE TABLE plan_sample_spells (
const result = await fromPostgres(sql);
- console.log('Parsing results:');
- console.log(
- '- Tables:',
- result.tables.map((t) => t.name)
- );
- console.log('- Table count:', result.tables.length);
- console.log('- Relationships:', result.relationships.length);
- console.log('- Enums:', result.enums?.length || 0);
-
- // Should have 3 tables
+ // Verify parsing results
expect(result.tables).toHaveLength(3);
+ expect(result.relationships).toHaveLength(2);
+ expect(result.enums).toBeDefined();
+ expect(result.enums).toHaveLength(5);
- // Check table names
+ // Verify table names
const tableNames = result.tables.map((t) => t.name).sort();
expect(tableNames).toEqual([
'plan_sample_spells',
@@ -60,19 +54,12 @@ CREATE TABLE plan_sample_spells (
'spells',
]);
- // Should have 2 relationships (both from plan_sample_spells)
- expect(result.relationships).toHaveLength(2);
-
// Check plan_sample_spells specifically
const planSampleSpells = result.tables.find(
(t) => t.name === 'plan_sample_spells'
);
expect(planSampleSpells).toBeDefined();
expect(planSampleSpells!.columns).toHaveLength(2);
-
- // Should have 5 enum types
- expect(result.enums).toBeDefined();
- expect(result.enums).toHaveLength(5);
});
it('should parse the exact junction table definition', async () => {
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-invalid-multiline-string.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-invalid-multiline-string.test.ts
index 83e68ddf7..b405274ea 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-invalid-multiline-string.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-invalid-multiline-string.test.ts
@@ -15,11 +15,6 @@ CREATE TABLE test_table (
const result = await fromPostgres(sql);
// Even with syntax error, it should try to parse what it can
- console.log('Result:', {
- tables: result.tables.length,
- warnings: result.warnings,
- });
-
// Should attempt to parse the table even if parser fails
expect(result.tables.length).toBeGreaterThanOrEqual(0);
});
@@ -43,12 +38,6 @@ CREATE TABLE table3 (
const result = await fromPostgres(sql);
- console.log('Multi-table result:', {
- tableCount: result.tables.length,
- tableNames: result.tables.map((t) => t.name),
- warnings: result.warnings?.length || 0,
- });
-
// Should parse at least table1 and table3
expect(result.tables.length).toBeGreaterThanOrEqual(2);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-junction-table-parsing.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-junction-table-parsing.test.ts
index ed6f4b1fa..57cdd8b9b 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-junction-table-parsing.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-junction-table-parsing.test.ts
@@ -14,12 +14,6 @@ CREATE TABLE wizard_spells (
const result = await fromPostgres(sql);
- console.log('Test results:', {
- tableCount: result.tables.length,
- tableNames: result.tables.map((t) => t.name),
- warnings: result.warnings,
- });
-
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('wizard_spells');
});
@@ -201,46 +195,23 @@ CREATE TABLE guild_master_actions (
// Count CREATE TABLE statements
const createTableMatches = sql.match(/CREATE TABLE/gi) || [];
- console.log(
- `\nFound ${createTableMatches.length} CREATE TABLE statements in file`
- );
-
- // Find all table names
- const tableNameMatches =
- sql.match(
- /CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/gi
- ) || [];
- const tableNames = tableNameMatches
- .map((match) => {
- const nameMatch = match.match(
- /CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i
- );
- return nameMatch ? nameMatch[1] : null;
- })
- .filter(Boolean);
-
- console.log('Table names found in SQL:', tableNames);
- console.log(
- 'quest_sample_rewards in list?',
- tableNames.includes('quest_sample_rewards')
- );
// Parse the file
const result = await fromPostgres(sql);
- console.log(`\nParsed ${result.tables.length} tables`);
- console.log(
- 'Parsed table names:',
- result.tables.map((t) => t.name).sort()
- );
-
const junctionTable = result.tables.find(
(t) => t.name.includes('_') && t.columns.length >= 2
);
- console.log('junction table found?', !!junctionTable);
// All CREATE TABLE statements should be parsed
expect(result.tables.length).toBe(createTableMatches.length);
expect(junctionTable).toBeDefined();
+
+ // Verify quest_sample_rewards is parsed
+ const questSampleRewards = result.tables.find(
+ (t) => t.name === 'quest_sample_rewards'
+ );
+ expect(questSampleRewards).toBeDefined();
+ expect(questSampleRewards!.columns).toHaveLength(2);
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-marketplace-database-parsing.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-marketplace-database-parsing.test.ts
index 11beb2a54..93aeb9fd4 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-marketplace-database-parsing.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-marketplace-database-parsing.test.ts
@@ -241,49 +241,13 @@ CREATE TABLE audit_logs (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`;
- console.log('Parsing SQL...');
- const startTime = Date.now();
const result = await fromPostgres(sql);
- const parseTime = Date.now() - startTime;
-
- console.log(`Parse completed in ${parseTime}ms`);
// Expected counts
const expectedTables = 27;
const expectedEnums = 15;
const minExpectedRelationships = 36; // Adjusted based on actual relationships in the schema
- console.log('\n=== PARSING RESULTS ===');
- console.log(
- `Tables parsed: ${result.tables.length} (expected: ${expectedTables})`
- );
- console.log(
- `Enums parsed: ${result.enums?.length || 0} (expected: ${expectedEnums})`
- );
- console.log(
- `Relationships parsed: ${result.relationships.length} (expected min: ${minExpectedRelationships})`
- );
- console.log(`Warnings: ${result.warnings?.length || 0}`);
-
- // List parsed tables
- console.log('\n=== TABLES PARSED ===');
- const tableNames = result.tables.map((t) => t.name).sort();
- tableNames.forEach((name) => console.log(`- ${name}`));
-
- // List enums
- if (result.enums && result.enums.length > 0) {
- console.log('\n=== ENUMS PARSED ===');
- result.enums.forEach((e) => {
- console.log(`- ${e.name}: ${e.values.length} values`);
- });
- }
-
- // Show warnings if any
- if (result.warnings && result.warnings.length > 0) {
- console.log('\n=== WARNINGS ===');
- result.warnings.forEach((w) => console.log(`- ${w}`));
- }
-
// Verify counts
expect(result.tables).toHaveLength(expectedTables);
expect(result.enums).toBeDefined();
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-minimal-type.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-minimal-type.test.ts
deleted file mode 100644
index 6ac32ca23..000000000
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-minimal-type.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { describe, it } from 'vitest';
-
-describe('node-sql-parser - CREATE TYPE handling', () => {
- it('should show exact parser error for CREATE TYPE', async () => {
- const { Parser } = await import('node-sql-parser');
- const parser = new Parser();
- const parserOpts = {
- database: 'PostgreSQL',
- };
-
- console.log('\n=== Testing CREATE TYPE statement ===');
- const createTypeSQL = `CREATE TYPE spell_element AS ENUM ('fire', 'water', 'earth', 'air');`;
-
- try {
- parser.astify(createTypeSQL, parserOpts);
- console.log('CREATE TYPE parsed successfully');
- } catch (error) {
- console.log('CREATE TYPE parse error:', (error as Error).message);
- }
-
- console.log('\n=== Testing CREATE EXTENSION statement ===');
- const createExtensionSQL = `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
-
- try {
- parser.astify(createExtensionSQL, parserOpts);
- console.log('CREATE EXTENSION parsed successfully');
- } catch (error) {
- console.log(
- 'CREATE EXTENSION parse error:',
- (error as Error).message
- );
- }
-
- console.log('\n=== Testing CREATE TABLE with custom type ===');
- const createTableWithTypeSQL = `CREATE TABLE wizards (
- id UUID PRIMARY KEY,
- element spell_element DEFAULT 'fire'
- );`;
-
- try {
- parser.astify(createTableWithTypeSQL, parserOpts);
- console.log('CREATE TABLE with custom type parsed successfully');
- } catch (error) {
- console.log(
- 'CREATE TABLE with custom type parse error:',
- (error as Error).message
- );
- }
-
- console.log('\n=== Testing CREATE TABLE with standard types only ===');
- const createTableStandardSQL = `CREATE TABLE wizards (
- id UUID PRIMARY KEY,
- element VARCHAR(20) DEFAULT 'fire'
- );`;
-
- try {
- parser.astify(createTableStandardSQL, parserOpts);
- console.log('CREATE TABLE with standard types parsed successfully');
- } catch (error) {
- console.log(
- 'CREATE TABLE with standard types parse error:',
- (error as Error).message
- );
- }
- });
-});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-parse-all-create-statements.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-parse-all-create-statements.test.ts
index f0c15ffec..592aea557 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-parse-all-create-statements.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-parse-all-create-statements.test.ts
@@ -29,13 +29,6 @@ CREATE TABLE schema1.table_with_schema (id INTEGER PRIMARY KEY);`;
// Count CREATE TABLE statements in the SQL
const createTableCount = (sql.match(/CREATE TABLE/gi) || []).length;
- console.log(`\nValidation:`);
- console.log(`- CREATE TABLE statements in SQL: ${createTableCount}`);
- console.log(`- Tables parsed: ${result.tables.length}`);
- console.log(
- `- Table names: ${result.tables.map((t) => t.name).join(', ')}`
- );
-
// All CREATE TABLE statements should result in a parsed table
expect(result.tables).toHaveLength(createTableCount);
@@ -82,20 +75,14 @@ CREATE TABLE complex_constraints (
const createTableCount = (sql.match(/CREATE TABLE/gi) || []).length;
- console.log(`\nEdge case validation:`);
- console.log(`- CREATE TABLE statements: ${createTableCount}`);
- console.log(`- Tables parsed: ${result.tables.length}`);
- console.log(
- `- Expected tables: only_fks, no_pk, empty_table, complex_constraints`
- );
- console.log(
- `- Actual tables: ${result.tables.map((t) => t.name).join(', ')}`
- );
- result.tables.forEach((t) => {
- console.log(`- ${t.name}: ${t.columns.length} columns`);
- });
-
// Even edge cases should be parsed
expect(result.tables).toHaveLength(createTableCount);
+
+ // Verify the expected tables are present
+ const tableNames = result.tables.map((t) => t.name).sort();
+ expect(tableNames).toContain('only_fks');
+ expect(tableNames).toContain('no_pk');
+ expect(tableNames).toContain('empty_table');
+ expect(tableNames).toContain('complex_constraints');
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-quest-status-enum-parsing.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-quest-status-enum-parsing.test.ts
index 1dd0a51b7..c2438aeae 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-quest-status-enum-parsing.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-quest-status-enum-parsing.test.ts
@@ -17,14 +17,8 @@ CREATE TYPE ritual_status AS ENUM ('pending', 'channeling', 'completed', 'failed
CREATE TYPE mana_status AS ENUM ('pending', 'charged', 'depleted');
`;
- console.log('Testing with fromPostgres...');
const result = await fromPostgres(sql);
- console.log(
- 'Enums found:',
- result.enums?.map((e) => e.name)
- );
-
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-real-world-import-example.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-real-world-import-example.test.ts
index ab61d6b16..0a112997c 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-real-world-import-example.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-real-world-import-example.test.ts
@@ -55,13 +55,6 @@ CREATE INDEX "grimoires_category_idx" ON "grimoires" ("category");
const result = await fromPostgres(sql);
- // Verify enum parsing
- console.log('\n=== IMPORT RESULTS ===');
- console.log(`Enums parsed: ${result.enums?.length || 0}`);
- console.log(`Tables parsed: ${result.tables.length}`);
- console.log(`Relationships found: ${result.relationships.length}`);
- console.log(`Warnings: ${result.warnings?.length || 0}`);
-
// All enums should be parsed despite schema qualification
expect(result.enums).toHaveLength(3);
expect(result.enums?.map((e) => e.name).sort()).toEqual([
@@ -98,12 +91,6 @@ CREATE INDEX "grimoires_category_idx" ON "grimoires" ("category");
'master',
'archmage',
]);
-
- // Log warnings for visibility
- if (result.warnings && result.warnings.length > 0) {
- console.log('\n=== WARNINGS ===');
- result.warnings.forEach((w) => console.log(`- ${w}`));
- }
});
it('should provide actionable feedback for common syntax issues', async () => {
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-schema-qualified-enums.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-schema-qualified-enums.test.ts
index 7c266b2a7..ebba36456 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-schema-qualified-enums.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-schema-qualified-enums.test.ts
@@ -16,13 +16,6 @@ CREATE TABLE "wizards" (
const result = await fromPostgres(sql);
- console.log('Enums found:', result.enums?.length || 0);
- if (result.enums) {
- result.enums.forEach((e) => {
- console.log(` - ${e.name}: ${e.values.join(', ')}`);
- });
- }
-
// Should find both enums
expect(result.enums).toHaveLength(2);
@@ -64,8 +57,7 @@ CREATE TABLE "dragons" (
expect(result.enums).toHaveLength(1);
expect(result.enums?.[0].name).toBe('dragon_type');
- // Table parsing might fail due to syntax error
- console.log('Tables found:', result.tables.length);
- console.log('Warnings:', result.warnings);
+ // Table parsing might succeed or fail due to missing space syntax
+ // The important thing is the enum was still parsed correctly
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-simple-enums.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-simple-enums.test.ts
index db46f6224..664ef3c58 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-simple-enums.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-simple-enums.test.ts
@@ -14,13 +14,6 @@ CREATE TYPE mana_status AS ENUM ('pending', 'charged', 'depleted');
const result = await fromPostgres(sql);
- console.log('Result enums:', result.enums?.length || 0);
- if (result.enums) {
- result.enums.forEach((e) => {
- console.log(` - ${e.name}`);
- });
- }
-
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
});
@@ -48,9 +41,6 @@ CREATE TYPE mana_status AS ENUM ('pending', 'charged', 'depleted');
for (const enumDef of enums) {
const result = await fromPostgres(enumDef.sql);
- console.log(`\nTesting ${enumDef.name}:`);
- console.log(` Found enums: ${result.enums?.length || 0}`);
-
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(1);
expect(result.enums![0].name).toBe(enumDef.name);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts
index f597e6011..f9cf5ed63 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts
@@ -42,14 +42,14 @@ CREATE TABLE book_spells (
(c) => c.name === 'spell_book_id'
);
expect(spellBookIdColumn).toBeDefined();
- expect(spellBookIdColumn!.type).toBe('UUID');
+ expect(spellBookIdColumn!.type).toBe('uuid');
expect(spellBookIdColumn!.nullable).toBe(false);
const spellIdColumn = bookSpells!.columns.find(
(c) => c.name === 'spell_id'
);
expect(spellIdColumn).toBeDefined();
- expect(spellIdColumn!.type).toBe('UUID');
+ expect(spellIdColumn!.type).toBe('uuid');
expect(spellIdColumn!.nullable).toBe(false);
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-plans-with-enums.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-plans-with-enums.test.ts
index 125f54fdc..0a0ac88c2 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-plans-with-enums.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-plans-with-enums.test.ts
@@ -44,16 +44,8 @@ CREATE TABLE plan_sample_spells (
PRIMARY KEY (spell_plan_id, spell_id)
);`;
- console.log('Testing exact SQL from forth example...');
-
const result = await fromPostgres(sql);
- console.log('Results:', {
- tables: result.tables.length,
- tableNames: result.tables.map((t) => t.name),
- warnings: result.warnings?.length || 0,
- });
-
// Should have 3 tables
expect(result.tables).toHaveLength(3);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-string-preservation.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-string-preservation.test.ts
index e740a7e9c..2e3064011 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-string-preservation.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-string-preservation.test.ts
@@ -11,15 +11,6 @@ CREATE TABLE spell_ingredients (
const result = await fromPostgres(sql);
- console.log('String preservation result:', {
- tableCount: result.tables.length,
- columns: result.tables[0]?.columns.map((c) => ({
- name: c.name,
- type: c.type,
- default: c.default,
- })),
- });
-
expect(result.tables).toHaveLength(1);
expect(result.tables[0].columns).toHaveLength(2);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-tables-with-missing-references.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-tables-with-missing-references.test.ts
index 97c55c08d..1fd8da611 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-tables-with-missing-references.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-tables-with-missing-references.test.ts
@@ -21,12 +21,6 @@ CREATE TABLE table3 (
const result = await fromPostgres(sql);
- console.log('Test results:', {
- tableCount: result.tables.length,
- tableNames: result.tables.map((t) => t.name),
- warnings: result.warnings,
- });
-
// Should parse all 3 tables even though table2 has undefined reference
expect(result.tables).toHaveLength(3);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-third-example-external-file.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-third-example-external-file.test.ts
index 1fe539786..c1e1a088c 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-third-example-external-file.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-third-example-external-file.test.ts
@@ -62,14 +62,6 @@ CREATE TABLE rewards (
// Use the improved parser
const parserResult = await fromPostgres(sql);
- console.log('\nParser Result:');
- console.log('- Enums found:', parserResult.enums?.length || 0);
- if (parserResult.enums) {
- parserResult.enums.forEach((e) => {
- console.log(` - ${e.name}: ${e.values.length} values`);
- });
- }
-
// Convert to diagram
const diagram = convertToChartDBDiagram(
parserResult,
@@ -77,33 +69,6 @@ CREATE TABLE rewards (
DatabaseType.POSTGRESQL
);
- console.log('\nDiagram Result:');
- console.log('- Custom types:', diagram.customTypes?.length || 0);
- if (diagram.customTypes) {
- diagram.customTypes.forEach((t) => {
- console.log(` - ${t.name} (${t.kind})`);
- });
- }
-
- // Check contracts table
- const contractsTable = diagram.tables?.find(
- (t) => t.name === 'contracts'
- );
- if (contractsTable) {
- console.log('\nContracts table enum fields:');
- const enumFields = ['status'];
- enumFields.forEach((fieldName) => {
- const field = contractsTable.fields.find(
- (f) => f.name === fieldName
- );
- if (field) {
- console.log(
- ` - ${field.name}: ${field.type.name} (id: ${field.type.id})`
- );
- }
- });
- }
-
// Assertions
expect(parserResult.enums).toHaveLength(5);
expect(diagram.customTypes).toHaveLength(5);
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-twenty-table-parsing.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-twenty-table-parsing.test.ts
index dcda8e8a0..1fc5f7557 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-twenty-table-parsing.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-twenty-table-parsing.test.ts
@@ -203,39 +203,18 @@ CREATE TABLE guild_master_actions (
const result = await fromPostgres(sql);
- console.log('\n=== PARSING RESULTS ===');
- console.log(`Tables parsed: ${result.tables.length}`);
- console.log(`Expected: ${expectedTables.length}`);
-
const parsedTableNames = result.tables.map((t) => t.name).sort();
- console.log('\nParsed tables:');
- parsedTableNames.forEach((name, i) => {
- console.log(` ${i + 1}. ${name}`);
- });
- // Find missing tables
+ // Find missing tables and verify none are missing
const missingTables = expectedTables.filter(
(expected) => !parsedTableNames.includes(expected)
);
- if (missingTables.length > 0) {
- console.log('\nMissing tables:');
- missingTables.forEach((name) => {
- console.log(` - ${name}`);
- });
- }
+ expect(missingTables).toHaveLength(0);
// Check for quest_sample_rewards specifically
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
- console.log(`\nquest_sample_rewards found: ${!!questSampleRewards}`);
- if (questSampleRewards) {
- console.log('quest_sample_rewards details:');
- console.log(` - Columns: ${questSampleRewards.columns.length}`);
- questSampleRewards.columns.forEach((col) => {
- console.log(` - ${col.name}: ${col.type}`);
- });
- }
// Verify all tables were parsed
expect(result.tables).toHaveLength(expectedTables.length);
@@ -249,11 +228,5 @@ CREATE TABLE guild_master_actions (
.map((c) => c.name)
.sort();
expect(columnNames).toEqual(['quest_template_id', 'reward_id']);
-
- // Check warnings if any
- if (result.warnings && result.warnings.length > 0) {
- console.log('\nWarnings:');
- result.warnings.forEach((w) => console.log(` - ${w}`));
- }
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql-common.ts b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql-common.ts
index 574ad920e..14ddd5863 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql-common.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql-common.ts
@@ -198,13 +198,17 @@ export function getTypeArgs(
if (!definition) return typeArgs;
- if (definition.length !== undefined) {
- typeArgs.length = definition.length;
- }
-
- if (definition.scale !== undefined && definition.precision !== undefined) {
- typeArgs.precision = definition.precision;
+ // node-sql-parser stores precision as 'length' for DECIMAL/NUMERIC types
+ // Check if scale is present to determine if this is a numeric type with precision
+ if (definition.scale !== undefined) {
+ // When scale exists, length represents precision (for DECIMAL/NUMERIC)
+ // or precision property is directly available
+ typeArgs.precision = definition.precision ?? definition.length;
typeArgs.scale = definition.scale;
+ } else if (definition.precision !== undefined) {
+ typeArgs.precision = definition.precision;
+ } else if (definition.length !== undefined) {
+ typeArgs.length = definition.length;
}
return typeArgs;
diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts
index 94dc5779c..84040e3ea 100644
--- a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts
+++ b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts
@@ -6,6 +6,7 @@ import type {
SQLIndex,
SQLForeignKey,
SQLEnumType,
+ SQLCheckConstraint,
} from '../../common';
import { buildSQLFromAST } from '../../common';
import { DatabaseType } from '@/lib/domain/database-type';
@@ -30,6 +31,7 @@ import {
interface ParsedStatement {
type:
| 'table'
+ | 'view'
| 'index'
| 'alter'
| 'function'
@@ -142,6 +144,13 @@ function preprocessSQL(sqlContent: string): PreprocessResult {
} else {
statements.push({ type: 'alter', sql: trimmedStmt });
}
+ } else if (
+ upperStmt.startsWith('CREATE VIEW') ||
+ upperStmt.startsWith('CREATE OR REPLACE VIEW') ||
+ upperStmt.includes('CREATE VIEW') ||
+ upperStmt.includes('CREATE OR REPLACE VIEW')
+ ) {
+ statements.push({ type: 'view', sql: trimmedStmt });
} else if (
upperStmt.startsWith('CREATE FUNCTION') ||
upperStmt.startsWith('CREATE OR REPLACE FUNCTION')
@@ -249,69 +258,176 @@ function splitSQLStatements(sql: string): string[] {
}
/**
- * Normalize PostgreSQL type aliases to standard types
+ * Set of serial type names for O(1) lookup
+ */
+const SERIAL_TYPES = new Set([
+ 'SERIAL',
+ 'SERIAL2',
+ 'SERIAL4',
+ 'SERIAL8',
+ 'BIGSERIAL',
+ 'SMALLSERIAL',
+]);
+
+/**
+ * Check if a type is a serial type
+ */
+function isSerialTypeName(typeName: string): boolean {
+ return SERIAL_TYPES.has(typeName.toUpperCase().split('(')[0]);
+}
+
+/**
+ * Check if a specific column has GENERATED AS IDENTITY syntax in the SQL
+ * @param sql The SQL statement containing the column definition
+ * @param columnName The name of the column to check
+ * @returns true if the column has GENERATED AS IDENTITY
+ */
+function hasGeneratedIdentity(sql: string, columnName: string): boolean {
+ // Create a regex pattern to find the column definition
+ // Match the column name (quoted or unquoted) followed by its definition until the next comma or closing paren
+ const escapedName = columnName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const pattern = new RegExp(
+ `["']?${escapedName}["']?\\s+[^,)]*GENERATED\\s+(?:BY\\s+DEFAULT|ALWAYS)\\s+AS\\s+IDENTITY`,
+ 'i'
+ );
+ return pattern.test(sql);
+}
+
+/**
+ * Normalize PostgreSQL type syntax to lowercase canonical form.
+ * This function handles parsing-level normalization only - it converts
+ * verbose SQL syntax to the preferred short form that getPreferredSynonym
+ * expects. It preserves semantic types like serial (does NOT convert to integer).
+ *
+ * The optional `length` parameter is used to resolve ambiguous types where
+ * the SQL parser returns a base type with a length modifier (e.g., 'SERIAL'
+ * with length=2 for 'serial2', or 'INT' with length=8 for 'int8').
+ *
+ * Type synonym resolution (e.g., integer→int) is handled by getPreferredSynonym.
*/
-function normalizePostgreSQLType(type: string): string {
+function normalizePostgreSQLType(
+ type: string,
+ length?: number | undefined
+): string {
const upperType = type.toUpperCase();
- // Handle types with parameters - more complex regex to handle CHARACTER VARYING
+ // Handle types with parameters (e.g., VARCHAR(255), NUMERIC(10,2))
const typeMatch = upperType.match(/^([\w\s]+?)(\(.+\))?$/);
- if (!typeMatch) return type;
+ if (!typeMatch) return type.toLowerCase();
const baseType = typeMatch[1].trim();
const params = typeMatch[2] || '';
let normalizedBase: string;
switch (baseType) {
- // Serial types
+ // Serial types - preserve as-is (they are valid PostgreSQL types)
+ // Handle parser quirk: 'SERIAL' with length=2 means 'serial2' (smallserial)
case 'SERIAL':
+ if (length === 2) {
+ normalizedBase = 'smallserial';
+ } else if (length === 8) {
+ normalizedBase = 'bigserial';
+ } else {
+ normalizedBase = 'serial';
+ }
+ break;
case 'SERIAL4':
- normalizedBase = 'INTEGER';
+ normalizedBase = 'serial';
break;
case 'BIGSERIAL':
case 'SERIAL8':
- normalizedBase = 'BIGINT';
+ normalizedBase = 'bigserial';
break;
case 'SMALLSERIAL':
case 'SERIAL2':
- normalizedBase = 'SMALLINT';
+ normalizedBase = 'smallserial';
break;
- // Integer aliases
+ // Integer types - normalize to lowercase canonical form
+ // Handle parser quirk: 'INT' with length=2 means 'int2' (smallint)
case 'INT':
+ if (length === 2) {
+ normalizedBase = 'smallint';
+ } else if (length === 8) {
+ normalizedBase = 'bigint';
+ } else {
+ normalizedBase = 'integer';
+ }
+ break;
case 'INT4':
- normalizedBase = 'INTEGER';
+ case 'INTEGER':
+ normalizedBase = 'integer';
break;
case 'INT2':
- normalizedBase = 'SMALLINT';
+ case 'SMALLINT':
+ normalizedBase = 'smallint';
break;
case 'INT8':
- normalizedBase = 'BIGINT';
+ case 'BIGINT':
+ normalizedBase = 'bigint';
break;
- // Boolean aliases
+ // Boolean
case 'BOOL':
- normalizedBase = 'BOOLEAN';
+ case 'BOOLEAN':
+ normalizedBase = 'boolean';
break;
- // Character types - use common names
+ // Character types - normalize verbose forms
case 'CHARACTER VARYING':
+ normalizedBase = 'varchar';
+ break;
case 'VARCHAR':
- normalizedBase = 'VARCHAR';
+ normalizedBase = 'varchar';
break;
case 'CHARACTER':
+ normalizedBase = 'char';
+ break;
case 'CHAR':
- normalizedBase = 'CHAR';
+ normalizedBase = 'char';
break;
- // Timestamp aliases
+ // Timestamp types
case 'TIMESTAMPTZ':
case 'TIMESTAMP WITH TIME ZONE':
- normalizedBase = 'TIMESTAMPTZ';
+ normalizedBase = 'timestamptz';
+ break;
+ case 'TIMESTAMP WITHOUT TIME ZONE':
+ case 'TIMESTAMP':
+ normalizedBase = 'timestamp';
+ break;
+ // Time types
+ case 'TIMETZ':
+ case 'TIME WITH TIME ZONE':
+ normalizedBase = 'timetz';
+ break;
+ case 'TIME WITHOUT TIME ZONE':
+ case 'TIME':
+ normalizedBase = 'time';
+ break;
+ // Floating point
+ case 'FLOAT4':
+ case 'REAL':
+ normalizedBase = 'real';
+ break;
+ case 'FLOAT8':
+ case 'DOUBLE PRECISION':
+ normalizedBase = 'double precision';
+ break;
+ // Bit types
+ case 'BIT VARYING':
+ normalizedBase = 'varbit';
+ break;
+ // Numeric types
+ case 'DECIMAL':
+ normalizedBase = 'numeric';
+ break;
+ case 'NUMERIC':
+ normalizedBase = 'numeric';
break;
default:
- // For unknown types (like enums), preserve original case
- return type;
+ // For unknown types (like enums, user-defined), preserve original in lowercase
+ return type.toLowerCase();
}
- // Return normalized type with original parameters preserved
- return normalizedBase + params;
+ // Return normalized type with parameters preserved (lowercase)
+ return normalizedBase + params.toLowerCase();
}
/**
@@ -372,17 +488,9 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
}
// Check if it's a serial type for increment flag
- const upperType = columnType.toUpperCase();
- const isSerialType = [
- 'SERIAL',
- 'SERIAL2',
- 'SERIAL4',
- 'SERIAL8',
- 'BIGSERIAL',
- 'SMALLSERIAL',
- ].includes(upperType.split('(')[0]);
-
- // Normalize the type
+ const isSerialType = isSerialTypeName(columnType);
+
+ // Normalize the type (preserves serial types)
columnType = normalizePostgreSQLType(columnType);
// Check for common constraints
@@ -469,6 +577,105 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
return columns;
}
+/**
+ * Extract columns from a CREATE VIEW statement
+ * Views can have explicit column names or derive them from the SELECT
+ */
+function extractColumnsFromView(sql: string): SQLColumn[] {
+ const columns: SQLColumn[] = [];
+
+ // First, try to extract explicit column list from CREATE VIEW viewname (col1, col2, ...) AS
+ const explicitColumnsMatch = sql.match(
+ /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:(?:"[^"]+"|[^"\s.]+)\.)?(?:"[^"]+"|[^"\s.(]+)\s*\(([^)]+)\)\s*AS/i
+ );
+
+ if (explicitColumnsMatch) {
+ // Parse explicit column list
+ const columnList = explicitColumnsMatch[1];
+ const columnNames = columnList
+ .split(',')
+ .map((col) => col.trim().replace(/^["']|["']$/g, ''));
+
+ for (const colName of columnNames) {
+ if (colName) {
+ columns.push({
+ name: colName,
+ type: 'text', // Default type for views since we don't know the actual type
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ });
+ }
+ }
+
+ return columns;
+ }
+
+ // If no explicit columns, try to extract from SELECT clause
+ // Match: AS SELECT ... FROM (extracting the column references)
+ const selectMatch = sql.match(/\bAS\s+SELECT\s+([\s\S]+?)\s+FROM\s+/i);
+
+ if (selectMatch) {
+ const selectClause = selectMatch[1];
+
+ // Handle SELECT * - we can't determine columns
+ if (selectClause.trim() === '*') {
+ return columns;
+ }
+
+ // Split by comma, but be careful of nested functions/expressions
+ let depth = 0;
+ let currentCol = '';
+ const selectParts: string[] = [];
+
+ for (const char of selectClause) {
+ if (char === '(' || char === '[') depth++;
+ else if (char === ')' || char === ']') depth--;
+ else if (char === ',' && depth === 0) {
+ selectParts.push(currentCol.trim());
+ currentCol = '';
+ continue;
+ }
+ currentCol += char;
+ }
+ if (currentCol.trim()) {
+ selectParts.push(currentCol.trim());
+ }
+
+ for (const part of selectParts) {
+ // Extract column name - handle aliases (AS name), qualified names (table.col), and expressions
+ let columnName = '';
+
+ // Check for alias: ... AS "name" or ... AS name
+ const aliasMatch = part.match(/\s+AS\s+["']?(\w+)["']?\s*$/i);
+ if (aliasMatch) {
+ columnName = aliasMatch[1];
+ } else {
+ // Try to extract the column reference
+ // Handle: col, table.col, "col", table."col"
+ const colRefMatch = part.match(
+ /(?:[\w"]+\.)?["']?(\w+)["']?\s*$/
+ );
+ if (colRefMatch) {
+ columnName = colRefMatch[1];
+ }
+ }
+
+ if (columnName && columnName !== '*') {
+ columns.push({
+ name: columnName,
+ type: 'text', // Default type for views
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ });
+ }
+ }
+ }
+
+ return columns;
+}
+
/**
* Extract enum type definition from CREATE TYPE statement
*/
@@ -627,6 +834,50 @@ function extractForeignKeysFromCreateTable(
return relationships;
}
+/**
+ * Extract CHECK constraints from CREATE TABLE statements
+ * Handles both inline column-level and table-level CHECK constraints
+ */
+function extractCheckConstraintsFromCreateTable(
+ sql: string
+): SQLCheckConstraint[] {
+ const constraints: SQLCheckConstraint[] = [];
+
+ // Extract the table body
+ const tableBodyMatch = sql.match(/\(([\s\S]+)\)/);
+ if (!tableBodyMatch) return constraints;
+
+ const tableBody = tableBodyMatch[1];
+
+ // Pattern for table-level CHECK constraints:
+ // CHECK (expression) or CONSTRAINT name CHECK (expression)
+ // We need to handle nested parentheses in the expression
+ const checkPattern = /(?:CONSTRAINT\s+(?:"[^"]+"|[^\s]+)\s+)?CHECK\s*\(/gi;
+ let match;
+
+ while ((match = checkPattern.exec(tableBody)) !== null) {
+ const startIdx = match.index + match[0].length;
+ let depth = 1;
+ let endIdx = startIdx;
+
+ // Find the matching closing parenthesis
+ for (let i = startIdx; i < tableBody.length && depth > 0; i++) {
+ if (tableBody[i] === '(') depth++;
+ else if (tableBody[i] === ')') depth--;
+ endIdx = i;
+ }
+
+ if (depth === 0) {
+ const expression = tableBody.substring(startIdx, endIdx).trim();
+ if (expression) {
+ constraints.push({ expression });
+ }
+ }
+ }
+
+ return constraints;
+}
+
/**
* Parse PostgreSQL SQL with improved error handling and statement filtering
*/
@@ -646,7 +897,7 @@ export async function fromPostgres(
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
- // First pass: collect all table names and custom types
+ // First pass: collect all table names, view names, and custom types
for (const stmt of statements) {
if (stmt.type === 'table') {
// Extract just the CREATE TABLE part if there are comments
@@ -671,6 +922,24 @@ export async function fromPostgres(
const tableKey = `${schemaName}.${tableName}`;
tableMap[tableKey] = generateId();
}
+ } else if (stmt.type === 'view') {
+ // Extract view name similar to table
+ const createViewIndex = stmt.sql.toUpperCase().indexOf('CREATE');
+ const sqlFromCreate =
+ createViewIndex >= 0
+ ? stmt.sql.substring(createViewIndex)
+ : stmt.sql;
+
+ // Matches: CREATE [OR REPLACE] VIEW [schema.]viewname
+ const viewMatch = sqlFromCreate.match(
+ /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i
+ );
+ if (viewMatch) {
+ const schemaName = viewMatch[1] || viewMatch[2] || 'public';
+ const viewName = viewMatch[3] || viewMatch[4];
+ const viewKey = `${schemaName}.${viewName}`;
+ tableMap[viewKey] = generateId();
+ }
} else if (stmt.type === 'type') {
// Extract enum type definition
const enumType = extractEnumFromSQL(stmt.sql);
@@ -684,6 +953,7 @@ export async function fromPostgres(
for (const stmt of statements) {
if (
stmt.type === 'table' ||
+ stmt.type === 'view' ||
stmt.type === 'index' ||
stmt.type === 'alter'
) {
@@ -708,8 +978,8 @@ export async function fromPostgres(
);
// Mark the statement as having parse errors but keep it for fallback processing
- if (stmt.type === 'table') {
- stmt.parsed = null; // Mark as failed but still a table
+ if (stmt.type === 'table' || stmt.type === 'view') {
+ stmt.parsed = null; // Mark as failed but still a table/view
}
}
}
@@ -820,107 +1090,93 @@ export async function fromPostgres(
}
}
- // First normalize the base type
- let normalizedBaseType = rawDataType;
- let isSerialType = false;
-
- // Check if it's a serial type first
- const upperType = rawDataType.toUpperCase();
- const typeLength = definition?.length as
+ // Check if it's a serial type
+ const isSerialType = isSerialTypeName(rawDataType);
+ const typeLength = columnDef.definition?.length as
| number
| undefined;
- if (upperType === 'SERIAL') {
- // Use length to determine the actual serial type
- if (typeLength === 2) {
- normalizedBaseType = 'SMALLINT';
- isSerialType = true;
- } else if (typeLength === 8) {
- normalizedBaseType = 'BIGINT';
- isSerialType = true;
- } else {
- // Default serial or serial4
- normalizedBaseType = 'INTEGER';
- isSerialType = true;
- }
- } else if (upperType === 'INT') {
- // Use length to determine the actual int type
- if (typeLength === 2) {
- normalizedBaseType = 'SMALLINT';
- } else if (typeLength === 8) {
- normalizedBaseType = 'BIGINT';
- } else {
- // Default int or int4
- normalizedBaseType = 'INTEGER';
- }
- } else {
- // Apply normalization for other types
- normalizedBaseType =
- normalizePostgreSQLType(rawDataType);
- }
-
- // Now handle parameters - but skip for integer types that shouldn't have them
- let finalDataType = normalizedBaseType;
-
- // Don't add parameters to INTEGER types that come from int4, int8, etc.
- const isNormalizedIntegerType =
- ['INTEGER', 'BIGINT', 'SMALLINT'].includes(
- normalizedBaseType
- ) &&
- (upperType === 'INT' || upperType === 'SERIAL');
+ // Check if this is an array type (node-sql-parser stores this separately)
+ const arrayInfo = definition?.array as
+ | { dimension?: number }
+ | undefined;
+ const isArrayType =
+ arrayInfo?.dimension !== undefined ||
+ rawDataType.endsWith('[]');
+
+ // Normalize the type (pass length to handle parser quirks like INT with length=8)
+ let finalDataType = normalizePostgreSQLType(
+ rawDataType,
+ typeLength
+ );
- if (!isSerialType && !isNormalizedIntegerType) {
- // Include precision/scale/length in the type string if available
+ // Add type parameters for non-serial, non-integer types
+ if (!isSerialType) {
const precision =
columnDef.definition?.precision;
const scale = columnDef.definition?.scale;
- const length = columnDef.definition?.length;
-
- // Also check if there's a suffix that includes the precision/scale
- const definition =
+ const suffix = (
columnDef.definition as Record<
string,
unknown
- >;
- const suffix = definition?.suffix;
+ >
+ )?.suffix;
- if (
- suffix &&
- Array.isArray(suffix) &&
- suffix.length > 0
- ) {
- // The suffix contains the full type parameters like (10,2)
- const params = suffix
- .map((s: unknown) => {
- if (
+ // Skip adding parameters to integer types (they don't have size params)
+ const isIntegerType = [
+ 'integer',
+ 'bigint',
+ 'smallint',
+ ].includes(finalDataType);
+
+ if (!isIntegerType) {
+ if (
+ suffix &&
+ Array.isArray(suffix) &&
+ suffix.length > 0
+ ) {
+ const params = suffix
+ .map((s: unknown) =>
typeof s === 'object' &&
s !== null &&
'value' in s
- ) {
- return String(
- (s as { value: unknown })
- .value
- );
- }
- return String(s);
- })
- .join(',');
- finalDataType = `${normalizedBaseType}(${params})`;
- } else if (precision !== undefined) {
- if (scale !== undefined) {
- finalDataType = `${normalizedBaseType}(${precision},${scale})`;
- } else {
- finalDataType = `${normalizedBaseType}(${precision})`;
+ ? String(
+ (
+ s as {
+ value: unknown;
+ }
+ ).value
+ )
+ : String(s)
+ )
+ .join(',');
+ finalDataType = `${finalDataType}(${params})`;
+ } else if (precision !== undefined) {
+ finalDataType =
+ scale !== undefined
+ ? `${finalDataType}(${precision},${scale})`
+ : `${finalDataType}(${precision})`;
+ } else if (
+ scale !== undefined &&
+ typeLength !== undefined
+ ) {
+ // For NUMERIC, node-sql-parser stores precision as 'length'
+ finalDataType = `${finalDataType}(${typeLength},${scale})`;
+ } else if (
+ typeLength !== undefined &&
+ typeLength !== null
+ ) {
+ finalDataType = `${finalDataType}(${typeLength})`;
}
- } else if (
- length !== undefined &&
- length !== null
- ) {
- // For VARCHAR, CHAR, etc.
- finalDataType = `${normalizedBaseType}(${length})`;
}
}
+ // Add array suffix if this is an array type
+ // (only if not already present from rawDataType)
+ if (isArrayType && !finalDataType.endsWith('[]')) {
+ finalDataType = `${finalDataType}[]`;
+ }
+
if (columnName) {
const isPrimaryKey =
columnDef.primary_key === 'primary key' ||
@@ -930,11 +1186,12 @@ export async function fromPostgres(
columns.push({
name: columnName,
type: finalDataType,
- nullable: isSerialType
- ? false
- : columnDef.nullable?.type !==
- 'not null',
- primaryKey: isPrimaryKey || isSerialType,
+ nullable:
+ isSerialType || isPrimaryKey
+ ? false
+ : columnDef.nullable?.type !==
+ 'not null',
+ primaryKey: isPrimaryKey,
unique: columnDef.unique === 'unique',
typeArgs: getTypeArgs(columnDef.definition),
default: isSerialType
@@ -944,13 +1201,11 @@ export async function fromPostgres(
isSerialType ||
columnDef.auto_increment ===
'auto_increment' ||
- // Check if the SQL contains GENERATED IDENTITY for this column
- (stmt.sql
- .toUpperCase()
- .includes('GENERATED') &&
- stmt.sql
- .toUpperCase()
- .includes('IDENTITY')),
+ // Check if the SQL contains GENERATED IDENTITY for this specific column
+ hasGeneratedIdentity(
+ stmt.sql,
+ columnName
+ ),
});
}
} else if (def.resource === 'constraint') {
@@ -962,7 +1217,11 @@ export async function fromPostgres(
) {
// Process primary key constraint
if (Array.isArray(constraintDef.definition)) {
- constraintDef.definition.forEach(
+ const pkColumns = constraintDef.definition;
+ const isSingleColumnPK =
+ pkColumns.length === 1;
+
+ pkColumns.forEach(
(colDef: ColumnReference) => {
const pkColumnName =
extractColumnName(colDef);
@@ -972,6 +1231,11 @@ export async function fromPostgres(
);
if (column) {
column.primaryKey = true;
+ // Only mark as unique if it's a single-column primary key
+ // Composite primary keys don't guarantee uniqueness of individual columns
+ if (isSingleColumnPK) {
+ column.unique = true;
+ }
}
}
);
@@ -992,6 +1256,11 @@ export async function fromPostgres(
);
relationships.push(...tableFKs);
+ // Extract check constraints from the original SQL
+ const checkConstraints = extractCheckConstraintsFromCreateTable(
+ stmt.sql
+ );
+
// Create table object
const table: SQLTable = {
id: tableId,
@@ -999,6 +1268,8 @@ export async function fromPostgres(
schema: schemaName,
columns,
indexes,
+ checkConstraints:
+ checkConstraints.length > 0 ? checkConstraints : undefined,
order: tables.length,
};
@@ -1044,6 +1315,10 @@ export async function fromPostgres(
);
relationships.push(...fks);
+ // Extract check constraints
+ const checkConstraints =
+ extractCheckConstraintsFromCreateTable(stmt.sql);
+
// Create table object
const table: SQLTable = {
id: tableId,
@@ -1051,6 +1326,10 @@ export async function fromPostgres(
schema: schemaName,
columns,
indexes: [],
+ checkConstraints:
+ checkConstraints.length > 0
+ ? checkConstraints
+ : undefined,
order: tables.length,
};
@@ -1063,6 +1342,47 @@ export async function fromPostgres(
}
}
+ // Third pass (continued): extract view definitions
+ for (const stmt of statements) {
+ if (stmt.type === 'view') {
+ // Extract view name
+ const createViewIndex = stmt.sql.toUpperCase().indexOf('CREATE');
+ const sqlFromCreate =
+ createViewIndex >= 0
+ ? stmt.sql.substring(createViewIndex)
+ : stmt.sql;
+
+ const viewMatch = sqlFromCreate.match(
+ /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i
+ );
+
+ if (viewMatch) {
+ const schemaName = viewMatch[1] || viewMatch[2] || 'public';
+ const viewName = viewMatch[3] || viewMatch[4];
+ const viewKey = `${schemaName}.${viewName}`;
+ const viewId = tableMap[viewKey];
+
+ if (viewId) {
+ // Extract columns from the view definition
+ const columns = extractColumnsFromView(stmt.sql);
+
+ // Create view object (as a table with isView: true)
+ const view: SQLTable = {
+ id: viewId,
+ name: viewName,
+ schema: schemaName,
+ columns,
+ indexes: [], // Views don't have indexes
+ order: tables.length,
+ isView: true,
+ };
+
+ tables.push(view);
+ }
+ }
+ }
+ }
+
// Fourth pass: process ALTER TABLE statements for foreign keys and ADD COLUMN
for (const stmt of statements) {
if (stmt.type === 'alter' && stmt.parsed) {
@@ -1233,81 +1553,61 @@ export async function fromPostgres(
const rawDataType = String(
definition?.dataType || 'TEXT'
);
- // console.log('expr:', JSON.stringify(expr, null, 2));
-
- // Normalize the type
- let normalizedBaseType =
- normalizePostgreSQLType(rawDataType);
// Check if it's a serial type
- const upperType = rawDataType.toUpperCase();
- const isSerialType = [
- 'SERIAL',
- 'SERIAL2',
- 'SERIAL4',
- 'SERIAL8',
- 'BIGSERIAL',
- 'SMALLSERIAL',
- ].includes(upperType.split('(')[0]);
-
- if (isSerialType) {
- const typeLength = definition?.length as
- | number
- | undefined;
- if (upperType === 'SERIAL') {
- if (typeLength === 2) {
- normalizedBaseType = 'SMALLINT';
- } else if (typeLength === 8) {
- normalizedBaseType = 'BIGINT';
- } else {
- normalizedBaseType = 'INTEGER';
- }
- }
- }
+ const isSerialType = isSerialTypeName(rawDataType);
+ const typeLength = definition?.length as
+ | number
+ | undefined;
- // Handle type parameters
- let finalDataType = normalizedBaseType;
- const isNormalizedIntegerType =
- ['INTEGER', 'BIGINT', 'SMALLINT'].includes(
- normalizedBaseType
- ) &&
- (upperType === 'INT' || upperType === 'SERIAL');
+ // Normalize the type (pass length to handle parser quirks)
+ let finalDataType = normalizePostgreSQLType(
+ rawDataType,
+ typeLength
+ );
- if (!isSerialType && !isNormalizedIntegerType) {
+ // Add type parameters for non-serial, non-integer types
+ if (!isSerialType) {
const precision = definition?.precision;
const scale = definition?.scale;
- const length = definition?.length;
const suffix =
(definition?.suffix as unknown[]) || [];
- if (suffix.length > 0) {
- const params = suffix
- .map((s: unknown) => {
- if (
+ const isIntegerType = [
+ 'integer',
+ 'bigint',
+ 'smallint',
+ ].includes(finalDataType);
+
+ if (!isIntegerType) {
+ if (suffix.length > 0) {
+ const params = suffix
+ .map((s: unknown) =>
typeof s === 'object' &&
s !== null &&
'value' in s
- ) {
- return String(
- (s as { value: unknown })
- .value
- );
- }
- return String(s);
- })
- .join(',');
- finalDataType = `${normalizedBaseType}(${params})`;
- } else if (precision !== undefined) {
- if (scale !== undefined) {
- finalDataType = `${normalizedBaseType}(${precision},${scale})`;
- } else {
- finalDataType = `${normalizedBaseType}(${precision})`;
+ ? String(
+ (
+ s as {
+ value: unknown;
+ }
+ ).value
+ )
+ : String(s)
+ )
+ .join(',');
+ finalDataType = `${finalDataType}(${params})`;
+ } else if (precision !== undefined) {
+ finalDataType =
+ scale !== undefined
+ ? `${finalDataType}(${precision},${scale})`
+ : `${finalDataType}(${precision})`;
+ } else if (
+ typeLength !== undefined &&
+ typeLength !== null
+ ) {
+ finalDataType = `${finalDataType}(${typeLength})`;
}
- } else if (
- length !== undefined &&
- length !== null
- ) {
- finalDataType = `${normalizedBaseType}(${length})`;
}
}
@@ -1352,20 +1652,14 @@ export async function fromPostgres(
nullable: nullable,
primaryKey:
definition?.primary_key === 'primary key' ||
- definition?.constraint === 'primary key' ||
- isSerialType,
+ definition?.constraint === 'primary key',
unique: isUnique,
default: defaultValue,
increment:
isSerialType ||
definition?.auto_increment ===
'auto_increment' ||
- (stmt.sql
- .toUpperCase()
- .includes('GENERATED') &&
- stmt.sql
- .toUpperCase()
- .includes('IDENTITY')),
+ hasGeneratedIdentity(stmt.sql, columnName),
};
// Add the column to the table if it doesn't already exist
@@ -1403,84 +1697,62 @@ export async function fromPostgres(
definition?.dataType || 'TEXT'
);
- // Normalize the type
- let normalizedBaseType =
- normalizePostgreSQLType(rawDataType);
-
// Check if it's a serial type
- const upperType = rawDataType.toUpperCase();
- const isSerialType = [
- 'SERIAL',
- 'SERIAL2',
- 'SERIAL4',
- 'SERIAL8',
- 'BIGSERIAL',
- 'SMALLSERIAL',
- ].includes(upperType.split('(')[0]);
-
- if (isSerialType) {
- const typeLength = definition?.length as
- | number
- | undefined;
- if (upperType === 'SERIAL') {
- if (typeLength === 2) {
- normalizedBaseType = 'SMALLINT';
- } else if (typeLength === 8) {
- normalizedBaseType = 'BIGINT';
- } else {
- normalizedBaseType = 'INTEGER';
- }
- }
- }
+ const isSerialType =
+ isSerialTypeName(rawDataType);
+ const typeLength = definition?.length as
+ | number
+ | undefined;
- // Handle type parameters
- let finalDataType = normalizedBaseType;
- const isNormalizedIntegerType =
- ['INTEGER', 'BIGINT', 'SMALLINT'].includes(
- normalizedBaseType
- ) &&
- (upperType === 'INT' ||
- upperType === 'SERIAL');
-
- if (!isSerialType && !isNormalizedIntegerType) {
+ // Normalize the type (pass length to handle parser quirks)
+ let finalDataType = normalizePostgreSQLType(
+ rawDataType,
+ typeLength
+ );
+
+ // Add type parameters for non-serial, non-integer types
+ if (!isSerialType) {
const precision =
columnDef.definition?.precision;
const scale = columnDef.definition?.scale;
- const length = columnDef.definition?.length;
const suffix =
(definition?.suffix as unknown[]) || [];
- if (suffix.length > 0) {
- const params = suffix
- .map((s: unknown) => {
- if (
+ const isIntegerType = [
+ 'integer',
+ 'bigint',
+ 'smallint',
+ ].includes(finalDataType);
+
+ if (!isIntegerType) {
+ if (suffix.length > 0) {
+ const params = suffix
+ .map((s: unknown) =>
typeof s === 'object' &&
s !== null &&
'value' in s
- ) {
- return String(
- (
- s as {
- value: unknown;
- }
- ).value
- );
- }
- return String(s);
- })
- .join(',');
- finalDataType = `${normalizedBaseType}(${params})`;
- } else if (precision !== undefined) {
- if (scale !== undefined) {
- finalDataType = `${normalizedBaseType}(${precision},${scale})`;
- } else {
- finalDataType = `${normalizedBaseType}(${precision})`;
+ ? String(
+ (
+ s as {
+ value: unknown;
+ }
+ ).value
+ )
+ : String(s)
+ )
+ .join(',');
+ finalDataType = `${finalDataType}(${params})`;
+ } else if (precision !== undefined) {
+ finalDataType =
+ scale !== undefined
+ ? `${finalDataType}(${precision},${scale})`
+ : `${finalDataType}(${precision})`;
+ } else if (
+ typeLength !== undefined &&
+ typeLength !== null
+ ) {
+ finalDataType = `${finalDataType}(${typeLength})`;
}
- } else if (
- length !== undefined &&
- length !== null
- ) {
- finalDataType = `${normalizedBaseType}(${length})`;
}
}
@@ -1496,8 +1768,7 @@ export async function fromPostgres(
columnDef.primary_key ===
'primary key' ||
columnDef.definition?.constraint ===
- 'primary key' ||
- isSerialType,
+ 'primary key',
unique: columnDef.unique === 'unique',
typeArgs: getTypeArgs(columnDef.definition),
default: isSerialType
@@ -1507,12 +1778,10 @@ export async function fromPostgres(
isSerialType ||
columnDef.auto_increment ===
'auto_increment' ||
- (stmt.sql
- .toUpperCase()
- .includes('GENERATED') &&
- stmt.sql
- .toUpperCase()
- .includes('IDENTITY')),
+ hasGeneratedIdentity(
+ stmt.sql,
+ columnName
+ ),
};
// Add the column to the table if it doesn't already exist
@@ -1915,12 +2184,29 @@ export async function fromPostgres(
createIndexStmt.index_name ||
`idx_${tableName}_${columns.join('_')}`;
+ // Extract index type from USING clause (e.g., USING GIN, USING HASH)
+ // The parser may store this in different properties
+ let indexType: string | undefined;
+ const indexUsing = createIndexStmt.index_using;
+ if (typeof indexUsing === 'string') {
+ indexType = indexUsing.toLowerCase();
+ } else if (
+ indexUsing &&
+ typeof indexUsing === 'object' &&
+ 'type' in indexUsing
+ ) {
+ indexType = String(
+ (indexUsing as { type: unknown }).type
+ ).toLowerCase();
+ }
+
table.indexes.push({
name: indexName,
columns,
unique:
createIndexStmt.index_type === 'unique' ||
createIndexStmt.unique === true,
+ type: indexType,
});
}
}
diff --git a/src/lib/data/sql-import/dialect-importers/sqlite/__tests__/sqlite-import.test.ts b/src/lib/data/sql-import/dialect-importers/sqlite/__tests__/sqlite-import.test.ts
new file mode 100644
index 000000000..aa8fb01c1
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/sqlite/__tests__/sqlite-import.test.ts
@@ -0,0 +1,228 @@
+import { describe, it, expect } from 'vitest';
+import { fromSQLite } from '../sqlite';
+
+describe('SQLite Import Tests', () => {
+ it('should parse SQLite script with sqlite_sequence table and all relationships', async () => {
+ const sql = `
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT,
+ age INTEGER
+);
+CREATE TABLE sqlite_sequence(name,seq);
+CREATE TABLE products (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT,
+ price REAL
+);
+CREATE TABLE user_products (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ product_id INTEGER NOT NULL,
+ purchased_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id),
+ FOREIGN KEY (product_id) REFERENCES products(id)
+);
+ `;
+
+ const result = await fromSQLite(sql);
+
+ // ============= CHECK TOTAL COUNTS =============
+ // Should have exactly 4 tables
+ expect(result.tables).toHaveLength(4);
+
+ // Should have exactly 2 foreign key relationships
+ expect(result.relationships).toHaveLength(2);
+
+ // ============= CHECK USERS TABLE =============
+ const usersTable = result.tables.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+ expect(usersTable?.columns).toHaveLength(3); // id, name, age
+
+ // Check each column in users table
+ expect(usersTable?.columns[0]).toMatchObject({
+ name: 'id',
+ type: 'INTEGER',
+ primaryKey: true,
+ increment: true,
+ nullable: false,
+ });
+ expect(usersTable?.columns[1]).toMatchObject({
+ name: 'name',
+ type: 'TEXT',
+ primaryKey: false,
+ nullable: true,
+ });
+ expect(usersTable?.columns[2]).toMatchObject({
+ name: 'age',
+ type: 'INTEGER',
+ primaryKey: false,
+ nullable: true,
+ });
+
+ // ============= CHECK SQLITE_SEQUENCE TABLE =============
+ const sqliteSequenceTable = result.tables.find(
+ (t) => t.name === 'sqlite_sequence'
+ );
+ expect(sqliteSequenceTable).toBeDefined();
+ expect(sqliteSequenceTable?.columns).toHaveLength(2); // name, seq
+
+ // Check columns in sqlite_sequence table
+ expect(sqliteSequenceTable?.columns[0]).toMatchObject({
+ name: 'name',
+ type: 'TEXT', // Should default to TEXT when no type specified
+ primaryKey: false,
+ nullable: true,
+ });
+ expect(sqliteSequenceTable?.columns[1]).toMatchObject({
+ name: 'seq',
+ type: 'TEXT', // Should default to TEXT when no type specified
+ primaryKey: false,
+ nullable: true,
+ });
+
+ // ============= CHECK PRODUCTS TABLE =============
+ const productsTable = result.tables.find((t) => t.name === 'products');
+ expect(productsTable).toBeDefined();
+ expect(productsTable?.columns).toHaveLength(3); // id, name, price
+
+ // Check each column in products table
+ expect(productsTable?.columns[0]).toMatchObject({
+ name: 'id',
+ type: 'INTEGER',
+ primaryKey: true,
+ increment: true,
+ nullable: false,
+ });
+ expect(productsTable?.columns[1]).toMatchObject({
+ name: 'name',
+ type: 'TEXT',
+ primaryKey: false,
+ nullable: true,
+ });
+ expect(productsTable?.columns[2]).toMatchObject({
+ name: 'price',
+ type: 'REAL',
+ primaryKey: false,
+ nullable: true,
+ });
+
+ // ============= CHECK USER_PRODUCTS TABLE =============
+ const userProductsTable = result.tables.find(
+ (t) => t.name === 'user_products'
+ );
+ expect(userProductsTable).toBeDefined();
+ expect(userProductsTable?.columns).toHaveLength(4); // id, user_id, product_id, purchased_at
+
+ // Check each column in user_products table
+ expect(userProductsTable?.columns[0]).toMatchObject({
+ name: 'id',
+ type: 'INTEGER',
+ primaryKey: true,
+ increment: true,
+ nullable: false,
+ });
+ expect(userProductsTable?.columns[1]).toMatchObject({
+ name: 'user_id',
+ type: 'INTEGER',
+ primaryKey: false,
+ nullable: false, // NOT NULL constraint
+ });
+ expect(userProductsTable?.columns[2]).toMatchObject({
+ name: 'product_id',
+ type: 'INTEGER',
+ primaryKey: false,
+ nullable: false, // NOT NULL constraint
+ });
+ expect(userProductsTable?.columns[3]).toMatchObject({
+ name: 'purchased_at',
+ type: 'TIMESTAMP', // DATETIME should map to TIMESTAMP
+ primaryKey: false,
+ nullable: true,
+ default: 'CURRENT_TIMESTAMP',
+ });
+
+ // ============= CHECK FOREIGN KEY RELATIONSHIPS =============
+ // FK 1: user_products.user_id -> users.id
+ const userIdFK = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'user_products' &&
+ r.sourceColumn === 'user_id' &&
+ r.targetTable === 'users' &&
+ r.targetColumn === 'id'
+ );
+ expect(userIdFK).toBeDefined();
+ expect(userIdFK).toMatchObject({
+ sourceTable: 'user_products',
+ sourceColumn: 'user_id',
+ targetTable: 'users',
+ targetColumn: 'id',
+ });
+
+ // FK 2: user_products.product_id -> products.id
+ const productIdFK = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'user_products' &&
+ r.sourceColumn === 'product_id' &&
+ r.targetTable === 'products' &&
+ r.targetColumn === 'id'
+ );
+ expect(productIdFK).toBeDefined();
+ expect(productIdFK).toMatchObject({
+ sourceTable: 'user_products',
+ sourceColumn: 'product_id',
+ targetTable: 'products',
+ targetColumn: 'id',
+ });
+ });
+
+ describe('Primary Key Uniqueness', () => {
+ it('should mark single-column primary key field as unique', async () => {
+ const sql = `
+CREATE TABLE table_1 (
+ id INTEGER NOT NULL,
+ CONSTRAINT pk_table_1_id PRIMARY KEY (id)
+);
+ `;
+
+ const result = await fromSQLite(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(true);
+ });
+
+ it('should not mark composite primary key fields as unique individually', async () => {
+ const sql = `
+CREATE TABLE table_1 (
+ id INTEGER NOT NULL,
+ field_2 INTEGER NOT NULL,
+ CONSTRAINT pk_table_1_id PRIMARY KEY (id, field_2)
+);
+ `;
+
+ const result = await fromSQLite(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(false);
+
+ const field2Column = table.columns.find(
+ (c) => c.name === 'field_2'
+ );
+ expect(field2Column).toBeDefined();
+ expect(field2Column?.primaryKey).toBe(true);
+ expect(field2Column?.unique).toBe(false);
+ });
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/sqlite/sqlite.ts b/src/lib/data/sql-import/dialect-importers/sqlite/sqlite.ts
index bd40500cb..fecb48a30 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlite/sqlite.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlite/sqlite.ts
@@ -5,6 +5,7 @@ import type {
SQLIndex,
SQLForeignKey,
SQLASTNode,
+ SQLCheckConstraint,
} from '../../common';
import type {
TableReference,
@@ -32,11 +33,11 @@ export async function fromSQLite(sqlContent: string): Promise {
const tableMap: Record = {}; // Maps table name to its ID
try {
- // SPECIAL HANDLING: Direct line-by-line parser for SQLite DDL
- // This ensures we preserve the exact data types from the original DDL
+ // SPECIAL HANDLING: Direct regex-based parser for SQLite DDL
+ // This ensures we handle all SQLite-specific syntax including tables without types
const directlyParsedTables = parseCreateTableStatements(sqlContent);
- // Check if we successfully parsed tables directly
+ // Always try direct parsing first as it's more reliable for SQLite
if (directlyParsedTables.length > 0) {
// Map the direct parsing results to the expected SQLParserResult format
directlyParsedTables.forEach((table) => {
@@ -56,8 +57,19 @@ export async function fromSQLite(sqlContent: string): Promise {
// Process foreign keys using the regex approach
findForeignKeysUsingRegex(sqlContent, tableMap, relationships);
- // Return the result
- return { tables, relationships };
+ // Create placeholder tables for any missing referenced tables
+ addPlaceholderTablesForFKReferences(
+ tables,
+ relationships,
+ tableMap
+ );
+
+ // Filter out any invalid relationships
+ const validRelationships = relationships.filter((rel) => {
+ return isValidForeignKeyRelationship(rel, tables);
+ });
+
+ return { tables, relationships: validRelationships };
}
// Preprocess SQL to handle SQLite quoted identifiers
@@ -111,6 +123,9 @@ export async function fromSQLite(sqlContent: string): Promise {
return isValidForeignKeyRelationship(rel, tables);
});
+ // Extract check constraints and add to tables
+ addCheckConstraintsToTables(sqlContent, tables);
+
return { tables, relationships: validRelationships };
} catch (error) {
console.error('Error parsing SQLite SQL:', error);
@@ -118,6 +133,61 @@ export async function fromSQLite(sqlContent: string): Promise {
}
}
+/**
+ * Extract check constraints from SQL and add them to existing tables
+ */
+function addCheckConstraintsToTables(
+ sqlContent: string,
+ tables: SQLTable[]
+): void {
+ // Find all CREATE TABLE statements and extract check constraints
+ const createTableRegex =
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([^\s(]+))\s*\(([\s\S]+?)\)(?:\s*;|\s*$)/gi;
+
+ let match;
+ while ((match = createTableRegex.exec(sqlContent)) !== null) {
+ const tableName = match[1] || match[2];
+ const tableBody = match[3];
+
+ // Find the table in our tables array
+ const table = tables.find(
+ (t) => t.name.toLowerCase() === tableName.toLowerCase()
+ );
+ if (!table) continue;
+
+ // Extract check constraints from this table body
+ const checkPattern =
+ /(?:CONSTRAINT\s+(?:"[^"]+"|[^\s]+)\s+)?CHECK\s*\(/gi;
+ let checkMatch;
+
+ const constraints: SQLCheckConstraint[] = [];
+
+ while ((checkMatch = checkPattern.exec(tableBody)) !== null) {
+ const startIdx = checkMatch.index + checkMatch[0].length;
+ let depth = 1;
+ let endIdx = startIdx;
+
+ // Find the matching closing parenthesis
+ for (let i = startIdx; i < tableBody.length && depth > 0; i++) {
+ if (tableBody[i] === '(') depth++;
+ else if (tableBody[i] === ')') depth--;
+ endIdx = i;
+ }
+
+ if (depth === 0) {
+ const expression = tableBody.substring(startIdx, endIdx).trim();
+ if (expression) {
+ constraints.push({ expression });
+ }
+ }
+ }
+
+ if (constraints.length > 0) {
+ table.checkConstraints = constraints;
+ }
+ }
+}
+
/**
* Parse SQLite CREATE TABLE statements directly to preserve exact type information
*/
@@ -128,103 +198,225 @@ function parseCreateTableStatements(sqlContent: string): {
const tables: {
name: string;
columns: SQLColumn[];
+ primaryKeyColumns?: string[];
}[] = [];
- // Split SQL content into lines
- const lines = sqlContent.split('\n');
-
- let currentTable: { name: string; columns: SQLColumn[] } | null = null;
- let inCreateTable = false;
+ // Remove comments before processing
+ const cleanedSQL = sqlContent
+ .split('\n')
+ .map((line) => {
+ const commentIndex = line.indexOf('--');
+ if (commentIndex >= 0) {
+ return line.substring(0, commentIndex);
+ }
+ return line;
+ })
+ .join('\n');
- // Process each line
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i].trim();
+ // Match all CREATE TABLE statements including those without column definitions
+ const createTableRegex =
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?\s*\(([^;]+?)\)\s*;/gis;
+ let match;
- // Skip empty lines and comments
- if (!line || line.startsWith('--')) {
- continue;
- }
+ while ((match = createTableRegex.exec(cleanedSQL)) !== null) {
+ const tableName = match[1];
+ const tableBody = match[2].trim();
- // Check for CREATE TABLE statement
- if (line.toUpperCase().startsWith('CREATE TABLE')) {
- // Extract table name
- const tableNameMatch =
- /CREATE\s+TABLE\s+(?:if\s+not\s+exists\s+)?["'`]?(\w+)["'`]?/i.exec(
- line
- );
- if (tableNameMatch && tableNameMatch[1]) {
- inCreateTable = true;
- currentTable = {
- name: tableNameMatch[1],
- columns: [],
- };
- }
- }
- // Check for end of CREATE TABLE statement
- else if (inCreateTable && line.includes(');')) {
- if (currentTable) {
- tables.push(currentTable);
+ const table: {
+ name: string;
+ columns: SQLColumn[];
+ primaryKeyColumns?: string[];
+ } = {
+ name: tableName,
+ columns: [],
+ primaryKeyColumns: [],
+ };
+
+ // Special case: sqlite_sequence or tables with columns but no types
+ if (tableName === 'sqlite_sequence' || !tableBody.includes(' ')) {
+ // Parse simple column list without types (e.g., "name,seq")
+ const simpleColumns = tableBody.split(',').map((col) => col.trim());
+ for (const colName of simpleColumns) {
+ if (
+ colName &&
+ !colName.toUpperCase().startsWith('FOREIGN KEY') &&
+ !colName.toUpperCase().startsWith('PRIMARY KEY') &&
+ !colName.toUpperCase().startsWith('UNIQUE') &&
+ !colName.toUpperCase().startsWith('CHECK') &&
+ !colName.toUpperCase().startsWith('CONSTRAINT')
+ ) {
+ table.columns.push({
+ name: colName.replace(/["'`]/g, ''),
+ type: 'TEXT', // Default to TEXT for untyped columns
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ default: '',
+ increment: false,
+ });
+ }
}
- inCreateTable = false;
- currentTable = null;
- }
- // Process column definitions inside CREATE TABLE
- else if (inCreateTable && currentTable && line.includes('"')) {
- // Column line pattern optimized for user's DDL format
- const columnPattern = /\s*["'`](\w+)["'`]\s+([A-Za-z0-9_]+)(.+)?/i;
- const match = columnPattern.exec(line);
-
- if (match) {
- const columnName = match[1];
- const rawType = match[2].toUpperCase();
- const restOfLine = match[3] || '';
-
- // Determine column properties
- const isPrimaryKey = restOfLine
- .toUpperCase()
- .includes('PRIMARY KEY');
- const isNotNull = restOfLine.toUpperCase().includes('NOT NULL');
- const isUnique = restOfLine.toUpperCase().includes('UNIQUE');
-
- // Extract default value
- let defaultValue = '';
- const defaultMatch = /DEFAULT\s+([^,\s)]+)/i.exec(restOfLine);
- if (defaultMatch) {
- defaultValue = defaultMatch[1];
+ } else {
+ // Parse normal table with typed columns
+ // Split by commas not inside parentheses
+ const columnDefs = [];
+ let current = '';
+ let parenDepth = 0;
+
+ for (let i = 0; i < tableBody.length; i++) {
+ const char = tableBody[i];
+ if (char === '(') parenDepth++;
+ else if (char === ')') parenDepth--;
+ else if (char === ',' && parenDepth === 0) {
+ columnDefs.push(current.trim());
+ current = '';
+ continue;
}
+ current += char;
+ }
+ if (current.trim()) {
+ columnDefs.push(current.trim());
+ }
+
+ for (const columnDef of columnDefs) {
+ const line = columnDef.trim();
+ const upperLine = line.toUpperCase();
- // Map to appropriate SQLite storage class
- let columnType = rawType;
- if (rawType === 'INTEGER' || rawType === 'INT') {
- columnType = 'INTEGER';
- } else if (
- ['REAL', 'FLOAT', 'DOUBLE', 'NUMERIC', 'DECIMAL'].includes(
- rawType
- )
+ // Handle table-level PRIMARY KEY constraint
+ // Matches: PRIMARY KEY (col1, col2) or CONSTRAINT name PRIMARY KEY (col1, col2)
+ if (
+ upperLine.startsWith('PRIMARY KEY') ||
+ (upperLine.startsWith('CONSTRAINT') &&
+ upperLine.includes('PRIMARY KEY'))
) {
- columnType = 'REAL';
- } else if (rawType === 'BLOB' || rawType === 'BINARY') {
- columnType = 'BLOB';
- } else if (
- ['TIMESTAMP', 'DATETIME', 'DATE'].includes(rawType)
+ const pkMatch = line.match(/PRIMARY\s+KEY\s*\(([^)]+)\)/i);
+ if (pkMatch) {
+ const pkCols = pkMatch[1]
+ .split(',')
+ .map((c) => c.trim().replace(/["'`]/g, ''));
+ table.primaryKeyColumns = pkCols;
+ }
+ continue;
+ }
+
+ // Skip other constraints
+ if (
+ upperLine.startsWith('FOREIGN KEY') ||
+ upperLine.startsWith('UNIQUE') ||
+ upperLine.startsWith('CHECK') ||
+ upperLine.startsWith('CONSTRAINT')
) {
- columnType = 'TIMESTAMP';
- } else {
- columnType = 'TEXT';
+ continue;
}
- // Add column to the table
- currentTable.columns.push({
- name: columnName,
- type: columnType,
- nullable: !isNotNull,
- primaryKey: isPrimaryKey,
- unique: isUnique || isPrimaryKey,
- default: defaultValue,
- increment: isPrimaryKey && columnType === 'INTEGER',
- });
+ // Parse column: handle both quoted and unquoted identifiers
+ // Pattern: [quotes]columnName[quotes] dataType [constraints]
+ const columnPattern = /^["'`]?([\w]+)["'`]?\s+(\w+)(.*)$/i;
+ const columnMatch = columnPattern.exec(line);
+
+ if (columnMatch) {
+ const columnName = columnMatch[1];
+ const rawType = columnMatch[2].toUpperCase();
+ const restOfLine = columnMatch[3] || '';
+ const upperRest = restOfLine.toUpperCase();
+
+ // Determine column properties
+ const isPrimaryKey = upperRest.includes('PRIMARY KEY');
+ const isAutoIncrement = upperRest.includes('AUTOINCREMENT');
+ const isNotNull =
+ upperRest.includes('NOT NULL') || isPrimaryKey;
+ const isUnique =
+ upperRest.includes('UNIQUE') || isPrimaryKey;
+
+ // Extract default value
+ let defaultValue = '';
+ const defaultMatch = /DEFAULT\s+([^,)]+)/i.exec(restOfLine);
+ if (defaultMatch) {
+ defaultValue = defaultMatch[1].trim();
+ // Remove quotes if present
+ if (
+ (defaultValue.startsWith("'") &&
+ defaultValue.endsWith("'")) ||
+ (defaultValue.startsWith('"') &&
+ defaultValue.endsWith('"'))
+ ) {
+ defaultValue = defaultValue.slice(1, -1);
+ }
+ }
+
+ // Map to appropriate SQLite storage class
+ let columnType = rawType;
+ if (rawType === 'INTEGER' || rawType === 'INT') {
+ columnType = 'INTEGER';
+ } else if (
+ [
+ 'REAL',
+ 'FLOAT',
+ 'DOUBLE',
+ 'NUMERIC',
+ 'DECIMAL',
+ ].includes(rawType)
+ ) {
+ columnType = 'REAL';
+ } else if (rawType === 'BLOB' || rawType === 'BINARY') {
+ columnType = 'BLOB';
+ } else if (
+ ['TIMESTAMP', 'DATETIME', 'DATE', 'TIME'].includes(
+ rawType
+ )
+ ) {
+ columnType = 'TIMESTAMP';
+ } else if (
+ ['TEXT', 'VARCHAR', 'CHAR', 'CLOB', 'STRING'].includes(
+ rawType
+ ) ||
+ rawType.startsWith('VARCHAR') ||
+ rawType.startsWith('CHAR')
+ ) {
+ columnType = 'TEXT';
+ } else {
+ // Default to TEXT for unknown types
+ columnType = 'TEXT';
+ }
+
+ // Add column to the table
+ table.columns.push({
+ name: columnName,
+ type: columnType,
+ nullable: !isNotNull,
+ primaryKey: isPrimaryKey,
+ unique: isUnique,
+ default: defaultValue,
+ increment:
+ isPrimaryKey &&
+ isAutoIncrement &&
+ columnType === 'INTEGER',
+ });
+ }
}
}
+
+ // Apply table-level PRIMARY KEY constraint to columns
+ if (table.primaryKeyColumns && table.primaryKeyColumns.length > 0) {
+ const isSingleColumnPK = table.primaryKeyColumns.length === 1;
+ for (const col of table.columns) {
+ if (table.primaryKeyColumns.includes(col.name)) {
+ col.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (isSingleColumnPK) {
+ col.unique = true;
+ }
+ // In SQLite, INTEGER PRIMARY KEY is auto-incrementing
+ if (col.type.toLowerCase() === 'integer') {
+ col.increment = true;
+ }
+ }
+ }
+ }
+
+ if (table.columns.length > 0 || tableName === 'sqlite_sequence') {
+ tables.push(table);
+ }
}
return tables;
@@ -390,12 +582,16 @@ function processCreateTableStatement(
// Process constraint definition
const constraintDef = def as ConstraintDefinition;
+ // Get columns from either columns or definition.columns
+ const constraintColumns =
+ constraintDef.columns || constraintDef.definition?.columns;
+
// Process PRIMARY KEY constraint
if (
constraintDef.constraint_type === 'primary key' &&
- constraintDef.columns
+ constraintColumns
) {
- primaryKeyColumns = constraintDef.columns
+ primaryKeyColumns = constraintColumns
.map(extractColumnName)
.filter(Boolean);
}
@@ -403,9 +599,9 @@ function processCreateTableStatement(
// Process UNIQUE constraint
if (
constraintDef.constraint_type === 'unique' &&
- constraintDef.columns
+ constraintColumns
) {
- const uniqueColumns = constraintDef.columns
+ const uniqueColumns = constraintColumns
.map(extractColumnName)
.filter(Boolean);
@@ -427,10 +623,16 @@ function processCreateTableStatement(
// Update primary key flags in columns
if (primaryKeyColumns.length > 0) {
+ const isSingleColumnPK = primaryKeyColumns.length === 1;
columns.forEach((column) => {
if (primaryKeyColumns.includes(column.name)) {
column.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (isSingleColumnPK) {
+ column.unique = true;
+ }
+
// In SQLite, INTEGER PRIMARY KEY is automatically an alias for ROWID (auto-incrementing)
if (column.type.toLowerCase() === 'integer') {
column.increment = true;
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-core.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-core.test.ts
index d1f7673d5..855433d3c 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-core.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-core.test.ts
@@ -347,4 +347,54 @@ describe('SQL Server Core Parser Tests', () => {
expect(titleIndex?.unique).toBe(true);
expect(titleIndex?.columns).toContain('Title');
});
+
+ describe('Primary Key Uniqueness', () => {
+ it('should mark single-column primary key field as unique', async () => {
+ const sql = `
+CREATE TABLE [dbo].[table_1] (
+ [id] BIGINT NOT NULL,
+ CONSTRAINT [pk_table_1_id] PRIMARY KEY ([id])
+);
+ `;
+
+ const result = await fromSQLServer(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(true);
+ });
+
+ it('should not mark composite primary key fields as unique individually', async () => {
+ const sql = `
+CREATE TABLE [dbo].[table_1] (
+ [id] BIGINT NOT NULL,
+ [field_2] BIGINT NOT NULL,
+ CONSTRAINT [pk_table_1_id] PRIMARY KEY ([id], [field_2])
+);
+ `;
+
+ const result = await fromSQLServer(sql);
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables[0];
+ expect(table.name).toBe('table_1');
+
+ const idColumn = table.columns.find((c) => c.name === 'id');
+ expect(idColumn).toBeDefined();
+ expect(idColumn?.primaryKey).toBe(true);
+ expect(idColumn?.unique).toBe(false);
+
+ const field2Column = table.columns.find(
+ (c) => c.name === 'field_2'
+ );
+ expect(field2Column).toBeDefined();
+ expect(field2Column?.primaryKey).toBe(true);
+ expect(field2Column?.unique).toBe(false);
+ });
+ });
});
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-exact-user-case.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-exact-user-case.test.ts
index 9842f0ec8..36ab39011 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-exact-user-case.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-exact-user-case.test.ts
@@ -20,32 +20,8 @@ CREATE TABLE [DBO].[SpellComponent](
[ITSSCHOOLMETA] [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),
[KEYATTR] CHAR (100), ) ON [PRIMARY]`;
- console.log('Testing complex fantasy SQL...');
- console.log(
- 'Number of CREATE TABLE statements:',
- (sql.match(/CREATE\s+TABLE/gi) || []).length
- );
-
const result = await fromSQLServer(sql);
- console.log(
- 'Result tables:',
- result.tables.map((t) => t.name)
- );
- console.log('Result relationships:', result.relationships.length);
-
- // Debug: Show actual relationships
- if (result.relationships.length === 0) {
- console.log('WARNING: No relationships found!');
- } else {
- console.log('Relationships found:');
- result.relationships.forEach((r) => {
- console.log(
- ` ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
- );
- });
- }
-
// Should create TWO tables
expect(result.tables).toHaveLength(2);
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-full-flow.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-full-flow.test.ts
index 6641f7fec..f56b7eae5 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-full-flow.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-full-flow.test.ts
@@ -43,7 +43,7 @@ CREATE TABLE [DBO].[SpellComponent](
r.targetFieldId && // Must have field IDs
diagram.tables?.some(
(t) =>
- t.id === r.sourceTableId && t.name === 'SpellDefinition'
+ t.id === r.targetTableId && t.name === 'SpellDefinition'
)
);
expect(fk1).toBeDefined();
@@ -60,28 +60,6 @@ CREATE TABLE [DBO].[SpellComponent](
)
);
expect(fk2).toBeDefined();
-
- console.log(
- 'Full flow test - Relationships created:',
- diagram.relationships?.length
- );
- diagram.relationships?.forEach((r) => {
- const sourceTable = diagram.tables?.find(
- (t) => t.id === r.sourceTableId
- );
- const targetTable = diagram.tables?.find(
- (t) => t.id === r.targetTableId
- );
- const sourceField = sourceTable?.fields.find(
- (f) => f.id === r.sourceFieldId
- );
- const targetField = targetTable?.fields.find(
- (f) => f.id === r.targetFieldId
- );
- console.log(
- ` ${sourceTable?.name}.${sourceField?.name} -> ${targetTable?.name}.${targetField?.name}`
- );
- });
});
it('should handle case-insensitive field matching', async () => {
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-multi-schema.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-multi-schema.test.ts
index 1b8256f36..93ac133e2 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-multi-schema.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-multi-schema.test.ts
@@ -415,10 +415,8 @@ ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_P
(name) => !foundRelationshipNames.includes(name)
);
- if (missingRelationships.length > 0) {
- console.log('Missing relationships:', missingRelationships);
- console.log('Found relationships:', foundRelationshipNames);
- }
+ // Verify all expected relationships were found
+ expect(missingRelationships.length).toBe(0);
// Verify relationships count - we have 32 working relationships
expect(result.relationships.length).toBe(32);
@@ -509,16 +507,6 @@ ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_P
r.targetSchema === 'realm'
);
expect(citiesToKingdoms).toBeDefined();
-
- console.log('Multi-schema test results:');
- console.log('Total schemas:', schemas.size);
- console.log('Total tables:', result.tables.length);
- console.log('Total relationships:', result.relationships.length);
- console.log(
- 'Cross-schema relationships:',
- crossSchemaRelationships.length
- );
- console.log('Within-schema relationships:', withinSchemaRels.length);
});
it('should handle mixed schema notation formats', async () => {
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-relationships.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-relationships.test.ts
index 9d1cb15ab..afb5707a6 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-relationships.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-relationships.test.ts
@@ -164,10 +164,6 @@ describe('SQL Server Foreign Key Relationship Tests', () => {
const result = await fromSQLServer(sql);
- // Debug output
- console.log('Total tables:', result.tables.length);
- console.log('Total relationships:', result.relationships.length);
-
// Check if we have the expected number of tables and relationships
expect(result.tables).toHaveLength(4);
expect(result.relationships).toHaveLength(4);
@@ -182,37 +178,22 @@ describe('SQL Server Foreign Key Relationship Tests', () => {
expect(spellCastingRel).toBeDefined();
- if (spellCastingRel) {
- // Find the corresponding tables
- const spellTable = result.tables.find(
- (t) => t.name === 'Spell' && t.schema === 'spellcasting'
- );
- const spellCastingProcessTable = result.tables.find(
- (t) =>
- t.name === 'SpellCastingProcess' &&
- t.schema === 'spellcasting'
- );
+ // Find the corresponding tables
+ const spellTable = result.tables.find(
+ (t) => t.name === 'Spell' && t.schema === 'spellcasting'
+ );
+ const spellCastingProcessTable = result.tables.find(
+ (t) =>
+ t.name === 'SpellCastingProcess' && t.schema === 'spellcasting'
+ );
- console.log('SpellCastingProcess relationship:', {
- sourceTableId: spellCastingRel.sourceTableId,
- targetTableId: spellCastingRel.targetTableId,
- spellCastingProcessTableId: spellCastingProcessTable?.id,
- spellTableId: spellTable?.id,
- isSourceIdValid:
- spellCastingRel.sourceTableId ===
- spellCastingProcessTable?.id,
- isTargetIdValid:
- spellCastingRel.targetTableId === spellTable?.id,
- });
-
- // Verify the IDs are properly linked
- expect(spellCastingRel.sourceTableId).toBeTruthy();
- expect(spellCastingRel.targetTableId).toBeTruthy();
- expect(spellCastingRel.sourceTableId).toBe(
- spellCastingProcessTable!.id
- );
- expect(spellCastingRel.targetTableId).toBe(spellTable!.id);
- }
+ // Verify the IDs are properly linked
+ expect(spellCastingRel!.sourceTableId).toBeTruthy();
+ expect(spellCastingRel!.targetTableId).toBeTruthy();
+ expect(spellCastingRel!.sourceTableId).toBe(
+ spellCastingProcessTable!.id
+ );
+ expect(spellCastingRel!.targetTableId).toBe(spellTable!.id);
// Check the apprentice self-referencing relationships
const apprenticeWizardRel = result.relationships.find(
@@ -241,13 +222,6 @@ describe('SQL Server Foreign Key Relationship Tests', () => {
r.targetTableId === ''
);
- if (relationshipsWithMissingIds.length > 0) {
- console.log(
- 'Relationships with missing IDs:',
- relationshipsWithMissingIds.slice(0, 5)
- );
- }
-
expect(relationshipsWithMissingIds).toHaveLength(0);
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-single-schema.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-single-schema.test.ts
index 1e408b2cd..2d7855e3e 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-single-schema.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-single-schema.test.ts
@@ -598,13 +598,6 @@ ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
const result = await fromSQLServer(sql);
- // Debug: log table names to see what's parsed
- console.log('Tables found:', result.tables.length);
- console.log(
- 'Table names:',
- result.tables.map((t) => t.name)
- );
-
// Verify correct number of tables
expect(result.tables.length).toBe(37); // Actually 37 tables after counting
@@ -614,7 +607,6 @@ ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
expect(schemas.has('dbo')).toBe(true);
// Verify correct number of relationships
- console.log('Relationships found:', result.relationships.length);
expect(result.relationships.length).toBe(55); // 55 foreign key relationships that can be parsed
// Verify all relationships have valid source and target table IDs
@@ -682,23 +674,5 @@ ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
expect(rel.sourceTableId).toBe(sourceTable?.id);
expect(rel.targetTableId).toBe(targetTable?.id);
}
-
- console.log('Single-schema test results:');
- console.log('Total tables:', result.tables.length);
- console.log('Total relationships:', result.relationships.length);
- console.log(
- 'All relationships properly linked:',
- validRelationships.length === result.relationships.length
- );
-
- // Sample of relationship names for verification
- const sampleRelationships = result.relationships
- .slice(0, 5)
- .map((r) => ({
- name: r.name,
- source: `${r.sourceTable}.${r.sourceColumn}`,
- target: `${r.targetTable}.${r.targetColumn}`,
- }));
- console.log('Sample relationships:', sampleRelationships);
});
});
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-verify-fk.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-verify-fk.test.ts
index c09cc5c19..14b2202d6 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-verify-fk.test.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-verify-fk.test.ts
@@ -54,20 +54,6 @@ CREATE TABLE [DBO].[SpellComponent](
expect(fk2?.targetColumn).toBe('SPELLID');
expect(fk2?.sourceTableId).toBeTruthy();
expect(fk2?.targetTableId).toBeTruthy();
-
- // Log for debugging
- console.log('\n=== FK Verification Results ===');
- console.log(
- 'Tables:',
- result.tables.map((t) => `${t.schema}.${t.name}`)
- );
- console.log('Total FKs found:', result.relationships.length);
- result.relationships.forEach((r, i) => {
- console.log(
- `FK ${i + 1}: ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
- );
- console.log(` IDs: ${r.sourceTableId} -> ${r.targetTableId}`);
- });
});
it('should parse inline FOREIGN KEY syntax correctly', async () => {
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-views.test.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-views.test.ts
new file mode 100644
index 000000000..7622a8e5c
--- /dev/null
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/__tests__/sqlserver-views.test.ts
@@ -0,0 +1,353 @@
+import { describe, it, expect } from 'vitest';
+import { fromSQLServer } from '../sqlserver';
+
+const SQL_WITH_VIEWS = `
+/* SQL Server (T-SQL) simple, "flat" schema: 12 tables + 6 views
+ No dynamic EXEC, just straightforward CREATE statements.
+*/
+
+CREATE SCHEMA demo;
+GO
+
+-- ----------------
+-- Tables (12)
+-- ----------------
+
+CREATE TABLE demo.organizations (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_organizations PRIMARY KEY,
+ name nvarchar(200) NOT NULL,
+ slug nvarchar(80) NOT NULL,
+ plan_tier nvarchar(30) NOT NULL CONSTRAINT DF_organizations_plan_tier DEFAULT ('free'),
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_organizations_created_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT UQ_organizations_slug UNIQUE (slug)
+);
+GO
+
+CREATE TABLE demo.users (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_users PRIMARY KEY,
+ email nvarchar(320) NOT NULL,
+ full_name nvarchar(200) NULL,
+ is_active bit NOT NULL CONSTRAINT DF_users_is_active DEFAULT (1),
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_users_created_at DEFAULT (SYSUTCDATETIME())
+);
+GO
+
+CREATE TABLE demo.org_memberships (
+ org_id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ role nvarchar(30) NOT NULL CONSTRAINT DF_org_memberships_role DEFAULT ('member'),
+ joined_at datetime2(3) NOT NULL CONSTRAINT DF_org_memberships_joined_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT PK_org_memberships PRIMARY KEY (org_id, user_id),
+ CONSTRAINT FK_org_memberships_org FOREIGN KEY (org_id) REFERENCES demo.organizations(id) ON DELETE CASCADE,
+ CONSTRAINT FK_org_memberships_user FOREIGN KEY (user_id) REFERENCES demo.users(id) ON DELETE CASCADE
+);
+GO
+
+CREATE TABLE demo.projects (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_projects PRIMARY KEY,
+ org_id bigint NOT NULL,
+ name nvarchar(200) NOT NULL,
+ [key] nvarchar(20) NOT NULL,
+ is_archived bit NOT NULL CONSTRAINT DF_projects_is_archived DEFAULT (0),
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_projects_created_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT FK_projects_org FOREIGN KEY (org_id) REFERENCES demo.organizations(id) ON DELETE CASCADE,
+ CONSTRAINT UQ_projects_org_key UNIQUE (org_id, [key])
+);
+GO
+
+CREATE TABLE demo.labels (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_labels PRIMARY KEY,
+ org_id bigint NOT NULL,
+ name nvarchar(80) NOT NULL,
+ color nvarchar(20) NOT NULL CONSTRAINT DF_labels_color DEFAULT ('#999999'),
+ CONSTRAINT FK_labels_org FOREIGN KEY (org_id) REFERENCES demo.organizations(id) ON DELETE CASCADE,
+ CONSTRAINT UQ_labels_org_name UNIQUE (org_id, name)
+);
+GO
+
+CREATE TABLE demo.issues (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_issues PRIMARY KEY,
+ project_id bigint NOT NULL,
+ title nvarchar(300) NOT NULL,
+ [description] nvarchar(max) NULL,
+ status nvarchar(30) NOT NULL CONSTRAINT DF_issues_status DEFAULT ('open'),
+ priority int NOT NULL CONSTRAINT DF_issues_priority DEFAULT (3),
+ created_by bigint NULL,
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_issues_created_at DEFAULT (SYSUTCDATETIME()),
+ closed_at datetime2(3) NULL,
+ CONSTRAINT FK_issues_project FOREIGN KEY (project_id) REFERENCES demo.projects(id) ON DELETE CASCADE,
+ CONSTRAINT FK_issues_created_by FOREIGN KEY (created_by) REFERENCES demo.users(id) ON DELETE SET NULL,
+ CONSTRAINT CK_issues_status CHECK (status IN ('open','in_progress','closed')),
+ CONSTRAINT CK_issues_priority CHECK (priority BETWEEN 1 AND 5)
+);
+GO
+
+CREATE TABLE demo.issue_labels (
+ issue_id bigint NOT NULL,
+ label_id bigint NOT NULL,
+ CONSTRAINT PK_issue_labels PRIMARY KEY (issue_id, label_id),
+ CONSTRAINT FK_issue_labels_issue FOREIGN KEY (issue_id) REFERENCES demo.issues(id) ON DELETE CASCADE,
+ CONSTRAINT FK_issue_labels_label FOREIGN KEY (label_id) REFERENCES demo.labels(id) ON DELETE CASCADE
+);
+GO
+
+CREATE TABLE demo.comments (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_comments PRIMARY KEY,
+ issue_id bigint NOT NULL,
+ author_id bigint NULL,
+ body nvarchar(max) NOT NULL,
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_comments_created_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT FK_comments_issue FOREIGN KEY (issue_id) REFERENCES demo.issues(id) ON DELETE CASCADE,
+ CONSTRAINT FK_comments_author FOREIGN KEY (author_id) REFERENCES demo.users(id) ON DELETE SET NULL
+);
+GO
+
+CREATE TABLE demo.api_keys (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_api_keys PRIMARY KEY,
+ org_id bigint NOT NULL,
+ name nvarchar(120) NOT NULL,
+ key_hash nvarchar(200) NOT NULL,
+ last_used_at datetime2(3) NULL,
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_api_keys_created_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT FK_api_keys_org FOREIGN KEY (org_id) REFERENCES demo.organizations(id) ON DELETE CASCADE,
+ CONSTRAINT UQ_api_keys_org_name UNIQUE (org_id, name)
+);
+GO
+
+CREATE TABLE demo.invoices (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_invoices PRIMARY KEY,
+ org_id bigint NOT NULL,
+ period_start date NOT NULL,
+ period_end date NOT NULL,
+ status nvarchar(30) NOT NULL CONSTRAINT DF_invoices_status DEFAULT ('open'),
+ total_cents int NOT NULL CONSTRAINT DF_invoices_total_cents DEFAULT (0),
+ created_at datetime2(3) NOT NULL CONSTRAINT DF_invoices_created_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT FK_invoices_org FOREIGN KEY (org_id) REFERENCES demo.organizations(id) ON DELETE CASCADE,
+ CONSTRAINT CK_invoices_period CHECK (period_end >= period_start),
+ CONSTRAINT CK_invoices_status CHECK (status IN ('open','paid','void')),
+ CONSTRAINT CK_invoices_total_cents CHECK (total_cents >= 0)
+);
+GO
+
+CREATE TABLE demo.payments (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_payments PRIMARY KEY,
+ invoice_id bigint NOT NULL,
+ provider nvarchar(30) NOT NULL,
+ amount_cents int NOT NULL,
+ paid_at datetime2(3) NOT NULL CONSTRAINT DF_payments_paid_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT FK_payments_invoice FOREIGN KEY (invoice_id) REFERENCES demo.invoices(id) ON DELETE CASCADE,
+ CONSTRAINT CK_payments_amount CHECK (amount_cents > 0)
+);
+GO
+
+CREATE TABLE demo.events (
+ id bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_events PRIMARY KEY,
+ org_id bigint NOT NULL,
+ user_id bigint NULL,
+ event_type nvarchar(80) NOT NULL,
+ metadata nvarchar(max) NOT NULL CONSTRAINT DF_events_metadata DEFAULT (N'{}'),
+ occurred_at datetime2(3) NOT NULL CONSTRAINT DF_events_occurred_at DEFAULT (SYSUTCDATETIME()),
+ CONSTRAINT FK_events_org FOREIGN KEY (org_id) REFERENCES demo.organizations(id) ON DELETE CASCADE,
+ CONSTRAINT FK_events_user FOREIGN KEY (user_id) REFERENCES demo.users(id) ON DELETE SET NULL,
+ CONSTRAINT CK_events_metadata_isjson CHECK (ISJSON(metadata) = 1)
+);
+GO
+
+-- ----------------
+-- Views (6)
+-- ----------------
+
+CREATE VIEW demo.v_active_org_members AS
+SELECT
+ o.id AS org_id,
+ o.slug AS org_slug,
+ u.id AS user_id,
+ u.email,
+ u.full_name,
+ m.role,
+ m.joined_at
+FROM demo.org_memberships m
+JOIN demo.organizations o ON o.id = m.org_id
+JOIN demo.users u ON u.id = m.user_id
+WHERE u.is_active = 1;
+GO
+
+CREATE VIEW demo.v_project_issue_summary AS
+SELECT
+ p.id AS project_id,
+ p.org_id,
+ p.name AS project_name,
+ p.[key] AS project_key,
+ SUM(CASE WHEN i.status = 'open' THEN 1 ELSE 0 END) AS open_issues,
+ SUM(CASE WHEN i.status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_issues,
+ SUM(CASE WHEN i.status = 'closed' THEN 1 ELSE 0 END) AS closed_issues,
+ COUNT(i.id) AS total_issues,
+ MAX(i.created_at) AS last_issue_created_at
+FROM demo.projects p
+LEFT JOIN demo.issues i ON i.project_id = p.id
+GROUP BY p.id, p.org_id, p.name, p.[key];
+GO
+
+CREATE VIEW demo.v_issue_details AS
+SELECT
+ i.id AS issue_id,
+ i.project_id,
+ p.org_id,
+ i.title,
+ i.status,
+ i.priority,
+ i.created_at,
+ i.closed_at,
+ i.created_by,
+ u.email AS created_by_email,
+ (SELECT COUNT(*) FROM demo.comments c WHERE c.issue_id = i.id) AS comment_count
+FROM demo.issues i
+JOIN demo.projects p ON p.id = i.project_id
+LEFT JOIN demo.users u ON u.id = i.created_by;
+GO
+
+CREATE VIEW demo.v_invoice_balances AS
+SELECT
+ inv.id AS invoice_id,
+ inv.org_id,
+ inv.period_start,
+ inv.period_end,
+ inv.status,
+ inv.total_cents,
+ ISNULL(SUM(pay.amount_cents), 0) AS paid_cents,
+ CASE
+ WHEN inv.total_cents - ISNULL(SUM(pay.amount_cents), 0) < 0 THEN 0
+ ELSE inv.total_cents - ISNULL(SUM(pay.amount_cents), 0)
+ END AS due_cents,
+ MAX(pay.paid_at) AS last_payment_at
+FROM demo.invoices inv
+LEFT JOIN demo.payments pay ON pay.invoice_id = inv.id
+GROUP BY inv.id, inv.org_id, inv.period_start, inv.period_end, inv.status, inv.total_cents;
+GO
+
+CREATE VIEW demo.v_recent_events AS
+SELECT
+ e.id,
+ e.org_id,
+ o.slug AS org_slug,
+ e.user_id,
+ u.email AS user_email,
+ e.event_type,
+ e.metadata,
+ e.occurred_at
+FROM demo.events e
+JOIN demo.organizations o ON o.id = e.org_id
+LEFT JOIN demo.users u ON u.id = e.user_id
+WHERE e.occurred_at >= DATEADD(day, -7, SYSUTCDATETIME());
+GO
+
+CREATE VIEW demo.v_org_activity_daily AS
+SELECT
+ e.org_id,
+ CAST(e.occurred_at AS date) AS [day],
+ COUNT(*) AS events_count,
+ SUM(CASE WHEN e.event_type = 'login' THEN 1 ELSE 0 END) AS logins
+FROM demo.events e
+WHERE e.occurred_at >= DATEADD(day, -30, SYSUTCDATETIME())
+GROUP BY e.org_id, CAST(e.occurred_at AS date);
+GO
+`;
+
+describe('SQL Server View Import', () => {
+ it('should import 12 tables and 6 views', async () => {
+ const result = await fromSQLServer(SQL_WITH_VIEWS);
+
+ // Count tables and views
+ const tables = result.tables.filter((t) => !t.isView);
+ const views = result.tables.filter((t) => t.isView);
+
+ expect(tables.length).toBe(12);
+ expect(views.length).toBe(6);
+ });
+
+ it('should correctly parse view names and schemas', async () => {
+ const result = await fromSQLServer(SQL_WITH_VIEWS);
+
+ const views = result.tables.filter((t) => t.isView);
+ const viewNames = views.map((v) => v.name).sort();
+
+ expect(viewNames).toEqual([
+ 'v_active_org_members',
+ 'v_invoice_balances',
+ 'v_issue_details',
+ 'v_org_activity_daily',
+ 'v_project_issue_summary',
+ 'v_recent_events',
+ ]);
+
+ // All views should be in the 'demo' schema
+ views.forEach((view) => {
+ expect(view.schema).toBe('demo');
+ });
+ });
+
+ it('should correctly parse table names and schemas', async () => {
+ const result = await fromSQLServer(SQL_WITH_VIEWS);
+
+ const tables = result.tables.filter((t) => !t.isView);
+ const tableNames = tables.map((t) => t.name).sort();
+
+ expect(tableNames).toEqual([
+ 'api_keys',
+ 'comments',
+ 'events',
+ 'invoices',
+ 'issue_labels',
+ 'issues',
+ 'labels',
+ 'org_memberships',
+ 'organizations',
+ 'payments',
+ 'projects',
+ 'users',
+ ]);
+
+ // All tables should be in the 'demo' schema
+ tables.forEach((table) => {
+ expect(table.schema).toBe('demo');
+ });
+ });
+
+ it('should extract columns from views', async () => {
+ const result = await fromSQLServer(SQL_WITH_VIEWS);
+
+ const activeOrgMembersView = result.tables.find(
+ (t) => t.name === 'v_active_org_members'
+ );
+
+ expect(activeOrgMembersView).toBeDefined();
+ expect(activeOrgMembersView?.isView).toBe(true);
+
+ // Check that columns were extracted from the SELECT clause
+ const columnNames = activeOrgMembersView?.columns.map((c) => c.name);
+ expect(columnNames).toContain('org_id');
+ expect(columnNames).toContain('org_slug');
+ expect(columnNames).toContain('user_id');
+ expect(columnNames).toContain('email');
+ expect(columnNames).toContain('full_name');
+ expect(columnNames).toContain('role');
+ expect(columnNames).toContain('joined_at');
+ });
+
+ it('should parse relationships correctly', async () => {
+ const result = await fromSQLServer(SQL_WITH_VIEWS);
+
+ // Check that foreign key relationships are parsed
+ expect(result.relationships.length).toBeGreaterThan(0);
+
+ // Check for specific relationships
+ const projectsOrgFk = result.relationships.find(
+ (r) =>
+ r.sourceTable === 'projects' &&
+ r.targetTable === 'organizations'
+ );
+ expect(projectsOrgFk).toBeDefined();
+ expect(projectsOrgFk?.sourceColumn).toBe('org_id');
+ expect(projectsOrgFk?.targetColumn).toBe('id');
+ });
+});
diff --git a/src/lib/data/sql-import/dialect-importers/sqlserver/sqlserver.ts b/src/lib/data/sql-import/dialect-importers/sqlserver/sqlserver.ts
index a0fe2fc70..bd9eb85e2 100644
--- a/src/lib/data/sql-import/dialect-importers/sqlserver/sqlserver.ts
+++ b/src/lib/data/sql-import/dialect-importers/sqlserver/sqlserver.ts
@@ -6,6 +6,7 @@ import type {
SQLIndex,
SQLForeignKey,
SQLASTNode,
+ SQLCheckConstraint,
} from '../../common';
import type {
TableReference,
@@ -20,6 +21,145 @@ import {
findTableWithSchemaSupport,
} from './sqlserver-common';
+/**
+ * Extract columns from a CREATE VIEW statement
+ * Views can have explicit column names or derive them from the SELECT
+ */
+function extractColumnsFromView(sql: string): SQLColumn[] {
+ const columns: SQLColumn[] = [];
+
+ // First, try to extract explicit column list from CREATE VIEW viewname (col1, col2, ...) AS
+ const explicitColumnsMatch = sql.match(
+ /CREATE\s+(?:OR\s+ALTER\s+)?VIEW\s+(?:\[?[^\]]+\]?\.)?(?:\[?[^\]]+\]?)\s*\(([^)]+)\)\s*(?:WITH\s+[^)]+\s*)?AS/i
+ );
+
+ if (explicitColumnsMatch) {
+ // Parse explicit column list
+ const columnList = explicitColumnsMatch[1];
+ const columnNames = columnList
+ .split(',')
+ .map((col) => col.trim().replace(/^\[|\]$/g, ''));
+
+ for (const colName of columnNames) {
+ if (colName) {
+ columns.push({
+ name: colName,
+ type: 'nvarchar', // Default type for views
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ });
+ }
+ }
+
+ return columns;
+ }
+
+ // If no explicit columns, try to extract from SELECT clause
+ const selectMatch = sql.match(/\bAS\s+SELECT\s+([\s\S]+?)\s+FROM\s+/i);
+
+ if (selectMatch) {
+ const selectClause = selectMatch[1];
+
+ // Handle SELECT * - we can't determine columns
+ if (selectClause.trim() === '*') {
+ return columns;
+ }
+
+ // Split by comma, but be careful of nested functions/expressions
+ let depth = 0;
+ let currentCol = '';
+ const selectParts: string[] = [];
+
+ for (const char of selectClause) {
+ if (char === '(' || char === '[') depth++;
+ else if (char === ')' || char === ']') depth--;
+ else if (char === ',' && depth === 0) {
+ selectParts.push(currentCol.trim());
+ currentCol = '';
+ continue;
+ }
+ currentCol += char;
+ }
+ if (currentCol.trim()) {
+ selectParts.push(currentCol.trim());
+ }
+
+ for (const part of selectParts) {
+ let columnName = '';
+
+ // Check for alias: ... AS [name] or ... AS name
+ const aliasMatch = part.match(/\s+AS\s+\[?(\w+)\]?\s*$/i);
+ if (aliasMatch) {
+ columnName = aliasMatch[1];
+ } else {
+ // Try to extract the column reference
+ const colRefMatch = part.match(
+ /(?:[\w[\]]+\.)?\[?(\w+)\]?\s*$/
+ );
+ if (colRefMatch) {
+ columnName = colRefMatch[1];
+ }
+ }
+
+ if (columnName && columnName !== '*') {
+ columns.push({
+ name: columnName,
+ type: 'nvarchar',
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ });
+ }
+ }
+ }
+
+ return columns;
+}
+
+/**
+ * Extract CHECK constraints from CREATE TABLE statements
+ */
+function extractCheckConstraintsFromCreateTable(
+ sql: string
+): SQLCheckConstraint[] {
+ const constraints: SQLCheckConstraint[] = [];
+
+ // Extract the table body
+ const tableBodyMatch = sql.match(/\(([\s\S]+)\)/);
+ if (!tableBodyMatch) return constraints;
+
+ const tableBody = tableBodyMatch[1];
+
+ // Pattern for CHECK constraints:
+ // CHECK (expression) or CONSTRAINT [name] CHECK (expression)
+ const checkPattern =
+ /(?:CONSTRAINT\s+(?:\[[^\]]+\]|[^\s]+)\s+)?CHECK\s*\(/gi;
+ let match;
+
+ while ((match = checkPattern.exec(tableBody)) !== null) {
+ const startIdx = match.index + match[0].length;
+ let depth = 1;
+ let endIdx = startIdx;
+
+ // Find the matching closing parenthesis
+ for (let i = startIdx; i < tableBody.length && depth > 0; i++) {
+ if (tableBody[i] === '(') depth++;
+ else if (tableBody[i] === ')') depth--;
+ endIdx = i;
+ }
+
+ if (depth === 0) {
+ const expression = tableBody.substring(startIdx, endIdx).trim();
+ if (expression) {
+ constraints.push({ expression });
+ }
+ }
+ }
+
+ return constraints;
+}
+
/**
* Preprocess SQL Server script to remove or modify parts that the parser can't handle
*/
@@ -371,6 +511,34 @@ function parseCreateTableManually(
continue;
}
+ // Handle standalone PRIMARY KEY definitions (without CONSTRAINT keyword)
+ // Format: PRIMARY KEY (column1, column2, ...)
+ if (part.match(/^\s*PRIMARY\s+KEY/i)) {
+ const pkColumnsMatch = part.match(
+ /PRIMARY\s+KEY(?:\s+CLUSTERED)?\s*\(([\s\S]+?)\)/i
+ );
+ if (pkColumnsMatch) {
+ const pkColumns = pkColumnsMatch[1].split(',').map((c) =>
+ c
+ .trim()
+ .replace(/\[|\]|\s+(ASC|DESC)/gi, '')
+ .trim()
+ );
+ const isSingleColumnPK = pkColumns.length === 1;
+ pkColumns.forEach((col) => {
+ const column = columns.find((c) => c.name === col);
+ if (column) {
+ column.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (isSingleColumnPK) {
+ column.unique = true;
+ }
+ }
+ });
+ }
+ continue;
+ }
+
// Handle constraint definitions
if (part.match(/^\s*CONSTRAINT/i)) {
// Parse constraints
@@ -394,9 +562,16 @@ function parseCreateTableManually(
.replace(/\[|\]|\s+(ASC|DESC)/gi, '')
.trim()
);
+ const isSingleColumnPK = pkColumns.length === 1;
pkColumns.forEach((col) => {
const column = columns.find((c) => c.name === col);
- if (column) column.primaryKey = true;
+ if (column) {
+ column.primaryKey = true;
+ // Only mark as unique if single-column PK
+ if (isSingleColumnPK) {
+ column.unique = true;
+ }
+ }
});
}
} else if (constraintType === 'UNIQUE') {
@@ -576,6 +751,9 @@ function parseCreateTableManually(
}
}
+ // Extract check constraints
+ const checkConstraints = extractCheckConstraintsFromCreateTable(statement);
+
// Add the table
tables.push({
id: tableId,
@@ -583,6 +761,8 @@ function parseCreateTableManually(
schema: schema,
columns,
indexes,
+ checkConstraints:
+ checkConstraints.length > 0 ? checkConstraints : undefined,
order: tables.length,
});
}
@@ -653,6 +833,50 @@ export async function fromSQLServer(
parseCreateTableManually(stmt, tables, tableMap, relationships);
}
+ // Parse CREATE VIEW statements
+ const createViewStatements = statements.filter((stmt) => {
+ const upperStmt = stmt.trim().toUpperCase();
+ return (
+ upperStmt.includes('CREATE VIEW') ||
+ upperStmt.includes('CREATE OR ALTER VIEW')
+ );
+ });
+
+ for (const stmt of createViewStatements) {
+ // Extract view name and schema
+ // Handle: CREATE VIEW schema.viewname, CREATE VIEW [schema].[viewname], etc.
+ const viewMatch = stmt.match(
+ /CREATE\s+(?:OR\s+ALTER\s+)?VIEW\s+(?:\[?(\w+)\]?\.)?\[?(\w+)\]?/i
+ );
+
+ if (viewMatch) {
+ // If there's a dot in the view reference, group 1 is schema, group 2 is name
+ // Otherwise, group 1 is undefined and group 2 is the name
+ const schema = viewMatch[1] || 'dbo';
+ const viewName = viewMatch[2];
+
+ if (viewName) {
+ const viewId = generateId();
+ const viewKey = `${schema}.${viewName}`;
+ tableMap[viewKey] = viewId;
+
+ // Extract columns from the view definition
+ const columns = extractColumnsFromView(stmt);
+
+ // Create view object (as a table with isView: true)
+ tables.push({
+ id: viewId,
+ name: viewName,
+ schema: schema,
+ columns,
+ indexes: [], // Views don't have indexes
+ order: tables.length,
+ isView: true,
+ });
+ }
+ }
+ }
+
// Preprocess the SQL content for node-sql-parser
const preprocessedSQL = preprocessSQLServerScript(sqlContent);
diff --git a/src/lib/data/sql-import/index.ts b/src/lib/data/sql-import/index.ts
index 47b387980..d5ff0ba80 100644
--- a/src/lib/data/sql-import/index.ts
+++ b/src/lib/data/sql-import/index.ts
@@ -5,10 +5,12 @@ import { fromPostgresDump } from './dialect-importers/postgresql/postgresql-dump
import { fromSQLServer } from './dialect-importers/sqlserver/sqlserver';
import { fromSQLite } from './dialect-importers/sqlite/sqlite';
+import { fromOracle, isOracleFormat } from './dialect-importers/oracle/oracle';
import type { SQLParserResult } from './common';
import { convertToChartDBDiagram } from './common';
import { adjustTablePositions } from '@/lib/domain/db-table';
import { fromMySQL, isMySQLFormat } from './dialect-importers/mysql/mysql';
+import { getTableIndexesWithPrimaryKey } from '@/lib/domain/db-index';
/**
* Detect if SQL content is from pg_dump format
@@ -137,6 +139,11 @@ export function detectDatabaseType(sqlContent: string): DatabaseType | null {
return DatabaseType.SQLITE;
}
+ // Check for Oracle format
+ if (isOracleFormat(sqlContent)) {
+ return DatabaseType.ORACLE;
+ }
+
// Look for database-specific keywords
if (
sqlContent.includes('SERIAL PRIMARY KEY') ||
@@ -190,7 +197,9 @@ export async function sqlImportToDiagram({
// Select the appropriate parser based on database type
switch (sourceDatabaseType) {
case DatabaseType.POSTGRESQL:
+ case DatabaseType.COCKROACHDB:
// Check if the SQL is from pg_dump and use the appropriate parser
+ // CockroachDB uses PostgreSQL-compatible syntax
if (isPgDumpFormat(sqlContent)) {
parserResult = await fromPostgresDump(sqlContent);
} else {
@@ -209,6 +218,9 @@ export async function sqlImportToDiagram({
case DatabaseType.SQLITE:
parserResult = await fromSQLite(sqlContent);
break;
+ case DatabaseType.ORACLE:
+ parserResult = await fromOracle(sqlContent);
+ break;
default:
throw new Error(`Unsupported database type: ${sourceDatabaseType}`);
}
@@ -226,14 +238,19 @@ export async function sqlImportToDiagram({
mode: 'perSchema',
});
- const sortedTables = adjustedTables.sort((a, b) => {
- if (a.isView === b.isView) {
- // Both are either tables or views, so sort alphabetically by name
- return a.name.localeCompare(b.name);
- }
- // If one is a view and the other is not, put tables first
- return a.isView ? 1 : -1;
- });
+ const sortedTables = adjustedTables
+ .map((table) => ({
+ ...table,
+ indexes: getTableIndexesWithPrimaryKey({ table }),
+ }))
+ .sort((a, b) => {
+ if (a.isView === b.isView) {
+ // Both are either tables or views, so sort alphabetically by name
+ return a.name.localeCompare(b.name);
+ }
+ // If one is a view and the other is not, put tables first
+ return a.isView ? 1 : -1;
+ });
return {
...diagram,
@@ -263,7 +280,8 @@ export async function parseSQLError({
// Validate SQL based on the database type
switch (sourceDatabaseType) {
case DatabaseType.POSTGRESQL:
- // PostgreSQL validation - check format and use appropriate parser
+ case DatabaseType.COCKROACHDB:
+ // PostgreSQL/CockroachDB validation - check format and use appropriate parser
if (isPgDumpFormat(sqlContent)) {
await fromPostgresDump(sqlContent);
} else {
@@ -283,7 +301,10 @@ export async function parseSQLError({
// SQLite validation
await fromSQLite(sqlContent);
break;
- // Add more database types here
+ case DatabaseType.ORACLE:
+ // Oracle validation
+ await fromOracle(sqlContent);
+ break;
default:
throw new Error(
`Unsupported database type: ${sourceDatabaseType}`
diff --git a/src/lib/data/sql-import/sql-validator.ts b/src/lib/data/sql-import/sql-validator.ts
index 83d7dacee..0fcf3a69d 100644
--- a/src/lib/data/sql-import/sql-validator.ts
+++ b/src/lib/data/sql-import/sql-validator.ts
@@ -13,6 +13,7 @@ import {
import { validateMySQLDialect } from './validators/mysql-validator';
import { validateSQLServerDialect } from './validators/sqlserver-validator';
import { validateSQLiteDialect } from './validators/sqlite-validator';
+import { validateOracleDialect } from './validators/oracle-validator';
// Re-export types for backward compatibility
export type { ValidationResult, ValidationError, ValidationWarning };
@@ -29,6 +30,8 @@ export function validateSQL(
): ValidationResult {
switch (databaseType) {
case DatabaseType.POSTGRESQL:
+ case DatabaseType.COCKROACHDB:
+ // CockroachDB uses PostgreSQL-compatible syntax
return validatePostgreSQLDialect(sql);
case DatabaseType.MYSQL:
@@ -44,6 +47,9 @@ export function validateSQL(
// MariaDB uses MySQL validator
return validateMySQLDialect(sql);
+ case DatabaseType.ORACLE:
+ return validateOracleDialect(sql);
+
default:
return {
isValid: false,
diff --git a/src/lib/data/sql-import/validators/mysql-validator.ts b/src/lib/data/sql-import/validators/mysql-validator.ts
index 9e83341a2..4b4b1f8b7 100644
--- a/src/lib/data/sql-import/validators/mysql-validator.ts
+++ b/src/lib/data/sql-import/validators/mysql-validator.ts
@@ -40,6 +40,7 @@ export function validateMySQLDialect(sql: string): ValidationResult {
// Check for common MySQL syntax patterns
const lines = sql.split('\n');
let tableCount = 0;
+ let viewCount = 0;
lines.forEach((line, index) => {
const trimmedLine = line.trim();
@@ -49,6 +50,11 @@ export function validateMySQLDialect(sql: string): ValidationResult {
tableCount++;
}
+ // Count CREATE VIEW statements
+ if (trimmedLine.match(/^\s*CREATE\s+(OR\s+REPLACE\s+)?VIEW/i)) {
+ viewCount++;
+ }
+
// Check for PostgreSQL-specific syntax that won't work in MySQL
if (trimmedLine.includes('SERIAL')) {
warnings.push({
@@ -65,10 +71,26 @@ export function validateMySQLDialect(sql: string): ValidationResult {
}
});
+ // Add import summary message
+ if (tableCount > 0 || viewCount > 0) {
+ const parts: string[] = [];
+ if (tableCount > 0) {
+ parts.push(`${tableCount} table${tableCount !== 1 ? 's' : ''}`);
+ }
+ if (viewCount > 0) {
+ parts.push(`${viewCount} view${viewCount !== 1 ? 's' : ''}`);
+ }
+ warnings.unshift({
+ message: `Found ${parts.join(' and ')} to import.`,
+ type: 'compatibility',
+ });
+ }
+
return {
isValid: errors.length === 0,
errors,
warnings,
tableCount,
+ viewCount,
};
}
diff --git a/src/lib/data/sql-import/validators/oracle-validator.ts b/src/lib/data/sql-import/validators/oracle-validator.ts
new file mode 100644
index 000000000..fcc068bc7
--- /dev/null
+++ b/src/lib/data/sql-import/validators/oracle-validator.ts
@@ -0,0 +1,137 @@
+/**
+ * Oracle SQL Validator
+ * Validates Oracle SQL syntax and provides helpful error messages
+ */
+
+import type {
+ ValidationResult,
+ ValidationError,
+ ValidationWarning,
+} from './postgresql-validator';
+
+/**
+ * Validates Oracle SQL syntax
+ * @param sql - The Oracle SQL to validate
+ * @returns ValidationResult with errors, warnings, and optional fixed SQL
+ */
+export function validateOracleDialect(sql: string): ValidationResult {
+ const errors: ValidationError[] = [];
+ const warnings: ValidationWarning[] = [];
+
+ // First check if the SQL is empty or just whitespace
+ if (!sql || !sql.trim()) {
+ errors.push({
+ line: 1,
+ message: 'SQL script is empty',
+ type: 'syntax',
+ suggestion: 'Add CREATE TABLE statements to import',
+ });
+ return {
+ isValid: false,
+ errors,
+ warnings,
+ tableCount: 0,
+ };
+ }
+
+ // Check for common Oracle syntax patterns
+ const lines = sql.split('\n');
+ let tableCount = 0;
+
+ lines.forEach((line, index) => {
+ const trimmedLine = line.trim();
+
+ // Count CREATE TABLE statements
+ if (trimmedLine.match(/^\s*CREATE\s+TABLE/i)) {
+ tableCount++;
+ }
+
+ // Check for syntax from other databases that won't work in Oracle
+ if (trimmedLine.includes('AUTO_INCREMENT')) {
+ warnings.push({
+ message: `Line ${index + 1}: AUTO_INCREMENT is MySQL syntax. Use GENERATED AS IDENTITY in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.includes('SERIAL')) {
+ warnings.push({
+ message: `Line ${index + 1}: SERIAL is PostgreSQL syntax. Use GENERATED AS IDENTITY in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.match(/\bIDENTITY\s*\(\s*\d+\s*,\s*\d+\s*\)/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: IDENTITY(seed, increment) is SQL Server syntax. Use GENERATED AS IDENTITY in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ // Check for MySQL-specific types
+ if (trimmedLine.match(/\bTINYINT\b/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: TINYINT is not an Oracle type. Consider using NUMBER(3) instead.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.match(/\bMEDIUMINT\b/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: MEDIUMINT is not an Oracle type. Consider using NUMBER(7) instead.`,
+ type: 'compatibility',
+ });
+ }
+
+ // Check for SQL Server-specific types
+ if (trimmedLine.match(/\bNVARCHAR\s*\(\s*max\s*\)/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: NVARCHAR(max) is SQL Server syntax. Use NCLOB in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.match(/\bVARCHAR\s*\(\s*max\s*\)/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: VARCHAR(max) is SQL Server syntax. Use CLOB in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.match(/\bUNIQUEIDENTIFIER\b/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: UNIQUEIDENTIFIER is SQL Server syntax. Use RAW(16) or SYS_GUID() in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.match(/\bDATETIME2\b/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: DATETIME2 is SQL Server syntax. Use TIMESTAMP in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+
+ // Check for PostgreSQL-specific syntax
+ if (trimmedLine.match(/\bJSONB\b/i)) {
+ warnings.push({
+ message: `Line ${index + 1}: JSONB is PostgreSQL syntax. Use JSON in Oracle 21c+ or CLOB for older versions.`,
+ type: 'compatibility',
+ });
+ }
+
+ if (trimmedLine.match(/::/)) {
+ warnings.push({
+ message: `Line ${index + 1}: :: cast syntax is PostgreSQL specific. Use CAST() in Oracle.`,
+ type: 'compatibility',
+ });
+ }
+ });
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ warnings,
+ tableCount,
+ };
+}
diff --git a/src/lib/data/sql-import/validators/postgresql-validator.ts b/src/lib/data/sql-import/validators/postgresql-validator.ts
index 35c7c588d..4fad9b854 100644
--- a/src/lib/data/sql-import/validators/postgresql-validator.ts
+++ b/src/lib/data/sql-import/validators/postgresql-validator.ts
@@ -9,6 +9,8 @@ export interface ValidationResult {
warnings: ValidationWarning[];
fixedSQL?: string;
tableCount?: number;
+ viewCount?: number;
+ relationshipCount?: number;
}
export interface ValidationError {
@@ -150,13 +152,7 @@ export function validatePostgreSQLDialect(sql: string): ValidationResult {
});
}
- // 5. Check for views
- if (/CREATE\s+(OR\s+REPLACE\s+)?VIEW/i.test(sql)) {
- warnings.push({
- message: `View definitions found. These will not be imported.`,
- type: 'compatibility',
- });
- }
+ // 5. Views are now supported - we'll add a message with counts later
// 6. Attempt to auto-fix common issues
let hasAutoFixes = false;
@@ -216,9 +212,33 @@ export function validatePostgreSQLDialect(sql: string): ValidationResult {
let tableCount = 0;
const createTableRegex =
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?[^"\s.]+?"?\.)?["'`]?[^"'`\s.(]+["'`]?/gi;
- const matches = sql.match(createTableRegex);
- if (matches) {
- tableCount = matches.length;
+ const tableMatches = sql.match(createTableRegex);
+ if (tableMatches) {
+ tableCount = tableMatches.length;
+ }
+
+ // 10. Count CREATE VIEW statements
+ let viewCount = 0;
+ const createViewRegex =
+ /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:"?[^"\s.]+?"?\.)?["'`]?[^"'`\s.(]+["'`]?/gi;
+ const viewMatches = sql.match(createViewRegex);
+ if (viewMatches) {
+ viewCount = viewMatches.length;
+ }
+
+ // 11. Add import summary message
+ if (tableCount > 0 || viewCount > 0) {
+ const parts: string[] = [];
+ if (tableCount > 0) {
+ parts.push(`${tableCount} table${tableCount !== 1 ? 's' : ''}`);
+ }
+ if (viewCount > 0) {
+ parts.push(`${viewCount} view${viewCount !== 1 ? 's' : ''}`);
+ }
+ warnings.unshift({
+ message: `Found ${parts.join(' and ')} to import.`,
+ type: 'compatibility',
+ });
}
return {
@@ -227,6 +247,7 @@ export function validatePostgreSQLDialect(sql: string): ValidationResult {
warnings,
fixedSQL: hasAutoFixes && fixedSQL !== sql ? fixedSQL : undefined,
tableCount,
+ viewCount,
};
}
diff --git a/src/lib/data/sql-import/validators/sqlserver-validator.ts b/src/lib/data/sql-import/validators/sqlserver-validator.ts
index dc2f38dcc..2ccf33abb 100644
--- a/src/lib/data/sql-import/validators/sqlserver-validator.ts
+++ b/src/lib/data/sql-import/validators/sqlserver-validator.ts
@@ -40,6 +40,7 @@ export function validateSQLServerDialect(sql: string): ValidationResult {
// Check for common SQL Server syntax patterns
const lines = sql.split('\n');
let tableCount = 0;
+ let viewCount = 0;
lines.forEach((line, index) => {
const trimmedLine = line.trim();
@@ -49,6 +50,11 @@ export function validateSQLServerDialect(sql: string): ValidationResult {
tableCount++;
}
+ // Count CREATE VIEW statements
+ if (trimmedLine.match(/^\s*CREATE\s+(OR\s+ALTER\s+)?VIEW/i)) {
+ viewCount++;
+ }
+
// Check for syntax from other databases that won't work in SQL Server
if (trimmedLine.includes('AUTO_INCREMENT')) {
warnings.push({
@@ -65,10 +71,26 @@ export function validateSQLServerDialect(sql: string): ValidationResult {
}
});
+ // Add import summary message
+ if (tableCount > 0 || viewCount > 0) {
+ const parts: string[] = [];
+ if (tableCount > 0) {
+ parts.push(`${tableCount} table${tableCount !== 1 ? 's' : ''}`);
+ }
+ if (viewCount > 0) {
+ parts.push(`${viewCount} view${viewCount !== 1 ? 's' : ''}`);
+ }
+ warnings.unshift({
+ message: `Found ${parts.join(' and ')} to import.`,
+ type: 'compatibility',
+ });
+ }
+
return {
isValid: errors.length === 0,
errors,
warnings,
tableCount,
+ viewCount,
};
}
diff --git a/src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts b/src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
index 5145ecddd..05ca8f6c2 100644
--- a/src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
+++ b/src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
@@ -596,10 +596,911 @@ describe('Apply DBML Changes - single table', () => {
// Check that the new field is added correctly
expect(result).toEqual(expectedResult);
});
+
+ it('should preserve index name', () => {
+ const sourceDiagram: Diagram = {
+ id: 'mqqwkkod9jb8',
+ name: 'buckle_db',
+ createdAt: new Date('2025-12-04T16:00:06.463Z'),
+ updatedAt: new Date('2025-12-04T16:06:49.070Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: 'r1rp4f64dtpifw7mub089bxy8',
+ name: 'buckle_diagrams_history',
+ schema: 'public',
+ x: 100,
+ y: 300,
+ fields: [
+ {
+ id: '0t0b4e7irmvw53w4qyz99zqm1',
+ name: 'rownum',
+ type: {
+ id: 'int',
+ name: 'int',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: false,
+ increment: true,
+ isArray: false,
+ createdAt: 1764864006454,
+ },
+ {
+ id: 'hugwwfirbk609ejryqqsvw3be',
+ name: 'id',
+ type: {
+ id: 'uuid',
+ name: 'uuid',
+ },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'd9sw9l864y0s6b9bc990y7ebi',
+ name: 'action_type',
+ type: {
+ id: 'varchar',
+ name: 'varchar',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ characterMaximumLength: '50',
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'tref4xugo95u21hd02j3s5q67',
+ name: 'diagram_id',
+ type: {
+ id: 'uuid',
+ name: 'uuid',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'v3g2bkhtozhthlnhs3bhu6s4h',
+ name: 'diagram_name',
+ type: {
+ id: 'varchar',
+ name: 'varchar',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ characterMaximumLength: '2500',
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: '79jq9oamvjw7j5i081pplb4ii',
+ name: 'database_type',
+ type: {
+ id: 'varchar',
+ name: 'varchar',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ characterMaximumLength: '50',
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'dd05cmr4ntvwu5aaeir6s4bco',
+ name: 'database_edition',
+ type: {
+ id: 'varchar',
+ name: 'varchar',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ characterMaximumLength: '50',
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: '7589goqn7n2xk3rg5gny817bb',
+ name: 'diagram_json',
+ type: {
+ id: 'json',
+ name: 'json',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'i5zaobzgwbyn8g9xdcskq4qc8',
+ name: 'changed_by_user_id',
+ type: {
+ id: 'uuid',
+ name: 'uuid',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'u9uana25zrknsc5g7t8sinegm',
+ name: 'account_id',
+ type: {
+ id: 'uuid',
+ name: 'uuid',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'jy29dbbfcr4zn5l585y75tob1',
+ name: 'created_at',
+ type: {
+ id: 'timestamp',
+ name: 'timestamp',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'k5jdbgind50qitigvmpuk00n3',
+ name: 'updated_at',
+ type: {
+ id: 'timestamp',
+ name: 'timestamp',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ increment: false,
+ isArray: false,
+ createdAt: 1764864006455,
+ },
+ ],
+ indexes: [
+ {
+ id: 'drzp2kp4a74ceiwh0jl8fvuy7',
+ name: 'index_diagrams_history_on_changed_by_user_id',
+ unique: false,
+ fieldIds: ['i5zaobzgwbyn8g9xdcskq4qc8'],
+ createdAt: 1764864006455,
+ type: 'btree',
+ },
+ {
+ id: 'r3gc7sxg5dv8o6ur2z2jen8vf',
+ name: 'index_diagrams_history_on_diagram_id',
+ unique: false,
+ fieldIds: ['tref4xugo95u21hd02j3s5q67'],
+ createdAt: 1764864006455,
+ type: 'btree',
+ },
+ {
+ id: '7v78ps9i99hkwt086wos3dywa',
+ name: 'index_diagrams_history_on_account_id',
+ unique: false,
+ fieldIds: ['u9uana25zrknsc5g7t8sinegm'],
+ createdAt: 1764864006455,
+ type: 'btree',
+ },
+ {
+ id: 'jzv2yaggsmorqfczz6g60s22y',
+ name: 'buckle_diagrams_history_new_pkey',
+ unique: true,
+ fieldIds: ['hugwwfirbk609ejryqqsvw3be'],
+ createdAt: 1764864006455,
+ isPrimaryKey: true,
+ },
+ ],
+ color: '#8eb7ff',
+ isView: false,
+ isMaterializedView: false,
+ createdAt: 1764864006455,
+ },
+ ],
+ relationships: [],
+ dependencies: [],
+ areas: [],
+ customTypes: [],
+ notes: [],
+ };
+
+ const targetDiagram: Diagram = {
+ id: 'mqqwkkod9jb8',
+ name: 'buckle_db',
+ createdAt: new Date('2025-12-04T16:00:06.463Z'),
+ updatedAt: new Date('2025-12-04T16:06:49.070Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: 'lexrean4b8pm5lg15ppmkywsv',
+ name: 'buckle_diagrams_history',
+ schema: '',
+ order: 0,
+ fields: [
+ {
+ id: '45z2qw9van4yyowwnbs21m767',
+ name: 'rownum',
+ type: {
+ name: 'int',
+ id: 'int',
+ },
+ nullable: false,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ increment: true,
+ },
+ {
+ id: 'rdef7ta48s1wd1tjjoozaw1lv',
+ name: 'id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: false,
+ primaryKey: true,
+ unique: true,
+ createdAt: 1764864417835,
+ },
+ {
+ id: '2mthge4c8s5yt1qe6zp2slsop',
+ name: 'action_type',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ characterMaximumLength: '50',
+ },
+ {
+ id: '8zpgr6s9whcuf43klrbjhaamg',
+ name: 'diagram_id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ },
+ {
+ id: '2rxqewro66448l3i7qo79f22d',
+ name: 'diagram_name',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ characterMaximumLength: '2500',
+ },
+ {
+ id: 'khvw72ifx0s1abkj70vn09gsy',
+ name: 'database_type',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ characterMaximumLength: '50',
+ },
+ {
+ id: 'vjle9bee169krs44qhr0xqnz8',
+ name: 'database_edition',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ characterMaximumLength: '50',
+ },
+ {
+ id: 'd6n910e7qnsc32lymsk02die3',
+ name: 'diagram_json',
+ type: {
+ name: 'json',
+ id: 'json',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ },
+ {
+ id: 'za4nagw0bx1824awleviede7b',
+ name: 'changed_by_user_id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ },
+ {
+ id: 'cspop4m5whpw8ckq51aafe58a',
+ name: 'account_id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ },
+ {
+ id: '2z2sixnpkv243xx7nuh2ki0fw',
+ name: 'created_at',
+ type: {
+ name: 'timestamp',
+ id: 'timestamp',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ },
+ {
+ id: '5jeve2jfv557ijuvwfv5j1t8m',
+ name: 'updated_at',
+ type: {
+ name: 'timestamp',
+ id: 'timestamp',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864417835,
+ },
+ ],
+ indexes: [
+ {
+ id: '3fsk7qhy0xosg4gr6dcul5t01',
+ name: 'pk_buckle_diagrams_history_id',
+ fieldIds: ['rdef7ta48s1wd1tjjoozaw1lv'],
+ unique: true,
+ isPrimaryKey: true,
+ createdAt: 1764864417836,
+ },
+ {
+ id: 'iujj3p438ueo1vpbv08szxln3',
+ name: 'index_diagrams_history_on_changed_by_user_id',
+ fieldIds: ['za4nagw0bx1824awleviede7b'],
+ unique: false,
+ createdAt: 1764864417836,
+ },
+ {
+ id: 'mq0eh23rt98izx8zhwg5rl0tf',
+ name: 'index_diagrams_history_on_diagram_id',
+ fieldIds: ['8zpgr6s9whcuf43klrbjhaamg'],
+ unique: false,
+ createdAt: 1764864417836,
+ },
+ {
+ id: '57wcn3nl6nxeaq0uuptoolo48',
+ name: 'index_diagrams_history_on_account_id',
+ fieldIds: ['cspop4m5whpw8ckq51aafe58a'],
+ unique: false,
+ createdAt: 1764864417836,
+ },
+ ],
+ x: 0,
+ y: 0,
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: 1764864417836,
+ },
+ ],
+ relationships: [],
+ dependencies: [],
+ areas: [],
+ customTypes: [],
+ notes: [],
+ };
+
+ const expectedResult: Diagram = {
+ id: 'mqqwkkod9jb8',
+ name: 'buckle_db',
+ createdAt: new Date('2025-12-04T16:00:06.463Z'),
+ updatedAt: new Date('2025-12-04T16:06:49.070Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: 'r1rp4f64dtpifw7mub089bxy8',
+ name: 'buckle_diagrams_history',
+ schema: 'public',
+ x: 100,
+ y: 300,
+ fields: [
+ {
+ id: '0t0b4e7irmvw53w4qyz99zqm1',
+ name: 'rownum',
+ type: {
+ name: 'int',
+ id: 'int',
+ },
+ nullable: false,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006454,
+ increment: true,
+ },
+ {
+ id: 'hugwwfirbk609ejryqqsvw3be',
+ name: 'id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: false,
+ primaryKey: true,
+ unique: true,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'd9sw9l864y0s6b9bc990y7ebi',
+ name: 'action_type',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ characterMaximumLength: '50',
+ },
+ {
+ id: 'tref4xugo95u21hd02j3s5q67',
+ name: 'diagram_id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'v3g2bkhtozhthlnhs3bhu6s4h',
+ name: 'diagram_name',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ characterMaximumLength: '2500',
+ },
+ {
+ id: '79jq9oamvjw7j5i081pplb4ii',
+ name: 'database_type',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ characterMaximumLength: '50',
+ },
+ {
+ id: 'dd05cmr4ntvwu5aaeir6s4bco',
+ name: 'database_edition',
+ type: {
+ name: 'varchar',
+ id: 'varchar',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ characterMaximumLength: '50',
+ },
+ {
+ id: '7589goqn7n2xk3rg5gny817bb',
+ name: 'diagram_json',
+ type: {
+ name: 'json',
+ id: 'json',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'i5zaobzgwbyn8g9xdcskq4qc8',
+ name: 'changed_by_user_id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'u9uana25zrknsc5g7t8sinegm',
+ name: 'account_id',
+ type: {
+ name: 'uuid',
+ id: 'uuid',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'jy29dbbfcr4zn5l585y75tob1',
+ name: 'created_at',
+ type: {
+ name: 'timestamp',
+ id: 'timestamp',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'k5jdbgind50qitigvmpuk00n3',
+ name: 'updated_at',
+ type: {
+ name: 'timestamp',
+ id: 'timestamp',
+ },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ ],
+ indexes: [
+ {
+ id: 'jzv2yaggsmorqfczz6g60s22y',
+ name: 'buckle_diagrams_history_new_pkey',
+ fieldIds: ['hugwwfirbk609ejryqqsvw3be'],
+ unique: true,
+ createdAt: 1764864006455,
+ isPrimaryKey: true,
+ },
+ {
+ id: 'drzp2kp4a74ceiwh0jl8fvuy7',
+ name: 'index_diagrams_history_on_changed_by_user_id',
+ fieldIds: ['i5zaobzgwbyn8g9xdcskq4qc8'],
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: 'r3gc7sxg5dv8o6ur2z2jen8vf',
+ name: 'index_diagrams_history_on_diagram_id',
+ fieldIds: ['tref4xugo95u21hd02j3s5q67'],
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ {
+ id: '7v78ps9i99hkwt086wos3dywa',
+ name: 'index_diagrams_history_on_account_id',
+ fieldIds: ['u9uana25zrknsc5g7t8sinegm'],
+ unique: false,
+ createdAt: 1764864006455,
+ },
+ ],
+ color: '#8eb7ff',
+ isView: false,
+ isMaterializedView: false,
+ createdAt: 1764864006455,
+ },
+ ],
+ relationships: [],
+ dependencies: [],
+ areas: [],
+ customTypes: [],
+ notes: [],
+ };
+
+ const result = applyDBMLChanges({
+ sourceDiagram,
+ targetDiagram,
+ });
+
+ // Check that the new field is added correctly
+ expect(result).toEqual(expectedResult);
+ });
});
-describe('Apply DBML Changes - relationships', () => {
- it('should preserve relationships when table change', () => {
+describe('Apply DBML Changes - relationships', () => {
+ it('should preserve relationships when table change', () => {
+ const sourceDiagram: Diagram = {
+ id: 'mqqwkkodrxxd',
+ name: 'Diagram 9',
+ createdAt: new Date('2025-07-30T15:44:53.967Z'),
+ updatedAt: new Date('2025-07-30T18:18:02.016Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ name: 'table_1',
+ x: 260,
+ y: 80,
+ fields: [
+ {
+ id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: true,
+ nullable: false,
+ primaryKey: true,
+ createdAt: 1753890297335,
+ },
+ ],
+ indexes: [],
+ color: '#4dee8a',
+ createdAt: 1753890297335,
+ isView: false,
+ order: 0,
+ parentAreaId: null,
+ },
+ {
+ id: 'km6d66gzzsihmyg9v04ppcets',
+ name: 'table_2',
+ x: -163.75,
+ y: -5,
+ fields: [
+ {
+ id: 'a9apya3ihmzfa1ponbeuy4znf',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: true,
+ nullable: false,
+ primaryKey: true,
+ createdAt: 1753899478715,
+ },
+ ],
+ indexes: [],
+ color: '#9ef07a',
+ createdAt: 1753899478715,
+ isView: false,
+ order: 1,
+ parentAreaId: null,
+ },
+ ],
+ relationships: [
+ {
+ id: 'snsf2455bwr0iegdsfwwwbzj9',
+ name: 'table_2_id_fk',
+ sourceTableId: 'km6d66gzzsihmyg9v04ppcets',
+ targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ sourceFieldId: 'a9apya3ihmzfa1ponbeuy4znf',
+ targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ createdAt: 1753899482016,
+ },
+ ],
+ dependencies: [],
+ areas: [],
+ customTypes: [],
+ };
+ const targetDiagram: Diagram = {
+ id: 'mqqwkkodrxxd',
+ name: 'Diagram 9',
+ createdAt: new Date('2025-07-30T15:44:53.967Z'),
+ updatedAt: new Date('2025-07-30T18:18:02.016Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: 'l6mvwq8ynw9glgf87ysslafjv',
+ name: 'table_1',
+ schema: 'public',
+ order: 0,
+ fields: [
+ {
+ id: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ name: 'id',
+ type: { name: 'bigint', id: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753899628831,
+ },
+ {
+ id: 'pg8z55iuk7ppbrb6fp9n3n2md',
+ name: 'name',
+ type: { name: 'int', id: 'int' },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1753899628831,
+ },
+ ],
+ indexes: [],
+ x: 0,
+ y: 0,
+ color: '#b067e9',
+ isView: false,
+ createdAt: 1753899628831,
+ },
+ {
+ id: '8kmv3noehjay988rp57ed69ff',
+ name: 'table_2',
+ schema: 'public',
+ order: 1,
+ fields: [
+ {
+ id: 'dqc01ynfl1gmg5nqctem2dqlq',
+ name: 'id',
+ type: { name: 'bigint', id: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753899628831,
+ },
+ ],
+ indexes: [],
+ x: 300,
+ y: 0,
+ color: '#ff9f74',
+ isView: false,
+ createdAt: 1753899628831,
+ },
+ ],
+ relationships: [
+ {
+ id: 'm7i7u56hf7xw2tfz9g5bi2w1u',
+ name: 'table_1_id_table_2_id',
+ sourceSchema: 'public',
+ targetSchema: 'public',
+ sourceTableId: 'l6mvwq8ynw9glgf87ysslafjv',
+ targetTableId: '8kmv3noehjay988rp57ed69ff',
+ sourceFieldId: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ targetFieldId: 'dqc01ynfl1gmg5nqctem2dqlq',
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ createdAt: 1753899628831,
+ },
+ ],
+ dependencies: [],
+ areas: [],
+ };
+ const expectedResult: Diagram = {
+ id: 'mqqwkkodrxxd',
+ name: 'Diagram 9',
+ createdAt: new Date('2025-07-30T15:44:53.967Z'),
+ updatedAt: new Date('2025-07-30T18:18:02.016Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ name: 'table_1',
+ x: 260,
+ y: 80,
+ fields: [
+ {
+ id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753890297335,
+ },
+ {
+ id: 'pg8z55iuk7ppbrb6fp9n3n2md',
+ name: 'name',
+ type: { name: 'int', id: 'int' },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1753899628831,
+ },
+ ],
+ indexes: [],
+ color: '#4dee8a',
+ createdAt: 1753890297335,
+ isView: false,
+ order: 0,
+ parentAreaId: null,
+ },
+ {
+ id: 'km6d66gzzsihmyg9v04ppcets',
+ name: 'table_2',
+ x: -163.75,
+ y: -5,
+ fields: [
+ {
+ id: 'a9apya3ihmzfa1ponbeuy4znf',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753899478715,
+ },
+ ],
+ indexes: [],
+ color: '#9ef07a',
+ createdAt: 1753899478715,
+ isView: false,
+ order: 1,
+ parentAreaId: null,
+ },
+ ],
+ relationships: [
+ {
+ id: 'snsf2455bwr0iegdsfwwwbzj9',
+ name: 'table_2_id_fk',
+ sourceTableId: 'km6d66gzzsihmyg9v04ppcets',
+ targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ sourceFieldId: 'a9apya3ihmzfa1ponbeuy4znf',
+ targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ createdAt: 1753899482016,
+ },
+ ],
+ dependencies: [],
+ areas: [],
+ customTypes: [],
+ };
+ const result = applyDBMLChanges({
+ sourceDiagram,
+ targetDiagram,
+ });
+ // Check that the new field is added correctly
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should remove relationship', () => {
const sourceDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
@@ -693,19 +1594,136 @@ describe('Apply DBML Changes - relationships', () => {
unique: false,
createdAt: 1753899628831,
},
+ ],
+ indexes: [],
+ x: 0,
+ y: 0,
+ color: '#b067e9',
+ isView: false,
+ createdAt: 1753899628831,
+ },
+ {
+ id: '8kmv3noehjay988rp57ed69ff',
+ name: 'table_2',
+ schema: 'public',
+ order: 1,
+ fields: [
{
- id: 'pg8z55iuk7ppbrb6fp9n3n2md',
- name: 'name',
- type: { name: 'int', id: 'int' },
- nullable: true,
- primaryKey: false,
+ id: 'dqc01ynfl1gmg5nqctem2dqlq',
+ name: 'id',
+ type: { name: 'bigint', id: 'bigint' },
+ nullable: false,
+ primaryKey: true,
unique: false,
createdAt: 1753899628831,
},
],
indexes: [],
- x: 0,
+ x: 300,
y: 0,
+ color: '#ff9f74',
+ isView: false,
+ createdAt: 1753899628831,
+ },
+ ],
+ relationships: [],
+ dependencies: [],
+ areas: [],
+ };
+ const expectedResult: Diagram = {
+ id: 'mqqwkkodrxxd',
+ name: 'Diagram 9',
+ createdAt: new Date('2025-07-30T15:44:53.967Z'),
+ updatedAt: new Date('2025-07-30T18:18:02.016Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ name: 'table_1',
+ x: 260,
+ y: 80,
+ fields: [
+ {
+ id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753890297335,
+ },
+ ],
+ indexes: [],
+ color: '#4dee8a',
+ createdAt: 1753890297335,
+ isView: false,
+ order: 0,
+ parentAreaId: null,
+ },
+ {
+ id: 'km6d66gzzsihmyg9v04ppcets',
+ name: 'table_2',
+ x: -163.75,
+ y: -5,
+ fields: [
+ {
+ id: 'a9apya3ihmzfa1ponbeuy4znf',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753899478715,
+ },
+ ],
+ indexes: [],
+ color: '#9ef07a',
+ createdAt: 1753899478715,
+ isView: false,
+ order: 1,
+ parentAreaId: null,
+ },
+ ],
+ relationships: [],
+ dependencies: [],
+ areas: [],
+ customTypes: [],
+ };
+ const result = applyDBMLChanges({
+ sourceDiagram,
+ targetDiagram,
+ });
+ // Check that the new field is added correctly
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should add relationship', () => {
+ const sourceDiagram: Diagram = {
+ id: 'mqqwkkodrxxd',
+ name: 'Diagram 9',
+ createdAt: new Date('2025-07-30T15:44:53.967Z'),
+ updatedAt: new Date('2025-07-30T18:18:02.016Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: 'l6mvwq8ynw9glgf87ysslafjv',
+ name: 'table_1',
+ schema: 'public',
+ order: 0,
+ fields: [
+ {
+ id: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ name: 'id',
+ type: { name: 'bigint', id: 'bigint' },
+ nullable: false,
+ primaryKey: true,
+ unique: false,
+ createdAt: 1753899628831,
+ },
+ ],
+ indexes: [],
+ x: 100,
+ y: 20,
color: '#b067e9',
isView: false,
createdAt: 1753899628831,
@@ -728,30 +1746,89 @@ describe('Apply DBML Changes - relationships', () => {
],
indexes: [],
x: 300,
- y: 0,
- color: '#ff9f74',
+ y: 580,
+ color: '#b067e9',
isView: false,
createdAt: 1753899628831,
},
],
- relationships: [
+ relationships: [],
+ dependencies: [],
+ areas: [],
+ };
+
+ const targetDiagram: Diagram = {
+ id: 'mqqwkkodrxxd',
+ name: 'Diagram 9',
+ createdAt: new Date('2025-07-30T15:44:53.967Z'),
+ updatedAt: new Date('2025-07-30T18:18:02.016Z'),
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
{
- id: 'm7i7u56hf7xw2tfz9g5bi2w1u',
- name: 'table_1_id_table_2_id',
- sourceSchema: 'public',
- targetSchema: 'public',
- sourceTableId: 'l6mvwq8ynw9glgf87ysslafjv',
- targetTableId: '8kmv3noehjay988rp57ed69ff',
- sourceFieldId: 'wehbg9d2y04xnt0jb2wrdm3v5',
- targetFieldId: 'dqc01ynfl1gmg5nqctem2dqlq',
+ id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ name: 'table_1',
+ x: 260,
+ y: 80,
+ fields: [
+ {
+ id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: true,
+ nullable: false,
+ primaryKey: true,
+ createdAt: 1753890297335,
+ },
+ ],
+ indexes: [],
+ color: '0',
+ createdAt: 1753890297335,
+ isView: false,
+ order: 0,
+ parentAreaId: null,
+ },
+ {
+ id: 'km6d66gzzsihmyg9v04ppcets',
+ name: 'table_2',
+ x: -163.75,
+ y: -5,
+ fields: [
+ {
+ id: 'a9apya3ihmzfa1ponbeuy4znf',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: true,
+ nullable: false,
+ primaryKey: true,
+ createdAt: 1753899478715,
+ },
+ ],
+ indexes: [],
+ color: '0',
+ createdAt: 1753899478715,
+ isView: false,
+ order: 1,
+ parentAreaId: null,
+ },
+ ],
+ relationships: [
+ {
+ id: 'snsf2455bwr0iegdsfwwwbzj9',
+ name: 'table_2_id_fk',
+ sourceTableId: 'km6d66gzzsihmyg9v04ppcets',
+ targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ sourceFieldId: 'a9apya3ihmzfa1ponbeuy4znf',
+ targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
sourceCardinality: 'one',
targetCardinality: 'one',
- createdAt: 1753899628831,
+ createdAt: 1753899482016,
},
],
dependencies: [],
areas: [],
+ customTypes: [],
};
+
const expectedResult: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
@@ -760,69 +1837,60 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ id: 'l6mvwq8ynw9glgf87ysslafjv',
name: 'table_1',
- x: 260,
- y: 80,
+ schema: 'public',
+ order: 0,
fields: [
{
- id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ id: 'wehbg9d2y04xnt0jb2wrdm3v5',
name: 'id',
- type: { id: 'bigint', name: 'bigint' },
+ type: { name: 'bigint', id: 'bigint' },
+ unique: true,
nullable: false,
primaryKey: true,
- unique: false,
- createdAt: 1753890297335,
- },
- {
- id: 'pg8z55iuk7ppbrb6fp9n3n2md',
- name: 'name',
- type: { name: 'int', id: 'int' },
- nullable: true,
- primaryKey: false,
- unique: false,
createdAt: 1753899628831,
},
],
indexes: [],
- color: '#4dee8a',
- createdAt: 1753890297335,
+ x: 100,
+ y: 20,
+ color: '#b067e9',
isView: false,
- order: 0,
- parentAreaId: null,
+ createdAt: 1753899628831,
},
{
- id: 'km6d66gzzsihmyg9v04ppcets',
+ id: '8kmv3noehjay988rp57ed69ff',
name: 'table_2',
- x: -163.75,
- y: -5,
+ schema: 'public',
+ order: 1,
fields: [
{
- id: 'a9apya3ihmzfa1ponbeuy4znf',
+ id: 'dqc01ynfl1gmg5nqctem2dqlq',
name: 'id',
- type: { id: 'bigint', name: 'bigint' },
+ type: { name: 'bigint', id: 'bigint' },
+ unique: true,
nullable: false,
primaryKey: true,
- unique: false,
- createdAt: 1753899478715,
+ createdAt: 1753899628831,
},
],
indexes: [],
- color: '#9ef07a',
- createdAt: 1753899478715,
+ x: 300,
+ y: 580,
+ color: '#b067e9',
isView: false,
- order: 1,
- parentAreaId: null,
+ createdAt: 1753899628831,
},
],
relationships: [
{
id: 'snsf2455bwr0iegdsfwwwbzj9',
name: 'table_2_id_fk',
- sourceTableId: 'km6d66gzzsihmyg9v04ppcets',
- targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
- sourceFieldId: 'a9apya3ihmzfa1ponbeuy4znf',
- targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
+ sourceTableId: '8kmv3noehjay988rp57ed69ff',
+ targetTableId: 'l6mvwq8ynw9glgf87ysslafjv',
+ sourceFieldId: 'dqc01ynfl1gmg5nqctem2dqlq',
+ targetFieldId: 'wehbg9d2y04xnt0jb2wrdm3v5',
sourceCardinality: 'one',
targetCardinality: 'one',
createdAt: 1753899482016,
@@ -832,6 +1900,7 @@ describe('Apply DBML Changes - relationships', () => {
areas: [],
customTypes: [],
};
+
const result = applyDBMLChanges({
sourceDiagram,
targetDiagram,
@@ -840,7 +1909,10 @@ describe('Apply DBML Changes - relationships', () => {
expect(result).toEqual(expectedResult);
});
- it('should remove relationship', () => {
+ it('should handle cardinality update when relationship direction is reversed', () => {
+ // Source has relationship: table_2.id -> table_1.id (one-to-one)
+ // Target has relationship: table_1.id -> table_2.id (one-to-many) - reversed direction
+ // Result should: preserve source direction but swap cardinalities from target
const sourceDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
@@ -849,13 +1921,14 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ id: 'table1-source-id',
name: 'table_1',
+ schema: 'public',
x: 260,
y: 80,
fields: [
{
- id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ id: 'field1-source-id',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
unique: true,
@@ -869,16 +1942,16 @@ describe('Apply DBML Changes - relationships', () => {
createdAt: 1753890297335,
isView: false,
order: 0,
- parentAreaId: null,
},
{
- id: 'km6d66gzzsihmyg9v04ppcets',
+ id: 'table2-source-id',
name: 'table_2',
+ schema: 'public',
x: -163.75,
y: -5,
fields: [
{
- id: 'a9apya3ihmzfa1ponbeuy4znf',
+ id: 'field2-source-id',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
unique: true,
@@ -892,17 +1965,16 @@ describe('Apply DBML Changes - relationships', () => {
createdAt: 1753899478715,
isView: false,
order: 1,
- parentAreaId: null,
},
],
relationships: [
{
- id: 'snsf2455bwr0iegdsfwwwbzj9',
+ id: 'rel-source-id',
name: 'table_2_id_fk',
- sourceTableId: 'km6d66gzzsihmyg9v04ppcets',
- targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
- sourceFieldId: 'a9apya3ihmzfa1ponbeuy4znf',
- targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
+ sourceTableId: 'table2-source-id',
+ targetTableId: 'table1-source-id',
+ sourceFieldId: 'field2-source-id',
+ targetFieldId: 'field1-source-id',
sourceCardinality: 'one',
targetCardinality: 'one',
createdAt: 1753899482016,
@@ -912,6 +1984,8 @@ describe('Apply DBML Changes - relationships', () => {
areas: [],
customTypes: [],
};
+
+ // Target has the relationship in reverse direction with different cardinalities
const targetDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
@@ -920,18 +1994,18 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: 'l6mvwq8ynw9glgf87ysslafjv',
+ id: 'table1-target-id',
name: 'table_1',
schema: 'public',
order: 0,
fields: [
{
- id: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ id: 'field1-target-id',
name: 'id',
type: { name: 'bigint', id: 'bigint' },
nullable: false,
primaryKey: true,
- unique: false,
+ unique: true,
createdAt: 1753899628831,
},
],
@@ -943,18 +2017,18 @@ describe('Apply DBML Changes - relationships', () => {
createdAt: 1753899628831,
},
{
- id: '8kmv3noehjay988rp57ed69ff',
+ id: 'table2-target-id',
name: 'table_2',
schema: 'public',
order: 1,
fields: [
{
- id: 'dqc01ynfl1gmg5nqctem2dqlq',
+ id: 'field2-target-id',
name: 'id',
type: { name: 'bigint', id: 'bigint' },
nullable: false,
primaryKey: true,
- unique: false,
+ unique: true,
createdAt: 1753899628831,
},
],
@@ -966,11 +2040,49 @@ describe('Apply DBML Changes - relationships', () => {
createdAt: 1753899628831,
},
],
- relationships: [],
+ relationships: [
+ {
+ // Relationship defined in reverse direction: table_1 -> table_2
+ // with cardinalities: source='one', target='many'
+ id: 'rel-target-id',
+ name: 'table_1_id_table_2_id',
+ sourceSchema: 'public',
+ targetSchema: 'public',
+ sourceTableId: 'table1-target-id',
+ targetTableId: 'table2-target-id',
+ sourceFieldId: 'field1-target-id',
+ targetFieldId: 'field2-target-id',
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ createdAt: 1753899628831,
+ },
+ ],
dependencies: [],
areas: [],
};
- const expectedResult: Diagram = {
+
+ const result = applyDBMLChanges({
+ sourceDiagram,
+ targetDiagram,
+ });
+
+ // Result should preserve source's direction (table_2 -> table_1)
+ // but with SWAPPED cardinalities from target
+ // Target: table_1(source='one') -> table_2(target='many')
+ // After swap for reverse match: table_2(source='many') -> table_1(target='one')
+ expect(result.relationships).toHaveLength(1);
+ expect(result.relationships![0].id).toBe('rel-source-id');
+ expect(result.relationships![0].sourceTableId).toBe('table2-source-id');
+ expect(result.relationships![0].targetTableId).toBe('table1-source-id');
+ expect(result.relationships![0].sourceCardinality).toBe('many');
+ expect(result.relationships![0].targetCardinality).toBe('one');
+ });
+
+ it('should update cardinality when relationship direction matches (direct match)', () => {
+ // Source has relationship: table_2.id -> table_1.id (one-to-one)
+ // Target has same direction with different cardinalities (many-to-one)
+ // Result should: preserve source IDs with updated cardinalities from target
+ const sourceDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
createdAt: new Date('2025-07-30T15:44:53.967Z'),
@@ -978,18 +2090,19 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: '8ftpn9qn0o2ddrvhzgdjro3zv',
+ id: 'table1-source-id',
name: 'table_1',
+ schema: 'public',
x: 260,
y: 80,
fields: [
{
- id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ id: 'field1-source-id',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
+ unique: true,
nullable: false,
primaryKey: true,
- unique: false,
createdAt: 1753890297335,
},
],
@@ -998,21 +2111,21 @@ describe('Apply DBML Changes - relationships', () => {
createdAt: 1753890297335,
isView: false,
order: 0,
- parentAreaId: null,
},
{
- id: 'km6d66gzzsihmyg9v04ppcets',
+ id: 'table2-source-id',
name: 'table_2',
+ schema: 'public',
x: -163.75,
y: -5,
fields: [
{
- id: 'a9apya3ihmzfa1ponbeuy4znf',
+ id: 'field2-source-id',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
+ unique: false,
nullable: false,
primaryKey: true,
- unique: false,
createdAt: 1753899478715,
},
],
@@ -1021,24 +2134,28 @@ describe('Apply DBML Changes - relationships', () => {
createdAt: 1753899478715,
isView: false,
order: 1,
- parentAreaId: null,
},
],
- relationships: [],
+ relationships: [
+ {
+ id: 'rel-source-id',
+ name: 'table_2_id_fk',
+ sourceTableId: 'table2-source-id',
+ targetTableId: 'table1-source-id',
+ sourceFieldId: 'field2-source-id',
+ targetFieldId: 'field1-source-id',
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ createdAt: 1753899482016,
+ },
+ ],
dependencies: [],
areas: [],
customTypes: [],
};
- const result = applyDBMLChanges({
- sourceDiagram,
- targetDiagram,
- });
- // Check that the new field is added correctly
- expect(result).toEqual(expectedResult);
- });
- it('should add relationship', () => {
- const sourceDiagram: Diagram = {
+ // Target has same direction with different cardinalities
+ const targetDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
createdAt: new Date('2025-07-30T15:44:53.967Z'),
@@ -1046,36 +2163,36 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: 'l6mvwq8ynw9glgf87ysslafjv',
+ id: 'table1-target-id',
name: 'table_1',
schema: 'public',
order: 0,
fields: [
{
- id: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ id: 'field1-target-id',
name: 'id',
type: { name: 'bigint', id: 'bigint' },
nullable: false,
primaryKey: true,
- unique: false,
+ unique: true,
createdAt: 1753899628831,
},
],
indexes: [],
- x: 100,
- y: 20,
+ x: 0,
+ y: 0,
color: '#b067e9',
isView: false,
createdAt: 1753899628831,
},
{
- id: '8kmv3noehjay988rp57ed69ff',
+ id: 'table2-target-id',
name: 'table_2',
schema: 'public',
order: 1,
fields: [
{
- id: 'dqc01ynfl1gmg5nqctem2dqlq',
+ id: 'field2-target-id',
name: 'id',
type: { name: 'bigint', id: 'bigint' },
nullable: false,
@@ -1086,18 +2203,52 @@ describe('Apply DBML Changes - relationships', () => {
],
indexes: [],
x: 300,
- y: 580,
- color: '#b067e9',
+ y: 0,
+ color: '#ff9f74',
isView: false,
createdAt: 1753899628831,
},
],
- relationships: [],
+ relationships: [
+ {
+ // Same direction as source: table_2 -> table_1
+ // with different cardinalities: source='many', target='one'
+ id: 'rel-target-id',
+ name: 'table_2_id_table_1_id',
+ sourceSchema: 'public',
+ targetSchema: 'public',
+ sourceTableId: 'table2-target-id',
+ targetTableId: 'table1-target-id',
+ sourceFieldId: 'field2-target-id',
+ targetFieldId: 'field1-target-id',
+ sourceCardinality: 'many',
+ targetCardinality: 'one',
+ createdAt: 1753899628831,
+ },
+ ],
dependencies: [],
areas: [],
};
- const targetDiagram: Diagram = {
+ const result = applyDBMLChanges({
+ sourceDiagram,
+ targetDiagram,
+ });
+
+ // Result should preserve source's IDs and direction
+ // with cardinalities directly from target (no swap needed)
+ expect(result.relationships).toHaveLength(1);
+ expect(result.relationships![0].id).toBe('rel-source-id');
+ expect(result.relationships![0].sourceTableId).toBe('table2-source-id');
+ expect(result.relationships![0].targetTableId).toBe('table1-source-id');
+ expect(result.relationships![0].sourceCardinality).toBe('many');
+ expect(result.relationships![0].targetCardinality).toBe('one');
+ });
+
+ it('should preserve cardinalities for new relationships', () => {
+ // Source has no relationships
+ // Target has a new relationship with specific cardinalities
+ const sourceDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
createdAt: new Date('2025-07-30T15:44:53.967Z'),
@@ -1105,13 +2256,14 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: '8ftpn9qn0o2ddrvhzgdjro3zv',
- name: 'table_1',
+ id: 'table1-source-id',
+ name: 'orders',
+ schema: 'public',
x: 260,
y: 80,
fields: [
{
- id: 'w9wlmimvjaci2krhfb4v9bhy0',
+ id: 'orders-id-source',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
unique: true,
@@ -1119,22 +2271,31 @@ describe('Apply DBML Changes - relationships', () => {
primaryKey: true,
createdAt: 1753890297335,
},
+ {
+ id: 'orders-customer-id-source',
+ name: 'customer_id',
+ type: { id: 'bigint', name: 'bigint' },
+ unique: false,
+ nullable: true,
+ primaryKey: false,
+ createdAt: 1753890297336,
+ },
],
indexes: [],
- color: '0',
+ color: '#4dee8a',
createdAt: 1753890297335,
isView: false,
order: 0,
- parentAreaId: null,
},
{
- id: 'km6d66gzzsihmyg9v04ppcets',
- name: 'table_2',
+ id: 'table2-source-id',
+ name: 'customers',
+ schema: 'public',
x: -163.75,
y: -5,
fields: [
{
- id: 'a9apya3ihmzfa1ponbeuy4znf',
+ id: 'customers-id-source',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
unique: true,
@@ -1144,32 +2305,20 @@ describe('Apply DBML Changes - relationships', () => {
},
],
indexes: [],
- color: '0',
+ color: '#9ef07a',
createdAt: 1753899478715,
isView: false,
order: 1,
- parentAreaId: null,
- },
- ],
- relationships: [
- {
- id: 'snsf2455bwr0iegdsfwwwbzj9',
- name: 'table_2_id_fk',
- sourceTableId: 'km6d66gzzsihmyg9v04ppcets',
- targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
- sourceFieldId: 'a9apya3ihmzfa1ponbeuy4znf',
- targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
- sourceCardinality: 'one',
- targetCardinality: 'one',
- createdAt: 1753899482016,
},
],
+ relationships: [],
dependencies: [],
areas: [],
customTypes: [],
};
- const expectedResult: Diagram = {
+ // Target has a new many-to-one relationship
+ const targetDiagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
createdAt: new Date('2025-07-30T15:44:53.967Z'),
@@ -1177,75 +2326,92 @@ describe('Apply DBML Changes - relationships', () => {
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
- id: 'l6mvwq8ynw9glgf87ysslafjv',
- name: 'table_1',
+ id: 'table1-target-id',
+ name: 'orders',
schema: 'public',
order: 0,
fields: [
{
- id: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ id: 'orders-id-target',
name: 'id',
type: { name: 'bigint', id: 'bigint' },
- unique: true,
nullable: false,
primaryKey: true,
+ unique: true,
createdAt: 1753899628831,
},
+ {
+ id: 'orders-customer-id-target',
+ name: 'customer_id',
+ type: { name: 'bigint', id: 'bigint' },
+ nullable: true,
+ primaryKey: false,
+ unique: false,
+ createdAt: 1753899628832,
+ },
],
indexes: [],
- x: 100,
- y: 20,
+ x: 0,
+ y: 0,
color: '#b067e9',
isView: false,
createdAt: 1753899628831,
},
{
- id: '8kmv3noehjay988rp57ed69ff',
- name: 'table_2',
+ id: 'table2-target-id',
+ name: 'customers',
schema: 'public',
order: 1,
fields: [
{
- id: 'dqc01ynfl1gmg5nqctem2dqlq',
+ id: 'customers-id-target',
name: 'id',
type: { name: 'bigint', id: 'bigint' },
- unique: true,
nullable: false,
primaryKey: true,
+ unique: true,
createdAt: 1753899628831,
},
],
indexes: [],
x: 300,
- y: 580,
- color: '#b067e9',
+ y: 0,
+ color: '#ff9f74',
isView: false,
createdAt: 1753899628831,
},
],
relationships: [
{
- id: 'snsf2455bwr0iegdsfwwwbzj9',
- name: 'table_2_id_fk',
- sourceTableId: '8kmv3noehjay988rp57ed69ff',
- targetTableId: 'l6mvwq8ynw9glgf87ysslafjv',
- sourceFieldId: 'dqc01ynfl1gmg5nqctem2dqlq',
- targetFieldId: 'wehbg9d2y04xnt0jb2wrdm3v5',
+ // New relationship: customers.id (one) <- orders.customer_id (many)
+ id: 'new-rel-id',
+ name: 'orders_customer_id_fk',
+ sourceSchema: 'public',
+ targetSchema: 'public',
+ sourceTableId: 'table2-target-id',
+ targetTableId: 'table1-target-id',
+ sourceFieldId: 'customers-id-target',
+ targetFieldId: 'orders-customer-id-target',
sourceCardinality: 'one',
- targetCardinality: 'one',
- createdAt: 1753899482016,
+ targetCardinality: 'many',
+ createdAt: 1753899628831,
},
],
dependencies: [],
areas: [],
- customTypes: [],
};
const result = applyDBMLChanges({
sourceDiagram,
targetDiagram,
});
- // Check that the new field is added correctly
- expect(result).toEqual(expectedResult);
+
+ // Result should have the new relationship with correct cardinalities
+ expect(result.relationships).toHaveLength(1);
+ expect(result.relationships![0].sourceCardinality).toBe('one');
+ expect(result.relationships![0].targetCardinality).toBe('many');
+ // IDs should be mapped to source IDs
+ expect(result.relationships![0].sourceTableId).toBe('table2-source-id');
+ expect(result.relationships![0].targetTableId).toBe('table1-source-id');
});
});
diff --git a/src/lib/dbml/apply-dbml/apply-dbml.ts b/src/lib/dbml/apply-dbml/apply-dbml.ts
index 064563c58..3901ad3bf 100644
--- a/src/lib/dbml/apply-dbml/apply-dbml.ts
+++ b/src/lib/dbml/apply-dbml/apply-dbml.ts
@@ -279,19 +279,71 @@ const updateTables = ({
return targetField;
});
- // Update indexes by matching on name within the table
+ // Update indexes - match by name first, then by semantic structure
+ // Build map of source indexes by name for quick lookup
const sourceIndexesByName = new Map();
sourceTable.indexes?.forEach((index) => {
sourceIndexesByName.set(index.name, index);
});
const updatedIndexes = targetTable.indexes?.map((targetIndex) => {
- const sourceIndex = sourceIndexesByName.get(targetIndex.name);
- if (sourceIndex) {
+ // First try to match by name
+ const sourceIndexByName = sourceIndexesByName.get(targetIndex.name);
+ if (sourceIndexByName) {
+ // Names match - preserve source's id, name, and createdAt
return {
...targetIndex,
- id: sourceIndex.id,
- createdAt: sourceIndex.createdAt,
+ id: sourceIndexByName.id,
+ name: sourceIndexByName.name,
+ createdAt: sourceIndexByName.createdAt,
+ };
+ }
+
+ // No name match - try semantic match by field IDs, unique, and isPrimaryKey
+ // Translate target field IDs to source field IDs for comparison
+ const targetFieldIdsAsSourceIds = targetIndex.fieldIds.map(
+ (fid) => idMappings.fields[fid] || fid
+ );
+
+ const sourceIndexBySemantic = sourceTable.indexes?.find(
+ (srcIndex) => {
+ // Skip if this source index was already matched by name
+ if (
+ sourceIndexesByName.has(srcIndex.name) &&
+ targetTable.indexes?.some(
+ (ti) => ti.name === srcIndex.name
+ )
+ ) {
+ return false;
+ }
+
+ // Compare field IDs (order matters for indexes)
+ if (
+ srcIndex.fieldIds.length !==
+ targetFieldIdsAsSourceIds.length
+ ) {
+ return false;
+ }
+ const fieldsMatch = srcIndex.fieldIds.every(
+ (fid, i) => fid === targetFieldIdsAsSourceIds[i]
+ );
+ if (!fieldsMatch) return false;
+
+ // Match unique and isPrimaryKey status
+ return (
+ srcIndex.unique === targetIndex.unique &&
+ !!srcIndex.isPrimaryKey === !!targetIndex.isPrimaryKey
+ );
+ }
+ );
+
+ if (sourceIndexBySemantic) {
+ // Semantic match - keep target's id and createdAt, use source's name
+ return {
+ ...targetIndex,
+ name: sourceIndexBySemantic.name,
+ id: sourceIndexBySemantic.id,
+ createdAt: sourceIndexBySemantic.createdAt,
};
}
return targetIndex;
@@ -302,6 +354,7 @@ const updateTables = ({
...sourceTable,
fields: updatedFields,
indexes: updatedIndexes,
+ checkConstraints: targetTable.checkConstraints,
comments: targetTable.comments,
};
@@ -402,6 +455,7 @@ const updateRelationships = (
sourceRelationships.forEach((sourceRel) => {
// Find matching target relationship by checking if the target has a relationship
// between the same tables and fields (using the ID mappings)
+ let isReverseMatch = false;
const targetRel = targetRelationships.find((tgtRel) => {
const mappedSourceTableId = idMappings.tables[tgtRel.sourceTableId];
const mappedTargetTableId = idMappings.tables[tgtRel.targetTableId];
@@ -421,16 +475,25 @@ const updateRelationships = (
sourceRel.sourceFieldId === mappedTargetFieldId &&
sourceRel.targetFieldId === mappedSourceFieldId;
+ if (reverseMatch && !directMatch) {
+ isReverseMatch = true;
+ }
+
return directMatch || reverseMatch;
});
if (targetRel) {
matchedTargetRelIds.add(targetRel.id);
// Preserve source relationship but update cardinalities from target
+ // If the relationship is matched in reverse direction, swap the cardinalities
const result: DBRelationship = {
...sourceRel,
- sourceCardinality: targetRel.sourceCardinality,
- targetCardinality: targetRel.targetCardinality,
+ sourceCardinality: isReverseMatch
+ ? targetRel.targetCardinality
+ : targetRel.sourceCardinality,
+ targetCardinality: isReverseMatch
+ ? targetRel.sourceCardinality
+ : targetRel.targetCardinality,
};
// Only include schema fields if they exist in the source relationship
@@ -620,6 +683,7 @@ export const applyDBMLChanges = ({
...sourceDiagram,
tables: finalTables.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
areas: targetDiagram.areas,
+ notes: targetDiagram.notes,
relationships: sortedRelationships,
dependencies: updatedDependencies,
customTypes: updatedCustomTypes,
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/1.dbml b/src/lib/dbml/dbml-export/__tests__/cases/1.dbml
index 8439e9c71..c11c94a1b 100644
--- a/src/lib/dbml/dbml-export/__tests__/cases/1.dbml
+++ b/src/lib/dbml/dbml-export/__tests__/cases/1.dbml
@@ -1124,66 +1124,6 @@ Table "public"."Users_37" {
"name" bigint
}
-Ref "fk_0_rel_uobqadav":"data"."Products_8"."created_at" < "data"."Products_3"."updated_at"
-
-Ref "fk_1_rel_tjwkm4vg":"data"."Products_8"."created_at" < "data"."Transactions_10"."updated_at"
-
-Ref "fk_2_rel_1a2cz1fe":"admin"."Transactions_2"."updated_at" < "admin"."Categories_11"."updated_at"
-
-Ref "fk_3_rel_rwfzm2uw":"data"."Products_8"."created_at" < "data"."Users_27"."name"
-
-Ref "fk_4_rel_9m17o0cp":"data"."Products_8"."created_at" < "data"."Inventory_26"."updated_at"
-
-Ref "fk_5_rel_knm2r9qn":"data"."Products_20"."updated_at" < "auth"."Logs_4"."name"
-
-Ref "fk_6_rel_funm318w":"data"."Products_20"."updated_at" < "auth"."Orders_22"."updated_at"
-
-Ref "fk_7_rel_1cum4y9j":"data"."Products_20"."updated_at" < "auth"."Profiles_23"."name"
-
-Ref "fk_8_rel_hdo6sheg":"data"."Products_8"."created_at" < "data"."Metrics_17"."name"
-
-Ref "fk_9_rel_g0i870xr":"data"."Products_8"."created_at" < "data"."Orders_19"."name"
-
-Ref "fk_10_rel_blbldnbz":"admin"."Transactions_14"."updated_at" < "data"."Products_20"."status"
-
-Ref "fk_11_rel_jvmpjimf":"data"."Products_20"."updated_at" < "admin"."Categories_17"."ref_id"
-
-Ref "fk_12_rel_h74tx8lr":"data"."Products_20"."updated_at" < "admin"."Metrics_15"."field_21"
-
-Ref "fk_13_rel_5sgf7la0":"data"."Products_8"."created_at" < "data"."Metrics_8"."updated_at"
-
-Ref "fk_14_rel_u9ksay74":"data"."Products_8"."created_at" < "data"."Orders_27"."name"
-
-Ref "fk_15_rel_mo1t38sr":"data"."Products_8"."created_at" < "data"."Transactions_12"."name"
-
-Ref "fk_16_rel_n62szguj":"data"."Products_8"."created_at" < "data"."Orders_30"."name"
-
-Ref "fk_17_rel_b64pkgtx":"data"."Products_20"."updated_at" < "data"."Products_8"."updated_at"
-
-Ref "fk_18_rel_f99mk7mw":"data"."Products_8"."created_at" < "data"."Inventory_4"."type"
-
-Ref "fk_19_rel_565yqyyg":"data"."Products_8"."created_at" < "data"."Metrics_36"."updated_at"
-
-Ref "fk_20_rel_6sp80jwg":"data"."Products_8"."created_at" < "data"."Categories_15"."updated_at"
-
-Ref "fk_21_rel_i56aygpy":"data"."Products_8"."created_at" < "data"."Categories_33"."updated_at"
-
-Ref "fk_22_rel_0h9cq9mi":"data"."Products_8"."created_at" < "data"."Orders_12"."created_at"
-
-Ref "fk_23_rel_ykx8br8b":"data"."Products_8"."created_at" < "data"."Orders_5"."name"
-
-Ref "fk_24_rel_vgg3z98u":"data"."Products_8"."created_at" < "data"."Profiles_34"."name"
-
-Ref "fk_25_rel_60ivpluf":"data"."Products_8"."created_at" < "data"."Transactions_5"."name"
-
-Ref "fk_26_rel_jdc4q2o1":"data"."Products_8"."created_at" < "data"."Orders_18"."updated_at"
-
-Ref "fk_27_rel_b861xb0o":"data"."Products_8"."created_at" < "data"."Products_11"."name"
-
-Ref "fk_28_rel_8dy39c3n":"data"."Products_8"."created_at" < "data"."Transactions_30"."updated_at"
-
-Ref "fk_29_rel_mpu6lu4f":"data"."Products_8"."created_at" < "data"."Metrics_4"."updated_at"
-
Table "app"."Products" {
"qgs_fid" int [pk, not null]
"geom" geometry
@@ -2973,9 +2913,9 @@ Table "data"."Products_20" {
"field_32" text
Indexes {
+ geom [name: "idx_Products_20_3"]
updated_at [unique, name: "idx_Products_20_1"]
updated_at [unique, name: "idx_Products_20_2"]
- geom [name: "idx_Products_20_3"]
}
}
@@ -6449,8 +6389,8 @@ Table "admin"."Transactions_2" {
"amount" nvarchar(500)
Indexes {
- updated_at [unique, name: "idx_Transactions_2_1"]
geom [name: "idx_Transactions_2_2"]
+ updated_at [unique, name: "idx_Transactions_2_1"]
}
}
@@ -6802,8 +6742,8 @@ Table "admin"."Transactions_14" {
"field_65" date
Indexes {
- updated_at [unique, name: "idx_Transactions_14_1"]
geom [name: "idx_Transactions_14_2"]
+ updated_at [unique, name: "idx_Transactions_14_1"]
}
}
@@ -7271,3 +7211,63 @@ Table "admin"."Orders_37" {
geom [name: "idx_Orders_37_1"]
}
}
+
+Ref "fk_0_rel_uobqadav":"data"."Products_8"."created_at" < "data"."Products_3"."updated_at"
+
+Ref "fk_1_rel_tjwkm4vg":"data"."Products_8"."created_at" < "data"."Transactions_10"."updated_at"
+
+Ref "fk_2_rel_1a2cz1fe":"admin"."Transactions_2"."updated_at" < "admin"."Categories_11"."updated_at"
+
+Ref "fk_3_rel_rwfzm2uw":"data"."Products_8"."created_at" < "data"."Users_27"."name"
+
+Ref "fk_4_rel_9m17o0cp":"data"."Products_8"."created_at" < "data"."Inventory_26"."updated_at"
+
+Ref "fk_5_rel_knm2r9qn":"data"."Products_20"."updated_at" < "auth"."Logs_4"."name"
+
+Ref "fk_6_rel_funm318w":"data"."Products_20"."updated_at" < "auth"."Orders_22"."updated_at"
+
+Ref "fk_7_rel_1cum4y9j":"data"."Products_20"."updated_at" < "auth"."Profiles_23"."name"
+
+Ref "fk_8_rel_hdo6sheg":"data"."Products_8"."created_at" < "data"."Metrics_17"."name"
+
+Ref "fk_9_rel_g0i870xr":"data"."Products_8"."created_at" < "data"."Orders_19"."name"
+
+Ref "fk_10_rel_blbldnbz":"admin"."Transactions_14"."updated_at" < "data"."Products_20"."status"
+
+Ref "fk_11_rel_jvmpjimf":"data"."Products_20"."updated_at" < "admin"."Categories_17"."ref_id"
+
+Ref "fk_12_rel_h74tx8lr":"data"."Products_20"."updated_at" < "admin"."Metrics_15"."field_21"
+
+Ref "fk_13_rel_5sgf7la0":"data"."Products_8"."created_at" < "data"."Metrics_8"."updated_at"
+
+Ref "fk_14_rel_u9ksay74":"data"."Products_8"."created_at" < "data"."Orders_27"."name"
+
+Ref "fk_15_rel_mo1t38sr":"data"."Products_8"."created_at" < "data"."Transactions_12"."name"
+
+Ref "fk_16_rel_n62szguj":"data"."Products_8"."created_at" < "data"."Orders_30"."name"
+
+Ref "fk_17_rel_b64pkgtx":"data"."Products_20"."updated_at" < "data"."Products_8"."updated_at"
+
+Ref "fk_18_rel_f99mk7mw":"data"."Products_8"."created_at" < "data"."Inventory_4"."type"
+
+Ref "fk_19_rel_565yqyyg":"data"."Products_8"."created_at" < "data"."Metrics_36"."updated_at"
+
+Ref "fk_20_rel_6sp80jwg":"data"."Products_8"."created_at" < "data"."Categories_15"."updated_at"
+
+Ref "fk_21_rel_i56aygpy":"data"."Products_8"."created_at" < "data"."Categories_33"."updated_at"
+
+Ref "fk_22_rel_0h9cq9mi":"data"."Products_8"."created_at" < "data"."Orders_12"."created_at"
+
+Ref "fk_23_rel_ykx8br8b":"data"."Products_8"."created_at" < "data"."Orders_5"."name"
+
+Ref "fk_24_rel_vgg3z98u":"data"."Products_8"."created_at" < "data"."Profiles_34"."name"
+
+Ref "fk_25_rel_60ivpluf":"data"."Products_8"."created_at" < "data"."Transactions_5"."name"
+
+Ref "fk_26_rel_jdc4q2o1":"data"."Products_8"."created_at" < "data"."Orders_18"."updated_at"
+
+Ref "fk_27_rel_b861xb0o":"data"."Products_8"."created_at" < "data"."Products_11"."name"
+
+Ref "fk_28_rel_8dy39c3n":"data"."Products_8"."created_at" < "data"."Transactions_30"."updated_at"
+
+Ref "fk_29_rel_mpu6lu4f":"data"."Products_8"."created_at" < "data"."Metrics_4"."updated_at"
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/3.dbml b/src/lib/dbml/dbml-export/__tests__/cases/3.dbml
new file mode 100644
index 000000000..d815d7d39
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/3.dbml
@@ -0,0 +1,8 @@
+Table "public"."guy_table" {
+ "id" integer [pk, not null]
+ "created_at" "timestamp without time zone" [not null]
+ "column3" text
+ "arrayfield" text[]
+ "field_5" "character varying"
+ "field_6" "character varying(100)"
+}
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/3.json b/src/lib/dbml/dbml-export/__tests__/cases/3.json
new file mode 100644
index 000000000..09b57ccc6
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/3.json
@@ -0,0 +1 @@
+{"id":"mqqwkkod7trl","name":"guy-db","databaseType":"postgresql","createdAt":"2025-09-10T18:45:32.817Z","updatedAt":"2025-09-10T19:15:21.682Z","tables":[{"id":"g2hv9mlo3qbyjnxdc44j1zxl2","name":"guy_table","schema":"public","x":100,"y":300,"fields":[{"id":"qdqgzmtxsi84ujfuktsvjuop8","name":"id","type":{"id":"integer","name":"integer"},"primaryKey":true,"unique":true,"nullable":false,"createdAt":1757529932816},{"id":"wsys99f86679ch6fbjryw0egr","name":"created_at","type":{"id":"timestamp_without_time_zone","name":"timestamp without time zone"},"primaryKey":false,"unique":false,"nullable":false,"createdAt":1757529932816},{"id":"ro39cba7sd290k90qjgzib8pi","name":"column3","type":{"id":"text","name":"text"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1757529932816},{"id":"6cntbu2orwk7kxlg0rcduqgbo","name":"arrayfield","type":{"id":"array","name":"array"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1757529932816},{"id":"7cz0ybdoov2m3wbgm9tlzatz0","name":"field_5","type":{"id":"character_varying","name":"character varying"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1757531685981},{"id":"zzwlyvqzz93oh0vv8f8qob103","name":"field_6","type":{"id":"character_varying","name":"character varying"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1757531713961,"characterMaximumLength":"100"}],"indexes":[{"id":"r0w71lnbnje2j9cz1t9j64rya","name":"guy_table_pkey","unique":true,"fieldIds":["qdqgzmtxsi84ujfuktsvjuop8"],"createdAt":1757529932816,"isPrimaryKey":true}],"color":"#8eb7ff","isView":false,"isMaterializedView":false,"createdAt":1757529932816,"diagramId":"mqqwkkod7trl"}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/4.dbml b/src/lib/dbml/dbml-export/__tests__/cases/4.dbml
new file mode 100644
index 000000000..084bd53bd
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/4.dbml
@@ -0,0 +1,7 @@
+Table "public"."orders" {
+ "order_id" integer [pk, not null, increment]
+ "customer_id" integer [not null]
+ "order_date" date [not null, default: `CURRENT_DATE`]
+ "total_amount" numeric [not null, default: 0]
+ "status" varchar(50) [not null, default: 'Pending']
+}
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/4.json b/src/lib/dbml/dbml-export/__tests__/cases/4.json
new file mode 100644
index 000000000..4157868ef
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/4.json
@@ -0,0 +1 @@
+{"id":"6b81a1787207","name":"SQL Import (postgresql)","createdAt":"2025-09-15T08:46:26.747Z","updatedAt":"2025-09-17T11:32:13.876Z","databaseType":"postgresql","tables":[{"id":"5ytf0yj9etpmm7mhmhvpu8kfj","name":"orders","schema":"public","order":1,"fields":[{"id":"w7l77cy9hylvlitdovt4ktdmk","name":"order_id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":true,"unique":false,"default":"","createdAt":1757925986747,"increment":true},{"id":"vz7747t5fxrb62v1eepmahv9v","name":"customer_id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1757925986747,"increment":false},{"id":"geq9qy6sv4ozl2lg9fvcyzxpf","name":"order_date","type":{"name":"date","id":"date","usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"CURRENT_DATE()","createdAt":1757925986747,"increment":false},{"id":"z928n7umvpec79t2eif7kmde9","name":"total_amount","type":{"name":"numeric","id":"numeric","fieldAttributes":{"precision":{"max":999,"min":1,"default":10},"scale":{"max":999,"min":0,"default":2}}},"nullable":false,"primaryKey":false,"unique":false,"default":"0","createdAt":1757925986747,"increment":false},{"id":"7bkrd0rp1s17bi1lnle6pesc7","name":"status","type":{"name":"varchar","id":"varchar","fieldAttributes":{"hasCharMaxLength":true},"usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"'Pending'","createdAt":1757925986747,"increment":false,"characterMaximumLength":"50"}],"indexes":[],"x":113,"y":747,"color":"#8eb7ff","isView":false,"createdAt":1757925986747,"diagramId":"6b81a1787207","parentAreaId":null}],"relationships":[],"dependencies":[],"storageMode":"project","lastProjectSavedAt":"2025-09-17T11:32:13.876Z","areas":[],"creationMethod":"imported","customTypes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/5.inline.dbml b/src/lib/dbml/dbml-export/__tests__/cases/5.inline.dbml
new file mode 100644
index 000000000..cec927925
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/5.inline.dbml
@@ -0,0 +1,129 @@
+Enum "cbhpm_entradas_tipo" {
+ "grupo"
+ "subgrupo"
+ "procedimento"
+}
+
+Enum "cid_entradas_tipo" {
+ "capitulo"
+ "agrupamento"
+ "categoria"
+ "subcategoria"
+}
+
+Enum "digital_signature_provider" {
+ "soluti"
+ "valid"
+}
+
+Enum "impresso_posicao" {
+ "start"
+ "center"
+ "end"
+}
+
+Enum "otp_provider" {
+ "clinic"
+ "soluti_bird_id"
+}
+
+Enum "tipo_cobranca" {
+ "valor"
+ "porte"
+}
+
+Enum "tipo_contato_movel" {
+ "celular"
+ "telefone_residencial"
+ "telefone_comercial"
+}
+
+Enum "tipo_contrato" {
+ "trial"
+ "common"
+}
+
+Enum "tipo_endereco" {
+ "residencial"
+ "comercial"
+ "cobranca"
+}
+
+Enum "tipo_espectro_autista" {
+ "leve"
+ "moderado"
+ "severo"
+}
+
+Enum "tipo_estado_civil" {
+ "nao_infomado"
+ "solteiro"
+ "casado"
+ "divorciado"
+ "viuvo"
+}
+
+Enum "tipo_etnia" {
+ "nao_infomado"
+ "branca"
+ "preta"
+ "parda"
+ "amarela"
+ "indigena"
+}
+
+Enum "tipo_excecao" {
+ "bloqueio"
+ "compromisso"
+}
+
+Enum "tipo_metodo_reajuste" {
+ "percentual"
+ "valor"
+}
+
+Enum "tipo_pessoa" {
+ "fisica"
+ "juridica"
+}
+
+Enum "tipo_procedimento" {
+ "consulta"
+ "exame_laboratorial"
+ "exame_imagem"
+ "procedimento_clinico"
+ "procedimento_cirurgico"
+ "terapia"
+ "outros"
+}
+
+Enum "tipo_relacionamento" {
+ "pai"
+ "mae"
+ "conjuge"
+ "filho_a"
+ "tutor_legal"
+ "contato_emergencia"
+ "outro"
+}
+
+Enum "tipo_sexo" {
+ "nao_infomado"
+ "masculino"
+ "feminino"
+ "intersexo"
+}
+
+Enum "tipo_status_agendamento" {
+ "em espera"
+ "faltou"
+ "ok"
+}
+
+Table "public"."organizacao_cfg_impressos" {
+ "id_organizacao" integer [pk, not null, ref: > "public"."organizacao"."id"]
+}
+
+Table "public"."organizacao" {
+ "id" integer [pk, not null]
+}
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/5.json b/src/lib/dbml/dbml-export/__tests__/cases/5.json
new file mode 100644
index 000000000..89bf8c112
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/5.json
@@ -0,0 +1 @@
+{"id":"891455fe2029","name":"fish after update (Copy)","createdAt":"2025-09-17T13:41:55.984Z","updatedAt":"2025-09-17T14:43:51.846Z","databaseType":"postgresql","tables":[{"id":"mow8twxpq4yx9ti2ke1rm3oz9","name":"organizacao_cfg_impressos","schema":"public","order":3,"fields":[{"id":"ub6gos45jgjus7a0ra9xditof","name":"id_organizacao","type":{"name":"integer","id":"integer","usageLevel":1},"nullable":false,"primaryKey":true,"unique":false,"createdAt":1757509919495}],"indexes":[{"id":"5fb2k4hawkhw3pyqvruoi3g6v","name":"pk_organizacao_cfg_impressos_id_organizacao","fieldIds":["ub6gos45jgjus7a0ra9xditof"],"unique":true,"isPrimaryKey":true,"createdAt":1757509919496}],"x":-345,"y":-186,"color":"#ffe374","isView":false,"createdAt":1757022310768,"diagramId":"891455fe2029","parentAreaId":null,"expanded":true},{"id":"zc47ktc9divcdt66ms7vrspod","name":"organizacao","schema":"public","order":1,"fields":[{"id":"uby47oadej9dm37i94je7fqqu","name":"id","type":{"name":"integer","id":"integer","usageLevel":1},"nullable":false,"primaryKey":true,"unique":false,"createdAt":1757022310768}],"indexes":[{"id":"z2a9g6q13mt1y5tk0rskwum7u","name":"pk_organizacao_id","fieldIds":["uby47oadej9dm37i94je7fqqu"],"unique":true,"isPrimaryKey":true,"createdAt":1757114454215},{"id":"ykxz0xuv6w39h5v86rcsd4ba6","name":"idx_tenancy_owner_id","fieldIds":["vpqwjanwu075sgny85ahih51b"],"unique":false,"createdAt":1757023762316}],"x":845,"y":-351,"color":"#ffe374","isView":false,"createdAt":1757022310768,"diagramId":"891455fe2029","parentAreaId":null,"expanded":true}],"relationships":[{"id":"ujdpzbivadyqy1zarb8xaadeo","name":"fk_organizacao_cfg_impressos_organizacao","sourceSchema":"public","targetSchema":"public","sourceTableId":"mow8twxpq4yx9ti2ke1rm3oz9","targetTableId":"zc47ktc9divcdt66ms7vrspod","sourceFieldId":"ub6gos45jgjus7a0ra9xditof","targetFieldId":"uby47oadej9dm37i94je7fqqu","sourceCardinality":"many","targetCardinality":"one","createdAt":1758059720108,"diagramId":"891455fe2029"},{"id":"k40xbk9m8o9tu2ga5y6el9xlv","name":"fk_organizacao_id_organizacao_cfg_impressos_id_organizacao","sourceSchema":"public","targetSchema":"public","sourceTableId":"zc47ktc9divcdt66ms7vrspod","targetTableId":"mow8twxpq4yx9ti2ke1rm3oz9","sourceFieldId":"uby47oadej9dm37i94je7fqqu","targetFieldId":"ub6gos45jgjus7a0ra9xditof","sourceCardinality":"many","targetCardinality":"one","createdAt":1758059720108,"diagramId":"891455fe2029"}],"dependencies":[],"storageMode":"project","lastProjectSavedAt":"2025-09-17T14:43:51.846Z","areas":[],"creationMethod":"imported","customTypes":[{"id":"ppxncapgjqymrcs6sdisxhdom","schema":"","name":"cbhpm_entradas_tipo","kind":"enum","values":["grupo","subgrupo","procedimento"],"order":0,"diagramId":"891455fe2029"},{"id":"l720lbz86bg906xs0d0rjwnwq","schema":"","name":"cid_entradas_tipo","kind":"enum","values":["capitulo","agrupamento","categoria","subcategoria"],"order":0,"diagramId":"891455fe2029"},{"id":"fcyvfh5ilzi1pxg1mazgtr0hb","schema":"","name":"digital_signature_provider","kind":"enum","values":["soluti","valid"],"order":0,"diagramId":"891455fe2029"},{"id":"x0mcajzpi8k1lg5th3o2o7qqh","schema":"","name":"impresso_posicao","kind":"enum","values":["start","center","end"],"order":0,"diagramId":"891455fe2029"},{"id":"hymo5s2uqjl1nkq3kui5ueidc","schema":"","name":"otp_provider","kind":"enum","values":["clinic","soluti_bird_id"],"order":0,"diagramId":"891455fe2029"},{"id":"r02sp4k2vu98x58n648u1hmtu","schema":"","name":"tipo_cobranca","kind":"enum","values":["valor","porte"],"order":0,"diagramId":"891455fe2029"},{"id":"pgyjouza40hqaurziiehcsnde","schema":"","name":"tipo_contato_movel","kind":"enum","values":["celular","telefone_residencial","telefone_comercial"],"order":0,"diagramId":"891455fe2029"},{"id":"ur3khr7o25tzffhzc1s32e7ox","schema":"","name":"tipo_contrato","kind":"enum","values":["trial","common"],"order":0,"diagramId":"891455fe2029"},{"id":"u5jxtrlu22j846e6b4qw9ilrp","schema":"","name":"tipo_endereco","kind":"enum","values":["residencial","comercial","cobranca"],"order":0,"diagramId":"891455fe2029"},{"id":"tat6vdey698u7sop929llxpuv","schema":"","name":"tipo_espectro_autista","kind":"enum","values":["leve","moderado","severo"],"order":0,"diagramId":"891455fe2029"},{"id":"kq7atvktkadu3pha5ur64wf0o","schema":"","name":"tipo_estado_civil","kind":"enum","values":["nao_infomado","solteiro","casado","divorciado","viuvo"],"order":0,"diagramId":"891455fe2029"},{"id":"c9f8bvh6oekcun7aqhmzwg3cx","schema":"","name":"tipo_etnia","kind":"enum","values":["nao_infomado","branca","preta","parda","amarela","indigena"],"order":0,"diagramId":"891455fe2029"},{"id":"u80frjtpqdz31dykzv93mipme","schema":"","name":"tipo_excecao","kind":"enum","values":["bloqueio","compromisso"],"order":0,"diagramId":"891455fe2029"},{"id":"fg46jmrydqjlkm7x968gndewe","schema":"","name":"tipo_metodo_reajuste","kind":"enum","values":["percentual","valor"],"order":0,"diagramId":"891455fe2029"},{"id":"s2q0t3fs7xuo806a4nig9kkue","schema":"","name":"tipo_pessoa","kind":"enum","values":["fisica","juridica"],"order":0,"diagramId":"891455fe2029"},{"id":"561lotk0s99gnddyqp3hj26la","schema":"","name":"tipo_procedimento","kind":"enum","values":["consulta","exame_laboratorial","exame_imagem","procedimento_clinico","procedimento_cirurgico","terapia","outros"],"order":0,"diagramId":"891455fe2029"},{"id":"itq8wzzlej4744z26crl8h0fr","schema":"","name":"tipo_relacionamento","kind":"enum","values":["pai","mae","conjuge","filho_a","tutor_legal","contato_emergencia","outro"],"order":0,"diagramId":"891455fe2029"},{"id":"r80q80je96w85udol4sccb0q0","schema":"","name":"tipo_sexo","kind":"enum","values":["nao_infomado","masculino","feminino","intersexo"],"order":0,"diagramId":"891455fe2029"},{"id":"jveox9qoccr17z6q8pjcyryrl","schema":"","name":"tipo_status_agendamento","kind":"enum","values":["em espera","faltou","ok"],"order":0,"diagramId":"891455fe2029"}]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/6.dbml b/src/lib/dbml/dbml-export/__tests__/cases/6.dbml
new file mode 100644
index 000000000..5fd1cb90a
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/6.dbml
@@ -0,0 +1,14 @@
+Table "users" {
+ "id" integer [pk, not null, increment]
+ "username" varchar(100) [unique, not null]
+ "email" varchar(255) [not null]
+}
+
+Table "posts" {
+ "post_id" bigint [pk, not null, increment]
+ "user_id" integer [not null]
+ "title" varchar(200) [not null]
+ "order_num" integer [not null, increment]
+}
+
+Ref "fk_0_fk_posts_users":"users"."id" < "posts"."user_id"
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/6.json b/src/lib/dbml/dbml-export/__tests__/cases/6.json
new file mode 100644
index 000000000..189f499bf
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/6.json
@@ -0,0 +1 @@
+{"id":"test_auto_increment","name":"Auto Increment Test (mysql)","createdAt":"2025-01-20T00:00:00.000Z","updatedAt":"2025-01-20T00:00:00.000Z","databaseType":"mysql","tables":[{"id":"table1","name":"users","order":1,"fields":[{"id":"field1","name":"id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":true,"unique":false,"default":"","increment":true,"createdAt":1705708800000},{"id":"field2","name":"username","type":{"id":"varchar","name":"varchar","fieldAttributes":{"hasCharMaxLength":true}},"nullable":false,"primaryKey":false,"unique":true,"default":"","increment":false,"characterMaximumLength":"100","createdAt":1705708800000},{"id":"field3","name":"email","type":{"id":"varchar","name":"varchar","fieldAttributes":{"hasCharMaxLength":true}},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":false,"characterMaximumLength":"255","createdAt":1705708800000}],"indexes":[],"x":100,"y":100,"color":"#8eb7ff","isView":false,"createdAt":1705708800000},{"id":"table2","name":"posts","order":2,"fields":[{"id":"field4","name":"post_id","type":{"id":"bigint","name":"bigint"},"nullable":false,"primaryKey":true,"unique":false,"default":"","increment":true,"createdAt":1705708800000},{"id":"field5","name":"user_id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":false,"createdAt":1705708800000},{"id":"field6","name":"title","type":{"id":"varchar","name":"varchar","fieldAttributes":{"hasCharMaxLength":true}},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":false,"characterMaximumLength":"200","createdAt":1705708800000},{"id":"field7","name":"order_num","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":true,"createdAt":1705708800000}],"indexes":[],"x":300,"y":100,"color":"#8eb7ff","isView":false,"createdAt":1705708800000}],"relationships":[{"id":"rel1","name":"fk_posts_users","sourceTableId":"table2","targetTableId":"table1","sourceFieldId":"field5","targetFieldId":"field1","type":"one_to_many","sourceCardinality":"many","targetCardinality":"one","createdAt":1705708800000}],"dependencies":[],"storageMode":"project","areas":[],"creationMethod":"manual","customTypes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/7.inline.dbml b/src/lib/dbml/dbml-export/__tests__/cases/7.inline.dbml
new file mode 100644
index 000000000..c4845a105
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/7.inline.dbml
@@ -0,0 +1,14 @@
+Table "clean"."wms_item" {
+ "id" int64 [note: '''| Źródło: [WMS].[dbo].[wms_items].[id] | Tabele docelowe: [BQ].[reporting].[dim_products_history].[wms_prod_id] | Czym jest dana kolumna: jest to \'WMS\'owe\' id produktu | Informacje dodatkowe: brak''']
+ "symbol" int64 [note: '''| Źródło: [WMS].[dbo].[wms_items].[symbol] | Tabele docelowe: [BQ].[reporting].[dim_products_history].[iai_prod_id] | Czym jest dana kolumna: jest to \'IAI\'owe\' id produktu | Informacje dodatkowe: brak''']
+ "ean_code" int64 [note: '| Źródło: [WMS].[dbo].[wms_items].[ean_code] | Tabele docelowe: [BQ].[reporting].[dim_products_history].[ean] | Czym jest dana kolumna: jest to kod ean produktu | Informacje dodatkowe: brak']
+ "status" string
+ "dwh_created_at" datetime
+ "dwh_modified_at" datetime
+}
+
+Table "reporting"."wms_dim_products_history" {
+ "iai_id_prod" int64 [ref: - "clean"."wms_item"."symbol"]
+ "wms_id_prod" int64 [ref: - "clean"."wms_item"."id"]
+ "ean" int64 [ref: - "clean"."wms_item"."ean_code"]
+}
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/7.json b/src/lib/dbml/dbml-export/__tests__/cases/7.json
new file mode 100644
index 000000000..068d0916e
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/7.json
@@ -0,0 +1 @@
+{"id":"123456","name":"part_day","createdAt":"2025-12-17T15:59:33.112Z","updatedAt":"2025-12-17T16:08:18.946Z","databaseType":"postgresql","tables":[{"id":"4ar0wqk9p8uao27desm3uicnh","name":"wms_item","schema":"clean","x":657.6858553896849,"y":-1160.4029631833816,"fields":[{"id":"ry3l377zs3rzgw5boxt2gvfrr","name":"id","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501660,"comments":"| Źródło: [WMS].[dbo].[wms_items].[id]\n| Tabele docelowe: [BQ].[reporting].[dim_products_history].[wms_prod_id]\n| Czym jest dana kolumna: jest to 'WMS'owe' id produktu\n| Informacje dodatkowe: brak"},{"id":"0uc3ksoc67sobp77ky20day3k","name":"symbol","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501660,"comments":"| Źródło: [WMS].[dbo].[wms_items].[symbol]\n| Tabele docelowe: [BQ].[reporting].[dim_products_history].[iai_prod_id]\n| Czym jest dana kolumna: jest to 'IAI'owe' id produktu\n| Informacje dodatkowe: brak"},{"id":"h4xw4k74c92j8we5xqyuama1h","name":"ean_code","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501660,"comments":"| Źródło: [WMS].[dbo].[wms_items].[ean_code]\n| Tabele docelowe: [BQ].[reporting].[dim_products_history].[ean]\n| Czym jest dana kolumna: jest to kod ean produktu\n| Informacje dodatkowe: brak"},{"id":"atbg7uwpvmp950g7maficlfw0","name":"status","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501660},{"id":"2ub0ze10x2h5nmr18jy9ld15f","name":"dwh_created_at","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501660},{"id":"4g346zspcm0fpo8xn1k1h8di6","name":"dwh_modified_at","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501660}],"indexes":[],"color":"#c05dcf","isView":false,"isMaterializedView":false,"createdAt":1763990501660,"diagramId":"ee570f764e8c","parentAreaId":null},{"id":"7xpdjh6r805y1i9q52oxqp8ek","name":"v_wms_custom_purchasing_report","schema":"reporting","x":3665,"y":-1253,"fields":[{"id":"065c6gtvxf6t5z6dimrpttgni","name":"id","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"fa1wr2byznk14th16wlvih16s","name":"oznaczenie_grupa","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"l11yzz2ewly3upk3nf7u58748","name":"oznaczenie_podgrupa","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"8jdn3uqwcg76jul14ov507gjd","name":"czy_ukryty","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"69r0j9y29l7d6twsqkm97ozi2","name":"symbol","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"srtnncq6mml59l9drm0nybet7","name":"ean_code","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"twrqdr8gw1bntjfey0x1hpfcv","name":"nazwa","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"4vh0skw5x2cwfq6jveuapzc3l","name":"marka","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"50z1u8kkzgpoqrmgjz7lorznp","name":"osoba_odp","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"u2iojx1ih0uz1kup8ypa0cnra","name":"liczba_sprzedanych_31","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"jgqeu8vv1xd20oyt9m432udhk","name":"wartosc_sprzedanych_31","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"n8pt4sox10okh5ewju0xg4ioc","name":"marza_netto_31","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"wvzd8gbs7qhb09ifkpgpu2z00","name":"liczba_sprzedanych_93","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"oc7jl0ttkk06azuj3vo1luvvz","name":"wartosc_sprzedanych_93","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"fro30vxy19jpzwx9kqqrzv2vz","name":"marza_netto_93","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"k3gxbt10cpmubhygoxwarrush","name":"liczba_sprzedanych_31_hurt","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"hra3tpnuyeq5tkbiit7qdrnya","name":"wartosc_marzy_netto_31_hurt","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"a3u8wm0utlkrpr2kbw79ihj5d","name":"liczba_sprzedanych_93_hurt","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"lvd1v9ydlwglj6wlptjgs7ntt","name":"wartosc_marzy_netto_93_hurt","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"j57floy0epumz26wi5tnq34mn","name":"m1","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"6m0vbr3khge0ufj4rg4zfgh2o","name":"m2","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"w48co8vku1l4rcgwpkat62wir","name":"m3","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"8t3ti2nqc4g6u0fqsl41u091x","name":"m4","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"jj69llyfwoo7uv9zbbpwkauo6","name":"m_cornery","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"tnc0ijgyvgyt77spjcxooh9mu","name":"m6","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"2twb5nf57ebi0j0s2drlwgrsc","name":"m7","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"y0iq5m63q0mevg7lbhttoh6k7","name":"m8","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"bkropk8qvmvpn11b6w3izp8fn","name":"aktualny_stan","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"83ewsvkhthzfid13l0c5cprnw","name":"stan_dyspozycyjny","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"d5z9kxdhsep9lh5cgw8hr72a3","name":"ostatnia_cena_zakupu","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"gyz640uscsw2jugo84ey7kg4t","name":"srednia_dzienna_sprzedaz_31","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"gzze6n7g7uxrh4owge9oa8rgc","name":"srednia_dzienna_sprzedaz_31_hurt","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"ycccdg156ljjnv9kl7yedj3vo","name":"zapas_tygodniowy","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"wymbq25uga9ui4uukk4t1gt07","name":"ocena_stanu_zapasow","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"x0ib2b5rz4eveebpmb4hsodxh","name":"za_duzo_za_malo","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"ld9phxmslceaeg9gs0rme6ds5","name":"za_duzo_za_malo_wartosc","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"96iwbopu3onncd0kcjgevvcz3","name":"zapas_bezpieczenstwa","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"an8pdap54ihqhxqmu2qlwp3ef","name":"ile_do_zamowienia","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"1m9d9x5ig1cpbb3jdhqqd6y6b","name":"ile_do_zamowienia_pomocnicza_kol","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"e2crjzm50jkbp8oavr76zhx2c","name":"ile_do_zamowienia_wartosc","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"9yw685j4mvip4obq2ej2eg46i","name":"wartosc_magazynu","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"ff635dj70w71u0hwqncn4pp6f","name":"wskaznik_wyprzedania","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"klvkeq0eticf9diz9tz4cckw2","name":"sredni_czas_dostawy","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"4j1k4ouo237martkdz4f4fbjh","name":"liczba_zamowionych_towaro","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"9bgtcwc0zdlm4ht55p85ilbo1","name":"wartosc_zamowionych_towarow","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"4bl0xcqaba95wgqnbvcxli88m","name":"data_dostawy","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"0gcscmuo093bmzofge4azjt98","name":"dostepnosc_dz","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"jfvl1ohsi5zn8vrmpw9t6v85l","name":"dostepnosc_ogolem","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"dhwwaoe5lmhz048uwvbm4fea1","name":"dostepnosc_producent","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"o8700bs336cyjxzjshk9016xh","name":"dni_niedostepnosci","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"sfhlg1fryhc8nng0761w2s9k1","name":"utracona_marza_ogolem","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"zssook4kp9tkpvmt2s8lancvk","name":"utracona_marza_ogolem_hurt","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"gw07shet2jnc64yab7nms6fqm","name":"utracona_marza_producent","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"s0mbrku8yergtox5mo1e6ygne","name":"utracona_marza_dz","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"dm3jb1ueiubrw0zdh2t4zwm8l","name":"wartosc_produktow_do_wyprzedania","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"v0dgfljzbuqds2o7w1ur8w4zu","name":"czy_jest_w_cornerze","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"rsr73cpxsnf3bouuqf8sfipqs","name":"corner_liczba_do_pobrania_z_m3","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"hqnewll73uig1sj0ka9aawhvu","name":"corner_liczba_do_domowienia","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"9hs4cxn8i9z3xw1urlzm62u2k","name":"wartosc_produktow_dla_cornera","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"tm40k9zk6p4lb0j8bvhqb3s0l","name":"data_raportu","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"2dit15f4cqut1r0vgcr9eva78","name":"dostawca_id","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"6ogwzd80ub8bcmp14tsdi2nu3","name":"dostawca_alternatywny_1_id","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"4qyqj2mo8r8xnq040qiywieza","name":"dostawca_alternatywny_2_id","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"zdgq7v8uir77lszbnpaxlsuwc","name":"dostawca_czas_dostawy","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"lcfo4jip8j80cns24edh824ny","name":"dostawca_alt_1_czas_dostawy","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"kx1sggwmo07wt2dxesjasctss","name":"dostawca_alt_2_czas_dostawy","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"bftipfp2pzqtv1jm9uq28pn0j","name":"wygladzenie","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"ucotgbhs7t6l7hb780mkdbe88","name":"wms_item_id","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"i1oofehor78puprqwv5z4hejj","name":"czy_zamowiono","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"ygkwptzgufqyri3fiz2qkfng9","name":"dokument_dostawy_wygenerowany","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"3gyqyhtfuqsup97e94jh1hk4p","name":"wartosc_zakupu_31","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"pjoygmnu8fjzzygjfug8g5x03","name":"wartosc_zakupu_93","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"zozcqq6uhifo2rqmlotu513x8","name":"stan_dyspozycyjny_idosell","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"cip1tnstdcz4k8g09u9xs9nw5","name":"stan_dyspozycyjny_idosell_czas_pobrania","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"gjw4h3b4vnus5zn8e572os3by","name":"planowana_data_braku_towaru","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"o3168z4bwfch177u73z794c94","name":"optymalny_zaps_szt","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"qo1nprop3r6is85wx84bt79hd","name":"optymalny_zaps_zl","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"dvh89qhfjldfetaqc9ig753jy","name":"planowana_data_przyjecia","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"h9r5ihgrlncdwpmohoxnemmfc","name":"srednia_sprzedaz_detal","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"o8pyragwx4lb8brfldwz681dw","name":"stan_dyspozycyjny_m1","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"dy4xmcw3lk588i8gl8mftz0u5","name":"stan_dyspozycyjny_m2","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"riq1swkbtifd6e2973d4q67wh","name":"stan_dyspozycyjny_m3","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"9suaobgw8qutzx1ig3xz63yt2","name":"stan_dyspozycyjny_m4","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"ocp5wpwp3lw697waos18tenvc","name":"stan_dyspozycyjny_m5","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"x75hg9frmzpf9mt1gvblw5bvm","name":"stan_dyspozycyjny_m6","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"g3bs3yzrllux70fkqwtowpsqg","name":"stan_dyspozycyjny_m7","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"73cporynhsihaz41tpzmsa24f","name":"stan_dyspozycyjny_m8","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"wnv5imrgl4mzuaythw11kwnx6","name":"stan_dyspozycyjny_m10","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"uz12waj4tn540wwe8kxfu58m5","name":"stan_dyspozycyjny_m11","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"dppywabdg4xrah5riq7cvwzhv","name":"stan_dyspozycyjny_m12","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"p5dptfc2cmglqw0y4wwnxg3r2","name":"stan_dyspozycyjny_m14","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"f15sbk382hi0a4s9ubjsj0vgk","name":"srednia_dzienna_sprzedaz_detal_v2","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"52n5mb5mz41yhw7xjnhhwszqm","name":"srednia_dzienna_sprzedaz_hurt_v2","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"1sew03l6q8k7kdy633q2sa8gf","name":"od_ilu_dni_0_dyspozycyjne_m2","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"xzgcifx7nh5q1wdfnfil7t20z","name":"od_ilu_dni_0_dyspozycyjne_m3","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"qq2djg8mxttrqigmaioxma5l4","name":"od_ilu_dni_0_dyspozycyjne_m8","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"7hr7n6y829cgs9tgwitm5sc9n","name":"sprzedane_31_detal_v2","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"pta430e3gegmjqh1u83qu8om5","name":"sprzedane_31_hurt_v2","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"5dskw6eair64zjxhavt4pozqo","name":"stan_dyspozycyjny_m16","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"81paj9okqmb2jcrsweelnqvn9","name":"dzial_zamawiajacy","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"3mifdjovaeiy2fhre9yv4vtk7","name":"dostepnosc_atrybut","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744}],"indexes":[],"color":"#b0b0b0","isView":true,"isMaterializedView":false,"createdAt":1763990501744,"diagramId":"ee570f764e8c","parentAreaId":"3c7d2957a2a0","width":224,"expanded":false},{"id":"exz2tqw57aas0lh3plykd60s5","name":"v_excel_fact_hours_worked","schema":"reporting","x":3012,"y":-744,"fields":[{"id":"blvvvw7vh0ymabk1wzzpixeei","name":"employee","type":{"id":"string","name":"string"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"0nyfomvyyegooaaeossi2c0si","name":"date","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"nckjz91k5rw4yej3wn8t9p2e0","name":"hours_worked","type":{"id":"numeric","name":"numeric"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"tma8utx5teob222hwjem7lauk","name":"dwh_created_at","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"u5h67tpsm90k1rqvvo2dfcrh0","name":"dwh_modified_at","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743}],"indexes":[],"color":"#b0b0b0","isView":true,"isMaterializedView":false,"createdAt":1763990501743,"diagramId":"ee570f764e8c","parentAreaId":"3c7d2957a2a0"},{"id":"kc981eyj8dvp79ryspcrjsfjt","name":"v_fact_history","x":2631,"y":-750,"fields":[{"id":"11vuw9tozyrlqcdwo7jh2gz1h","name":"id_stock","type":{"id":"int64","name":"int64"},"unique":true,"nullable":false,"primaryKey":true,"createdAt":1765195421482},{"id":"x2lfcqzp1vgnz41slb43pfhed","name":"id_prod","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765195624756},{"id":"1v7px8tajb2zfl4y8neipb6a5","name":"quantity","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765195638206},{"id":"gk7zvwthvz7uo0f7mpvc12nkz","name":"stock_value","type":{"id":"float64","name":"float64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765195643871},{"id":"iauxjbylvgp8hsni8vkkquy1q","name":"quantity_available","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765195650594},{"id":"y4gby1n38j26kr5h0opxoo5lp","name":"quantity_available_value","type":{"id":"float64","name":"float64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765195670377}],"indexes":[{"id":"ef8rzcdkxvx2xcsu9pjzk4yaq","name":"pk_table_104_id","fieldIds":["11vuw9tozyrlqcdwo7jh2gz1h"],"unique":true,"isPrimaryKey":true,"createdAt":1765195421482}],"color":"#b0b0b0","createdAt":1765195421482,"isView":true,"order":103,"schema":"reporting","parentAreaId":"3c7d2957a2a0","diagramId":"ee570f764e8c"},{"id":"kctanox6k2vooyeccq880l750","name":"v_limit_table","x":3349,"y":-761,"fields":[{"id":"pl6so405ptxvgvehjgjfrq5jz","name":"date","type":{"id":"date","name":"date"},"unique":true,"nullable":false,"primaryKey":true,"createdAt":1765196599539},{"id":"be83xx25u34yci0n9mvkuwl3s","name":"department","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196684872},{"id":"nxvtb7ida3x4dfxaopr9yh015","name":"wartosc_zamowien_nieoplaconych","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196690650},{"id":"vufypmo8lmypi8g0ftjzxs840","name":"wartosc_magazynu","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196697674},{"id":"qlrj1n6adzp1jr8qim3edyr0n","name":"wartosc_towarow_w_drodze","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196701511},{"id":"m8zzs6i8toyzm62b0lcbtsg5y","name":"limit","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196706551},{"id":"revkdpyc9ce33932p0bqulehq","name":"wartosc_zlozonych_zamowien","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196709415},{"id":"2fl7ioq85hyknz343k2yw54zx","name":"wykorzystanie_limitu","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765196716821}],"indexes":[{"id":"3ukfyh5ssrgvuzovtgo889fe5","name":"pk_table_104_id","fieldIds":["pl6so405ptxvgvehjgjfrq5jz"],"unique":true,"isPrimaryKey":true,"createdAt":1765196599539}],"color":"#b0b0b0","createdAt":1765196599539,"isView":true,"order":103,"schema":"reporting","diagramId":"ee570f764e8c","parentAreaId":"3c7d2957a2a0"},{"id":"kuq6rp4pcy692r81fv7gzxj66","name":"v_dim_offers","schema":"reporting","x":2677,"y":-1253,"fields":[{"id":"oj856uan2zn0ugn4ogsxwvbti","name":"id_offer","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"4z34eyttqd1qsrt2qwvqmh97r","name":"id_prod","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"8bkki8we2fpav8jsen2lsoifp","name":"shop","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"bbjaynx26kb66wbhh3cpxhlrm","name":"start_date","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"ndjn3ogkuqdyxm1iemz0v0bw1","name":"end_date","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"3jjxsel8md1n233da73sdyjmq","name":"offer_discount","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"mjqi7kde0xb46mqnifm28rdq4","name":"dwh_created_at","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743},{"id":"su9ey43rw0zlftxxz9grpudnm","name":"dwh_modified_at","type":{"id":"datetime","name":"datetime"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501743}],"indexes":[],"color":"#b0b0b0","isView":true,"isMaterializedView":false,"createdAt":1763990501743,"diagramId":"ee570f764e8c","parentAreaId":"3c7d2957a2a0"},{"id":"la8abk9zhcli83cdtw8xd1ecf","name":"wms_dim_products_history","schema":"reporting","x":1395,"y":-1166,"fields":[{"id":"rknfchbhxk1ujs1q1y6ew36di","name":"iai_id_prod","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"kmmzdszo23ak3xbwhoni88zdw","name":"wms_id_prod","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744},{"id":"5baaq93izkuwclwqw6r9811v5","name":"ean","type":{"id":"int64","name":"int64"},"primaryKey":false,"unique":false,"nullable":true,"increment":false,"isArray":false,"createdAt":1763990501744}],"indexes":[],"color":"#8eb7ff","isView":false,"isMaterializedView":false,"createdAt":1763990501744,"diagramId":"ee570f764e8c","parentAreaId":null,"expanded":true,"width":245},{"id":"z422yw3ofycp6wtve9p0o34sv","name":"v_dim_products","x":2684,"y":-263,"fields":[{"id":"njh0av4adz7u1aqyqs9pxxs0f","name":"store","type":{"id":"int64","name":"int64"},"unique":true,"nullable":false,"primaryKey":true,"createdAt":1765200295259},{"id":"pf0ax394h6u5vuwy98mblzf3e","name":"iai_id_prod","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200341491},{"id":"ixqpukstti25z3acg0wtcgq6x","name":"wms_id_prod","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200349152},{"id":"y8tsw57ikl80wdmiudkpgxdck","name":"prod_name","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200355586},{"id":"tw2tvh6z0ao9i7v12ok99t59n","name":"ean","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200363448},{"id":"q4zpfzu0oc9u4kyvig3pdakxo","name":"brand","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200369069},{"id":"rkd5x4s923gd0nyli0e9707f3","name":"primary_supplier","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200374925},{"id":"6sdetn3l710f18tbte6agzxhh","name":"purchaser","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200386185},{"id":"xaa7x3694g6brwvr8jlu5wjlp","name":"visibility","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200393667},{"id":"ijj3t3dt5cov4bhrkv1qinwdn","name":"wk_status","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200402158}],"indexes":[{"id":"ux6jglqcbc8xwriy40csa4r3j","name":"pk_table_105_id","fieldIds":["njh0av4adz7u1aqyqs9pxxs0f"],"unique":true,"isPrimaryKey":true,"createdAt":1765200295259}],"color":"#b0b0b0","createdAt":1765200295259,"isView":true,"order":104,"schema":"reporting","parentAreaId":"3c7d2957a2a0","diagramId":"ee570f764e8c"},{"id":"zs3va9l5vk8hpcasbk31hjbpf","name":"v_saldeo_invoices","x":3653,"y":-743,"fields":[{"id":"1rbb8m2ysqr6kx5ct4rxx98is","name":"document_id","type":{"id":"int64","name":"int64"},"unique":true,"nullable":false,"primaryKey":true,"createdAt":1765200445463},{"id":"7wlmg57z84pob10aadxnt5pt7","name":"invoice_no","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200603397},{"id":"mrf1xrfoaohacoblgp0qx9toq","name":"posting_date","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200615305},{"id":"itlcv7jmqjcylztdqqxlf0as3","name":"issue_date","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200623453},{"id":"0uqmmkeb021dne3yntsxpml94","name":"account","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200629267},{"id":"qoh0k21fhxwxz8likwygjywag","name":"value","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200633225},{"id":"ore3qf354akhxgwxbsd8viu9n","name":"description","type":{"id":"int64","name":"int64"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1765200639325}],"indexes":[{"id":"bxoy0klnlikwh0wzbga717r1v","name":"pk_table_106_id","fieldIds":["1rbb8m2ysqr6kx5ct4rxx98is"],"unique":true,"isPrimaryKey":true,"createdAt":1765200445463}],"color":"#b0b0b0","createdAt":1765200445463,"isView":true,"order":105,"schema":"reporting","parentAreaId":"3c7d2957a2a0","diagramId":"ee570f764e8c"}],"relationships":[{"id":"m8vh59qvvt1fcht0bt4plifyi","name":"wms_item_ean_code_fk","sourceSchema":"clean","sourceTableId":"4ar0wqk9p8uao27desm3uicnh","targetSchema":"reporting","targetTableId":"la8abk9zhcli83cdtw8xd1ecf","sourceFieldId":"h4xw4k74c92j8we5xqyuama1h","targetFieldId":"5baaq93izkuwclwqw6r9811v5","sourceCardinality":"one","targetCardinality":"one","createdAt":1765201247296,"diagramId":"ee570f764e8c"},{"id":"q8zjxicc5o8tmf0lcvub0qr8u","name":"wms_item_id_fk","sourceSchema":"clean","sourceTableId":"4ar0wqk9p8uao27desm3uicnh","targetSchema":"reporting","targetTableId":"la8abk9zhcli83cdtw8xd1ecf","sourceFieldId":"ry3l377zs3rzgw5boxt2gvfrr","targetFieldId":"kmmzdszo23ak3xbwhoni88zdw","sourceCardinality":"one","targetCardinality":"one","createdAt":1765201235426,"diagramId":"ee570f764e8c"},{"id":"1qyrc0xmvdunurk3fgok1znnh","name":"wms_item_symbol_fk","sourceSchema":"clean","sourceTableId":"4ar0wqk9p8uao27desm3uicnh","targetSchema":"reporting","targetTableId":"la8abk9zhcli83cdtw8xd1ecf","sourceFieldId":"0uc3ksoc67sobp77ky20day3k","targetFieldId":"rknfchbhxk1ujs1q1y6ew36di","sourceCardinality":"one","targetCardinality":"one","createdAt":1765201238826,"diagramId":"ee570f764e8c"}],"dependencies":[{"id":"9nle0dtxzzw0421t6il80ey6w","tableId":"la8abk9zhcli83cdtw8xd1ecf","dependentTableId":"z422yw3ofycp6wtve9p0o34sv","dependentSchema":"reporting","schema":"reporting","createdAt":1765200412900,"diagramId":"ee570f764e8c"}],"storageMode":"project","lastProjectSavedAt":"2025-12-17T16:08:21.398Z","areas":[],"creationMethod":"imported","customTypes":[],"notes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/8.dbml b/src/lib/dbml/dbml-export/__tests__/cases/8.dbml
new file mode 100644
index 000000000..1f00d4d3c
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/8.dbml
@@ -0,0 +1,30 @@
+Table "pokemon"."abilities" {
+ "abil_id" int [pk, not null]
+ "abil_name" varchar(500) [not null]
+}
+
+Table "pokemon"."pokemon_abilities" {
+ "pok_id" int [not null]
+ "abil_id" int [not null]
+ "is_hidden" int [not null]
+ "slot" int [not null]
+ "music" bigint
+
+ Indexes {
+ (pok_id, slot) [pk]
+ abil_id [name: "pokemon_abilities_pokemon_abil_id"]
+ is_hidden [name: "pokemon_abilities_pokemon_ix_pokemon_abilities_is_hidden"]
+ }
+}
+
+Table "pokemon"."pokemon" {
+ "pok_id" int [pk, not null]
+ "pok_name" varchar(500) [not null]
+ "pok_height" int
+ "pok_weight" int
+ "pok_base_experience" int
+}
+
+Ref "fk_0_fk_pokemon_abilities_pok_id_pokemon_pok_id":"pokemon"."pokemon"."pok_id" < "pokemon"."pokemon_abilities"."pok_id"
+
+Ref "fk_1_fk_pokemon_abilities_abil_id_abilities_abil_id":"pokemon"."abilities"."abil_id" < "pokemon"."pokemon_abilities"."abil_id"
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/8.inline.dbml b/src/lib/dbml/dbml-export/__tests__/cases/8.inline.dbml
new file mode 100644
index 000000000..4a8de52c7
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/8.inline.dbml
@@ -0,0 +1,26 @@
+Table "pokemon"."abilities" {
+ "abil_id" int [pk, not null]
+ "abil_name" varchar(500) [not null]
+}
+
+Table "pokemon"."pokemon_abilities" {
+ "pok_id" int [not null, ref: > "pokemon"."pokemon"."pok_id"]
+ "abil_id" int [not null, ref: > "pokemon"."abilities"."abil_id"]
+ "is_hidden" int [not null]
+ "slot" int [not null]
+ "music" bigint
+
+ Indexes {
+ (pok_id, slot) [pk]
+ abil_id [name: "pokemon_abilities_pokemon_abil_id"]
+ is_hidden [name: "pokemon_abilities_pokemon_ix_pokemon_abilities_is_hidden"]
+ }
+}
+
+Table "pokemon"."pokemon" {
+ "pok_id" int [pk, not null]
+ "pok_name" varchar(500) [not null]
+ "pok_height" int
+ "pok_weight" int
+ "pok_base_experience" int
+}
diff --git a/src/lib/dbml/dbml-export/__tests__/cases/8.json b/src/lib/dbml/dbml-export/__tests__/cases/8.json
new file mode 100644
index 000000000..c162fc926
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/cases/8.json
@@ -0,0 +1 @@
+{"id":"0kfb46kt4fqz","name":"SQL Import (postgresql)","createdAt":"2025-12-18T12:58:52.104Z","updatedAt":"2025-12-18T13:08:00.838Z","databaseType":"postgresql","tables":[{"id":"meui8qxt4ui15zn0wbpn8kf4h","name":"abilities","schema":"pokemon","order":13,"fields":[{"id":"40alqf0d9uw4pkhc43yg0xbsy","name":"abil_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":true,"default":"","createdAt":1766062732104,"increment":false},{"id":"3iipmd7o8icmen0jn8124gl9n","name":"abil_name","type":{"name":"varchar","id":"varchar","fieldAttributes":{"hasCharMaxLength":true},"usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false,"characterMaximumLength":"500"}],"indexes":[],"x":-1330.1839360655863,"y":-318.9120909292717,"color":"#8eb7ff","isView":false,"createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"},{"id":"ndqyi89iebp8w1my9t4su9nlo","name":"pokemon_abilities","schema":"pokemon","order":5,"fields":[{"id":"v99oydscnxxyqg6knbycm39aj","name":"pok_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"vrwyacj8mb0oj6fspen6vqpc1","name":"abil_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"1eg794qd6zw2i281os7ycw27f","name":"is_hidden","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"k6dj4hreohg8ufybojn6re6wz","name":"slot","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"a35s1d192eompkn7yvuxng62d","name":"music","type":{"name":"bigint","id":"bigint"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false}],"indexes":[{"id":"hluplg0pndapd1fk086cawlsl","name":"pokemon_abilities_pokemon_ix_pokemon_abilities_is_hidden","fieldIds":["1eg794qd6zw2i281os7ycw27f"],"unique":false,"createdAt":1766062732104},{"id":"7t8x8wl5hjrsi914w95hpn2r9","name":"pokemon_abilities_pokemon_abil_id","fieldIds":["vrwyacj8mb0oj6fspen6vqpc1"],"unique":false,"createdAt":1766062732104}],"x":-958.2017535132559,"y":-415.8808703335114,"color":"#8eb7ff","isView":false,"createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"},{"id":"uf6nokjc3llz4dmfze0bjhzyo","name":"pokemon","schema":"pokemon","order":10,"fields":[{"id":"zqyqypyht1uaum3sce3bvwlhi","name":"pok_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":true,"default":"","createdAt":1766062732104,"increment":false},{"id":"d7o7wjf1gowlsq5p3etnper8o","name":"pok_name","type":{"name":"varchar","id":"varchar","fieldAttributes":{"hasCharMaxLength":true},"usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false,"characterMaximumLength":"500"},{"id":"yvw4bu3bgozb8hz7z3t8nkmz8","name":"pok_height","type":{"id":"int","name":"int"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"snci66nxy39m97a7eouwybn3x","name":"pok_weight","type":{"id":"int","name":"int"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"ekqck9gw9z1ktciuh95ryhpy9","name":"pok_base_experience","type":{"id":"int","name":"int"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false}],"indexes":[],"x":-627.5363678111918,"y":-454.0032933604259,"color":"#8eb7ff","isView":false,"createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"}],"relationships":[{"id":"ylhfaz43z8l46l6zlwpsmjdql","name":"fk_pokemon_abilities_pok_id_pokemon_pok_id","sourceSchema":"pokemon","targetSchema":"pokemon","sourceTableId":"uf6nokjc3llz4dmfze0bjhzyo","targetTableId":"ndqyi89iebp8w1my9t4su9nlo","sourceFieldId":"zqyqypyht1uaum3sce3bvwlhi","targetFieldId":"v99oydscnxxyqg6knbycm39aj","sourceCardinality":"one","targetCardinality":"many","createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"},{"id":"pcw4h4jau7sbprolm8b1m4f6k","name":"fk_pokemon_abilities_abil_id_abilities_abil_id","sourceSchema":"pokemon","targetSchema":"pokemon","sourceTableId":"meui8qxt4ui15zn0wbpn8kf4h","targetTableId":"ndqyi89iebp8w1my9t4su9nlo","sourceFieldId":"40alqf0d9uw4pkhc43yg0xbsy","targetFieldId":"vrwyacj8mb0oj6fspen6vqpc1","sourceCardinality":"one","targetCardinality":"many","createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"}],"dependencies":[],"areas":[],"customTypes":[],"notes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-export/__tests__/dbml-export-invalid-check-constraints.test.ts b/src/lib/dbml/dbml-export/__tests__/dbml-export-invalid-check-constraints.test.ts
new file mode 100644
index 000000000..d658e2803
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/dbml-export-invalid-check-constraints.test.ts
@@ -0,0 +1,247 @@
+import { describe, it, expect } from 'vitest';
+import { generateDBMLFromDiagram } from '../dbml-export';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { Diagram } from '@/lib/domain/diagram';
+
+describe('DBML Export - Invalid Check Constraints', () => {
+ it('should not fail when table has invalid check constraints', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'table_1',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ unique: false,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ checkConstraints: [
+ {
+ id: 'check1',
+ expression: 'id > 0', // Valid
+ createdAt: Date.now(),
+ },
+ {
+ id: 'check2',
+ expression: '(a < b)', // Valid
+ createdAt: Date.now(),
+ },
+ {
+ id: 'check3',
+ expression: '(a a)', // Invalid - no operator
+ createdAt: Date.now(),
+ },
+ ],
+ },
+ ],
+ relationships: [],
+ };
+
+ // Should not throw
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Should not contain error message
+ expect(result.error).toBeUndefined();
+ expect(result.standardDbml).not.toContain('Error generating DBML');
+
+ // Should contain the table
+ expect(result.standardDbml).toContain('table_1');
+
+ // Should contain valid check constraints
+ expect(result.standardDbml).toContain('id > 0');
+ expect(result.standardDbml).toContain('(a < b)');
+
+ // Should NOT contain invalid check constraint
+ expect(result.standardDbml).not.toContain('(a a)');
+ });
+
+ it('should handle table with only invalid check constraints', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'products',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'price',
+ type: { id: 'decimal', name: 'decimal' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ checkConstraints: [
+ {
+ id: 'check1',
+ expression: 'price >', // Invalid - incomplete
+ createdAt: Date.now(),
+ },
+ {
+ id: 'check2',
+ expression: '> 0', // Invalid - no left operand
+ createdAt: Date.now(),
+ },
+ ],
+ },
+ ],
+ relationships: [],
+ };
+
+ // Should not throw
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Should not contain error message
+ expect(result.error).toBeUndefined();
+ expect(result.standardDbml).not.toContain('Error generating DBML');
+
+ // Should contain the table
+ expect(result.standardDbml).toContain('products');
+
+ // Should NOT contain a checks block (no valid constraints)
+ expect(result.standardDbml).not.toContain('checks {');
+ });
+
+ it('should handle empty check constraint expressions', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'users',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'age',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ checkConstraints: [
+ {
+ id: 'check1',
+ expression: '', // Empty
+ createdAt: Date.now(),
+ },
+ {
+ id: 'check2',
+ expression: ' ', // Whitespace only
+ createdAt: Date.now(),
+ },
+ {
+ id: 'check3',
+ expression: 'age >= 0', // Valid
+ createdAt: Date.now(),
+ },
+ ],
+ },
+ ],
+ relationships: [],
+ };
+
+ // Should not throw
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Should not contain error message
+ expect(result.error).toBeUndefined();
+
+ // Should contain valid constraint
+ expect(result.standardDbml).toContain('age >= 0');
+ });
+
+ it('should handle unbalanced parentheses in check constraints', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'orders',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'status',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ checkConstraints: [
+ {
+ id: 'check1',
+ expression: "(status = 'active'", // Missing closing paren
+ createdAt: Date.now(),
+ },
+ ],
+ },
+ ],
+ relationships: [],
+ };
+
+ // Should not throw
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Should not contain error message
+ expect(result.error).toBeUndefined();
+ expect(result.standardDbml).not.toContain('Error generating DBML');
+
+ // Should contain the table
+ expect(result.standardDbml).toContain('orders');
+
+ // Should NOT contain the invalid constraint
+ expect(result.standardDbml).not.toContain("(status = 'active'");
+ });
+});
diff --git a/src/lib/dbml/dbml-export/__tests__/dbml-export-issue-fix.test.ts b/src/lib/dbml/dbml-export/__tests__/dbml-export-issue-fix.test.ts
index 213cc0636..15a3061b5 100644
--- a/src/lib/dbml/dbml-export/__tests__/dbml-export-issue-fix.test.ts
+++ b/src/lib/dbml/dbml-export/__tests__/dbml-export-issue-fix.test.ts
@@ -79,8 +79,9 @@ describe('DBML Export - Issue Fixes', () => {
const result = generateDBMLFromDiagram(diagram);
// Check that inline DBML has merged attributes in a single bracket
+ // Relationship is many-to-one (source=many, target=one), so source field gets ref: >
expect(result.inlineDbml).toContain(
- '"id" bigint [pk, not null, ref: < "service_tenant"."tenant_id"]'
+ '"id" bigint [pk, not null, ref: > "service_tenant"."tenant_id"]'
);
// Should NOT have separate brackets like [pk, not null] [ref: < ...]
@@ -210,8 +211,9 @@ describe('DBML Export - Issue Fixes', () => {
const result = generateDBMLFromDiagram(diagram);
// Check inline DBML preserves schema in references
- // The foreign key is on the users.tenant_id field, referencing service.tenant.id
- expect(result.inlineDbml).toContain('ref: < "service"."tenant"."id"');
+ // Relationship is many-to-one (source=many, target=one)
+ // The inline ref goes on users.tenant_id (source) with ref: > pointing to service.tenant.id (target)
+ expect(result.inlineDbml).toContain('ref: > "service"."tenant"."id"');
});
it('should wrap table and field names with spaces in quotes instead of replacing with underscores', () => {
@@ -344,12 +346,13 @@ describe('DBML Export - Issue Fixes', () => {
expect(result.standardDbml).not.toContain('idx_user_name');
// Check inline DBML as well - the ref is on the order details table
+ // Relationship is many-to-one (source=many, target=one), so source field gets ref: >
expect(result.inlineDbml).toContain(
- '"user id" bigint [not null, ref: < "user profile"."user id"]'
+ '"user id" bigint [not null, ref: > "user profile"."user id"]'
);
});
- it('should export table and field comments to DBML', () => {
+ it('should export table and field comments to DBML for PostgreSQL', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Test',
@@ -495,8 +498,9 @@ describe('DBML Export - Issue Fixes', () => {
expect(result.inlineDbml).toContain(
'"email" varchar(255) [unique, not null, note: \'User email address\']'
);
+ // Relationship is many-to-one (source=many, target=one), so source field gets ref: >
expect(result.inlineDbml).toContain(
- '"user_id" bigint [not null, note: \'Reference to the user who created the post\', ref: < "users"."id"]'
+ '"user_id" bigint [not null, note: \'Reference to the user who created the post\', ref: > "users"."id"]'
);
// In standard DBML, field comments should use the note attribute syntax
@@ -519,6 +523,239 @@ describe('DBML Export - Issue Fixes', () => {
);
});
+ it('should export table and field comments to DBML for MySQL', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.MYSQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'pl_a_cmsn',
+ comments: 'Commission table',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'mandt',
+ type: { id: 'char', name: 'char' },
+ primaryKey: true,
+ nullable: false,
+ unique: false,
+ comments: 'Mandant',
+ collation: null,
+ default: null,
+ characterMaximumLength: '3',
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field2',
+ name: 'policy_no',
+ type: { id: 'char', name: 'char' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ comments: 'Policennummer',
+ collation: null,
+ default: null,
+ characterMaximumLength: '25',
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field3',
+ name: 'validity_from',
+ type: { id: 'date', name: 'date' },
+ primaryKey: false,
+ nullable: false,
+ unique: false,
+ comments: 'Gültigkeitsdatum bis',
+ collation: null,
+ default: null,
+ characterMaximumLength: null,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Check table exists in DBML
+ expect(result.standardDbml).toContain('Table "pl_a_cmsn" {');
+
+ // Check table comments are preserved for MySQL
+ expect(result.standardDbml).toContain("Note: 'Commission table'");
+
+ // Check field comments are preserved with note: syntax
+ expect(result.standardDbml).toContain(
+ '"mandt" char(3) [pk, not null, note: \'Mandant\']'
+ );
+ expect(result.standardDbml).toContain(
+ '"policy_no" char(25) [not null, note: \'Policennummer\']'
+ );
+ expect(result.standardDbml).toContain(
+ '"validity_from" date [not null, note: \'Gültigkeitsdatum bis\']'
+ );
+
+ // Also check inline DBML
+ expect(result.inlineDbml).toContain('Table "pl_a_cmsn" {');
+ expect(result.inlineDbml).toContain("Note: 'Commission table'");
+ expect(result.inlineDbml).toContain(
+ '"mandt" char(3) [pk, not null, note: \'Mandant\']'
+ );
+ expect(result.inlineDbml).toContain(
+ '"policy_no" char(25) [not null, note: \'Policennummer\']'
+ );
+ expect(result.inlineDbml).toContain(
+ '"validity_from" date [not null, note: \'Gültigkeitsdatum bis\']'
+ );
+ });
+
+ it('should handle multiline comments for MySQL tables and fields', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.MYSQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'users',
+ comments:
+ 'This is a multiline\ntable comment\nwith multiple lines',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ unique: false,
+ comments: 'This is a\nmultiline field\ncomment',
+ collation: null,
+ default: null,
+ characterMaximumLength: null,
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field2',
+ name: 'description',
+ type: { id: 'text', name: 'text' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ comments:
+ 'Field with\n\ntabs\tand\n spaces \nand newlines',
+ collation: null,
+ default: null,
+ characterMaximumLength: null,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Check that multiline table comment is preserved as single line at the end
+ expect(result.standardDbml).toContain('Table "users" {');
+ expect(result.standardDbml).toContain(
+ "Note: 'This is a multiline table comment with multiple lines'"
+ );
+ // Note should be at the end of the table, before closing brace
+ expect(result.standardDbml).toMatch(
+ /Table "users" \{[\s\S]*Note: 'This is a multiline table comment with multiple lines'\s*\}/m
+ );
+
+ // Check that multiline field comments are preserved as single line
+ expect(result.standardDbml).toContain(
+ '"id" bigint [pk, not null, note: \'This is a multiline field comment\']'
+ );
+ expect(result.standardDbml).toContain(
+ '"description" text [note: \'Field with tabs and spaces and newlines\']'
+ );
+
+ // Also verify in inline DBML
+ expect(result.inlineDbml).toContain(
+ "Note: 'This is a multiline table comment with multiple lines'"
+ );
+ expect(result.inlineDbml).toContain(
+ '"id" bigint [pk, not null, note: \'This is a multiline field comment\']'
+ );
+ });
+
+ it('should handle multiline comments for PostgreSQL tables and fields', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'products',
+ comments: 'Product catalog\nwith detailed\ninformation',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'sku',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: true,
+ nullable: false,
+ unique: false,
+ comments: 'Stock Keeping Unit\nUnique identifier',
+ collation: null,
+ default: null,
+ characterMaximumLength: '50',
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Check that multiline comments are flattened for PostgreSQL too
+ expect(result.standardDbml).toContain('Table "products" {');
+ expect(result.standardDbml).toContain(
+ "Note: 'Product catalog with detailed information'"
+ );
+ // Note should be at the end of the table
+ expect(result.standardDbml).toMatch(
+ /Table "products" \{[\s\S]*Note: 'Product catalog with detailed information'\s*\}/m
+ );
+ expect(result.standardDbml).toContain(
+ '"sku" varchar(50) [pk, not null, note: \'Stock Keeping Unit Unique identifier\']'
+ );
+ });
+
it('should preserve tables with same name but different schemas', () => {
const diagram: Diagram = {
id: 'test-diagram',
@@ -1033,15 +1270,18 @@ describe('DBML Export - Issue Fixes', () => {
const result = generateDBMLFromDiagram(diagram);
+ // For 1:1 relationships, symbol is '-' (one-to-one)
+ // Inline ref goes on target side (table_1) with ref: - pointing to source (table_2)
const expectedInlineDBML = `Table "table_1" {
- "id" bigint [pk, not null]
+ "id" bigint [pk, not null, ref: - "table_2"."id"]
}
Table "table_2" {
- "id" bigint [pk, not null, ref: < "table_1"."id"]
+ "id" bigint [pk, not null]
}
`;
+ // Standard DBML: source - target (one-to-one relationship)
const expectedStandardDBML = `Table "table_1" {
"id" bigint [pk, not null]
}
@@ -1050,7 +1290,7 @@ Table "table_2" {
"id" bigint [pk, not null]
}
-Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
+Ref "fk_0_table_2_id_fk":"table_2"."id" - "table_1"."id"
`;
expect(result.inlineDbml).toBe(expectedInlineDBML);
@@ -1270,12 +1510,14 @@ Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
expect(result.standardDbml).toContain('Table "user_activities" {');
// Check that the entity_id field in user_activities has multiple relationships in inline DBML
+ // All relationships are many-to-one (source=many, target=one), so source field gets ref: >
// The field should have both references in a single bracket
expect(result.inlineDbml).toContain(
- '"entity_id" integer [not null, ref: < "posts"."id", ref: < "reviews"."id"]'
+ '"entity_id" integer [not null, ref: > "posts"."id", ref: > "reviews"."id"]'
);
// Check that standard DBML has separate Ref entries for each relationship
+ // Format: target < source for many-to-one relationships (target is one, source is many)
expect(result.standardDbml).toContain(
'Ref "fk_0_fk_posts_user":"users"."id" < "posts"."user_id"'
);
@@ -1388,8 +1630,9 @@ Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
"id" bigint [pk, not null]
}`);
+ // Relationship is many-to-one (source=many, target=one), so source field gets ref: >
expect(result.inlineDbml).toContain(`Table "table_2" {
- "id" bigint [pk, not null, ref: < "table_1"."id"]
+ "id" bigint [pk, not null, ref: > "table_1"."id"]
}`);
// The issue was that it would generate:
@@ -1412,4 +1655,148 @@ Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
(result.inlineDbml.match(/}/g) || []).length;
expect(braceBalance).toBe(0);
});
+
+ it('should export GIN index type in DBML', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'test',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ unique: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field2',
+ name: 'ginsss',
+ type: { id: 'int[]', name: 'int[]' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ isArray: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [
+ {
+ id: 'idx1',
+ name: 'test_index_2',
+ fieldIds: ['field2'],
+ unique: false,
+ type: 'gin',
+ createdAt: Date.now(),
+ },
+ ],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // The index should include type: gin
+ expect(result.standardDbml).toContain('type: gin');
+ expect(result.standardDbml).toContain('test_index_2');
+ });
+
+ it('should export composite GIN index type in DBML', () => {
+ const diagram: Diagram = {
+ id: 'test-diagram',
+ name: 'Test',
+ databaseType: DatabaseType.POSTGRESQL,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ tables: [
+ {
+ id: 'table1',
+ name: 'merchants',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: 'field1',
+ name: 'id',
+ type: { id: 'bigint', name: 'bigint' },
+ primaryKey: true,
+ nullable: false,
+ unique: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field2',
+ name: 'supported_licenses',
+ type: { id: 'text[]', name: 'text[]' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ isArray: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field3',
+ name: 'supported_countries',
+ type: { id: 'text[]', name: 'text[]' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ isArray: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: 'field4',
+ name: 'supported_currencies',
+ type: { id: 'text[]', name: 'text[]' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ isArray: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [
+ {
+ id: 'idx1',
+ name: 'index_4',
+ fieldIds: ['field2', 'field3', 'field4'],
+ unique: false,
+ type: 'gin',
+ createdAt: Date.now(),
+ },
+ ],
+ color: 'blue',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // The composite index should include type: gin
+ expect(result.standardDbml).toContain('type: gin');
+ expect(result.standardDbml).toContain('index_4');
+ // Should have the composite format (col1, col2, col3)
+ expect(result.standardDbml).toContain(
+ 'supported_licenses, supported_countries, supported_currencies'
+ );
+ });
});
diff --git a/src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts b/src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
index 30031907a..266f71064 100644
--- a/src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
+++ b/src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
@@ -4,9 +4,12 @@ import { importDBMLToDiagram } from '../../dbml-import/dbml-import';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Self-Referencing Relationships', () => {
- it('should preserve self-referencing relationships in DBML export', async () => {
- // Create a DBML with self-referencing relationship (general_ledger example)
- const inputDBML = `
+ it(
+ 'should preserve self-referencing relationships in DBML export',
+ { timeout: 30000 },
+ async () => {
+ // Create a DBML with self-referencing relationship (general_ledger example)
+ const inputDBML = `
Table "finance"."general_ledger" {
"ledger_id" bigint [pk]
"account_name" varchar(100)
@@ -16,39 +19,36 @@ Table "finance"."general_ledger" {
}
`;
- // Import the DBML
- const diagram = await importDBMLToDiagram(inputDBML, {
- databaseType: DatabaseType.POSTGRESQL,
- });
-
- // Verify the relationship was imported
- expect(diagram.relationships).toBeDefined();
- expect(diagram.relationships?.length).toBe(1);
-
- // Verify it's a self-referencing relationship
- const relationship = diagram.relationships![0];
- expect(relationship.sourceTableId).toBe(relationship.targetTableId);
-
- // Export back to DBML
- const exportResult = generateDBMLFromDiagram(diagram);
-
- // Check inline format
- expect(exportResult.inlineDbml).toContain('reversal_id');
- // The DBML parser correctly interprets FK as: target < source
- expect(exportResult.inlineDbml).toMatch(
- /ref:\s*<\s*"finance"\."general_ledger"\."ledger_id"/
- );
-
- // Check standard format
- expect(exportResult.standardDbml).toContain('Ref ');
- expect(exportResult.standardDbml).toMatch(
- /"finance"\."general_ledger"\."ledger_id"\s*<\s*"finance"\."general_ledger"\."reversal_id"/
- );
-
- console.log(
- '✅ Self-referencing relationship preserved in DBML export'
- );
- });
+ // Import the DBML
+ const diagram = await importDBMLToDiagram(inputDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify the relationship was imported
+ expect(diagram.relationships).toBeDefined();
+ expect(diagram.relationships?.length).toBe(1);
+
+ // Verify it's a self-referencing relationship
+ const relationship = diagram.relationships![0];
+ expect(relationship.sourceTableId).toBe(relationship.targetTableId);
+
+ // Export back to DBML
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ // Check inline format
+ expect(exportResult.inlineDbml).toContain('reversal_id');
+ // FK fields use ref: > to indicate "I reference other"
+ expect(exportResult.inlineDbml).toMatch(
+ /ref:\s*>\s*"finance"\."general_ledger"\."ledger_id"/
+ );
+
+ // Check standard format
+ expect(exportResult.standardDbml).toContain('Ref ');
+ expect(exportResult.standardDbml).toMatch(
+ /"finance"\."general_ledger"\."ledger_id"\s*<\s*"finance"\."general_ledger"\."reversal_id"/
+ );
+ }
+ );
it('should handle self-referencing relationships in employee hierarchy', async () => {
// Create an employee table with manager relationship
@@ -75,8 +75,8 @@ Table "employees" {
// Check that the self-reference is preserved
expect(exportResult.inlineDbml).toContain('manager_id');
- // The DBML parser correctly interprets FK as: target < source
- expect(exportResult.inlineDbml).toMatch(/ref:\s*<\s*"employees"\."id"/);
+ // FK fields use ref: > to indicate "I reference other"
+ expect(exportResult.inlineDbml).toMatch(/ref:\s*>\s*"employees"\."id"/);
});
it('should handle multiple self-referencing relationships', async () => {
@@ -110,8 +110,8 @@ Table "categories" {
expect(exportResult.inlineDbml).toContain('related_id');
// Count the number of ref: statements
- // The DBML parser correctly interprets FK as: target < source
- const refMatches = exportResult.inlineDbml.match(/ref:\s* to indicate "I reference other"
+ const refMatches = exportResult.inlineDbml.match(/ref:\s*>/g);
expect(refMatches?.length).toBe(2);
});
@@ -135,9 +135,9 @@ Table "hr"."staff" {
const exportResult = generateDBMLFromDiagram(diagram);
// Should preserve the schema in the reference
- // The DBML parser correctly interprets FK as: target < source
+ // FK fields use ref: > to indicate "I reference other"
expect(exportResult.inlineDbml).toMatch(
- /ref:\s*<\s*"hr"\."staff"\."staff_id"/
+ /ref:\s*>\s*"hr"\."staff"\."staff_id"/
);
});
diff --git a/src/lib/dbml/dbml-export/__tests__/empty-tables.test.ts b/src/lib/dbml/dbml-export/__tests__/empty-tables.test.ts
new file mode 100644
index 000000000..79b704c19
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/empty-tables.test.ts
@@ -0,0 +1,205 @@
+import { describe, it, expect } from 'vitest';
+import { generateDBMLFromDiagram } from '../dbml-export';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { Diagram } from '@/lib/domain/diagram';
+import { generateId, generateDiagramId } from '@/lib/utils';
+
+describe('DBML Export - Empty Tables', () => {
+ it('should filter out tables with no fields', () => {
+ const diagram: Diagram = {
+ id: generateDiagramId(),
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'valid_table',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'empty_table',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [], // Empty fields array
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'another_valid_table',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: 'name',
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Verify the DBML doesn't contain the empty table
+ expect(result.inlineDbml).not.toContain('empty_table');
+ expect(result.standardDbml).not.toContain('empty_table');
+
+ // Verify the valid tables are still present
+ expect(result.inlineDbml).toContain('valid_table');
+ expect(result.inlineDbml).toContain('another_valid_table');
+ });
+
+ it('should handle diagram with only empty tables', () => {
+ const diagram: Diagram = {
+ id: generateDiagramId(),
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'empty_table_1',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'empty_table_2',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Should not error and should return empty DBML (or just enums if any)
+ expect(result.inlineDbml).toBeTruthy();
+ expect(result.standardDbml).toBeTruthy();
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should filter out table that becomes empty after removing invalid fields', () => {
+ const diagram: Diagram = {
+ id: generateDiagramId(),
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'table_with_only_empty_field_names',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: '', // Empty field name - will be filtered
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: '', // Empty field name - will be filtered
+ type: { id: 'varchar', name: 'varchar' },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'valid_table',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const result = generateDBMLFromDiagram(diagram);
+
+ // Table with only empty field names should be filtered out
+ expect(result.inlineDbml).not.toContain(
+ 'table_with_only_empty_field_names'
+ );
+ // Valid table should remain
+ expect(result.inlineDbml).toContain('valid_table');
+ });
+});
diff --git a/src/lib/dbml/dbml-export/__tests__/export-sql-dbml-cases.test.ts b/src/lib/dbml/dbml-export/__tests__/export-sql-dbml-cases.test.ts
index 356a032e0..e32a95bf6 100644
--- a/src/lib/dbml/dbml-export/__tests__/export-sql-dbml-cases.test.ts
+++ b/src/lib/dbml/dbml-export/__tests__/export-sql-dbml-cases.test.ts
@@ -4,44 +4,82 @@ import { generateDBMLFromDiagram } from '../dbml-export';
import * as fs from 'fs';
import * as path from 'path';
-describe('DBML Export - Diagram Case 1 Tests', () => {
- it('should handle case 1 diagram', { timeout: 30000 }, async () => {
- // Read the JSON file
- const jsonPath = path.join(__dirname, 'cases', '1.json');
- const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
+const testCase = (caseNumber: string) => {
+ // Read the JSON file
+ const jsonPath = path.join(__dirname, 'cases', `${caseNumber}.json`);
+ const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
- // Parse the JSON and convert to diagram
- const diagram = diagramFromJSONInput(jsonContent);
+ // Parse the JSON and convert to diagram
+ const diagram = diagramFromJSONInput(jsonContent);
- // Generate DBML from the diagram
- const result = generateDBMLFromDiagram(diagram);
- const generatedDBML = result.standardDbml;
+ // Generate DBML from the diagram
+ const result = generateDBMLFromDiagram(diagram);
- // Read the expected DBML file
- const dbmlPath = path.join(__dirname, 'cases', '1.dbml');
- const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
+ // Check for both regular and inline DBML files
+ const regularDbmlPath = path.join(__dirname, 'cases', `${caseNumber}.dbml`);
+ const inlineDbmlPath = path.join(
+ __dirname,
+ 'cases',
+ `${caseNumber}.inline.dbml`
+ );
- // Compare the generated DBML with the expected DBML
- expect(generatedDBML).toBe(expectedDBML);
+ const hasRegularDbml = fs.existsSync(regularDbmlPath);
+ const hasInlineDbml = fs.existsSync(inlineDbmlPath);
+
+ // Test regular DBML if file exists
+ if (hasRegularDbml) {
+ const expectedRegularDBML = fs.readFileSync(regularDbmlPath, 'utf-8');
+ expect(result.standardDbml).toBe(expectedRegularDBML);
+ }
+
+ // Test inline DBML if file exists
+ if (hasInlineDbml) {
+ const expectedInlineDBML = fs.readFileSync(inlineDbmlPath, 'utf-8');
+ expect(result.inlineDbml).toBe(expectedInlineDBML);
+ }
+
+ // Ensure at least one DBML file exists
+ if (!hasRegularDbml && !hasInlineDbml) {
+ throw new Error(
+ `No DBML file found for test case ${caseNumber}. Expected either ${caseNumber}.dbml or ${caseNumber}.inline.dbml`
+ );
+ }
+};
+
+describe('DBML Export cases', () => {
+ it('should handle case 1 diagram', { timeout: 60000 }, async () => {
+ testCase('1');
});
it('should handle case 2 diagram', { timeout: 30000 }, async () => {
- // Read the JSON file
- const jsonPath = path.join(__dirname, 'cases', '2.json');
- const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
+ testCase('2');
+ });
- // Parse the JSON and convert to diagram
- const diagram = diagramFromJSONInput(jsonContent);
+ it('should handle case 3 diagram', { timeout: 30000 }, async () => {
+ testCase('3');
+ });
- // Generate DBML from the diagram
- const result = generateDBMLFromDiagram(diagram);
- const generatedDBML = result.standardDbml;
+ it('should handle case 4 diagram', { timeout: 30000 }, async () => {
+ testCase('4');
+ });
+
+ it('should handle case 5 diagram', { timeout: 30000 }, async () => {
+ testCase('5');
+ });
- // Read the expected DBML file
- const dbmlPath = path.join(__dirname, 'cases', '2.dbml');
- const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
+ it(
+ 'should handle case 6 diagram - auto increment',
+ { timeout: 30000 },
+ async () => {
+ testCase('6');
+ }
+ );
+
+ it('should handle case 7 diagram', { timeout: 30000 }, async () => {
+ testCase('7');
+ });
- // Compare the generated DBML with the expected DBML
- expect(generatedDBML).toBe(expectedDBML);
+ it('should handle case 8 diagram', { timeout: 30000 }, async () => {
+ testCase('8');
});
});
diff --git a/src/lib/dbml/dbml-export/__tests__/timestamp-with-timezone.test.ts b/src/lib/dbml/dbml-export/__tests__/timestamp-with-timezone.test.ts
new file mode 100644
index 000000000..f6a5853ab
--- /dev/null
+++ b/src/lib/dbml/dbml-export/__tests__/timestamp-with-timezone.test.ts
@@ -0,0 +1,248 @@
+import { describe, it, expect } from 'vitest';
+import { generateDBMLFromDiagram } from '../dbml-export';
+import { importDBMLToDiagram } from '../../dbml-import/dbml-import';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { Diagram } from '@/lib/domain/diagram';
+import { generateId, generateDiagramId } from '@/lib/utils';
+
+describe('DBML Export - Timestamp with Time Zone', () => {
+ it('should preserve "timestamp with time zone" type through export and reimport', async () => {
+ // Create a diagram with timestamp with time zone field
+ const diagram: Diagram = {
+ id: generateDiagramId(),
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'events',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'created_at',
+ type: {
+ id: 'timestamp_with_time_zone',
+ name: 'timestamp with time zone',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'updated_at',
+ type: {
+ id: 'timestamp_without_time_zone',
+ name: 'timestamp without time zone',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ // Export to DBML
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ // Verify the DBML contains quoted multi-word types
+ expect(exportResult.inlineDbml).toContain('"timestamp with time zone"');
+ expect(exportResult.inlineDbml).toContain(
+ '"timestamp without time zone"'
+ );
+
+ // Reimport the DBML
+ const reimportedDiagram = await importDBMLToDiagram(
+ exportResult.inlineDbml,
+ {
+ databaseType: DatabaseType.POSTGRESQL,
+ }
+ );
+
+ // Verify the types are preserved
+ const table = reimportedDiagram.tables?.find(
+ (t) => t.name === 'events'
+ );
+ expect(table).toBeDefined();
+
+ const createdAtField = table?.fields.find(
+ (f) => f.name === 'created_at'
+ );
+ const updatedAtField = table?.fields.find(
+ (f) => f.name === 'updated_at'
+ );
+
+ expect(createdAtField?.type.name).toBe('timestamptz');
+ expect(updatedAtField?.type.name).toBe('timestamp');
+ });
+
+ it('should handle time with time zone types', async () => {
+ const diagram: Diagram = {
+ id: generateDiagramId(),
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'schedules',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'start_time',
+ type: {
+ id: 'time_with_time_zone',
+ name: 'time with time zone',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'end_time',
+ type: {
+ id: 'time_without_time_zone',
+ name: 'time without time zone',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ expect(exportResult.inlineDbml).toContain('"time with time zone"');
+ expect(exportResult.inlineDbml).toContain('"time without time zone"');
+
+ const reimportedDiagram = await importDBMLToDiagram(
+ exportResult.inlineDbml,
+ {
+ databaseType: DatabaseType.POSTGRESQL,
+ }
+ );
+
+ const table = reimportedDiagram.tables?.find(
+ (t) => t.name === 'schedules'
+ );
+ const startTimeField = table?.fields.find(
+ (f) => f.name === 'start_time'
+ );
+ const endTimeField = table?.fields.find((f) => f.name === 'end_time');
+
+ expect(startTimeField?.type.name).toBe('timetz');
+ expect(endTimeField?.type.name).toBe('time');
+ });
+
+ it('should handle double precision type', async () => {
+ const diagram: Diagram = {
+ id: generateDiagramId(),
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ {
+ id: generateId(),
+ name: 'measurements',
+ schema: 'public',
+ x: 0,
+ y: 0,
+ fields: [
+ {
+ id: generateId(),
+ name: 'id',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: true,
+ unique: true,
+ nullable: false,
+ createdAt: Date.now(),
+ },
+ {
+ id: generateId(),
+ name: 'value',
+ type: {
+ id: 'double_precision',
+ name: 'double precision',
+ },
+ primaryKey: false,
+ unique: false,
+ nullable: true,
+ createdAt: Date.now(),
+ },
+ ],
+ indexes: [],
+ color: '#8eb7ff',
+ isView: false,
+ createdAt: Date.now(),
+ },
+ ],
+ relationships: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ expect(exportResult.inlineDbml).toContain('"double precision"');
+
+ const reimportedDiagram = await importDBMLToDiagram(
+ exportResult.inlineDbml,
+ {
+ databaseType: DatabaseType.POSTGRESQL,
+ }
+ );
+
+ const table = reimportedDiagram.tables?.find(
+ (t) => t.name === 'measurements'
+ );
+ const valueField = table?.fields.find((f) => f.name === 'value');
+
+ expect(valueField?.type.name).toBe('double precision');
+ });
+});
diff --git a/src/lib/dbml/dbml-export/dbml-export.ts b/src/lib/dbml/dbml-export/dbml-export.ts
index b0049583d..fb3495f01 100644
--- a/src/lib/dbml/dbml-export/dbml-export.ts
+++ b/src/lib/dbml/dbml-export/dbml-export.ts
@@ -3,9 +3,10 @@ import { exportBaseSQL } from '@/lib/data/sql-export/export-sql-script';
import type { Diagram } from '@/lib/domain/diagram';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
-import { type DBField } from '@/lib/domain/db-field';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
+import { validateCheckConstraint } from '@/lib/check-constraints/check-constraints-validator';
+import type { DBRelationship } from '@/lib/domain/db-relationship';
// Use DBCustomType for generating Enum DBML
const generateEnumsDBML = (customTypes: DBCustomType[] | undefined): string => {
@@ -219,12 +220,48 @@ export const sanitizeSQLforDBML = (sql: string): string => {
return sanitized;
};
+// Find the matching closing bracket, properly handling quoted strings within brackets
+const findClosingBracket = (str: string, openBracketIndex: number): number => {
+ let i = openBracketIndex + 1;
+ const len = str.length;
+
+ while (i < len) {
+ const char = str[i];
+
+ if (char === ']') return i;
+
+ // Skip quoted strings (triple, single, or double)
+ if (char === "'" || char === '"') {
+ const isTriple =
+ char === "'" && str[i + 1] === "'" && str[i + 2] === "'";
+ const quote = isTriple ? "'''" : char;
+ const quoteLen = quote.length;
+ i += quoteLen;
+
+ while (i < len) {
+ if (str[i] === '\\') {
+ i += 2; // Skip escaped char
+ } else if (str.startsWith(quote, i)) {
+ i += quoteLen;
+ break;
+ } else {
+ i++;
+ }
+ }
+ continue;
+ }
+ i++;
+ }
+ return -1;
+};
+
// Post-process DBML to convert separate Ref statements to inline refs
const convertToInlineRefs = (dbml: string): string => {
// Extract all Ref statements - Updated pattern to handle schema.table.field format
// Matches both "table"."field" and "schema"."table"."field" formats
+ // Now supports cardinality symbols: < (one-to-many), > (many-to-one), - (one-to-one), <> (many-to-many)
const refPattern =
- /Ref\s+"([^"]+)"\s*:\s*(?:"([^"]+)"\.)?"([^"]+)"\."([^"]+)"\s*([<>*])\s*(?:"([^"]+)"\.)?"([^"]+)"\."([^"]+)"/g;
+ /Ref\s+"([^"]+)"\s*:\s*(?:"([^"]+)"\.)?"([^"]+)"\."([^"]+)"\s*(<>|[<>\-*])\s*(?:"([^"]+)"\.)?"([^"]+)"\."([^"]+)"/g;
const refs: Array<{
refName: string;
sourceSchema?: string;
@@ -338,24 +375,71 @@ const convertToInlineRefs = (dbml: string): string => {
refs.forEach((ref) => {
let targetTableName, fieldNameToModify, inlineRefSyntax, relatedTable;
+ // Build the reference strings for both sides
+ const sourceRef = ref.sourceSchema
+ ? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
+ : `"${ref.sourceTable}"."${ref.sourceField}"`;
+ const targetRef = ref.targetSchema
+ ? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
+ : `"${ref.targetTable}"."${ref.targetField}"`;
+
+ // After parsing Ref "name":LEFT [symbol] RIGHT:
+ // ref.sourceTable = LEFT, ref.targetTable = RIGHT
+ //
+ // For Ref A < B: A is one, B is many, B has FK pointing to A
+ // For Ref A > B: A is many, B is one, A has FK pointing to B
+ //
+ // Inline ref semantics:
+ // - ref: > other = "I reference other" (I have FK pointing to other)
+ // - ref: < other = "other references me" (other has FK pointing to me)
+ //
+ // Both one-to-many and many-to-one Refs use '<' symbol (A < B format)
+ // where A=one, B=many. FK is always on B (the many side).
+
if (ref.direction === '<') {
+ // Ref: A < B where A=one, B=many. FK is on B.
+ // In parsed: ref.sourceTable=A (one), ref.targetTable=B (many)
+ // Inline ref goes on B (many side) with: ref: > A (B references A)
+ targetTableName = ref.targetSchema
+ ? `${ref.targetSchema}.${ref.targetTable}`
+ : ref.targetTable;
+ fieldNameToModify = ref.targetField;
+ inlineRefSyntax = `ref: > ${sourceRef}`; // B references A
+ relatedTable = ref.sourceTable;
+ } else if (ref.direction === '>') {
+ // Ref: A > B where A=many, B=one. FK is on A.
+ // In parsed: ref.sourceTable=A (many), ref.targetTable=B (one)
+ // Inline ref goes on A (many side) with: ref: > B (A references B)
+ targetTableName = ref.sourceSchema
+ ? `${ref.sourceSchema}.${ref.sourceTable}`
+ : ref.sourceTable;
+ fieldNameToModify = ref.sourceField;
+ inlineRefSyntax = `ref: > ${targetRef}`; // A references B
+ relatedTable = ref.targetTable;
+ } else if (ref.direction === '-') {
+ // one-to-one: A - B
+ // Convention: inline ref on B pointing to A
targetTableName = ref.targetSchema
? `${ref.targetSchema}.${ref.targetTable}`
: ref.targetTable;
fieldNameToModify = ref.targetField;
- const sourceRef = ref.sourceSchema
- ? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
- : `"${ref.sourceTable}"."${ref.sourceField}"`;
- inlineRefSyntax = `ref: < ${sourceRef}`;
+ inlineRefSyntax = `ref: - ${sourceRef}`;
+ relatedTable = ref.sourceTable;
+ } else if (ref.direction === '<>') {
+ // many-to-many: A <> B
+ // Convention: inline ref on B pointing to A
+ targetTableName = ref.targetSchema
+ ? `${ref.targetSchema}.${ref.targetTable}`
+ : ref.targetTable;
+ fieldNameToModify = ref.targetField;
+ inlineRefSyntax = `ref: <> ${sourceRef}`;
relatedTable = ref.sourceTable;
} else {
+ // Default fallback (e.g., '*' or unknown)
targetTableName = ref.sourceSchema
? `${ref.sourceSchema}.${ref.sourceTable}`
: ref.sourceTable;
fieldNameToModify = ref.sourceField;
- const targetRef = ref.targetSchema
- ? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
- : `"${ref.targetTable}"."${ref.targetField}"`;
inlineRefSyntax = `ref: > ${targetRef}`;
relatedTable = ref.targetTable;
}
@@ -373,59 +457,49 @@ const convertToInlineRefs = (dbml: string): string => {
// 2. Apply all refs to fields
fieldRefs.forEach((fieldData, fieldKey) => {
- // fieldKey might be "schema.table.field" or just "table.field"
const lastDotIndex = fieldKey.lastIndexOf('.');
const tableName = fieldKey.substring(0, lastDotIndex);
const fieldName = fieldKey.substring(lastDotIndex + 1);
const tableData = tableMap.get(tableName);
- if (tableData) {
- // Updated pattern to capture field definition and all existing attributes in brackets
- const fieldPattern = new RegExp(
- `^([ \t]*"${fieldName}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
- 'gm'
- );
- let newContent = tableData.content;
+ if (!tableData) return;
- newContent = newContent.replace(
- fieldPattern,
- (lineMatch, fieldPart, existingBrackets, commentPart) => {
- // Collect all attributes from existing brackets
- const allAttributes: string[] = [];
- if (existingBrackets) {
- // Extract all bracket contents
- const bracketPattern = /\[([^\]]*)\]/g;
- let bracketMatch;
- while (
- (bracketMatch = bracketPattern.exec(lineMatch)) !==
- null
- ) {
- const content = bracketMatch[1].trim();
- if (content) {
- allAttributes.push(content);
- }
- }
- }
+ const fieldStartPattern = new RegExp(`^([ \\t]*)"${fieldName}"\\s+`);
+ const lines = tableData.content.split('\n');
+ let modified = false;
- // Add all refs for this field
- allAttributes.push(...fieldData.refs);
+ const newLines = lines.map((line) => {
+ const match = fieldStartPattern.exec(line);
+ if (!match) return line;
- // Combine all attributes into a single bracket
- const combinedAttributes = allAttributes.join(', ');
+ modified = true;
+ const leadingSpaces = match[1];
+ const bracketStart = line.indexOf('[');
- // Preserve original spacing from fieldPart
- const leadingSpaces = fieldPart.match(/^(\s*)/)?.[1] || '';
- const fieldDefWithoutSpaces = fieldPart.trim();
+ // Extract field definition (before bracket) and existing attributes
+ const fieldDef =
+ bracketStart !== -1
+ ? line.substring(0, bracketStart).trim()
+ : line.trim();
- return `${leadingSpaces}${fieldDefWithoutSpaces} [${combinedAttributes}]${commentPart || ''}`;
- }
- );
+ const existingContent =
+ bracketStart !== -1
+ ? line.substring(
+ bracketStart + 1,
+ findClosingBracket(line, bracketStart)
+ )
+ : null;
- // Update the table content if modified
- if (newContent !== tableData.content) {
- tableData.content = newContent;
- tableMap.set(tableName, tableData);
- }
+ const attributes = existingContent
+ ? [existingContent.trim(), ...fieldData.refs]
+ : fieldData.refs;
+
+ return `${leadingSpaces}${fieldDef} [${attributes.join(', ')}]`;
+ });
+
+ if (modified) {
+ tableData.content = newLines.join('\n');
+ tableMap.set(tableName, tableData);
}
});
@@ -502,38 +576,35 @@ const convertToInlineRefs = (dbml: string): string => {
return cleanedDbml;
};
-// Function to check for DBML reserved keywords
-const isDBMLKeyword = (name: string): boolean => {
- const keywords = new Set([
- 'YES',
- 'NO',
- 'TRUE',
- 'FALSE',
- 'NULL', // DBML reserved keywords (boolean literals)
- ]);
- return keywords.has(name.toUpperCase());
-};
-
-// Function to check for SQL keywords (add more if needed)
-const isSQLKeyword = (name: string): boolean => {
- const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Common SQL keywords
- return keywords.has(name.toUpperCase());
-};
-
// Function to remove duplicate relationships from the diagram
const deduplicateRelationships = (diagram: Diagram): Diagram => {
if (!diagram.relationships) return diagram;
const seenRelationships = new Set();
+ const seenBidirectional = new Set();
const uniqueRelationships = diagram.relationships.filter((rel) => {
// Create a unique key based on the relationship endpoints
const relationshipKey = `${rel.sourceTableId}-${rel.sourceFieldId}->${rel.targetTableId}-${rel.targetFieldId}`;
+ // Create a normalized key that's the same for both directions
+ const normalizedKey = [
+ `${rel.sourceTableId}-${rel.sourceFieldId}`,
+ `${rel.targetTableId}-${rel.targetFieldId}`,
+ ]
+ .sort()
+ .join('<->');
+
if (seenRelationships.has(relationshipKey)) {
- return false; // Skip duplicate
+ return false; // Skip exact duplicate
+ }
+
+ if (seenBidirectional.has(normalizedKey)) {
+ // This is a bidirectional relationship, skip the second one
+ return false;
}
seenRelationships.add(relationshipKey);
+ seenBidirectional.add(normalizedKey);
return true; // Keep unique relationship
});
@@ -543,48 +614,6 @@ const deduplicateRelationships = (diagram: Diagram): Diagram => {
};
};
-// Function to append comment statements for renamed tables and fields
-const appendRenameComments = (
- baseScript: string,
- sqlRenamedTables: Map,
- fieldRenames: Array<{
- table: string;
- originalName: string;
- newName: string;
- }>,
- finalDiagramForExport: Diagram
-): string => {
- let script = baseScript;
-
- // Append COMMENTS for tables renamed due to SQL keywords
- sqlRenamedTables.forEach((originalName, newName) => {
- const escapedOriginal = originalName.replace(/'/g, "\\'");
- // Find the table to get its schema
- const table = finalDiagramForExport.tables?.find(
- (t) => t.name === newName
- );
- const tableIdentifier = table?.schema
- ? `"${table.schema}"."${newName}"`
- : `"${newName}"`;
- script += `\nCOMMENT ON TABLE ${tableIdentifier} IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
- });
-
- // Append COMMENTS for fields renamed due to SQL keyword conflicts
- fieldRenames.forEach(({ table, originalName, newName }) => {
- const escapedOriginal = originalName.replace(/'/g, "\\'");
- // Find the table to get its schema
- const tableObj = finalDiagramForExport.tables?.find(
- (t) => t.name === table
- );
- const tableIdentifier = tableObj?.schema
- ? `"${tableObj.schema}"."${table}"`
- : `"${table}"`;
- script += `\nCOMMENT ON COLUMN ${tableIdentifier}."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
- });
-
- return script;
-};
-
// Fix DBML formatting to ensure consistent display of char and varchar types
const normalizeCharTypeFormat = (dbml: string): string => {
// Replace "char (N)" with "char(N)" to match varchar's formatting
@@ -596,6 +625,13 @@ const normalizeCharTypeFormat = (dbml: string): string => {
.replace(/character \(([0-9]+)\)/g, 'character($1)');
};
+// Fix array types that are incorrectly quoted by DBML importer
+const fixArrayTypes = (dbml: string): string => {
+ // Remove quotes around array types like "text[]" -> text[]
+ // Matches patterns like: "fieldname" "type[]" and replaces with "fieldname" type[]
+ return dbml.replace(/(\s+"[^"]+"\s+)"([^"\s]+\[\])"/g, '$1$2');
+};
+
// Fix table definitions with incorrect bracket syntax
const fixTableBracketSyntax = (dbml: string): string => {
// Fix patterns like Table [schema].[table] to Table "schema"."table"
@@ -622,6 +658,210 @@ const fixMultilineTableNames = (dbml: string): string => {
);
};
+// Restore increment attribute for auto-incrementing fields
+const restoreIncrementAttribute = (dbml: string, tables: DBTable[]): string => {
+ if (!tables || tables.length === 0) return dbml;
+
+ let result = dbml;
+
+ tables.forEach((table) => {
+ // Find fields with increment=true
+ const incrementFields = table.fields.filter((f) => f.increment);
+
+ incrementFields.forEach((field) => {
+ // Build the table identifier pattern
+ const tableIdentifier = table.schema
+ ? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
+ : `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
+
+ // Escape field name for regex
+ const escapedFieldName = field.name.replace(
+ /[.*+?^${}()|[\]\\]/g,
+ '\\$&'
+ );
+
+ // Pattern to match the field line with existing attributes in brackets
+ // Matches: "field_name" type [existing, attributes]
+ const fieldPattern = new RegExp(
+ `(Table ${tableIdentifier} \\{[^}]*?^\\s*"${escapedFieldName}"[^\\[\\n]+)(\\[[^\\]]*\\])`,
+ 'gms'
+ );
+
+ result = result.replace(
+ fieldPattern,
+ (match, fieldPart, brackets) => {
+ // Check if increment already exists
+ if (brackets.includes('increment')) {
+ return match;
+ }
+
+ // Add increment to the attributes
+ const newBrackets = brackets.replace(']', ', increment]');
+ return fieldPart + newBrackets;
+ }
+ );
+ });
+ });
+
+ return result;
+};
+
+// Restore check constraints that may have been lost during DBML export
+// The @dbml/core importer doesn't support check constraints natively
+const restoreCheckConstraints = (dbml: string, tables: DBTable[]): string => {
+ if (!tables || tables.length === 0) return dbml;
+
+ let result = dbml;
+
+ tables.forEach((table) => {
+ // Filter out empty and invalid expressions
+ const validChecks = (table.checkConstraints ?? []).filter(
+ (c) =>
+ c.expression &&
+ c.expression.trim() &&
+ validateCheckConstraint(c.expression)
+ );
+
+ if (validChecks.length === 0) {
+ return;
+ }
+
+ // Build the table identifier pattern once for this table
+ const tableIdentifier = table.schema
+ ? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
+ : `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
+
+ // Pattern to match the entire table block
+ const tableBlockPattern = new RegExp(
+ `(Table ${tableIdentifier} \\{)([\\s\\S]*?)(^\\})`,
+ 'gm'
+ );
+
+ result = result.replace(
+ tableBlockPattern,
+ (match, tableStart, tableContent, tableEnd) => {
+ // Check if a checks block already exists
+ if (/^\s*checks\s*\{/m.test(tableContent)) {
+ return match;
+ }
+
+ // Build the checks block
+ const checksContent = validChecks
+ .map((check) => ` \`${check.expression}\``)
+ .join('\n');
+
+ const checksBlock = `\n checks {\n${checksContent}\n }\n`;
+
+ // Add the checks block at the end, before the closing brace
+ return `${tableStart}${tableContent}${checksBlock}${tableEnd}`;
+ }
+ );
+ });
+
+ return result;
+};
+
+// Restore table and field notes/comments that may have been lost during DBML export
+// This handles databases where @dbml/core doesn't recognize the comment syntax
+// (e.g., MySQL's inline COMMENT syntax). For databases like PostgreSQL where
+// notes are already preserved, this function detects existing notes and skips them.
+const restoreNotes = (dbml: string, tables: DBTable[]): string => {
+ if (!tables || tables.length === 0) return dbml;
+
+ let result = dbml;
+
+ // Helper function to escape comments for DBML
+ const escapeComment = (comment: string): string => {
+ return comment
+ .replace(/\r?\n/g, ' ') // Replace newlines with spaces
+ .replace(/\s+/g, ' ') // Normalize multiple spaces
+ .trim() // Remove leading/trailing whitespace
+ .replace(/\\/g, '\\\\')
+ .replace(/'/g, "\\'")
+ .replace(/"/g, '\\"');
+ };
+
+ tables.forEach((table) => {
+ // Build the table identifier pattern once for this table
+ const tableIdentifier = table.schema
+ ? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
+ : `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
+
+ // Restore table-level notes
+ if (table.comments) {
+ const escapedComment = escapeComment(table.comments);
+
+ // Pattern to match the entire table block
+ const tableBlockPattern = new RegExp(
+ `(Table ${tableIdentifier} \\{)([\\s\\S]*?)(^\\})`,
+ 'gm'
+ );
+
+ result = result.replace(
+ tableBlockPattern,
+ (match, tableStart, tableContent, tableEnd) => {
+ // Check if a Note already exists ANYWHERE in this table
+ if (/^\s*Note:\s*'/m.test(tableContent)) {
+ // Note already exists, don't add another one
+ return match;
+ }
+
+ // Add the Note at the end, before the closing brace (like PostgreSQL)
+ return `${tableStart}${tableContent}\n Note: '${escapedComment}'\n${tableEnd}`;
+ }
+ );
+ }
+
+ // Restore field-level notes
+ const fieldsWithComments = table.fields.filter((f) => f.comments);
+
+ fieldsWithComments.forEach((field) => {
+ // Escape field name for regex
+ const escapedFieldName = field.name.replace(
+ /[.*+?^${}()|[\]\\]/g,
+ '\\$&'
+ );
+
+ // Escape the comment text for use in the replacement
+ const escapedComment = escapeComment(field.comments!);
+
+ // Pattern to match the field line
+ // We need to match the complete field definition including array types
+ // Format: "field_name" type_with_size_and_arrays [attributes]
+ // Examples: "id" bigint [pk], "items" text[] [not null], "name" varchar(100) [unique]
+ const fieldPattern = new RegExp(
+ `(Table ${tableIdentifier} \\{[^}]*?^\\s*"${escapedFieldName}"\\s+\\S+(?:\\([^)]*\\))?(?:\\[\\])?)(\\s*\\[[^\\]]*\\])?`,
+ 'gms'
+ );
+
+ result = result.replace(
+ fieldPattern,
+ (match, fieldPart, brackets) => {
+ // Check if note already exists
+ if (brackets && brackets.includes('note:')) {
+ return match;
+ }
+
+ // Add note to the attributes
+ if (brackets) {
+ // If brackets exist, add note to them
+ const newBrackets = brackets.replace(
+ ']',
+ `, note: '${escapedComment}']`
+ );
+ return fieldPart + newBrackets;
+ } else {
+ // If no brackets, create new ones with note
+ return fieldPart + ` [note: '${escapedComment}']`;
+ }
+ }
+ );
+ });
+ });
+
+ return result;
+};
+
// Restore composite primary key names in the DBML
const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
@@ -661,6 +901,87 @@ const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
return result;
};
+// Restore index types (like GIN) that are lost during SQL to DBML conversion
+// The @dbml/core importer doesn't preserve the USING clause from CREATE INDEX statements
+const restoreIndexTypes = (dbml: string, tables: DBTable[]): string => {
+ if (!tables || tables.length === 0) return dbml;
+
+ let result = dbml;
+
+ tables.forEach((table) => {
+ // Find indexes with non-default types (not btree, and not null/undefined)
+ const indexesWithType = table.indexes.filter(
+ (idx) => idx.type && idx.type !== 'btree' && !idx.isPrimaryKey // PK indexes don't need type restoration
+ );
+
+ if (indexesWithType.length === 0) return;
+
+ // Build the table identifier pattern
+ const tableIdentifier = table.schema
+ ? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
+ : `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
+
+ indexesWithType.forEach((index) => {
+ // Get the field names for this index
+ const fieldNames = index.fieldIds
+ .map((fieldId) => {
+ const field = table.fields.find((f) => f.id === fieldId);
+ return field ? field.name : null;
+ })
+ .filter((name): name is string => name !== null);
+
+ if (fieldNames.length === 0) return;
+
+ // Escape the index name for regex
+ const escapedIndexName = index.name.replace(
+ /[.*+?^${}()|[\]\\]/g,
+ '\\$&'
+ );
+
+ // Build pattern to match index line in DBML
+ // For single column: field_name [name: "index_name"] or field_name [unique, name: "index_name"]
+ // For composite: (field1, field2) [name: "index_name"]
+ let indexColumnPattern: string;
+ if (fieldNames.length === 1) {
+ // Single column index
+ indexColumnPattern = fieldNames[0].replace(
+ /[.*+?^${}()|[\]\\]/g,
+ '\\$&'
+ );
+ } else {
+ // Composite index: (col1, col2, ...)
+ const escapedFields = fieldNames
+ .map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ .join(',\\s*');
+ indexColumnPattern = `\\(${escapedFields}\\)`;
+ }
+
+ // Pattern to match the index line with its attributes
+ // Captures: 1) the column(s), 2) optional unique/pk attributes, 3) rest of attributes including name
+ const indexLinePattern = new RegExp(
+ `(Table ${tableIdentifier} \\{[\\s\\S]*?Indexes \\{[\\s\\S]*?)(${indexColumnPattern})\\s*\\[([^\\]]*name:\\s*"${escapedIndexName}"[^\\]]*)\\]`,
+ 'g'
+ );
+
+ result = result.replace(
+ indexLinePattern,
+ (match, prefix, columns, attributes) => {
+ // Check if type is already present
+ if (/type:\s*\w+/.test(attributes)) {
+ return match;
+ }
+
+ // Add type at the beginning of attributes
+ const newAttributes = `type: ${index.type}, ${attributes}`;
+ return `${prefix}${columns} [${newAttributes}]`;
+ }
+ );
+ });
+ });
+
+ return result;
+};
+
// Restore schema information that may have been stripped by the DBML importer
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
@@ -684,39 +1005,52 @@ const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
// Single table with this name - simple case
const table = tablesGroup[0].table;
if (table.schema) {
- // Match table definition without schema (e.g., Table "users" {)
- const tablePattern = new RegExp(
- `Table\\s+"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\s*{`,
- 'g'
- );
- const schemaTableName = `Table "${table.schema}"."${table.name}" {`;
- result = result.replace(tablePattern, schemaTableName);
-
- // Update references in Ref statements
const escapedTableName = table.name.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
-
- // Pattern 1: In Ref definitions - :"tablename"."field"
- const refDefPattern = new RegExp(
- `(Ref\\s+"[^"]+")\\s*:\\s*"${escapedTableName}"\\."([^"]+)"`,
- 'g'
- );
- result = result.replace(
- refDefPattern,
- `$1:"${table.schema}"."${table.name}"."$2"`
+ const escapedSchema = table.schema.replace(
+ /[.*+?^${}()|[\]\\]/g,
+ '\\$&'
);
- // Pattern 2: In Ref targets - [<>] "tablename"."field"
- const refTargetPattern = new RegExp(
- `([<>])\\s*"${escapedTableName}"\\."([^"]+)"`,
+ // Check if the schema is already present in the table definition
+ const schemaAlreadyPresent = new RegExp(
+ `Table\\s+"${escapedSchema}"\\."${escapedTableName}"\\s*{`,
'g'
- );
- result = result.replace(
- refTargetPattern,
- `$1 "${table.schema}"."${table.name}"."$2"`
- );
+ ).test(result);
+
+ // Only add schema if it's not already present
+ if (!schemaAlreadyPresent) {
+ // Match table definition without schema (e.g., Table "users" {)
+ const tablePattern = new RegExp(
+ `Table\\s+"${escapedTableName}"\\s*{`,
+ 'g'
+ );
+ const schemaTableName = `Table "${table.schema}"."${table.name}" {`;
+ result = result.replace(tablePattern, schemaTableName);
+
+ // Update references in Ref statements
+ // Pattern 1: In Ref definitions - :"tablename"."field"
+ const refDefPattern = new RegExp(
+ `(Ref\\s+"[^"]+")\\s*:\\s*"${escapedTableName}"\\."([^"]+)"`,
+ 'g'
+ );
+ result = result.replace(
+ refDefPattern,
+ `$1:"${table.schema}"."${table.name}"."$2"`
+ );
+
+ // Pattern 2: In Ref targets - [<>] "tablename"."field"
+ const refTargetPattern = new RegExp(
+ `([<>])\\s*"${escapedTableName}"\\."([^"]+)"`,
+ 'g'
+ );
+ result = result.replace(
+ refTargetPattern,
+ `$1 "${table.schema}"."${table.name}"."$2"`
+ );
+ }
}
} else {
// Multiple tables with the same name - need to be more careful
@@ -771,9 +1105,129 @@ const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
return result;
};
+// Function to extract only Ref statements from DBML
+const extractRelationshipsDbml = (dbml: string): string => {
+ const lines = dbml.split('\n');
+ const refLines = lines.filter((line) => line.trim().startsWith('Ref '));
+ return refLines.join('\n').trim();
+};
+
+// Generate Ref statements from diagram relationships with correct cardinality symbols
+// Note: relationships should already be processed with sanitized names (fk_N_name format)
+// Format: referenced_table [symbol] fk_table (matches @dbml/core order)
+// - For many-to-one (source has FK): target [symbol] source
+// - For one-to-many (target has FK): source [symbol] target
+// - For one-to-one: target [symbol] source (convention)
+// - For many-to-many: target [symbol] source (convention)
+const generateRelationshipsDbmlFromDiagram = (
+ relationships: DBRelationship[],
+ tables: DBTable[]
+): string => {
+ if (!relationships || relationships.length === 0) {
+ return '';
+ }
+
+ // Build lookup maps once for O(1) access - improves performance for large diagrams
+ const tableMap = new Map();
+ const fieldMap = new Map();
+
+ for (const table of tables) {
+ tableMap.set(table.id, table);
+ for (const field of table.fields) {
+ fieldMap.set(field.id, { table, fieldName: field.name });
+ }
+ }
+
+ const refStatements: string[] = [];
+
+ for (const rel of relationships) {
+ const sourceTable = tableMap.get(rel.sourceTableId);
+ const targetTable = tableMap.get(rel.targetTableId);
+ const sourceFieldInfo = fieldMap.get(rel.sourceFieldId);
+ const targetFieldInfo = fieldMap.get(rel.targetFieldId);
+
+ // Skip invalid relationships (missing table or field)
+ if (
+ !sourceTable ||
+ !targetTable ||
+ !sourceFieldInfo ||
+ !targetFieldInfo
+ ) {
+ continue;
+ }
+
+ // Build quoted table.field references
+ const sourceRef = sourceTable.schema
+ ? `"${sourceTable.schema}"."${sourceTable.name}"."${sourceFieldInfo.fieldName}"`
+ : `"${sourceTable.name}"."${sourceFieldInfo.fieldName}"`;
+
+ const targetRef = targetTable.schema
+ ? `"${targetTable.schema}"."${targetTable.name}"."${targetFieldInfo.fieldName}"`
+ : `"${targetTable.name}"."${targetFieldInfo.fieldName}"`;
+
+ // Determine order and symbol based on cardinality
+ // To preserve @dbml/core output format while adding correct symbols:
+ // - @dbml/core always outputs: referenced_table < fk_table (regardless of actual cardinality)
+ // - We preserve the order but use correct symbol based on actual cardinality
+ //
+ // DBML semantics:
+ // - `A < B` means: A is ONE, B is MANY
+ // - `A > B` means: A is MANY, B is ONE
+ // - `A - B` means: one-to-one
+ // - `A <> B` means: many-to-many
+ let leftRef: string;
+ let rightRef: string;
+ let symbol: string;
+
+ if (
+ rel.sourceCardinality === 'one' &&
+ rel.targetCardinality === 'many'
+ ) {
+ // one-to-many: source (one) has many target
+ // Format: source < target (source is one, target is many)
+ leftRef = sourceRef;
+ rightRef = targetRef;
+ symbol = '<';
+ } else if (
+ rel.sourceCardinality === 'many' &&
+ rel.targetCardinality === 'one'
+ ) {
+ // many-to-one: source (many) belongs to target (one)
+ // Format: target < source (to match @dbml/core order, target is one, source is many)
+ leftRef = targetRef;
+ rightRef = sourceRef;
+ symbol = '<';
+ } else if (
+ rel.sourceCardinality === 'one' &&
+ rel.targetCardinality === 'one'
+ ) {
+ // one-to-one
+ // Format: source - target
+ leftRef = sourceRef;
+ rightRef = targetRef;
+ symbol = '-';
+ } else {
+ // many-to-many
+ // Format: source <> target
+ leftRef = sourceRef;
+ rightRef = targetRef;
+ symbol = '<>';
+ }
+
+ // rel.name is already sanitized (fk_N_name format) by generateDBMLFromDiagram
+ refStatements.push(
+ `Ref "${rel.name}":${leftRef} ${symbol} ${rightRef}`
+ );
+ }
+
+ // Join with blank lines to match @dbml/core format
+ return refStatements.join('\n\n');
+};
+
export interface DBMLExportResult {
standardDbml: string;
inlineDbml: string;
+ relationshipsDbml: string;
error?: string;
}
@@ -790,31 +1244,37 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
};
}) ?? [];
- // Remove duplicate tables (consider both schema and table name)
+ // Filter out empty tables and duplicates in a single pass for performance
const seenTableIdentifiers = new Set();
- const uniqueTables = sanitizedTables.filter((table) => {
+ const tablesWithFields = sanitizedTables.filter((table) => {
+ // Skip tables with no fields (empty tables cause DBML export to fail)
+ if (table.fields.length === 0) {
+ return false;
+ }
+
// Create a unique identifier combining schema and table name
const tableIdentifier = table.schema
? `${table.schema}.${table.name}`
: table.name;
+ // Skip duplicate tables
if (seenTableIdentifiers.has(tableIdentifier)) {
- return false; // Skip duplicate
+ return false;
}
seenTableIdentifiers.add(tableIdentifier);
- return true; // Keep unique table
+ return true; // Keep unique, non-empty table
});
// Create the base filtered diagram structure
const filteredDiagram: Diagram = {
...diagram,
- tables: uniqueTables,
+ tables: tablesWithFields,
relationships:
diagram.relationships?.filter((rel) => {
- const sourceTable = uniqueTables.find(
+ const sourceTable = tablesWithFields.find(
(t) => t.id === rel.sourceTableId
);
- const targetTable = uniqueTables.find(
+ const targetTable = tablesWithFields.find(
(t) => t.id === rel.targetTableId
);
const sourceFieldExists = sourceTable?.fields.some(
@@ -836,106 +1296,46 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
// Sanitize field names ('from'/'to' in 'relation' table)
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
- // --- Final sanitization and renaming pass ---
- // Only rename keywords for PostgreSQL/SQLite
- // For other databases, we'll wrap problematic names in quotes instead
- const shouldRenameKeywords =
- diagram.databaseType === DatabaseType.POSTGRESQL ||
- diagram.databaseType === DatabaseType.SQLITE;
- const sqlRenamedTables = new Map();
- const fieldRenames: Array<{
- table: string;
- originalName: string;
- newName: string;
- }> = [];
-
+ // Simplified processing - handle duplicate field names and filter invalid check constraints
const processTable = (table: DBTable) => {
- const originalName = table.name;
- let safeTableName = originalName;
-
- // If name contains spaces or special characters, wrap in quotes
- if (/[^\w]/.test(originalName)) {
- safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
- }
-
- // Rename table if it's a keyword (PostgreSQL/SQLite only)
- if (
- shouldRenameKeywords &&
- (isDBMLKeyword(originalName) || isSQLKeyword(originalName))
- ) {
- const newName = `${originalName}_table`;
- sqlRenamedTables.set(newName, originalName);
- safeTableName = /[^\w]/.test(newName)
- ? `"${newName.replace(/"/g, '\\"')}"`
- : newName;
- }
- // For other databases, just quote DBML keywords
- else if (!shouldRenameKeywords && isDBMLKeyword(originalName)) {
- safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
- }
-
const fieldNameCounts = new Map();
const processedFields = table.fields.map((field) => {
- let finalSafeName = field.name;
-
- // If field name contains spaces or special characters, wrap in quotes
- if (/[^\w]/.test(field.name)) {
- finalSafeName = `"${field.name.replace(/"/g, '\\"')}"`;
- }
-
// Handle duplicate field names
const count = fieldNameCounts.get(field.name) || 0;
if (count > 0) {
const newName = `${field.name}_${count + 1}`;
- finalSafeName = /[^\w]/.test(newName)
- ? `"${newName.replace(/"/g, '\\"')}"`
- : newName;
+ return {
+ ...field,
+ name: newName,
+ };
}
fieldNameCounts.set(field.name, count + 1);
-
- // Create sanitized field
- const sanitizedField: DBField = {
- ...field,
- name: finalSafeName,
- };
-
- // Rename field if it's a keyword (PostgreSQL/SQLite only)
- if (
- shouldRenameKeywords &&
- (isDBMLKeyword(field.name) || isSQLKeyword(field.name))
- ) {
- const newFieldName = `${field.name}_field`;
- fieldRenames.push({
- table: safeTableName,
- originalName: field.name,
- newName: newFieldName,
- });
- sanitizedField.name = /[^\w]/.test(newFieldName)
- ? `"${newFieldName.replace(/"/g, '\\"')}"`
- : newFieldName;
- }
- // For other databases, just quote DBML keywords
- else if (!shouldRenameKeywords && isDBMLKeyword(field.name)) {
- sanitizedField.name = `"${field.name.replace(/"/g, '\\"')}"`;
- }
-
- return sanitizedField;
+ return field;
});
+ // Filter out empty and invalid check constraint expressions
+ const validCheckConstraints = (table.checkConstraints ?? []).filter(
+ (c) =>
+ c.expression &&
+ c.expression.trim() &&
+ validateCheckConstraint(c.expression)
+ );
+
return {
...table,
- name: safeTableName,
fields: processedFields,
indexes: (table.indexes || [])
.filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
.map((index) => ({
...index,
- name: index.name
- ? /[^\w]/.test(index.name)
- ? `"${index.name.replace(/"/g, '\\"')}"`
- : index.name
- : `idx_${Math.random().toString(36).substring(2, 8)}`,
+ name:
+ index.name ||
+ `idx_${Math.random().toString(36).substring(2, 8)}`,
})),
+ checkConstraints:
+ validCheckConstraints.length > 0
+ ? validCheckConstraints
+ : undefined,
};
};
@@ -968,46 +1368,64 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
diagram: finalDiagramForExport, // Use final diagram
targetDatabaseType: diagram.databaseType,
isDBMLFlow: true,
+ skipFKGeneration: true, // We generate Refs directly with correct cardinality
});
baseScript = sanitizeSQLforDBML(baseScript);
- // Append comments for renamed tables and fields (PostgreSQL/SQLite only)
- if (
- shouldRenameKeywords &&
- (sqlRenamedTables.size > 0 || fieldRenames.length > 0)
- ) {
- baseScript = appendRenameComments(
- baseScript,
- sqlRenamedTables,
- fieldRenames,
- finalDiagramForExport
- );
- }
-
- standard = normalizeCharTypeFormat(
- fixMultilineTableNames(
- fixTableBracketSyntax(
- importer.import(
- baseScript,
- databaseTypeToImportFormat(diagram.databaseType)
+ standard = fixArrayTypes(
+ normalizeCharTypeFormat(
+ fixMultilineTableNames(
+ fixTableBracketSyntax(
+ importer.import(
+ baseScript,
+ databaseTypeToImportFormat(diagram.databaseType)
+ )
)
)
)
);
// Restore schema information that may have been stripped by DBML importer
- standard = restoreTableSchemas(standard, uniqueTables);
+ standard = restoreTableSchemas(standard, tablesWithFields);
// Restore composite primary key names
- standard = restoreCompositePKNames(standard, uniqueTables);
+ standard = restoreCompositePKNames(standard, tablesWithFields);
+
+ // Restore increment attribute for auto-incrementing fields
+ standard = restoreIncrementAttribute(standard, tablesWithFields);
+
+ // Restore table and field notes/comments that may have been lost during DBML export
+ standard = restoreNotes(standard, tablesWithFields);
+
+ // Restore check constraints that may have been lost during DBML export
+ standard = restoreCheckConstraints(standard, tablesWithFields);
+
+ // Restore index types (like GIN) that are lost during SQL to DBML conversion
+ standard = restoreIndexTypes(standard, tablesWithFields);
+
+ // Generate cardinality-aware Ref statements from the diagram relationships
+ // (FK generation is skipped in SQL, so @dbml/core doesn't generate any Refs)
+ const cardinalityAwareRefs = generateRelationshipsDbmlFromDiagram(
+ finalDiagramForExport.relationships ?? [],
+ finalDiagramForExport.tables ?? []
+ );
+
+ // Append our Ref statements if we have relationships
+ if (cardinalityAwareRefs) {
+ // Clean up trailing whitespace/newlines and add proper spacing
+ standard =
+ standard.trimEnd() + '\n\n' + cardinalityAwareRefs + '\n';
+ }
// Prepend Enum DBML to the standard output
if (enumsDBML) {
standard = enumsDBML + '\n\n' + standard;
}
- inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
+ inline = fixArrayTypes(
+ normalizeCharTypeFormat(convertToInlineRefs(standard))
+ );
// Clean up excessive empty lines in both outputs
standard = standard.replace(/\n\s*\n\s*\n/g, '\n\n');
@@ -1043,5 +1461,13 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
}
}
- return { standardDbml: standard, inlineDbml: inline, error: errorMsg };
+ // Extract relationships DBML from standard output
+ const relationshipsDbml = extractRelationshipsDbml(standard);
+
+ return {
+ standardDbml: standard,
+ inlineDbml: inline,
+ relationshipsDbml,
+ error: errorMsg,
+ };
}
diff --git a/src/lib/dbml/dbml-import/__tests__/cases/1.dbml b/src/lib/dbml/dbml-import/__tests__/cases/1.dbml
new file mode 100644
index 000000000..38aee97f9
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/cases/1.dbml
@@ -0,0 +1,3 @@
+Table "public"."table_3"{
+ "id" bigint [pk]
+}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-import/__tests__/cases/1.json b/src/lib/dbml/dbml-import/__tests__/cases/1.json
new file mode 100644
index 000000000..eaef2f158
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/cases/1.json
@@ -0,0 +1 @@
+{"id":"mqqwkkodxt6p","name":"Diagram 3","createdAt":"2025-09-16T15:33:25.300Z","updatedAt":"2025-09-16T15:33:31.563Z","databaseType":"postgresql","tables":[{"id":"loyxg6mafzos5u971uirjs3zh","name":"table_3","schema":"","order":0,"fields":[{"id":"29e2p9bom0uxo1n0a9ze5auuy","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758036805300}],"indexes":[{"id":"5gf0aeptch1uk1bxv0x89wxxe","name":"pk_table_3_id","fieldIds":["29e2p9bom0uxo1n0a9ze5auuy"],"unique":true,"isPrimaryKey":true,"createdAt":1758036811564}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758036805300,"diagramId":"mqqwkkodxt6p"}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-import/__tests__/cases/2.dbml b/src/lib/dbml/dbml-import/__tests__/cases/2.dbml
new file mode 100644
index 000000000..62ef97124
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/cases/2.dbml
@@ -0,0 +1,7 @@
+Table "table_3" {
+ "id" bigint [pk]
+}
+
+Table "table_2" {
+ "id" bigint [pk, not null, ref: - "table_3"."id"]
+}
diff --git a/src/lib/dbml/dbml-import/__tests__/cases/2.json b/src/lib/dbml/dbml-import/__tests__/cases/2.json
new file mode 100644
index 000000000..29d20bcaf
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/cases/2.json
@@ -0,0 +1 @@
+{"id":"mqqwkkod6r09","name":"Diagram 10","createdAt":"2025-09-16T15:47:40.655Z","updatedAt":"2025-09-16T15:47:50.179Z","databaseType":"postgresql","tables":[{"id":"6xbco4ihmuiyv2heuw9fggbgx","name":"table_3","schema":"","order":0,"fields":[{"id":"rxftaey7uxvq5qg6ix1hbak1c","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758037660654}],"indexes":[{"id":"vsyjjaq2l58urkh9qm2g9hqhd","name":"pk_table_3_id","fieldIds":["rxftaey7uxvq5qg6ix1hbak1c"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660654}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660654,"diagramId":"mqqwkkod6r09"},{"id":"klu6k5ntddcxfdsu0fsfcwbiw","name":"table_2","schema":"","order":1,"fields":[{"id":"qq2415tivmtvun8vd727d9mr2","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758037660655}],"indexes":[{"id":"cvv7sgmq07i9y54lz9a97nah5","name":"pk_table_2_id","fieldIds":["qq2415tivmtvun8vd727d9mr2"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660655}],"x":300,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"relationships":[{"id":"yw2pbcumsabuncc6rjnp3n87t","name":"table_3_id_table_2_id","sourceSchema":"","targetSchema":"","sourceTableId":"6xbco4ihmuiyv2heuw9fggbgx","targetTableId":"klu6k5ntddcxfdsu0fsfcwbiw","sourceFieldId":"rxftaey7uxvq5qg6ix1hbak1c","targetFieldId":"qq2415tivmtvun8vd727d9mr2","sourceCardinality":"one","targetCardinality":"one","createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"dependencies":[],"areas":[],"customTypes":[]}
\ No newline at end of file
diff --git a/src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts b/src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
index 6cc405137..210f76a08 100644
--- a/src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
+++ b/src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
@@ -53,12 +53,12 @@ Table "landlord"."users_master_table" {
expect(uniqueIndex!.unique).toBe(true);
});
- it('should export composite primary key with CONSTRAINT name in PostgreSQL', async () => {
+ it('should export composite primary key without CONSTRAINT name in PostgreSQL (auto-generated)', async () => {
const dbmlContent = `
Table "users" {
"id" bigint [not null]
"tenant_id" bigint [not null]
-
+
Indexes {
(id, tenant_id) [pk, name: "pk_users_composite"]
}
@@ -71,19 +71,17 @@ Table "users" {
const sqlScript = exportPostgreSQL({ diagram });
- // Check that the SQL contains the named constraint
- expect(sqlScript).toContain(
- 'CONSTRAINT "pk_users_composite" PRIMARY KEY ("id", "tenant_id")'
- );
- expect(sqlScript).not.toContain('PRIMARY KEY ("id", "tenant_id"),'); // Should not have unnamed PK
+ // PK constraint names are auto-generated by the database to avoid duplicates
+ expect(sqlScript).toContain('PRIMARY KEY ("id", "tenant_id")');
+ expect(sqlScript).not.toContain('CONSTRAINT "pk_users_composite"');
});
- it('should export composite primary key with CONSTRAINT name in MySQL', async () => {
+ it('should export composite primary key without CONSTRAINT name in MySQL (auto-generated)', async () => {
const dbmlContent = `
Table "orders" {
"order_id" int [not null]
"product_id" int [not null]
-
+
Indexes {
(order_id, product_id) [pk, name: "orders_order_product_pk"]
}
@@ -96,18 +94,17 @@ Table "orders" {
const sqlScript = exportMySQL({ diagram });
- // Check that the SQL contains the named constraint
- expect(sqlScript).toContain(
- 'CONSTRAINT `orders_order_product_pk` PRIMARY KEY (`order_id`, `product_id`)'
- );
+ // PK constraint names are auto-generated by the database to avoid duplicates
+ expect(sqlScript).toContain('PRIMARY KEY (`order_id`, `product_id`)');
+ expect(sqlScript).not.toContain('CONSTRAINT `orders_order_product_pk`');
});
- it('should export composite primary key with CONSTRAINT name in MSSQL', async () => {
+ it('should export composite primary key without CONSTRAINT name in MSSQL (auto-generated)', async () => {
const dbmlContent = `
Table "products" {
"category_id" int [not null]
"product_id" int [not null]
-
+
Indexes {
(category_id, product_id) [pk, name: "pk_products"]
}
@@ -120,10 +117,11 @@ Table "products" {
const sqlScript = exportMSSQL({ diagram });
- // Check that the SQL contains the named constraint
+ // PK constraint names are auto-generated by the database to avoid duplicates
expect(sqlScript).toContain(
- 'CONSTRAINT [pk_products] PRIMARY KEY ([category_id], [product_id])'
+ 'PRIMARY KEY ([category_id], [product_id])'
);
+ expect(sqlScript).not.toContain('CONSTRAINT [pk_products]');
});
it('should merge duplicate PK index with name', async () => {
@@ -177,14 +175,15 @@ Table "simple" {
expect(diagram.tables).toBeDefined();
const table = diagram.tables![0];
- // PK index should not exist for composite PK without name
+ // PK index should exist but with empty name (auto-generated)
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
expect(pkIndex).toBeDefined();
+ expect(pkIndex!.name).toBe('');
const sqlScript = exportPostgreSQL({ diagram });
- // Should have unnamed PRIMARY KEY
+ // Should have unnamed PRIMARY KEY (no CONSTRAINT for auto-generated PK index)
expect(sqlScript).toContain('PRIMARY KEY ("x", "y")');
- expect(sqlScript).toContain('CONSTRAINT');
+ expect(sqlScript).not.toContain('CONSTRAINT');
});
});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-array-fields.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-array-fields.test.ts
new file mode 100644
index 000000000..570b49065
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-array-fields.test.ts
@@ -0,0 +1,317 @@
+import { describe, it, expect } from 'vitest';
+import { importDBMLToDiagram } from '../dbml-import';
+import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('DBML Array Fields - Fantasy RPG Theme', () => {
+ describe('Import - Spell and Magic Arrays', () => {
+ it('should import spell components as array fields', async () => {
+ const dbml = `
+Table "magic"."spells" {
+ "id" uuid [pk, not null]
+ "name" varchar(200) [not null]
+ "level" integer [not null]
+ "components" text[] [note: 'Magical components: bat wing, dragon scale, phoenix feather']
+ "elemental_types" varchar(50)[] [note: 'Elements: fire, water, earth, air']
+ "mana_cost" integer [not null]
+ "created_at" timestamp [not null]
+
+ Indexes {
+ (name, level) [unique, name: "unique_spell"]
+ }
+}
+`;
+
+ const result = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(result.tables).toHaveLength(1);
+
+ const table = result.tables![0];
+ expect(table.name).toBe('spells');
+ expect(table.schema).toBe('magic');
+
+ // Find the array fields
+ const components = table.fields.find(
+ (f) => f.name === 'components'
+ );
+ const elementalTypes = table.fields.find(
+ (f) => f.name === 'elemental_types'
+ );
+
+ // Verify they are marked as arrays
+ expect(components).toBeDefined();
+ expect(components?.isArray).toBe(true);
+ expect(components?.type.name).toBe('text');
+
+ expect(elementalTypes).toBeDefined();
+ expect(elementalTypes?.isArray).toBe(true);
+ expect(elementalTypes?.type.name).toBe('varchar');
+ expect(elementalTypes?.characterMaximumLength).toBe('50');
+
+ // Verify non-array fields don't have isArray set
+ const idField = table.fields.find((f) => f.name === 'id');
+ expect(idField?.isArray).toBeUndefined();
+ });
+
+ it('should import hero inventory with various array types', async () => {
+ const dbml = `
+Table "heroes" {
+ "id" bigint [pk]
+ "name" varchar(100) [not null]
+ "abilities" varchar(100)[]
+ "inventory_slots" integer[]
+ "skill_levels" decimal(5, 2)[]
+ "quest_log" text[]
+}
+`;
+
+ const result = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const table = result.tables![0];
+
+ const abilities = table.fields.find((f) => f.name === 'abilities');
+ expect(abilities?.isArray).toBe(true);
+ expect(abilities?.type.name).toBe('varchar');
+ expect(abilities?.characterMaximumLength).toBe('100');
+
+ const inventorySlots = table.fields.find(
+ (f) => f.name === 'inventory_slots'
+ );
+ expect(inventorySlots?.isArray).toBe(true);
+ expect(inventorySlots?.type.name).toBe('int');
+
+ const skillLevels = table.fields.find(
+ (f) => f.name === 'skill_levels'
+ );
+ expect(skillLevels?.isArray).toBe(true);
+ expect(skillLevels?.type.name).toBe('numeric');
+ expect(skillLevels?.precision).toBe(5);
+ expect(skillLevels?.scale).toBe(2);
+
+ const questLog = table.fields.find((f) => f.name === 'quest_log');
+ expect(questLog?.isArray).toBe(true);
+ expect(questLog?.type.name).toBe('text');
+ });
+
+ it('should handle mixed array and non-array fields in creature table', async () => {
+ const dbml = `
+Table "bestiary"."creatures" {
+ "id" uuid [pk]
+ "species_name" varchar(100) [not null]
+ "habitats" varchar(50)[]
+ "danger_level" integer [not null]
+ "resistances" varchar(50)[]
+ "is_tameable" boolean [not null]
+}
+`;
+
+ const result = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const table = result.tables![0];
+
+ // Non-array fields
+ const id = table.fields.find((f) => f.name === 'id');
+ expect(id?.isArray).toBeUndefined();
+
+ const speciesName = table.fields.find(
+ (f) => f.name === 'species_name'
+ );
+ expect(speciesName?.isArray).toBeUndefined();
+
+ const dangerLevel = table.fields.find(
+ (f) => f.name === 'danger_level'
+ );
+ expect(dangerLevel?.isArray).toBeUndefined();
+
+ // Array fields
+ const habitats = table.fields.find((f) => f.name === 'habitats');
+ expect(habitats?.isArray).toBe(true);
+
+ const resistances = table.fields.find(
+ (f) => f.name === 'resistances'
+ );
+ expect(resistances?.isArray).toBe(true);
+ });
+ });
+
+ describe('Round-trip - Quest and Adventure Arrays', () => {
+ it('should preserve quest rewards array through export and re-import', async () => {
+ const originalDbml = `
+Table "adventures"."quests" {
+ "id" uuid [pk, not null]
+ "title" varchar(200) [not null]
+ "difficulty" varchar(20) [not null]
+ "reward_items" text[] [note: 'Legendary sword, enchanted armor, healing potion']
+ "required_skills" varchar(100)[]
+ "experience_points" integer [not null]
+ "gold_reward" decimal(10, 2) [not null]
+ "created_at" timestamp [not null]
+
+ Indexes {
+ (title, difficulty) [unique, name: "unique_quest"]
+ }
+}
+`;
+
+ // Import the DBML
+ const diagram = await importDBMLToDiagram(originalDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify array fields were imported correctly
+ const table = diagram.tables![0];
+ const rewardItems = table.fields.find(
+ (f) => f.name === 'reward_items'
+ );
+ const requiredSkills = table.fields.find(
+ (f) => f.name === 'required_skills'
+ );
+
+ expect(rewardItems?.isArray).toBe(true);
+ expect(requiredSkills?.isArray).toBe(true);
+
+ // Export back to DBML
+ const { standardDbml: exportedDbml } =
+ generateDBMLFromDiagram(diagram);
+
+ // Verify the exported DBML contains array syntax
+ expect(exportedDbml).toContain('text[]');
+ expect(exportedDbml).toContain('"reward_items" text[]');
+ expect(exportedDbml).toContain('"required_skills" varchar(100)[]');
+
+ // Re-import the exported DBML
+ const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify array fields are still marked as arrays
+ const reimportedTable = reimportedDiagram.tables![0];
+ const reimportedRewards = reimportedTable.fields.find(
+ (f) => f.name === 'reward_items'
+ );
+ const reimportedSkills = reimportedTable.fields.find(
+ (f) => f.name === 'required_skills'
+ );
+
+ expect(reimportedRewards?.isArray).toBe(true);
+ expect(reimportedSkills?.isArray).toBe(true);
+ });
+
+ it('should handle guild members with different array types in round-trip', async () => {
+ const originalDbml = `
+Table "guilds"."members" {
+ "id" uuid [pk]
+ "name" varchar(100) [not null]
+ "class_specializations" varchar(50)[]
+ "completed_quest_ids" integer[]
+ "skill_ratings" decimal(3, 1)[]
+ "titles_earned" text[]
+}
+`;
+
+ // Import
+ const diagram = await importDBMLToDiagram(originalDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Export
+ const { standardDbml: exportedDbml } =
+ generateDBMLFromDiagram(diagram);
+
+ // Verify exported DBML has correct array syntax with types
+ expect(exportedDbml).toContain('varchar(50)[]');
+ expect(exportedDbml).toContain('int[]');
+ expect(exportedDbml).toContain('numeric(3,1)[]');
+ expect(exportedDbml).toContain('text[]');
+
+ // Re-import
+ const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const table = reimportedDiagram.tables![0];
+
+ const classSpecs = table.fields.find(
+ (f) => f.name === 'class_specializations'
+ );
+ expect(classSpecs?.isArray).toBe(true);
+ expect(classSpecs?.characterMaximumLength).toBe('50');
+
+ const questIds = table.fields.find(
+ (f) => f.name === 'completed_quest_ids'
+ );
+ expect(questIds?.isArray).toBe(true);
+
+ const skillRatings = table.fields.find(
+ (f) => f.name === 'skill_ratings'
+ );
+ expect(skillRatings?.isArray).toBe(true);
+ expect(skillRatings?.precision).toBe(3);
+ expect(skillRatings?.scale).toBe(1);
+
+ const titles = table.fields.find((f) => f.name === 'titles_earned');
+ expect(titles?.isArray).toBe(true);
+ });
+
+ it('should preserve dungeon loot tables with mixed array and non-array fields', async () => {
+ const originalDbml = `
+Table "dungeons"."loot_tables" {
+ "id" bigint [pk]
+ "dungeon_name" varchar(150) [not null]
+ "boss_name" varchar(100)
+ "common_drops" text[]
+ "rare_drops" text[]
+ "legendary_drops" text[]
+ "gold_range_min" integer [not null]
+ "gold_range_max" integer [not null]
+ "drop_rates" decimal(5, 2)[]
+}
+`;
+
+ // Import, export, and re-import
+ const diagram = await importDBMLToDiagram(originalDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const { standardDbml: exportedDbml } =
+ generateDBMLFromDiagram(diagram);
+
+ const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const table = reimportedDiagram.tables![0];
+
+ // Verify non-array fields
+ expect(
+ table.fields.find((f) => f.name === 'id')?.isArray
+ ).toBeUndefined();
+ expect(
+ table.fields.find((f) => f.name === 'dungeon_name')?.isArray
+ ).toBeUndefined();
+ expect(
+ table.fields.find((f) => f.name === 'gold_range_min')?.isArray
+ ).toBeUndefined();
+
+ // Verify array fields
+ expect(
+ table.fields.find((f) => f.name === 'common_drops')?.isArray
+ ).toBe(true);
+ expect(
+ table.fields.find((f) => f.name === 'rare_drops')?.isArray
+ ).toBe(true);
+ expect(
+ table.fields.find((f) => f.name === 'legendary_drops')?.isArray
+ ).toBe(true);
+ expect(
+ table.fields.find((f) => f.name === 'drop_rates')?.isArray
+ ).toBe(true);
+ });
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-cardinality-import.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-cardinality-import.test.ts
new file mode 100644
index 000000000..7f5b1d008
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-cardinality-import.test.ts
@@ -0,0 +1,389 @@
+import { describe, it, expect } from 'vitest';
+import { importDBMLToDiagram } from '../dbml-import';
+import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('DBML Cardinality Import', () => {
+ describe('Inline ref cardinality symbols', () => {
+ it('should import many-to-one relationship (ref: >)', async () => {
+ // ref: > means "I (FK) reference other (PK)" - many-to-one
+ // Parser returns: [referenced_table (one), table_with_ref (many)]
+ const dbml = `
+Table "orders" {
+ "id" int [pk]
+ "customer_id" int [ref: > "customers"."id"]
+}
+
+Table "customers" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // source = customers.id (PK/referenced) is one
+ // target = orders.customer_id (FK/referencing) is many
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('many');
+ });
+
+ it('should import one-to-many relationship (ref: <)', async () => {
+ // ref: < means "other (FK) references me (PK)" - one-to-many
+ // Parser returns: [other_table (many), table_with_ref (one)]
+ const dbml = `
+Table "customers" {
+ "id" int [pk, ref: < "orders"."customer_id"]
+ "name" varchar(100)
+}
+
+Table "orders" {
+ "id" int [pk]
+ "customer_id" int
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // source = orders.customer_id (the other/FK field) is many
+ // target = customers.id (the field with ref/PK) is one
+ expect(rel.sourceCardinality).toBe('many');
+ expect(rel.targetCardinality).toBe('one');
+ });
+
+ it('should import one-to-one relationship (ref: -)', async () => {
+ // ref: - means one-to-one relationship
+ const dbml = `
+Table "users" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+
+Table "user_profiles" {
+ "id" int [pk]
+ "user_id" int [unique, ref: - "users"."id"]
+ "bio" text
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // Both sides should be one
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('one');
+ });
+
+ it('should import many-to-many relationship (ref: <>)', async () => {
+ // ref: <> means many-to-many relationship
+ const dbml = `
+Table "students" {
+ "id" int [pk, ref: <> "courses"."id"]
+ "name" varchar(100)
+}
+
+Table "courses" {
+ "id" int [pk]
+ "title" varchar(200)
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // Both sides should be many
+ expect(rel.sourceCardinality).toBe('many');
+ expect(rel.targetCardinality).toBe('many');
+ });
+ });
+
+ describe('Standalone Ref cardinality symbols', () => {
+ it('should import many-to-one with standalone Ref >', async () => {
+ const dbml = `
+Table "orders" {
+ "id" int [pk]
+ "customer_id" int
+}
+
+Table "customers" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+
+Ref: "orders"."customer_id" > "customers"."id"
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // orders.customer_id (source) is many, customers.id (target) is one
+ expect(rel.sourceCardinality).toBe('many');
+ expect(rel.targetCardinality).toBe('one');
+ });
+
+ it('should import one-to-many with standalone Ref <', async () => {
+ const dbml = `
+Table "customers" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+
+Table "orders" {
+ "id" int [pk]
+ "customer_id" int
+}
+
+Ref: "customers"."id" < "orders"."customer_id"
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // customers.id (source) is one, orders.customer_id (target) is many
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('many');
+ });
+
+ it('should import one-to-one with standalone Ref -', async () => {
+ const dbml = `
+Table "users" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+
+Table "profiles" {
+ "id" int [pk]
+ "user_id" int [unique]
+}
+
+Ref: "profiles"."user_id" - "users"."id"
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // Both sides should be one
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('one');
+ });
+
+ it('should import many-to-many with standalone Ref <>', async () => {
+ const dbml = `
+Table "students" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+
+Table "courses" {
+ "id" int [pk]
+ "title" varchar(200)
+}
+
+Ref: "students"."id" <> "courses"."id"
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // Both sides should be many
+ expect(rel.sourceCardinality).toBe('many');
+ expect(rel.targetCardinality).toBe('many');
+ });
+ });
+
+ describe('Round-trip cardinality preservation', () => {
+ it('should preserve many-to-one cardinality through export and re-import', async () => {
+ const inputDbml = `
+Table "posts" {
+ "id" int [pk]
+ "author_id" int [ref: > "authors"."id"]
+ "title" varchar(200)
+}
+
+Table "authors" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+`;
+ // Import
+ const diagram = await importDBMLToDiagram(inputDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Export
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ // Re-import
+ const reimportedDiagram = await importDBMLToDiagram(
+ exportResult.inlineDbml,
+ { databaseType: DatabaseType.POSTGRESQL }
+ );
+
+ // Verify cardinality preserved (source=PK side, target=FK side)
+ const rel = reimportedDiagram.relationships![0];
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('many');
+ });
+
+ it('should preserve one-to-one cardinality through export and re-import', async () => {
+ const inputDbml = `
+Table "users" {
+ "id" int [pk]
+}
+
+Table "settings" {
+ "id" int [pk]
+ "user_id" int [unique, ref: - "users"."id"]
+}
+`;
+ const diagram = await importDBMLToDiagram(inputDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ const reimportedDiagram = await importDBMLToDiagram(
+ exportResult.inlineDbml,
+ { databaseType: DatabaseType.POSTGRESQL }
+ );
+
+ const rel = reimportedDiagram.relationships![0];
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('one');
+ });
+
+ it('should preserve many-to-many cardinality through export and re-import', async () => {
+ const inputDbml = `
+Table "tags" {
+ "id" int [pk, ref: <> "articles"."id"]
+ "name" varchar(50)
+}
+
+Table "articles" {
+ "id" int [pk]
+ "title" varchar(200)
+}
+`;
+ const diagram = await importDBMLToDiagram(inputDbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const exportResult = generateDBMLFromDiagram(diagram);
+
+ const reimportedDiagram = await importDBMLToDiagram(
+ exportResult.inlineDbml,
+ { databaseType: DatabaseType.POSTGRESQL }
+ );
+
+ const rel = reimportedDiagram.relationships![0];
+ expect(rel.sourceCardinality).toBe('many');
+ expect(rel.targetCardinality).toBe('many');
+ });
+ });
+
+ describe('Complex cardinality scenarios', () => {
+ it('should handle multiple relationships with different cardinalities', async () => {
+ const dbml = `
+Table "comments" {
+ "id" int [pk]
+ "post_id" int [ref: > "posts"."id"]
+ "user_id" int [ref: > "users"."id"]
+ "parent_id" int [ref: > "comments"."id"]
+ "content" text
+}
+
+Table "posts" {
+ "id" int [pk]
+ "title" varchar(200)
+}
+
+Table "users" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(3);
+
+ // All should be one-to-many from source perspective (source=PK, target=FK)
+ diagram.relationships?.forEach((rel) => {
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('many');
+ });
+ });
+
+ it('should handle self-referencing with correct cardinality', async () => {
+ const dbml = `
+Table "employees" {
+ "id" int [pk]
+ "name" varchar(100)
+ "manager_id" int [ref: > "employees"."id"]
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // Self-referencing: source=id (one), target=manager_id (many)
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('many');
+ expect(rel.sourceTableId).toBe(rel.targetTableId);
+ });
+
+ it('should handle schema-qualified tables with cardinality', async () => {
+ const dbml = `
+Table "sales"."orders" {
+ "id" int [pk]
+ "customer_id" int [ref: > "crm"."customers"."id"]
+}
+
+Table "crm"."customers" {
+ "id" int [pk]
+ "name" varchar(100)
+}
+`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.relationships?.length).toBe(1);
+ const rel = diagram.relationships![0];
+
+ // source=customers.id (one), target=orders.customer_id (many)
+ expect(rel.sourceCardinality).toBe('one');
+ expect(rel.targetCardinality).toBe('many');
+ });
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-check-constraints-validation.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-check-constraints-validation.test.ts
new file mode 100644
index 000000000..0f57ec07f
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-check-constraints-validation.test.ts
@@ -0,0 +1,318 @@
+import { describe, it, expect } from 'vitest';
+import { preprocessDBML, importDBMLToDiagram } from '../dbml-import';
+import { DBMLValidationError } from '../dbml-import-error';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('DBML Check Constraints Validation', () => {
+ describe('preprocessDBML - table-level check constraints', () => {
+ it('should accept valid check constraint expressions', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+
+ checks {
+ \`age >= 0\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ const { tableChecks } = preprocessDBML(dbml);
+ expect(tableChecks.get('users')).toHaveLength(1);
+ expect(tableChecks.get('users')![0].expression).toBe('age >= 0');
+ });
+
+ it('should accept multiple valid check constraints', () => {
+ const dbml = `
+Table products {
+ id int [pk]
+ price decimal
+ quantity int
+
+ checks {
+ \`price > 0\`
+ \`quantity >= 0\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ const { tableChecks } = preprocessDBML(dbml);
+ expect(tableChecks.get('products')).toHaveLength(2);
+ });
+
+ it('should accept complex check constraint expressions', () => {
+ const dbml = `
+Table orders {
+ id int [pk]
+ status varchar
+
+ checks {
+ \`status IN ('pending', 'completed', 'cancelled')\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should accept BETWEEN expressions', () => {
+ const dbml = `
+Table ratings {
+ id int [pk]
+ score int
+
+ checks {
+ \`score BETWEEN 1 AND 5\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should reject incomplete expressions - missing right operand', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+
+ checks {
+ \`age >\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).toThrow(DBMLValidationError);
+ try {
+ preprocessDBML(dbml);
+ } catch (e) {
+ expect(e).toBeInstanceOf(DBMLValidationError);
+ const error = e as DBMLValidationError;
+ expect(error.message).toContain('Invalid check constraint');
+ expect(error.message).toContain('incomplete');
+ }
+ });
+
+ it('should reject unbalanced parentheses', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ status varchar
+
+ checks {
+ \`(status = 'active'\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).toThrow(DBMLValidationError);
+ try {
+ preprocessDBML(dbml);
+ } catch (e) {
+ expect(e).toBeInstanceOf(DBMLValidationError);
+ const error = e as DBMLValidationError;
+ expect(error.message).toContain('parenthes');
+ }
+ });
+
+ it('should reject expressions starting with operator', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+
+ checks {
+ \`> 0\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).toThrow(DBMLValidationError);
+ });
+
+ it('should provide line number in error', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+
+ checks {
+ \`age >\`
+ }
+}`;
+ try {
+ preprocessDBML(dbml);
+ expect.fail('Should have thrown');
+ } catch (e) {
+ expect(e).toBeInstanceOf(DBMLValidationError);
+ const error = e as DBMLValidationError;
+ expect(error.dbmlError.line).toBeGreaterThan(0);
+ }
+ });
+ });
+
+ describe('preprocessDBML - field-level check constraints', () => {
+ it('should accept valid field-level check constraint', () => {
+ const dbml = `
+Table products {
+ id int [pk]
+ price decimal [not null, check: \`price > 0\`]
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ const { fieldChecks } = preprocessDBML(dbml);
+ expect(fieldChecks.get('products')?.get('price')?.expression).toBe(
+ 'price > 0'
+ );
+ });
+
+ it('should reject invalid field-level check constraint', () => {
+ const dbml = `
+Table products {
+ id int [pk]
+ price decimal [not null, check: \`price >\`]
+}`;
+ expect(() => preprocessDBML(dbml)).toThrow(DBMLValidationError);
+ try {
+ preprocessDBML(dbml);
+ } catch (e) {
+ expect(e).toBeInstanceOf(DBMLValidationError);
+ const error = e as DBMLValidationError;
+ expect(error.message).toContain('Invalid check constraint');
+ expect(error.message).toContain('price');
+ }
+ });
+ });
+
+ describe('importDBMLToDiagram - check constraint validation', () => {
+ it('should successfully import valid check constraints', async () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+ status varchar
+
+ checks {
+ \`age >= 0 AND age <= 150\`
+ \`status IN ('active', 'inactive')\`
+ }
+}`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.tables).toHaveLength(1);
+ const table = diagram.tables![0];
+ expect(table.checkConstraints).toHaveLength(2);
+ expect(table.checkConstraints![0].expression).toBe(
+ 'age >= 0 AND age <= 150'
+ );
+ expect(table.checkConstraints![1].expression).toBe(
+ "status IN ('active', 'inactive')"
+ );
+ });
+
+ it('should throw validation error for invalid check constraint during import', async () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+
+ checks {
+ \`age <\`
+ }
+}`;
+ await expect(
+ importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ })
+ ).rejects.toThrow(DBMLValidationError);
+ });
+
+ it('should import named check constraints', async () => {
+ const dbml = `
+Table products {
+ id int [pk]
+ price decimal
+
+ checks {
+ \`price > 0\` [name: 'positive_price']
+ }
+}`;
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const table = diagram.tables![0];
+ expect(table.checkConstraints).toHaveLength(1);
+ expect(table.checkConstraints![0].expression).toBe('price > 0');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle check constraints with quoted identifiers', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ "user name" varchar
+
+ checks {
+ \`"user name" IS NOT NULL\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should handle check constraints with SQL Server bracket notation', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ age int
+
+ checks {
+ \`[age] >= 0 AND [age] <= 100\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should handle check constraints with function calls', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ email varchar
+
+ checks {
+ \`LENGTH(email) > 0\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should handle NOT LIKE expressions', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ code varchar
+
+ checks {
+ \`code NOT LIKE 'TEST%'\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should handle NOT IN expressions', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ status varchar
+
+ checks {
+ \`status NOT IN ('deleted', 'banned')\`
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ });
+
+ it('should handle empty checks block gracefully', () => {
+ const dbml = `
+Table users {
+ id int [pk]
+
+ checks {
+ }
+}`;
+ expect(() => preprocessDBML(dbml)).not.toThrow();
+ const { tableChecks } = preprocessDBML(dbml);
+ expect(tableChecks.has('users')).toBe(false);
+ });
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts
new file mode 100644
index 000000000..44bd2f630
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts
@@ -0,0 +1,426 @@
+import { describe, it, expect } from 'vitest';
+import { importDBMLToDiagram } from '../dbml-import';
+import * as fs from 'fs';
+import * as path from 'path';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+import type { DBRelationship } from '@/lib/domain/db-relationship';
+import { defaultSchemas } from '@/lib/data/default-schemas';
+
+// Type for field map entries
+interface FieldMapEntry {
+ tableName: string;
+ fieldName: string;
+}
+
+// Helper function to compare field properties (excluding IDs and timestamps)
+function expectFieldsMatch(
+ actualFields: DBField[],
+ expectedFields: DBField[]
+): void {
+ expect(actualFields).toHaveLength(expectedFields.length);
+
+ for (let i = 0; i < actualFields.length; i++) {
+ const actual = actualFields[i];
+ const expected = expectedFields[i];
+
+ // Compare field properties (excluding ID and createdAt)
+ expect(actual.name).toBe(expected.name);
+
+ // Handle type comparison (could be string or object with name property)
+ if (typeof expected.type === 'object' && expected.type?.name) {
+ expect(actual.type?.name).toBe(expected.type.name);
+ } else if (typeof expected.type === 'string') {
+ expect(actual.type?.name).toBe(expected.type);
+ }
+
+ // Boolean flags with defaults
+ expect(actual.primaryKey).toBe(expected.primaryKey || false);
+ expect(actual.unique).toBe(expected.unique || false);
+ expect(actual.nullable).toBe(expected.nullable ?? true);
+
+ // Optional boolean flag
+ if (expected.increment !== undefined) {
+ expect(actual.increment).toBe(expected.increment);
+ }
+
+ // Optional string/number properties
+ if (expected.characterMaximumLength !== undefined) {
+ expect(actual.characterMaximumLength).toBe(
+ expected.characterMaximumLength
+ );
+ }
+
+ if (expected.precision !== undefined) {
+ expect(actual.precision).toBe(expected.precision);
+ }
+
+ if (expected.scale !== undefined) {
+ expect(actual.scale).toBe(expected.scale);
+ }
+
+ if (expected.default !== undefined) {
+ expect(actual.default).toBe(expected.default);
+ }
+
+ if (expected.collation !== undefined) {
+ expect(actual.collation).toBe(expected.collation);
+ }
+
+ if (expected.comments !== undefined) {
+ expect(actual.comments).toBe(expected.comments);
+ }
+ }
+}
+
+// Helper function to compare table properties (excluding IDs)
+function expectTablesMatch(
+ actualTables: DBTable[],
+ expectedTables: DBTable[],
+ databaseType: DatabaseType
+): void {
+ expect(actualTables).toHaveLength(expectedTables.length);
+
+ // Sort tables by name for consistent comparison
+ const sortedActual = [...actualTables].sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+ const sortedExpected = [...expectedTables].sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+
+ for (let i = 0; i < sortedActual.length; i++) {
+ const actual = sortedActual[i];
+ const expected = sortedExpected[i];
+
+ // Compare table properties (excluding ID and position)
+ expect(actual.name).toBe(expected.name);
+
+ // Schema comparison - handle differences in how schemas are represented
+ if (expected.schema) {
+ const defaultSchema = defaultSchemas[databaseType];
+ if (defaultSchema && expected.schema === defaultSchema) {
+ // DBML parser might not include default schema or might handle it differently
+ expect(
+ actual.schema === expected.schema ||
+ actual.schema === '' ||
+ actual.schema === undefined
+ ).toBeTruthy();
+ } else {
+ expect(actual.schema).toBe(expected.schema);
+ }
+ }
+
+ // Compare fields
+ expectFieldsMatch(actual.fields, expected.fields);
+
+ // Check indexes exist for tables with primary keys
+ const hasPrimaryKeyField = actual.fields.some((f) => f.primaryKey);
+ if (hasPrimaryKeyField) {
+ expect(actual.indexes).toBeDefined();
+ expect(actual.indexes.length).toBeGreaterThan(0);
+
+ const pkIndex = actual.indexes.find((idx) => idx.isPrimaryKey);
+ expect(pkIndex).toBeDefined();
+ expect(pkIndex?.unique).toBe(true);
+ }
+
+ // Check comments if present
+ if (expected.comments !== undefined) {
+ expect(actual.comments).toBe(expected.comments);
+ }
+ }
+}
+
+// Helper function to compare relationships (excluding IDs)
+function expectRelationshipsMatch(
+ actualRelationships: DBRelationship[],
+ expectedRelationships: DBRelationship[],
+ actualTables: DBTable[],
+ expectedTables: DBTable[]
+): void {
+ expect(actualRelationships).toHaveLength(expectedRelationships.length);
+
+ // Create lookup maps for table and field names by ID
+ const expectedTableMap = new Map(expectedTables.map((t) => [t.id, t.name]));
+ const actualTableMap = new Map(actualTables.map((t) => [t.id, t.name]));
+
+ const expectedFieldMap = new Map();
+ const actualFieldMap = new Map();
+
+ expectedTables.forEach((table) => {
+ table.fields.forEach((field) => {
+ expectedFieldMap.set(field.id, {
+ tableName: table.name,
+ fieldName: field.name,
+ });
+ });
+ });
+
+ actualTables.forEach((table) => {
+ table.fields.forEach((field) => {
+ actualFieldMap.set(field.id, {
+ tableName: table.name,
+ fieldName: field.name,
+ });
+ });
+ });
+
+ // Sort relationships for consistent comparison
+ const sortRelationships = (
+ rels: DBRelationship[],
+ tableMap: Map,
+ fieldMap: Map
+ ) => {
+ return [...rels].sort((a, b) => {
+ const aSourceTable = tableMap.get(a.sourceTableId) || '';
+ const bSourceTable = tableMap.get(b.sourceTableId) || '';
+ const aTargetTable = tableMap.get(a.targetTableId) || '';
+ const bTargetTable = tableMap.get(b.targetTableId) || '';
+
+ const tableCompare =
+ aSourceTable.localeCompare(bSourceTable) ||
+ aTargetTable.localeCompare(bTargetTable);
+ if (tableCompare !== 0) return tableCompare;
+
+ const aSourceField = fieldMap.get(a.sourceFieldId)?.fieldName || '';
+ const bSourceField = fieldMap.get(b.sourceFieldId)?.fieldName || '';
+ const aTargetField = fieldMap.get(a.targetFieldId)?.fieldName || '';
+ const bTargetField = fieldMap.get(b.targetFieldId)?.fieldName || '';
+
+ return (
+ aSourceField.localeCompare(bSourceField) ||
+ aTargetField.localeCompare(bTargetField)
+ );
+ });
+ };
+
+ const sortedActual = sortRelationships(
+ actualRelationships,
+ actualTableMap,
+ actualFieldMap
+ );
+ const sortedExpected = sortRelationships(
+ expectedRelationships,
+ expectedTableMap,
+ expectedFieldMap
+ );
+
+ for (let i = 0; i < sortedActual.length; i++) {
+ const actual = sortedActual[i];
+ const expected = sortedExpected[i];
+
+ // Get table and field names for comparison
+ const actualSourceTable = actualTableMap.get(actual.sourceTableId);
+ const actualTargetTable = actualTableMap.get(actual.targetTableId);
+ const expectedSourceTable = expectedTableMap.get(
+ expected.sourceTableId
+ );
+ const expectedTargetTable = expectedTableMap.get(
+ expected.targetTableId
+ );
+
+ const actualSourceField = actualFieldMap.get(actual.sourceFieldId);
+ const actualTargetField = actualFieldMap.get(actual.targetFieldId);
+ const expectedSourceField = expectedFieldMap.get(
+ expected.sourceFieldId
+ );
+ const expectedTargetField = expectedFieldMap.get(
+ expected.targetFieldId
+ );
+
+ // Compare relationship by table and field names
+ expect(actualSourceTable).toBe(expectedSourceTable);
+ expect(actualTargetTable).toBe(expectedTargetTable);
+ expect(actualSourceField?.fieldName).toBe(
+ expectedSourceField?.fieldName
+ );
+ expect(actualTargetField?.fieldName).toBe(
+ expectedTargetField?.fieldName
+ );
+
+ // Compare cardinality
+ expect(actual.sourceCardinality).toBe(expected.sourceCardinality);
+ expect(actual.targetCardinality).toBe(expected.targetCardinality);
+
+ // Compare relationship name if present
+ if (expected.name !== undefined) {
+ expect(actual.name).toBe(expected.name);
+ }
+ }
+}
+
+// Main test helper function
+async function testDBMLImportCase(caseNumber: string): Promise {
+ // Read the DBML file
+ const dbmlPath = path.join(__dirname, 'cases', `${caseNumber}.dbml`);
+ const dbmlContent = fs.readFileSync(dbmlPath, 'utf-8');
+
+ // Read the expected JSON file
+ const jsonPath = path.join(__dirname, 'cases', `${caseNumber}.json`);
+ const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
+ const expectedData = JSON.parse(jsonContent);
+
+ // Import DBML to diagram
+ const result = await importDBMLToDiagram(dbmlContent, {
+ databaseType: expectedData.databaseType || DatabaseType.POSTGRESQL,
+ });
+
+ // Check basic diagram properties
+ expect(result.name).toBe('DBML Import'); // Name is always 'DBML Import'
+ expect(result.databaseType).toBe(expectedData.databaseType);
+
+ // Check tables and fields
+ expectTablesMatch(
+ result.tables || [],
+ expectedData.tables || [],
+ expectedData.databaseType || DatabaseType.POSTGRESQL
+ );
+
+ // Check relationships
+ expectRelationshipsMatch(
+ result.relationships || [],
+ expectedData.relationships || [],
+ result.tables || [],
+ expectedData.tables || []
+ );
+}
+
+describe('DBML Import cases', () => {
+ it('should handle case 1 - simple table with pk and unique', async () => {
+ await testDBMLImportCase('1');
+ });
+
+ it('should handle case 2 - tables with relationships', async () => {
+ await testDBMLImportCase('2');
+ });
+
+ it('should handle table with default values', async () => {
+ const dbmlContent = `Table "public"."products" {
+ "id" bigint [pk, not null]
+ "name" varchar(255) [not null]
+ "price" decimal(10,2) [not null, default: 0]
+ "is_active" boolean [not null, default: true]
+ "status" varchar(50) [not null, default: "deprecated"]
+ "description" varchar(100) [default: \`complex "value" with quotes\`]
+ "created_at" timestamp [not null, default: "now()"]
+
+ Indexes {
+ (name) [name: "idx_products_name"]
+ }
+}`;
+
+ const result = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables![0];
+ expect(table.name).toBe('products');
+ expect(table.fields).toHaveLength(7);
+
+ // Check numeric default (0)
+ const priceField = table.fields.find((f) => f.name === 'price');
+ expect(priceField?.default).toBe('0');
+
+ // Check boolean default (true)
+ const isActiveField = table.fields.find((f) => f.name === 'is_active');
+ expect(isActiveField?.default).toBe('true');
+
+ // Check string default with all quotes removed
+ const statusField = table.fields.find((f) => f.name === 'status');
+ expect(statusField?.default).toBe('deprecated');
+
+ // Check backtick string - all quotes removed
+ const descField = table.fields.find((f) => f.name === 'description');
+ expect(descField?.default).toBe('complex value with quotes');
+
+ // Check function default with all quotes removed
+ const createdAtField = table.fields.find(
+ (f) => f.name === 'created_at'
+ );
+ expect(createdAtField?.default).toBe('now()');
+ });
+
+ it('should handle auto-increment fields correctly', async () => {
+ const dbmlContent = `Table "public"."table_1" {
+ "id" integer [pk, not null, increment]
+ "field_2" bigint [increment]
+ "field_3" serial [increment]
+ "field_4" varchar(100) [not null]
+}`;
+
+ const result = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables![0];
+ expect(table.name).toBe('table_1');
+ expect(table.fields).toHaveLength(4);
+
+ // field with [pk, not null, increment] - should be not null and increment
+ const idField = table.fields.find((f) => f.name === 'id');
+ expect(idField?.increment).toBe(true);
+ expect(idField?.nullable).toBe(false);
+ expect(idField?.primaryKey).toBe(true);
+
+ // field with [increment] only - should be not null and increment
+ // (auto-increment requires NOT NULL even if not explicitly stated)
+ const field2 = table.fields.find((f) => f.name === 'field_2');
+ expect(field2?.increment).toBe(true);
+ expect(field2?.nullable).toBe(false); // CRITICAL: must be false!
+
+ // SERIAL type with [increment] - should be not null and increment
+ const field3 = table.fields.find((f) => f.name === 'field_3');
+ expect(field3?.increment).toBe(true);
+ expect(field3?.nullable).toBe(false);
+ expect(field3?.type?.name).toBe('serial');
+
+ // Regular field with [not null] - should be not null, no increment
+ const field4 = table.fields.find((f) => f.name === 'field_4');
+ expect(field4?.increment).toBeUndefined();
+ expect(field4?.nullable).toBe(false);
+ });
+
+ it('should handle SERIAL types without increment attribute', async () => {
+ const dbmlContent = `Table "public"."test_table" {
+ "id" serial [pk]
+ "counter" bigserial
+ "small_counter" smallserial
+ "regular" integer
+}`;
+
+ const result = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(result.tables).toHaveLength(1);
+ const table = result.tables![0];
+ expect(table.fields).toHaveLength(4);
+
+ // SERIAL type without [increment] - should STILL be not null (type requires it)
+ const idField = table.fields.find((f) => f.name === 'id');
+ expect(idField?.type?.name).toBe('serial');
+ expect(idField?.nullable).toBe(false); // CRITICAL: Type requires NOT NULL
+ expect(idField?.primaryKey).toBe(true);
+
+ // BIGSERIAL without [increment] - should be not null
+ const counterField = table.fields.find((f) => f.name === 'counter');
+ expect(counterField?.type?.name).toBe('bigserial');
+ expect(counterField?.nullable).toBe(false); // CRITICAL: Type requires NOT NULL
+
+ // SMALLSERIAL without [increment] - should be not null
+ const smallCounterField = table.fields.find(
+ (f) => f.name === 'small_counter'
+ );
+ expect(smallCounterField?.type?.name).toBe('smallserial');
+ expect(smallCounterField?.nullable).toBe(false); // CRITICAL: Type requires NOT NULL
+
+ // Regular INTEGER - should be nullable by default
+ const regularField = table.fields.find((f) => f.name === 'regular');
+ expect(regularField?.type?.name).toBe('int');
+ expect(regularField?.nullable).toBe(true); // No NOT NULL constraint
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts
index c81be0a18..13fe91bcc 100644
--- a/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
+import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Import - Fantasy Examples', () => {
describe('Magical Academy System', () => {
@@ -149,7 +150,9 @@ Table ranks {
max_spell_level integer [not null]
}`;
- const diagram = await importDBMLToDiagram(magicalAcademyDBML);
+ const diagram = await importDBMLToDiagram(magicalAcademyDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify tables
expect(diagram.tables).toHaveLength(8);
@@ -366,7 +369,9 @@ Note marketplace_note {
'This marketplace handles both standard purchases and barter trades'
}`;
- const diagram = await importDBMLToDiagram(marketplaceDBML);
+ const diagram = await importDBMLToDiagram(marketplaceDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify tables
expect(diagram.tables).toHaveLength(7);
@@ -567,7 +572,9 @@ Note quest_system_note {
'Quest difficulty and status use enums that will be converted to varchar'
}`;
- const diagram = await importDBMLToDiagram(questSystemDBML);
+ const diagram = await importDBMLToDiagram(questSystemDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify tables
expect(diagram.tables).toHaveLength(7);
@@ -657,15 +664,17 @@ Table projects {
priority enum // inline enum without values - will be converted to varchar
}`;
- const diagram = await importDBMLToDiagram(dbmlWithEnums);
+ const diagram = await importDBMLToDiagram(dbmlWithEnums, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify customTypes are created for enums
expect(diagram.customTypes).toBeDefined();
expect(diagram.customTypes).toHaveLength(3); // job_status, hr.employee_type, grade
- // Check job_status enum
+ // Check job_status enum (PostgreSQL default schema is 'public')
const jobStatusEnum = diagram.customTypes?.find(
- (ct) => ct.name === 'job_status' && !ct.schema
+ (ct) => ct.name === 'job_status' && ct.schema === 'public'
);
expect(jobStatusEnum).toBeDefined();
expect(jobStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
@@ -689,9 +698,9 @@ Table projects {
'intern',
]);
- // Check grade enum with quoted values
+ // Check grade enum with quoted values (PostgreSQL default schema is 'public')
const gradeEnum = diagram.customTypes?.find(
- (ct) => ct.name === 'grade' && !ct.schema
+ (ct) => ct.name === 'grade' && ct.schema === 'public'
);
expect(gradeEnum).toBeDefined();
expect(gradeEnum?.kind).toBe(DBCustomTypeKind.enum);
@@ -744,7 +753,9 @@ Table orders {
status order_status [not null]
}`;
- const diagram = await importDBMLToDiagram(dbmlWithEnumNotes);
+ const diagram = await importDBMLToDiagram(dbmlWithEnumNotes, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify enum is created
expect(diagram.customTypes).toHaveLength(1);
@@ -788,14 +799,16 @@ Table admin.users {
status admin.status
}`;
- const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames);
+ const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify both enums are created
expect(diagram.customTypes).toHaveLength(2);
- // Check public.status enum
+ // Check public.status enum (PostgreSQL default schema is 'public')
const publicStatusEnum = diagram.customTypes?.find(
- (ct) => ct.name === 'status' && !ct.schema
+ (ct) => ct.name === 'status' && ct.schema === 'public'
);
expect(publicStatusEnum).toBeDefined();
expect(publicStatusEnum?.values).toEqual([
@@ -817,9 +830,9 @@ Table admin.users {
]);
// Verify fields reference correct enums
- // Note: 'public' schema is converted to empty string
+ // Note: 'public' schema is the default for PostgreSQL
const publicUsersTable = diagram.tables?.find(
- (t) => t.name === 'users' && t.schema === ''
+ (t) => t.name === 'users' && t.schema === 'public'
);
const adminUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'admin'
@@ -891,7 +904,9 @@ Note dragon_note {
'Dragons are very protective of their hoards!'
}`;
- const diagram = await importDBMLToDiagram(edgeCaseDBML);
+ const diagram = await importDBMLToDiagram(edgeCaseDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify preprocessing worked
expect(diagram.tables).toHaveLength(2);
@@ -956,7 +971,9 @@ Note dragon_note {
it('should handle empty DBML gracefully', async () => {
const emptyDBML = '';
- const diagram = await importDBMLToDiagram(emptyDBML);
+ const diagram = await importDBMLToDiagram(emptyDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(0);
expect(diagram.relationships).toHaveLength(0);
@@ -969,7 +986,9 @@ Note dragon_note {
/* Multi-line
comment */
`;
- const diagram = await importDBMLToDiagram(commentOnlyDBML);
+ const diagram = await importDBMLToDiagram(commentOnlyDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(0);
expect(diagram.relationships).toHaveLength(0);
@@ -980,7 +999,9 @@ Note dragon_note {
Table empty_table {
id int
}`;
- const diagram = await importDBMLToDiagram(minimalDBML);
+ const diagram = await importDBMLToDiagram(minimalDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(1);
expect(diagram.tables?.[0]?.fields).toHaveLength(1);
@@ -996,7 +1017,9 @@ Table "aa"."users" {
Table "bb"."users" {
id integer [primary key]
}`;
- const diagram = await importDBMLToDiagram(dbml);
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(2);
@@ -1017,11 +1040,11 @@ Table "bb"."users" {
expect(bbUsersTable?.fields).toHaveLength(1);
expect(aaUsersTable?.fields[0].name).toBe('id');
- expect(aaUsersTable?.fields[0].type.id).toBe('integer');
+ expect(aaUsersTable?.fields[0].type.id).toBe('int');
expect(aaUsersTable?.fields[0].primaryKey).toBe(true);
expect(bbUsersTable?.fields[0].name).toBe('id');
- expect(bbUsersTable?.fields[0].type.id).toBe('integer');
+ expect(bbUsersTable?.fields[0].type.id).toBe('int');
expect(bbUsersTable?.fields[0].primaryKey).toBe(true);
});
@@ -1051,7 +1074,7 @@ Table "public_2"."posts" {
"id" varchar(500) [pk]
"title" varchar(500)
"content" text
- "user_id" varchar(500) [ref: < "public"."users"."id"]
+ "user_id" varchar(500) [ref: > "public"."users"."id"]
"created_at" timestamp
Indexes {
@@ -1063,22 +1086,24 @@ Table "public_2"."posts" {
Table "public_3"."comments" {
"id" varchar(500) [pk]
"content" text
- "post_id" varchar(500) [ref: < "public_2"."posts"."id"]
- "user_id" varchar(500) [ref: < "public"."users"."id"]
+ "post_id" varchar(500) [ref: > "public_2"."posts"."id"]
+ "user_id" varchar(500) [ref: > "public"."users"."id"]
"created_at" timestamp
Indexes {
id [unique, name: "public_3_index_1"]
}
}`;
- const diagram = await importDBMLToDiagram(dbml);
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
// Verify tables
expect(diagram.tables).toHaveLength(3);
// Note: 'public' schema is converted to empty string
const usersTable = diagram.tables?.find(
- (t) => t.name === 'users' && t.schema === ''
+ (t) => t.name === 'users' && t.schema === 'public'
);
const postsTable = diagram.tables?.find(
(t) => t.name === 'posts' && t.schema === 'public_2'
@@ -1256,7 +1281,9 @@ Table products {
Note: 'This table stores product information'
}`;
- const diagram = await importDBMLToDiagram(dbmlWithTableNote);
+ const diagram = await importDBMLToDiagram(dbmlWithTableNote, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(1);
const productsTable = diagram.tables?.[0];
@@ -1273,7 +1300,9 @@ Table orders {
total numeric(10,2) [note: 'Order total including tax']
}`;
- const diagram = await importDBMLToDiagram(dbmlWithFieldNote);
+ const diagram = await importDBMLToDiagram(dbmlWithFieldNote, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(1);
const ordersTable = diagram.tables?.[0];
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts
index 694853b86..220505b2c 100644
--- a/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts
@@ -1,10 +1,12 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import {
preprocessDBML,
sanitizeDBML,
importDBMLToDiagram,
} from '../dbml-import';
import { Parser } from '@dbml/core';
+import { DatabaseType } from '@/lib/domain/database-type';
+import * as dataTypes from '@/lib/data/data-types/data-types';
describe('DBML Import', () => {
describe('preprocessDBML', () => {
@@ -22,7 +24,7 @@ TableGroup "Test Group" [color: #CA4243] {
Table posts {
id int
}`;
- const result = preprocessDBML(dbml);
+ const { content: result } = preprocessDBML(dbml);
expect(result).not.toContain('TableGroup');
expect(result).toContain('Table users');
expect(result).toContain('Table posts');
@@ -37,20 +39,20 @@ Table users {
Note note_test {
'This is a note'
}`;
- const result = preprocessDBML(dbml);
+ const { content: result } = preprocessDBML(dbml);
expect(result).not.toContain('Note');
expect(result).toContain('Table users');
});
- it('should convert array types to text', () => {
+ it('should remove array syntax while preserving base type', () => {
const dbml = `
Table users {
tags text[]
domains varchar[]
}`;
- const result = preprocessDBML(dbml);
+ const { content: result } = preprocessDBML(dbml);
expect(result).toContain('tags text');
- expect(result).toContain('domains text');
+ expect(result).toContain('domains varchar');
expect(result).not.toContain('[]');
});
@@ -60,7 +62,7 @@ Table users {
status enum
verification_type enum // comment here
}`;
- const result = preprocessDBML(dbml);
+ const { content: result } = preprocessDBML(dbml);
expect(result).toContain('status varchar');
expect(result).toContain('verification_type varchar');
expect(result).not.toContain('enum');
@@ -71,7 +73,7 @@ Table users {
Table users [headercolor: #24BAB1] {
id int
}`;
- const result = preprocessDBML(dbml);
+ const { content: result } = preprocessDBML(dbml);
expect(result).toContain('Table users {');
expect(result).not.toContain('headercolor');
});
@@ -105,7 +107,9 @@ Note note_test {
'This is a test note'
}`;
- const diagram = await importDBMLToDiagram(complexDBML);
+ const diagram = await importDBMLToDiagram(complexDBML, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
expect(diagram.tables).toHaveLength(2);
expect(diagram.relationships).toHaveLength(1);
@@ -149,7 +153,7 @@ Note note_1750185617764 {
}`;
// Test that preprocessing handles all issues
- const preprocessed = preprocessDBML(problematicDBML);
+ const { content: preprocessed } = preprocessDBML(problematicDBML);
const sanitized = sanitizeDBML(preprocessed);
// Should not throw
@@ -174,4 +178,126 @@ Note note_1750185617764 {
expect(result).toContain('нужна таблица справочник?');
});
});
+
+ describe('Type Synonym Resolution', () => {
+ it('should call getPreferredSynonym for PostgreSQL types and use resolved types', async () => {
+ // Spy on getPreferredSynonym
+ const getPreferredSynonymSpy = vi.spyOn(
+ dataTypes,
+ 'getPreferredSynonym'
+ );
+
+ // Mock return value for 'character varying' -> 'varchar'
+ getPreferredSynonymSpy.mockImplementation(
+ (typeName, databaseType) => {
+ if (
+ typeName === 'character varying' &&
+ databaseType === DatabaseType.POSTGRESQL
+ ) {
+ return {
+ id: 'varchar',
+ name: 'varchar',
+ fieldAttributes: { hasCharMaxLength: true },
+ usageLevel: 1,
+ } as const;
+ }
+ return null;
+ }
+ );
+
+ const dbml = `
+ Table users {
+ id int [pk]
+ name "character varying"(255)
+ email "character varying"(100)
+ }
+ `;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify getPreferredSynonym was called
+ expect(getPreferredSynonymSpy).toHaveBeenCalled();
+ expect(getPreferredSynonymSpy).toHaveBeenCalledWith(
+ 'character varying',
+ DatabaseType.POSTGRESQL
+ );
+
+ // Verify the resolved type was used in the diagram
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const nameField = usersTable?.fields.find((f) => f.name === 'name');
+ expect(nameField?.type.id).toBe('varchar');
+ expect(nameField?.type.name).toBe('varchar');
+
+ const emailField = usersTable?.fields.find(
+ (f) => f.name === 'email'
+ );
+ expect(emailField?.type.id).toBe('varchar');
+ expect(emailField?.type.name).toBe('varchar');
+
+ // Restore the original implementation
+ getPreferredSynonymSpy.mockRestore();
+ });
+ });
+
+ describe('Schema Handling with defaultSchemas', () => {
+ it('should use defaultSchema when table schema is empty for PostgreSQL', async () => {
+ const dbml = `
+ Table users {
+ id int [pk]
+ }
+ `;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.tables?.[0]?.schema).toBe('public');
+ });
+
+ it('should use defaultSchema when table schema is empty for SQL Server', async () => {
+ const dbml = `
+ Table users {
+ id int [pk]
+ }
+ `;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.SQL_SERVER,
+ });
+
+ expect(diagram.tables?.[0]?.schema).toBe('dbo');
+ });
+
+ it('should have undefined schema for database types without defaultSchema', async () => {
+ const dbml = `
+ Table users {
+ id int [pk]
+ }
+ `;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.SQLITE,
+ });
+
+ expect(diagram.tables?.[0]?.schema).toBeUndefined();
+ });
+
+ it('should preserve explicit schema even when different from default', async () => {
+ const dbml = `
+ Table "custom_schema"."users" {
+ id int [pk]
+ }
+ `;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.tables?.[0]?.schema).toBe('custom_schema');
+ });
+ });
});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts
new file mode 100644
index 000000000..aa82b397d
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts
@@ -0,0 +1,159 @@
+import { describe, it, expect } from 'vitest';
+import { DatabaseType } from '@/lib/domain/database-type';
+import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
+
+// This test verifies the DBML integration without UI components
+describe('DBML Integration Tests', () => {
+ it('should handle DBML import in create diagram flow', async () => {
+ const dbmlContent = `
+Table users {
+ id uuid [pk, not null]
+ email varchar [unique, not null]
+ created_at timestamp
+}
+
+Table posts {
+ id uuid [pk]
+ title varchar
+ content text
+ user_id uuid [ref: > users.id]
+ created_at timestamp
+}
+
+Table comments {
+ id uuid [pk]
+ content text
+ post_id uuid [ref: > posts.id]
+ user_id uuid [ref: > users.id]
+}
+
+// This will be ignored
+TableGroup "Content" {
+ posts
+ comments
+}
+
+// This will be ignored too
+Note test_note {
+ 'This is a test note'
+}`;
+
+ const diagram = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify basic structure
+ expect(diagram).toBeDefined();
+ expect(diagram.tables).toHaveLength(3);
+ expect(diagram.relationships).toHaveLength(3);
+
+ // Verify tables
+ const tableNames = diagram.tables?.map((t) => t.name).sort();
+ expect(tableNames).toEqual(['comments', 'posts', 'users']);
+
+ // Verify users table
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+ expect(usersTable?.fields).toHaveLength(3);
+
+ const emailField = usersTable?.fields.find((f) => f.name === 'email');
+ expect(emailField?.unique).toBe(true);
+ expect(emailField?.nullable).toBe(false);
+
+ // Verify relationships
+ // There should be 3 relationships total
+ expect(diagram.relationships).toHaveLength(3);
+
+ // Find the relationship from users to posts (DBML ref is: posts.user_id > users.id)
+ // This creates a relationship FROM users TO posts (one user has many posts)
+ const postsTable = diagram.tables?.find((t) => t.name === 'posts');
+ const usersTableId = usersTable?.id;
+
+ const userPostRelation = diagram.relationships?.find(
+ (r) =>
+ r.sourceTableId === usersTableId &&
+ r.targetTableId === postsTable?.id
+ );
+
+ expect(userPostRelation).toBeDefined();
+ expect(userPostRelation?.sourceCardinality).toBe('one');
+ expect(userPostRelation?.targetCardinality).toBe('many');
+ });
+
+ it('should handle DBML with special features', async () => {
+ const dbmlContent = `
+// Enum will be converted to varchar
+Table users {
+ id int [pk]
+ status enum
+ tags text[] // Array will be converted to text
+ favorite_product_id int
+}
+
+Table products [headercolor: #FF0000] {
+ id int [pk]
+ name varchar
+ price decimal(10,2)
+}
+
+Ref: products.id < users.favorite_product_id`;
+
+ const diagram = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ expect(diagram.tables).toHaveLength(2);
+
+ // Check enum conversion
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ const statusField = usersTable?.fields.find((f) => f.name === 'status');
+ expect(statusField?.type.id).toBe('varchar');
+
+ // Check array type conversion
+ const tagsField = usersTable?.fields.find((f) => f.name === 'tags');
+ expect(tagsField?.type.id).toBe('text');
+
+ // Check that header color was removed
+ const productsTable = diagram.tables?.find(
+ (t) => t.name === 'products'
+ );
+ expect(productsTable).toBeDefined();
+ expect(productsTable?.name).toBe('products');
+ });
+
+ it('should handle empty or invalid DBML gracefully', async () => {
+ // Empty DBML
+ const emptyDiagram = await importDBMLToDiagram('', {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+ expect(emptyDiagram.tables).toHaveLength(0);
+ expect(emptyDiagram.relationships).toHaveLength(0);
+
+ // Only comments
+ const commentDiagram = await importDBMLToDiagram('// Just a comment', {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+ expect(commentDiagram.tables).toHaveLength(0);
+ expect(commentDiagram.relationships).toHaveLength(0);
+ });
+
+ it('should preserve diagram metadata when importing DBML', async () => {
+ const dbmlContent = `Table test {
+ id int [pk]
+}`;
+ const diagram = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.GENERIC,
+ });
+
+ // Default values
+ expect(diagram.name).toBe('DBML Import');
+ expect(diagram.databaseType).toBe(DatabaseType.GENERIC);
+
+ // These can be overridden by the dialog
+ diagram.name = 'My Custom Diagram';
+ diagram.databaseType = DatabaseType.POSTGRESQL;
+
+ expect(diagram.name).toBe('My Custom Diagram');
+ expect(diagram.databaseType).toBe(DatabaseType.POSTGRESQL);
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-multi-schema-relationships.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-multi-schema-relationships.test.ts
new file mode 100644
index 000000000..b38a7fd7a
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-multi-schema-relationships.test.ts
@@ -0,0 +1,245 @@
+import { describe, it, expect } from 'vitest';
+import { importDBMLToDiagram } from '../dbml-import';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('DBML Multi-Schema Relationships', () => {
+ /**
+ * When importing DBML with multiple schemas that have tables with the same name,
+ * relationships must be correctly matched to the table in the specified schema.
+ *
+ * For example, if we have:
+ * - "sales"."products" (schema=sales, name=products)
+ * - "inventory"."products" (schema=inventory, name=products)
+ *
+ * And a relationship references "inventory"."products", it should NOT incorrectly
+ * match to "sales"."products".
+ *
+ * The fix: Use the schemaName property from the DBML parser's endpoint objects
+ * to correctly match tables by both name AND schema.
+ */
+
+ describe('Inline Ref Syntax', () => {
+ it('should correctly match relationship to table in specified schema when same table name exists in multiple schemas', async () => {
+ // This DBML has:
+ // - sales.products (different schema)
+ // - inventory.product_suppliers (references inventory.products)
+ // - inventory.products (same schema as the FK table)
+ //
+ // The inline ref should point to inventory.products, NOT sales.products
+ const dbmlContent = `
+Table "sales"."products" {
+ "id" bigint [pk, not null]
+ "store_id" bigint [not null]
+ "category_id" bigint
+ "metadata" jsonb
+}
+
+Table "inventory"."product_suppliers" {
+ "id" bigint [pk, not null]
+ "product_id" bigint [not null, ref: > "inventory"."products"."id"]
+ "supplier_id" bigint [not null]
+ "unit_cost" bigint
+ "lead_time_days" bigint
+}
+
+Table "inventory"."products" {
+ "id" bigint [pk, not null]
+ "warehouse_id" bigint [not null]
+ "category" varchar(500) [not null]
+ "subcategory" varchar(500) [not null]
+ "sku" varchar(500)
+ "name" varchar(500) [not null]
+ "description" bigint
+ "unit_price" numeric(38,18) [not null, default: 0]
+ "wholesale_price" numeric(38,18) [not null, default: 0]
+ "quantity" bigint [not null, default: 1]
+ "created_at" timestamp [not null]
+ "updated_at" timestamp [not null]
+ "supplier_ref" bigint
+}
+`;
+
+ const diagram = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify tables are imported correctly
+ expect(diagram.tables).toHaveLength(3);
+
+ const salesProducts = diagram.tables?.find(
+ (t) => t.name === 'products' && t.schema === 'sales'
+ );
+ const inventoryProducts = diagram.tables?.find(
+ (t) => t.name === 'products' && t.schema === 'inventory'
+ );
+ const productSuppliers = diagram.tables?.find(
+ (t) =>
+ t.name === 'product_suppliers' && t.schema === 'inventory'
+ );
+
+ expect(salesProducts).toBeDefined();
+ expect(inventoryProducts).toBeDefined();
+ expect(productSuppliers).toBeDefined();
+
+ // Verify the relationship exists
+ expect(diagram.relationships).toHaveLength(1);
+ const relationship = diagram.relationships![0];
+
+ // The DBML parser returns endpoints in order based on the ref syntax.
+ // For inline ref: `ref: > "inventory"."products"."id"` on product_id field:
+ // - endpoints[0]: inventory.products.id (PK side, relation: '1')
+ // - endpoints[1]: inventory.product_suppliers.product_id (FK side, relation: '*')
+ //
+ // The CRITICAL check: both endpoints should use inventory schema tables,
+ // NOT sales schema. The relationship should connect:
+ // - inventory.products (NOT sales.products)
+ // - inventory.product_suppliers
+
+ // Both schemas should be inventory (not sales)
+ expect(relationship.sourceSchema).toBe('inventory');
+ expect(relationship.targetSchema).toBe('inventory');
+
+ // The relationship should involve inventory.products and product_suppliers
+ const relationshipTableIds = [
+ relationship.sourceTableId,
+ relationship.targetTableId,
+ ];
+ expect(relationshipTableIds).toContain(inventoryProducts!.id);
+ expect(relationshipTableIds).toContain(productSuppliers!.id);
+
+ // CRITICAL: sales.products should NOT be part of this relationship
+ expect(relationshipTableIds).not.toContain(salesProducts!.id);
+
+ // Verify the fields are from the correct tables
+ const productIdField = productSuppliers?.fields.find(
+ (f) => f.name === 'product_id'
+ );
+ const inventoryProductsIdField = inventoryProducts?.fields.find(
+ (f) => f.name === 'id'
+ );
+
+ expect(productIdField).toBeDefined();
+ expect(inventoryProductsIdField).toBeDefined();
+
+ const relationshipFieldIds = [
+ relationship.sourceFieldId,
+ relationship.targetFieldId,
+ ];
+ expect(relationshipFieldIds).toContain(productIdField!.id);
+ expect(relationshipFieldIds).toContain(
+ inventoryProductsIdField!.id
+ );
+ });
+ });
+
+ describe('Standalone Ref Syntax', () => {
+ it('should correctly match relationship using explicit Ref statement with schema-qualified names', async () => {
+ // Same scenario but with standalone Ref syntax instead of inline ref
+ // The explicit Ref uses fully qualified names: "schema"."table"."field"
+ const dbmlContent = `
+Table "sales"."products" {
+ "id" bigint [pk, not null]
+ "store_id" bigint [not null]
+ "category_id" bigint
+ "metadata" jsonb
+}
+
+Table "inventory"."product_suppliers" {
+ "id" bigint [pk, not null]
+ "product_id" bigint [not null]
+ "supplier_id" bigint [not null]
+ "unit_cost" bigint
+ "lead_time_days" bigint
+}
+
+Table "inventory"."products" {
+ "id" bigint [pk, not null]
+ "warehouse_id" bigint [not null]
+ "category" varchar(500) [not null]
+ "subcategory" varchar(500) [not null]
+ "sku" varchar(500)
+ "name" varchar(500) [not null]
+ "description" bigint
+ "unit_price" numeric(38,18) [not null, default: 0]
+ "wholesale_price" numeric(38,18) [not null, default: 0]
+ "quantity" bigint [not null, default: 1]
+ "created_at" timestamp [not null]
+ "updated_at" timestamp [not null]
+ "supplier_ref" bigint
+}
+
+Ref "fk_products_product_suppliers":"inventory"."products"."id" < "inventory"."product_suppliers"."product_id"
+`;
+
+ const diagram = await importDBMLToDiagram(dbmlContent, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ // Verify tables are imported correctly
+ expect(diagram.tables).toHaveLength(3);
+
+ const salesProducts = diagram.tables?.find(
+ (t) => t.name === 'products' && t.schema === 'sales'
+ );
+ const inventoryProducts = diagram.tables?.find(
+ (t) => t.name === 'products' && t.schema === 'inventory'
+ );
+ const productSuppliers = diagram.tables?.find(
+ (t) =>
+ t.name === 'product_suppliers' && t.schema === 'inventory'
+ );
+
+ expect(salesProducts).toBeDefined();
+ expect(inventoryProducts).toBeDefined();
+ expect(productSuppliers).toBeDefined();
+
+ // Verify the relationship exists
+ expect(diagram.relationships).toHaveLength(1);
+ const relationship = diagram.relationships![0];
+
+ // The Ref syntax is: "inventory"."products"."id" < "inventory"."product_suppliers"."product_id"
+ // Both endpoints explicitly reference the inventory schema.
+ //
+ // The CRITICAL check: both endpoints should be matched to inventory schema tables,
+ // NOT sales schema.
+
+ // Both schemas should be inventory (not sales)
+ expect(relationship.sourceSchema).toBe('inventory');
+ expect(relationship.targetSchema).toBe('inventory');
+
+ // The relationship should involve inventory.products and product_suppliers
+ const relationshipTableIds = [
+ relationship.sourceTableId,
+ relationship.targetTableId,
+ ];
+ expect(relationshipTableIds).toContain(inventoryProducts!.id);
+ expect(relationshipTableIds).toContain(productSuppliers!.id);
+
+ // CRITICAL: sales.products should NOT be part of this relationship
+ expect(relationshipTableIds).not.toContain(salesProducts!.id);
+
+ // Verify the fields are from the correct tables
+ const productIdField = productSuppliers?.fields.find(
+ (f) => f.name === 'product_id'
+ );
+ const inventoryProductsIdField = inventoryProducts?.fields.find(
+ (f) => f.name === 'id'
+ );
+
+ expect(productIdField).toBeDefined();
+ expect(inventoryProductsIdField).toBeDefined();
+
+ const relationshipFieldIds = [
+ relationship.sourceFieldId,
+ relationship.targetFieldId,
+ ];
+ expect(relationshipFieldIds).toContain(productIdField!.id);
+ expect(relationshipFieldIds).toContain(
+ inventoryProductsIdField!.id
+ );
+
+ // Verify the relationship name is preserved from the Ref definition
+ expect(relationship.name).toBeDefined();
+ });
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-pk-not-null.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-pk-not-null.test.ts
new file mode 100644
index 000000000..af70c639d
--- /dev/null
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-pk-not-null.test.ts
@@ -0,0 +1,64 @@
+import { describe, it, expect } from 'vitest';
+import { importDBMLToDiagram } from '../dbml-import';
+import { DatabaseType } from '@/lib/domain/database-type';
+
+describe('DBML Import - Primary Key NOT NULL', () => {
+ it('should mark primary key columns as NOT NULL', async () => {
+ const dbml = `
+Table users {
+ id int [pk]
+ name varchar(100)
+}`;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const usersTable = diagram.tables?.find((t) => t.name === 'users');
+ expect(usersTable).toBeDefined();
+
+ const idField = usersTable?.fields.find((f) => f.name === 'id');
+ expect(idField?.primaryKey).toBe(true);
+ expect(idField?.nullable).toBe(false);
+
+ // Non-PK field should remain nullable by default
+ const nameField = usersTable?.fields.find((f) => f.name === 'name');
+ expect(nameField?.primaryKey).toBeFalsy();
+ expect(nameField?.nullable).toBe(true);
+ });
+
+ it('should mark composite primary key columns as NOT NULL', async () => {
+ const dbml = `
+Table order_items {
+ order_id int
+ product_id int
+ quantity int
+
+ indexes {
+ (order_id, product_id) [pk]
+ }
+}`;
+
+ const diagram = await importDBMLToDiagram(dbml, {
+ databaseType: DatabaseType.POSTGRESQL,
+ });
+
+ const table = diagram.tables?.find((t) => t.name === 'order_items');
+ expect(table).toBeDefined();
+
+ const orderIdField = table?.fields.find((f) => f.name === 'order_id');
+ expect(orderIdField?.primaryKey).toBe(true);
+ expect(orderIdField?.nullable).toBe(false);
+
+ const productIdField = table?.fields.find(
+ (f) => f.name === 'product_id'
+ );
+ expect(productIdField?.primaryKey).toBe(true);
+ expect(productIdField?.nullable).toBe(false);
+
+ // Non-PK field should remain nullable
+ const quantityField = table?.fields.find((f) => f.name === 'quantity');
+ expect(quantityField?.primaryKey).toBeFalsy();
+ expect(quantityField?.nullable).toBe(true);
+ });
+});
diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts
index 27c26f4cd..2c8e8b19c 100644
--- a/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts
+++ b/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts
@@ -39,13 +39,10 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
databaseType: DatabaseType.MYSQL,
});
- // Verify no 'public' schema was added
+ // Verify schema is undefined for MySQL (no default schema)
expect(diagram.tables).toBeDefined();
diagram.tables?.forEach((table) => {
- expect(table.schema).toBe('');
- console.log(
- `✓ Table "${table.name}" has no schema (MySQL behavior)`
- );
+ expect(table.schema).toBeUndefined();
});
// Check specific tables
@@ -53,7 +50,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
(t) => t.name === 'wizards'
);
expect(wizardsTable).toBeDefined();
- expect(wizardsTable?.schema).toBe('');
+ expect(wizardsTable?.schema).toBeUndefined();
// Check that reserved keywords are preserved as field names
const yesField = wizardsTable?.fields.find((f) => f.name === 'Yes');
@@ -129,8 +126,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
expect(resultField?.name).toBe(sourceField.name);
});
});
-
- console.log('✓ All IDs preserved after DBML round-trip');
});
});
@@ -167,7 +162,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
const heroesTable = diagram.tables?.find(
(t) => t.name === 'heroes'
);
- expect(heroesTable?.schema).toBe(''); // 'public' should be converted to empty
+ expect(heroesTable?.schema).toBe('public'); // PostgreSQL default schema
const secretQuestsTable = diagram.tables?.find(
(t) => t.name === 'secret_quests'
@@ -177,10 +172,10 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
const artifactsTable = diagram.tables?.find(
(t) => t.name === 'artifacts'
);
- expect(artifactsTable?.schema).toBe(''); // No schema = empty string
+ expect(artifactsTable?.schema).toBe('public'); // No schema = default schema
});
- it('should rename reserved keywords for PostgreSQL', async () => {
+ it('should handle reserved keywords for PostgreSQL', async () => {
const dbmlContent = `
Table "magic_items" {
"id" bigint [pk]
@@ -197,10 +192,9 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
const exported = generateDBMLFromDiagram(diagram);
- // For PostgreSQL, keywords should be renamed in export
- expect(exported.standardDbml).toContain('Order_field');
- expect(exported.standardDbml).toContain('Yes_field');
- expect(exported.standardDbml).toContain('No_field');
+ expect(exported.standardDbml).toContain('Order');
+ expect(exported.standardDbml).toContain('Yes');
+ expect(exported.standardDbml).toContain('No');
});
});
@@ -228,24 +222,18 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
}
);
- // For MySQL, 'public' schema should be stripped
+ // For MySQL, 'public' schema should become undefined (no default schema)
mysqlDiagram.tables?.forEach((table) => {
- expect(table.schema).toBe('');
- console.log(
- `✓ MySQL: Table "${table.name}" has no schema (public was stripped)`
- );
+ expect(table.schema).toBeUndefined();
});
- // Now test with PostgreSQL - public should also be stripped (it's the default)
+ // For PostgreSQL, 'public' is the default schema
const pgDiagram = await importDBMLToDiagram(dbmlWithPublicSchema, {
databaseType: DatabaseType.POSTGRESQL,
});
pgDiagram.tables?.forEach((table) => {
- expect(table.schema).toBe('');
- console.log(
- `✓ PostgreSQL: Table "${table.name}" has no schema (public is default)`
- );
+ expect(table.schema).toBe('public');
});
});
@@ -276,7 +264,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
expect(magicTable?.schema).toBe('fantasy');
expect(questTable?.schema).toBe('adventure');
- console.log('✓ Custom schemas preserved correctly');
});
});
@@ -396,8 +383,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
// Perform 3 round-trips
for (let cycle = 1; cycle <= 3; cycle++) {
- console.log(`🔄 Round-trip cycle ${cycle}`);
-
// Export
const exported = generateDBMLFromDiagram(currentDiagram);
@@ -430,8 +415,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
expect(currentTable?.id).toBe(original.id);
});
}
-
- console.log('✓ Data integrity maintained through 3 cycles');
});
});
});
diff --git a/src/lib/dbml/dbml-import/dbml-import-error.ts b/src/lib/dbml/dbml-import/dbml-import-error.ts
index f6286f435..a13ec284d 100644
--- a/src/lib/dbml/dbml-import/dbml-import-error.ts
+++ b/src/lib/dbml/dbml-import/dbml-import-error.ts
@@ -1,4 +1,6 @@
import type { CompilerError } from '@dbml/core/types/parse/error';
+import type { DatabaseType } from '@/lib/domain/database-type';
+import { databaseSupportsArrays } from '@/lib/domain/database-capabilities';
export interface DBMLError {
message: string;
@@ -6,8 +8,59 @@ export interface DBMLError {
column: number;
}
+export class DBMLValidationError extends Error {
+ public readonly dbmlError: DBMLError;
+
+ constructor(message: string, line: number, column: number = 1) {
+ super(message);
+ this.name = 'DBMLValidationError';
+ this.dbmlError = { message, line, column };
+ }
+}
+
+export const getPositionFromIndex = (
+ content: string,
+ matchIndex: number
+): { line: number; column: number } => {
+ const lines = content.substring(0, matchIndex).split('\n');
+ return {
+ line: lines.length,
+ column: lines[lines.length - 1].length + 1,
+ };
+};
+
+export const validateArrayTypesForDatabase = (
+ content: string,
+ databaseType: DatabaseType
+): void => {
+ // Only validate if database doesn't support arrays
+ if (databaseSupportsArrays(databaseType)) {
+ return;
+ }
+
+ const arrayFieldPattern = /"?(\w+)"?\s+(\w+(?:\(\d+(?:,\s*\d+)?\))?)\[\]/g;
+ const matches = [...content.matchAll(arrayFieldPattern)];
+
+ for (const match of matches) {
+ const fieldName = match[1];
+ const dataType = match[2];
+ const { line, column } = getPositionFromIndex(content, match.index!);
+
+ throw new DBMLValidationError(
+ `Array types are not supported for ${databaseType} database. Field "${fieldName}" has array type "${dataType}[]" which is not allowed.`,
+ line,
+ column
+ );
+ }
+};
+
export function parseDBMLError(error: unknown): DBMLError | null {
try {
+ // Check for our custom DBMLValidationError
+ if (error instanceof DBMLValidationError) {
+ return error.dbmlError;
+ }
+
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
diff --git a/src/lib/dbml/dbml-import/dbml-import.ts b/src/lib/dbml/dbml-import/dbml-import.ts
index 969a37cef..aa1196cf1 100644
--- a/src/lib/dbml/dbml-import/dbml-import.ts
+++ b/src/lib/dbml/dbml-import/dbml-import.ts
@@ -1,11 +1,17 @@
import { Parser } from '@dbml/core';
import type { Diagram } from '@/lib/domain/diagram';
-import { generateDiagramId, generateId } from '@/lib/utils';
+import { generateDiagramId, generateId, isStringEmpty } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
+import { defaultSchemas } from '@/lib/data/default-schemas';
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
import type { DBField } from '@/lib/domain/db-field';
+import type { DBCheckConstraint } from '@/lib/domain/db-check-constraint';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
-import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
+import {
+ findDataTypeDataById,
+ getPreferredSynonym,
+ requiresNotNull,
+} from '@/lib/data/data-types/data-types';
import { defaultTableColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type Field from '@dbml/core/types/model_structure/field';
@@ -14,11 +20,52 @@ import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
+import {
+ validateArrayTypesForDatabase,
+ DBMLValidationError,
+ getPositionFromIndex,
+} from './dbml-import-error';
+import { validateCheckConstraintWithDetails } from '@/lib/check-constraints/check-constraints-validator';
+
+export const defaultDBMLDiagramName = 'DBML Import';
-// Preprocess DBML to handle unsupported features
-export const preprocessDBML = (content: string): string => {
+interface FieldCheckConstraint {
+ expression: string;
+}
+
+interface TableCheckConstraint {
+ expression: string;
+ name?: string;
+}
+
+interface PreprocessDBMLResult {
+ content: string;
+ arrayFields: Map>;
+ fieldChecks: Map>;
+ tableChecks: Map;
+}
+
+// Helper to find matching closing brace
+const findMatchingBrace = (str: string, startIndex: number): number => {
+ let depth = 1;
+ for (let i = startIndex; i < str.length && depth > 0; i++) {
+ if (str[i] === '{') depth++;
+ else if (str[i] === '}') depth--;
+ if (depth === 0) return i;
+ }
+ return -1;
+};
+
+export const preprocessDBML = (content: string): PreprocessDBMLResult => {
let processed = content;
+ // Track array fields found during preprocessing
+ const arrayFields = new Map>();
+ // Track field-level check constraints: Map>
+ const fieldChecks = new Map>();
+ // Track table-level check constraints: Map
+ const tableChecks = new Map();
+
// Remove TableGroup blocks (not supported by parser)
processed = processed.replace(/TableGroup\s+[^{]*\{[^}]*\}/gs, '');
@@ -28,8 +75,146 @@ export const preprocessDBML = (content: string): string => {
// Don't remove enum definitions - we'll parse them
// processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
- // Handle array types by converting them to text
- processed = processed.replace(/(\w+)\[\]/g, 'text');
+ // Handle array types by tracking them and converting syntax for DBML parser
+ // Note: DBML doesn't officially support array syntax, so we convert type[] to type
+ // but track which fields should be arrays
+
+ // First, find all Table declarations and extract their bodies properly
+ // Pattern matches: Table "schema"."name" { or Table name { or Table "name" {
+ const tableStartPattern =
+ /Table\s+(?:(?:"([^"]+)"\.)?(?:"([^"]+)"|([a-zA-Z_]\w*)))\s*(?:\[[^\]]*\])?\s*\{/g;
+ let tableMatch;
+
+ while ((tableMatch = tableStartPattern.exec(content)) !== null) {
+ const schema = tableMatch[1] || '';
+ const tableName = tableMatch[2] || tableMatch[3];
+ const openBraceIndex = tableMatch.index + tableMatch[0].length - 1;
+ const closeBraceIndex = findMatchingBrace(content, openBraceIndex + 1);
+
+ if (closeBraceIndex === -1) continue;
+
+ const tableBody = content.substring(
+ openBraceIndex + 1,
+ closeBraceIndex
+ );
+ const fullTableName = schema ? `${schema}.${tableName}` : tableName;
+
+ // Find array field declarations within this table
+ const arrayFieldPattern = /"?(\w+)"?\s+(\w+(?:\([^)]+\))?)\[\]/g;
+ let fieldMatch;
+
+ while ((fieldMatch = arrayFieldPattern.exec(tableBody)) !== null) {
+ const fieldName = fieldMatch[1];
+
+ if (!arrayFields.has(fullTableName)) {
+ arrayFields.set(fullTableName, new Set());
+ }
+ arrayFields.get(fullTableName)!.add(fieldName);
+ }
+
+ // Extract field-level check constraints: check: `expression`
+ // Pattern matches lines like: price decimal [not null, check: `price > 0`]
+ const fieldCheckPattern =
+ /^\s*"?(\w+)"?\s+\w+[^\n[]*\[[^\]]*check:\s*`([^`]+)`/gm;
+ let checkMatch;
+
+ while ((checkMatch = fieldCheckPattern.exec(tableBody)) !== null) {
+ const fieldName = checkMatch[1];
+ const expression = checkMatch[2];
+
+ // Validate the check constraint expression
+ const validationResult =
+ validateCheckConstraintWithDetails(expression);
+ if (!validationResult.isValid) {
+ // Calculate position in original content
+ const expressionStartInTableBody =
+ checkMatch.index + checkMatch[0].indexOf(expression);
+ const expressionStartInContent =
+ openBraceIndex + 1 + expressionStartInTableBody;
+ const { line, column } = getPositionFromIndex(
+ content,
+ expressionStartInContent
+ );
+ throw new DBMLValidationError(
+ `Invalid check constraint expression "${expression}" on field "${fieldName}": ${validationResult.error}`,
+ line,
+ column
+ );
+ }
+
+ if (!fieldChecks.has(fullTableName)) {
+ fieldChecks.set(fullTableName, new Map());
+ }
+ fieldChecks.get(fullTableName)!.set(fieldName, { expression });
+ }
+
+ // Extract table-level checks block: checks { `expression` [name: 'name'] }
+ const checksBlockPattern = /checks\s*\{([^}]*)\}/gs;
+ const checksBlockMatch = checksBlockPattern.exec(tableBody);
+
+ if (checksBlockMatch) {
+ const checksContent = checksBlockMatch[1];
+ const checksBlockStartInTableBody = checksBlockMatch.index;
+
+ // Parse individual check constraints within the block
+ // Pattern: `expression` or `expression` [name: 'name']
+ const checkItemPattern =
+ /`([^`]+)`(?:\s*\[(?:[^\]]*name:\s*['"]([^'"]+)['"])?[^\]]*\])?/g;
+ let checkItemMatch;
+
+ const constraints: TableCheckConstraint[] = [];
+ while (
+ (checkItemMatch = checkItemPattern.exec(checksContent)) !== null
+ ) {
+ const expression = checkItemMatch[1];
+
+ // Validate the check constraint expression
+ const validationResult =
+ validateCheckConstraintWithDetails(expression);
+ if (!validationResult.isValid) {
+ // Calculate position in original content
+ // checksContent starts after "checks {"
+ const checksBlockHeaderLength =
+ checksBlockMatch[0].indexOf(checksContent);
+ const expressionStartInChecksContent =
+ checkItemMatch.index + 1; // +1 to skip the opening backtick
+ const expressionStartInContent =
+ openBraceIndex +
+ 1 +
+ checksBlockStartInTableBody +
+ checksBlockHeaderLength +
+ expressionStartInChecksContent;
+ const { line, column } = getPositionFromIndex(
+ content,
+ expressionStartInContent
+ );
+ throw new DBMLValidationError(
+ `Invalid check constraint expression "${expression}": ${validationResult.error}`,
+ line,
+ column
+ );
+ }
+
+ constraints.push({
+ expression,
+ name: checkItemMatch[2] || undefined,
+ });
+ }
+
+ if (constraints.length > 0) {
+ tableChecks.set(fullTableName, constraints);
+ }
+ }
+ }
+
+ // Now convert array syntax for DBML parser (keep the base type, remove [])
+ processed = processed.replace(/(\w+(?:\(\d+(?:,\s*\d+)?\))?)\[\]/g, '$1');
+
+ // Remove check: `...` from field attributes (not supported by parser)
+ processed = processed.replace(/,?\s*check:\s*`[^`]+`/g, '');
+
+ // Remove checks { ... } blocks (not supported by parser)
+ processed = processed.replace(/\s*checks\s*\{[^}]*\}/gs, '');
// Handle inline enum types without values by converting to varchar
processed = processed.replace(
@@ -44,7 +229,7 @@ export const preprocessDBML = (content: string): string => {
'Table $1 {'
);
- return processed;
+ return { content: processed, arrayFields, fieldChecks, tableChecks };
};
// Simple function to replace Spanish special characters
@@ -83,10 +268,12 @@ interface DBMLField {
pk?: boolean;
not_null?: boolean;
increment?: boolean;
+ isArray?: boolean;
characterMaximumLength?: string | null;
precision?: number | null;
scale?: number | null;
note?: string | { value: string } | null;
+ default?: string | null;
}
interface DBMLIndexColumn {
@@ -112,9 +299,10 @@ interface DBMLTable {
}
interface DBMLEndpoint {
+ schemaName?: string;
tableName: string;
fieldNames: string[];
- relation: string;
+ relation: '1' | '*'; // '1' = one, '*' = many (from @dbml/core parser)
}
interface DBMLRef {
@@ -168,27 +356,16 @@ const mapDBMLTypeToDataType = (
} satisfies DataTypeData;
};
-const determineCardinality = (
- field: DBField,
- referencedField: DBField
-): { sourceCardinality: string; targetCardinality: string } => {
- const isSourceUnique = field.unique || field.primaryKey;
- const isTargetUnique = referencedField.unique || referencedField.primaryKey;
- if (isSourceUnique && isTargetUnique) {
- return { sourceCardinality: 'one', targetCardinality: 'one' };
- } else if (isSourceUnique) {
- return { sourceCardinality: 'one', targetCardinality: 'many' };
- } else if (isTargetUnique) {
- return { sourceCardinality: 'many', targetCardinality: 'one' };
- } else {
- return { sourceCardinality: 'many', targetCardinality: 'many' };
- }
+// Convert @dbml/core relation values to cardinality
+// The parser uses '1' for "one" side and '*' for "many" side
+const relationToCardinality = (relation: '1' | '*'): Cardinality => {
+ return relation === '1' ? 'one' : 'many';
};
export const importDBMLToDiagram = async (
dbmlContent: string,
- options?: {
- databaseType?: DatabaseType;
+ options: {
+ databaseType: DatabaseType;
}
): Promise => {
try {
@@ -196,7 +373,7 @@ export const importDBMLToDiagram = async (
if (!dbmlContent.trim()) {
return {
id: generateDiagramId(),
- name: 'DBML Import',
+ name: defaultDBMLDiagramName,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables: [],
relationships: [],
@@ -205,16 +382,24 @@ export const importDBMLToDiagram = async (
};
}
+ // Validate array types BEFORE preprocessing (preprocessing removes [])
+ validateArrayTypesForDatabase(dbmlContent, options.databaseType);
+
const parser = new Parser();
// Preprocess and sanitize DBML content
- const preprocessedContent = preprocessDBML(dbmlContent);
+ const {
+ content: preprocessedContent,
+ arrayFields,
+ fieldChecks,
+ tableChecks,
+ } = preprocessDBML(dbmlContent);
const sanitizedContent = sanitizeDBML(preprocessedContent);
// Handle content that becomes empty after preprocessing
if (!sanitizedContent.trim()) {
return {
id: generateDiagramId(),
- name: 'DBML Import',
+ name: defaultDBMLDiagramName,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables: [],
relationships: [],
@@ -229,7 +414,7 @@ export const importDBMLToDiagram = async (
if (!parsedData.schemas || parsedData.schemas.length === 0) {
return {
id: generateDiagramId(),
- name: 'DBML Import',
+ name: defaultDBMLDiagramName,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables: [],
relationships: [],
@@ -274,6 +459,12 @@ export const importDBMLToDiagram = async (
enums,
});
+ // Also check the preferred synonym for field attributes (e.g., decimal → numeric)
+ const preferredType = options.databaseType
+ ? getPreferredSynonym(dataType.name, options.databaseType)
+ : null;
+ const effectiveType = preferredType ?? dataType;
+
// Check if this is a character type that should have a max length
const baseTypeName = typeName
.replace(/\(.*\)/, '')
@@ -290,8 +481,8 @@ export const importDBMLToDiagram = async (
characterMaximumLength: args[0],
};
} else if (
- dataType.fieldAttributes?.precision &&
- dataType.fieldAttributes?.scale
+ effectiveType.fieldAttributes?.precision &&
+ effectiveType.fieldAttributes?.scale
) {
const precisionNum = args?.[0] ? parseInt(args[0]) : undefined;
const scaleNum = args?.[1] ? parseInt(args[1]) : undefined;
@@ -332,6 +523,33 @@ export const importDBMLToDiagram = async (
schema: schemaName,
note: table.note,
fields: table.fields.map((field): DBMLField => {
+ // Extract default value and remove all quotes
+ let defaultValue: string | undefined;
+ if (
+ field.dbdefault !== undefined &&
+ field.dbdefault !== null
+ ) {
+ const rawDefault = String(
+ field.dbdefault.value
+ );
+ defaultValue = rawDefault.replace(/['"`]/g, '');
+ }
+
+ // Check if this field should be an array
+ const fullTableName = schemaName
+ ? `${schemaName}.${table.name}`
+ : table.name;
+
+ let isArray = arrayFields
+ .get(fullTableName)
+ ?.has(field.name);
+
+ if (!isArray && schemaName) {
+ isArray = arrayFields
+ .get(table.name)
+ ?.has(field.name);
+ }
+
return {
name: field.name,
type: field.type,
@@ -339,7 +557,9 @@ export const importDBMLToDiagram = async (
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
+ isArray: isArray || undefined,
note: field.note,
+ default: defaultValue,
...getFieldExtraAttributes(field, allEnums),
} satisfies DBMLField;
}),
@@ -424,14 +644,22 @@ export const importDBMLToDiagram = async (
if (schema.enums) {
schema.enums.forEach((enumDef) => {
// Get schema name from enum or use schema's name
- const enumSchema =
+ // DBML parser uses 'public' as its default - treat it as empty
+ const rawEnumSchema =
typeof enumDef.schema === 'string'
? enumDef.schema
: enumDef.schema?.name || schema.name;
+ const defaultSchema = defaultSchemas[options.databaseType];
+ const isEnumSchemaEmpty =
+ isStringEmpty(rawEnumSchema) ||
+ rawEnumSchema === 'public';
+ const enumSchema = isEnumSchemaEmpty
+ ? defaultSchema
+ : rawEnumSchema;
allEnums.push({
name: enumDef.name,
- schema: enumSchema === 'public' ? '' : enumSchema,
+ schema: enumSchema,
values: enumDef.values || [],
note: enumDef.note,
});
@@ -471,21 +699,41 @@ export const importDBMLToDiagram = async (
}
}
+ // Map DBML type to DataType
+ const mappedType = mapDBMLTypeToDataType(field.type.type_name, {
+ ...options,
+ enums: extractedData.enums,
+ });
+
+ // Check if there's a preferred synonym for this type
+ const preferredType = getPreferredSynonym(
+ mappedType.name,
+ options.databaseType
+ );
+
+ // Use the preferred synonym if it exists, otherwise use the mapped type
+ const finalType = preferredType ?? mappedType;
+
return {
id: generateId(),
name: field.name.replace(/['"]/g, ''),
- type: mapDBMLTypeToDataType(field.type.type_name, {
- ...options,
- enums: extractedData.enums,
- }),
- nullable: !field.not_null,
+ type: finalType,
+ nullable:
+ field.increment ||
+ field.pk ||
+ requiresNotNull(field.type.type_name)
+ ? false
+ : !field.not_null,
primaryKey: field.pk || false,
- unique: field.unique || false,
+ unique: field.unique || field.pk || false, // Primary keys are always unique
createdAt: Date.now(),
characterMaximumLength: field.characterMaximumLength,
precision: field.precision,
scale: field.scale,
+ ...(field.increment ? { increment: field.increment } : {}),
+ ...(field.isArray ? { isArray: field.isArray } : {}),
...(fieldComment ? { comments: fieldComment } : {}),
+ ...(field.default ? { default: field.default } : {}),
};
});
@@ -504,13 +752,14 @@ export const importDBMLToDiagram = async (
if (dbmlIndex.name) {
compositePKIndexName = dbmlIndex.name;
}
- // Mark fields as primary keys
+ // Mark fields as primary keys and NOT NULL
dbmlIndex.columns.forEach((col) => {
const columnName =
typeof col === 'string' ? col : col.value;
const field = fields.find((f) => f.name === columnName);
if (field) {
field.primaryKey = true;
+ field.nullable = false;
}
});
}
@@ -623,18 +872,69 @@ export const importDBMLToDiagram = async (
}
}
+ // Get raw schema from DBML, then apply defaultSchema if empty
+ // DBML parser uses 'public' as its default - treat it as empty
+ const defaultSchema = defaultSchemas[options.databaseType];
+ const rawSchema =
+ typeof table.schema === 'string'
+ ? table.schema
+ : table.schema?.name;
+ const isSchemaEmpty =
+ isStringEmpty(rawSchema) || rawSchema === 'public';
+ const tableSchema = isSchemaEmpty ? defaultSchema : rawSchema;
+
+ // Build check constraints (all as table-level)
+ // Try with schema first, then without (since original DBML might not have schema)
+ const rawTableSchemaForChecks = rawSchema || '';
+ const fullTableNameForTableChecks = rawTableSchemaForChecks
+ ? `${rawTableSchemaForChecks}.${table.name}`
+ : table.name;
+
+ const allCheckConstraints: DBCheckConstraint[] = [];
+
+ // Convert field-level check constraints to table-level
+ const fieldChecksDefs =
+ fieldChecks.get(fullTableNameForTableChecks) ||
+ fieldChecks.get(table.name);
+ if (fieldChecksDefs) {
+ fieldChecksDefs.forEach((check) => {
+ allCheckConstraints.push({
+ id: generateId(),
+ expression: check.expression,
+ createdAt: Date.now(),
+ });
+ });
+ }
+
+ // Add table-level check constraints
+ const tableCheckConstraintsDefs =
+ tableChecks.get(fullTableNameForTableChecks) ||
+ tableChecks.get(table.name);
+ if (tableCheckConstraintsDefs) {
+ tableCheckConstraintsDefs.forEach((check) => {
+ allCheckConstraints.push({
+ id: generateId(),
+ expression: check.expression,
+ createdAt: Date.now(),
+ });
+ });
+ }
+
+ const checkConstraints: DBCheckConstraint[] | undefined =
+ allCheckConstraints.length > 0
+ ? allCheckConstraints
+ : undefined;
+
const tableToReturn: DBTable = {
id: generateId(),
name: table.name.replace(/['"]/g, ''),
- schema:
- typeof table.schema === 'string'
- ? table.schema === 'public'
- ? ''
- : table.schema
- : table.schema?.name || '',
+ schema: tableSchema,
order: index,
fields,
indexes,
+ ...(checkConstraints && checkConstraints.length > 0
+ ? { checkConstraints }
+ : {}),
x: col * tableSpacing,
y: row * tableSpacing,
color: defaultTableColor,
@@ -651,22 +951,36 @@ export const importDBMLToDiagram = async (
};
});
+ // Helper to find table by name and schema from endpoint
+ const findTableByEndpoint = (
+ endpoint: DBMLEndpoint
+ ): DBTable | undefined => {
+ const tableName = endpoint.tableName.replace(/['"]/g, '');
+ const endpointSchema = endpoint.schemaName?.replace(/['"]/g, '');
+
+ // Normalize endpoint schema the same way tables are normalized:
+ // empty or 'public' → use database default schema
+ const defaultSchema = defaultSchemas[options.databaseType];
+ const isEndpointSchemaEmpty =
+ isStringEmpty(endpointSchema) || endpointSchema === 'public';
+ const normalizedEndpointSchema = isEndpointSchemaEmpty
+ ? defaultSchema
+ : endpointSchema;
+
+ return tables.find(
+ (t) =>
+ t.name === tableName &&
+ (normalizedEndpointSchema === undefined ||
+ t.schema === normalizedEndpointSchema)
+ );
+ };
+
// Create relationships using the refs
const relationships: DBRelationship[] = extractedData.refs.map(
(ref) => {
const [source, target] = ref.endpoints;
- const sourceTable = tables.find(
- (t) =>
- t.name === source.tableName.replace(/['"]/g, '') &&
- (!source.tableName.includes('.') ||
- t.schema === source.tableName.split('.')[0])
- );
- const targetTable = tables.find(
- (t) =>
- t.name === target.tableName.replace(/['"]/g, '') &&
- (!target.tableName.includes('.') ||
- t.schema === target.tableName.split('.')[0])
- );
+ const sourceTable = findTableByEndpoint(source);
+ const targetTable = findTableByEndpoint(target);
if (!sourceTable || !targetTable) {
throw new Error('Invalid relationship: tables not found');
@@ -683,8 +997,14 @@ export const importDBMLToDiagram = async (
throw new Error('Invalid relationship: fields not found');
}
- const { sourceCardinality, targetCardinality } =
- determineCardinality(sourceField, targetField);
+ // Use the relation values from @dbml/core parser
+ // These directly represent the cardinality: '1' = one, '*' = many
+ const sourceCardinality = relationToCardinality(
+ source.relation
+ );
+ const targetCardinality = relationToCardinality(
+ target.relation
+ );
return {
id: generateId(),
@@ -695,8 +1015,8 @@ export const importDBMLToDiagram = async (
targetTableId: targetTable.id,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
- sourceCardinality: sourceCardinality as Cardinality,
- targetCardinality: targetCardinality as Cardinality,
+ sourceCardinality,
+ targetCardinality,
createdAt: Date.now(),
};
}
@@ -734,7 +1054,7 @@ export const importDBMLToDiagram = async (
return {
id: generateDiagramId(),
- name: 'DBML Import',
+ name: defaultDBMLDiagramName,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables,
relationships,
diff --git a/src/lib/dbml/dbml-import/verify-dbml.ts b/src/lib/dbml/dbml-import/verify-dbml.ts
new file mode 100644
index 000000000..9bada1837
--- /dev/null
+++ b/src/lib/dbml/dbml-import/verify-dbml.ts
@@ -0,0 +1,65 @@
+import { Parser } from '@dbml/core';
+import { preprocessDBML, sanitizeDBML } from './dbml-import';
+import type { DBMLError } from './dbml-import-error';
+import {
+ parseDBMLError,
+ validateArrayTypesForDatabase,
+} from './dbml-import-error';
+import type { DatabaseType } from '@/lib/domain/database-type';
+
+export const verifyDBML = (
+ content: string,
+ {
+ databaseType,
+ }: {
+ databaseType: DatabaseType;
+ }
+):
+ | {
+ hasError: true;
+ error: unknown;
+ parsedError?: DBMLError;
+ errorText: string;
+ }
+ | {
+ hasError: false;
+ } => {
+ try {
+ // Validate array types BEFORE preprocessing (preprocessing removes [])
+ validateArrayTypesForDatabase(content, databaseType);
+
+ const { content: preprocessedContent } = preprocessDBML(content);
+ const sanitizedContent = sanitizeDBML(preprocessedContent);
+
+ const parser = new Parser();
+ parser.parse(sanitizedContent, 'dbmlv2');
+ } catch (e) {
+ const parsedError = parseDBMLError(e);
+ if (parsedError) {
+ return {
+ hasError: true,
+ parsedError: parsedError,
+ error: e,
+ errorText: parsedError.message,
+ };
+ } else {
+ if (e instanceof Error) {
+ return {
+ hasError: true,
+ error: e,
+ errorText: e.message,
+ };
+ }
+
+ return {
+ hasError: true,
+ error: e,
+ errorText: JSON.stringify(e),
+ };
+ }
+ }
+
+ return {
+ hasError: false,
+ };
+};
diff --git a/src/lib/domain/config.ts b/src/lib/domain/config.ts
index 06e4e3bca..85c1b67bf 100644
--- a/src/lib/domain/config.ts
+++ b/src/lib/domain/config.ts
@@ -1,4 +1,26 @@
export interface ChartDBConfig {
defaultDiagramId: string;
exportActions?: Date[];
+ appName?: string;
+ appLogo?: string;
+ hideSocialLinks?: boolean;
+ primaryColor?: string;
}
+
+export const getConfigAssetUrl = (
+ assetPath?: string | null
+): string | undefined => {
+ if (!assetPath) return undefined;
+ const trimmed = assetPath.trim();
+ if (!trimmed) return undefined;
+ if (
+ trimmed.startsWith('http://') ||
+ trimmed.startsWith('https://') ||
+ trimmed.startsWith('data:')
+ ) {
+ return trimmed;
+ }
+ const normalized = trimmed.replace(/^[\\/]+/, '');
+ if (!normalized) return undefined;
+ return `/api/config/assets/${encodeURIComponent(normalized)}`;
+};
diff --git a/src/lib/domain/database-capabilities.ts b/src/lib/domain/database-capabilities.ts
new file mode 100644
index 000000000..e4a20ab97
--- /dev/null
+++ b/src/lib/domain/database-capabilities.ts
@@ -0,0 +1,76 @@
+import { DatabaseType } from './database-type';
+
+export interface DatabaseCapabilities {
+ supportsArrays?: boolean;
+ supportsCustomTypes?: boolean;
+ supportsSchemas?: boolean;
+ supportsComments?: boolean;
+ supportsCheckConstraints?: boolean;
+}
+
+export const DATABASE_CAPABILITIES: Record =
+ {
+ [DatabaseType.POSTGRESQL]: {
+ supportsArrays: true,
+ supportsCustomTypes: true,
+ supportsSchemas: true,
+ supportsComments: true,
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.COCKROACHDB]: {
+ supportsArrays: true,
+ supportsSchemas: true,
+ supportsComments: true,
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.MYSQL]: {
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.MARIADB]: {
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.SQL_SERVER]: {
+ supportsSchemas: true,
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.SQLITE]: {
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.CLICKHOUSE]: {
+ supportsSchemas: true,
+ },
+ [DatabaseType.ORACLE]: {
+ supportsSchemas: true,
+ supportsComments: true,
+ supportsCheckConstraints: true,
+ },
+ [DatabaseType.GENERIC]: {},
+ };
+
+export const getDatabaseCapabilities = (
+ databaseType: DatabaseType
+): DatabaseCapabilities => {
+ return DATABASE_CAPABILITIES[databaseType];
+};
+
+export const databaseSupportsArrays = (databaseType: DatabaseType): boolean => {
+ return getDatabaseCapabilities(databaseType).supportsArrays ?? false;
+};
+
+export const databaseTypesWithCommentSupport: DatabaseType[] = Object.keys(
+ DATABASE_CAPABILITIES
+).filter(
+ (dbType) => DATABASE_CAPABILITIES[dbType as DatabaseType].supportsComments
+) as DatabaseType[];
+
+export const supportsCustomTypes = (databaseType: DatabaseType): boolean => {
+ return getDatabaseCapabilities(databaseType).supportsCustomTypes ?? false;
+};
+
+export const supportsCheckConstraints = (
+ databaseType: DatabaseType
+): boolean => {
+ return (
+ getDatabaseCapabilities(databaseType).supportsCheckConstraints ?? false
+ );
+};
diff --git a/src/lib/domain/database-type.ts b/src/lib/domain/database-type.ts
index 426cd6daa..5168cdfec 100644
--- a/src/lib/domain/database-type.ts
+++ b/src/lib/domain/database-type.ts
@@ -9,9 +9,3 @@ export enum DatabaseType {
COCKROACHDB = 'cockroachdb',
ORACLE = 'oracle',
}
-
-export const databaseTypesWithCommentSupport: DatabaseType[] = [
- DatabaseType.POSTGRESQL,
- DatabaseType.COCKROACHDB,
- DatabaseType.ORACLE,
-];
diff --git a/src/lib/domain/db-check-constraint.ts b/src/lib/domain/db-check-constraint.ts
new file mode 100644
index 000000000..826692e26
--- /dev/null
+++ b/src/lib/domain/db-check-constraint.ts
@@ -0,0 +1,13 @@
+import { z } from 'zod';
+
+export interface DBCheckConstraint {
+ id: string;
+ expression: string;
+ createdAt: number;
+}
+
+export const dbCheckConstraintSchema: z.ZodType = z.object({
+ id: z.string(),
+ expression: z.string(),
+ createdAt: z.number(),
+});
diff --git a/src/lib/domain/db-field.ts b/src/lib/domain/db-field.ts
index 042530e3d..387366654 100644
--- a/src/lib/domain/db-field.ts
+++ b/src/lib/domain/db-field.ts
@@ -2,9 +2,10 @@ import { z } from 'zod';
import {
dataTypeSchema,
findDataTypeDataById,
+ supportsArrayDataType,
type DataType,
} from '../data/data-types/data-types';
-import type { DatabaseType } from './database-type';
+import { DatabaseType } from './database-type';
export interface DBField {
id: string;
@@ -14,6 +15,7 @@ export interface DBField {
unique: boolean;
nullable: boolean;
increment?: boolean | null;
+ isArray?: boolean | null;
createdAt: number;
characterMaximumLength?: string | null;
precision?: number | null;
@@ -21,6 +23,7 @@ export interface DBField {
default?: string | null;
collation?: string | null;
comments?: string | null;
+ check?: string | null;
}
export const dbFieldSchema: z.ZodType = z.object({
@@ -31,6 +34,7 @@ export const dbFieldSchema: z.ZodType = z.object({
unique: z.boolean(),
nullable: z.boolean(),
increment: z.boolean().or(z.null()).optional(),
+ isArray: z.boolean().or(z.null()).optional(),
createdAt: z.number(),
characterMaximumLength: z.string().or(z.null()).optional(),
precision: z.number().or(z.null()).optional(),
@@ -38,6 +42,7 @@ export const dbFieldSchema: z.ZodType = z.object({
default: z.string().or(z.null()).optional(),
collation: z.string().or(z.null()).optional(),
comments: z.string().or(z.null()).optional(),
+ check: z.string().or(z.null()).optional(),
});
export const generateDBFieldSuffix = (
@@ -52,11 +57,26 @@ export const generateDBFieldSuffix = (
typeId?: string;
} = {}
): string => {
+ let suffix = '';
+
if (databaseType && forceExtended && typeId) {
- return generateExtendedSuffix(field, databaseType, typeId);
+ suffix = generateExtendedSuffix(field, databaseType, typeId);
+ } else {
+ suffix = generateStandardSuffix(field);
+ }
+
+ // Add array notation if field is an array
+ if (
+ field.isArray &&
+ supportsArrayDataType(
+ typeId ?? field.type.id,
+ databaseType ?? DatabaseType.GENERIC
+ )
+ ) {
+ suffix += '[]';
}
- return generateStandardSuffix(field);
+ return suffix;
};
const generateExtendedSuffix = (
diff --git a/src/lib/domain/db-index.ts b/src/lib/domain/db-index.ts
index b9668daf5..1f047611d 100644
--- a/src/lib/domain/db-index.ts
+++ b/src/lib/domain/db-index.ts
@@ -2,6 +2,7 @@ import { z } from 'zod';
import { generateId } from '../utils';
import { DatabaseType } from './database-type';
import type { DBTable } from './db-table';
+import type { DBField } from './db-field';
export const INDEX_TYPES = [
'btree',
@@ -41,10 +42,66 @@ export const dbIndexSchema: z.ZodType = z.object({
isPrimaryKey: z.boolean().or(z.null()).optional(),
});
-export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
- [DatabaseType.POSTGRESQL]: ['btree', 'hash'],
+export const databaseIndexTypes: Record =
+ {
+ [DatabaseType.POSTGRESQL]: ['btree', 'hash', 'gin'],
+ [DatabaseType.COCKROACHDB]: ['btree', 'hash', 'gin'],
+ [DatabaseType.MYSQL]: undefined,
+ [DatabaseType.MARIADB]: undefined,
+ [DatabaseType.SQL_SERVER]: undefined,
+ [DatabaseType.SQLITE]: undefined,
+ [DatabaseType.CLICKHOUSE]: undefined,
+ [DatabaseType.ORACLE]: undefined,
+ [DatabaseType.GENERIC]: undefined,
+ };
+
+export const defaultIndexTypeForDatabase: Record<
+ DatabaseType,
+ IndexType | undefined
+> = {
+ [DatabaseType.POSTGRESQL]: 'btree',
+ [DatabaseType.COCKROACHDB]: 'btree',
+ [DatabaseType.MYSQL]: undefined,
+ [DatabaseType.MARIADB]: undefined,
+ [DatabaseType.SQL_SERVER]: undefined,
+ [DatabaseType.SQLITE]: undefined,
+ [DatabaseType.CLICKHOUSE]: undefined,
+ [DatabaseType.ORACLE]: undefined,
+ [DatabaseType.GENERIC]: undefined,
+};
+
+// Data types that support GIN indexes in PostgreSQL/CockroachDB
+const GIN_SUPPORTED_TYPES = ['jsonb', 'json', 'tsvector', 'hstore'] as const;
+
+export const supportsGinIndex = (field: DBField): boolean => {
+ if (field.isArray) return true;
+ const typeLower = field.type.id.toLowerCase();
+ return GIN_SUPPORTED_TYPES.includes(
+ typeLower as (typeof GIN_SUPPORTED_TYPES)[number]
+ );
};
+export const canFieldsUseGinIndex = (fields: DBField[]): boolean => {
+ return fields.length > 0 && fields.every(supportsGinIndex);
+};
+
+export interface IndexTypeConfig {
+ label: string;
+ value: IndexType;
+ disabledTooltip?: string;
+}
+
+export const INDEX_TYPE_CONFIGS: IndexTypeConfig[] = [
+ { label: 'B-tree (default)', value: 'btree' },
+ { label: 'Hash', value: 'hash' },
+ {
+ label: 'GIN',
+ value: 'gin',
+ disabledTooltip:
+ 'GIN indexes require array, jsonb, json, tsvector, or hstore types',
+ },
+];
+
export const getTablePrimaryKeyIndex = ({
table,
}: {
@@ -66,9 +123,10 @@ export const getTablePrimaryKeyIndex = ({
};
} else {
// Create new PK index for primary key(s)
+ // Use empty name for auto-generated PK indexes to indicate no CONSTRAINT should be used
const pkIndex: DBIndex = {
id: generateId(),
- name: `pk_${table.name}_${primaryKeyFields.map((f) => f.name).join('_')}`,
+ name: '',
fieldIds: pkFieldIds,
unique: true,
isPrimaryKey: true,
diff --git a/src/lib/domain/db-schema.ts b/src/lib/domain/db-schema.ts
index e1fe87f94..3cafa8956 100644
--- a/src/lib/domain/db-schema.ts
+++ b/src/lib/domain/db-schema.ts
@@ -1,4 +1,5 @@
-import { DatabaseType } from './database-type';
+import { DATABASE_CAPABILITIES } from './database-capabilities';
+import type { DatabaseType } from './database-type';
export interface DBSchema {
id: string;
@@ -18,10 +19,8 @@ export const schemaNameToDomainSchemaName = (
? undefined
: schema?.trim();
-export const databasesWithSchemas: DatabaseType[] = [
- DatabaseType.POSTGRESQL,
- DatabaseType.SQL_SERVER,
- DatabaseType.CLICKHOUSE,
- DatabaseType.COCKROACHDB,
- DatabaseType.ORACLE,
-];
+export const databasesWithSchemas: DatabaseType[] = Object.keys(
+ DATABASE_CAPABILITIES
+).filter(
+ (dbType) => DATABASE_CAPABILITIES[dbType as DatabaseType].supportsSchemas
+) as DatabaseType[];
diff --git a/src/lib/domain/db-table.ts b/src/lib/domain/db-table.ts
index 87f6e62f9..246542cd3 100644
--- a/src/lib/domain/db-table.ts
+++ b/src/lib/domain/db-table.ts
@@ -1,7 +1,11 @@
import { dbIndexSchema, type DBIndex } from './db-index';
import { dbFieldSchema, type DBField } from './db-field';
import type { DBRelationship } from './db-relationship';
-import { deepCopy } from '../utils';
+import {
+ dbCheckConstraintSchema,
+ type DBCheckConstraint,
+} from './db-check-constraint';
+import { deepCopy, findContainingArea } from '../utils';
import { schemaNameToDomainSchemaName } from './db-schema';
import { z } from 'zod';
import type { Area } from './area';
@@ -19,6 +23,7 @@ export interface DBTable {
y: number;
fields: DBField[];
indexes: DBIndex[];
+ checkConstraints?: DBCheckConstraint[] | null;
color: string;
isView: boolean;
isMaterializedView?: boolean | null;
@@ -38,6 +43,7 @@ export const dbTableSchema: z.ZodType = z.object({
y: z.number(),
fields: z.array(dbFieldSchema),
indexes: z.array(dbIndexSchema),
+ checkConstraints: z.array(dbCheckConstraintSchema).or(z.null()).optional(),
color: z.string(),
isView: z.boolean(),
isMaterializedView: z.boolean().or(z.null()).optional(),
@@ -78,6 +84,13 @@ export const adjustTablePositions = ({
return adjustTablePositionsWithoutAreas(tables, relationships, mode);
}
+ // Update parentAreaId based on geometric containment before grouping
+ // This ensures tables that are visually inside an area get assigned to it
+ tables.forEach((table) => {
+ const containingArea = findContainingArea(table, areas);
+ table.parentAreaId = containingArea?.id || null;
+ });
+
// Group tables by their parent area
const tablesByArea = new Map();
diff --git a/src/lib/domain/diagram.ts b/src/lib/domain/diagram.ts
index a0548e418..c53e39cce 100644
--- a/src/lib/domain/diagram.ts
+++ b/src/lib/domain/diagram.ts
@@ -10,6 +10,8 @@ import { dbTableSchema } from './db-table';
import { areaSchema, type Area } from './area';
import type { DBCustomType } from './db-custom-type';
import { dbCustomTypeSchema } from './db-custom-type';
+import type { Note } from './note';
+import { noteSchema } from './note';
export interface Diagram {
id: string;
@@ -21,6 +23,7 @@ export interface Diagram {
dependencies?: DBDependency[];
areas?: Area[];
customTypes?: DBCustomType[];
+ notes?: Note[];
createdAt: Date;
updatedAt: Date;
}
@@ -35,6 +38,7 @@ export const diagramSchema: z.ZodType = z.object({
dependencies: z.array(dbDependencySchema).optional(),
areas: z.array(areaSchema).optional(),
customTypes: z.array(dbCustomTypeSchema).optional(),
+ notes: z.array(noteSchema).optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
diff --git a/src/lib/domain/diff/area-diff.ts b/src/lib/domain/diff/area-diff.ts
new file mode 100644
index 000000000..b533ec1a8
--- /dev/null
+++ b/src/lib/domain/diff/area-diff.ts
@@ -0,0 +1,79 @@
+import { z } from 'zod';
+import type { Area } from '../area';
+
+export type AreaDiffAttribute = keyof Pick<
+ Area,
+ 'name' | 'color' | 'x' | 'y' | 'width' | 'height'
+>;
+
+const areaDiffAttributeSchema: z.ZodType = z.union([
+ z.literal('name'),
+ z.literal('color'),
+ z.literal('x'),
+ z.literal('y'),
+ z.literal('width'),
+ z.literal('height'),
+]);
+
+export interface AreaDiffChanged {
+ object: 'area';
+ type: 'changed';
+ areaId: string;
+ newAreaId: string;
+ attribute: AreaDiffAttribute;
+ oldValue?: string | number | null;
+ newValue?: string | number | null;
+}
+
+export const AreaDiffChangedSchema: z.ZodType = z.object({
+ object: z.literal('area'),
+ type: z.literal('changed'),
+ areaId: z.string(),
+ newAreaId: z.string(),
+ attribute: areaDiffAttributeSchema,
+ oldValue: z.union([z.string(), z.number(), z.null()]).optional(),
+ newValue: z.union([z.string(), z.number(), z.null()]).optional(),
+});
+
+export interface AreaDiffRemoved {
+ object: 'area';
+ type: 'removed';
+ areaId: string;
+}
+
+export const AreaDiffRemovedSchema: z.ZodType = z.object({
+ object: z.literal('area'),
+ type: z.literal('removed'),
+ areaId: z.string(),
+});
+
+export interface AreaDiffAdded {
+ object: 'area';
+ type: 'added';
+ areaAdded: T;
+}
+
+export const createAreaDiffAddedSchema = (
+ areaSchema: z.ZodType
+): z.ZodType> => {
+ return z.object({
+ object: z.literal('area'),
+ type: z.literal('added'),
+ areaAdded: areaSchema,
+ }) as z.ZodType>;
+};
+
+export type AreaDiff =
+ | AreaDiffChanged
+ | AreaDiffRemoved
+ | AreaDiffAdded;
+
+export const createAreaDiffSchema = (
+ areaSchema: z.ZodType
+): z.ZodType> => {
+ return z.union([
+ AreaDiffChangedSchema,
+ AreaDiffRemovedSchema,
+ createAreaDiffAddedSchema(areaSchema),
+ ]) as z.ZodType>;
+};
diff --git a/src/lib/domain/diff/check-constraint-diff.ts b/src/lib/domain/diff/check-constraint-diff.ts
new file mode 100644
index 000000000..683872af6
--- /dev/null
+++ b/src/lib/domain/diff/check-constraint-diff.ts
@@ -0,0 +1,78 @@
+import { z } from 'zod';
+import type { DBCheckConstraint } from '../db-check-constraint';
+
+export type CheckConstraintDiffAttribute = 'expression';
+
+export const checkConstraintDiffAttributeSchema: z.ZodType =
+ z.literal('expression');
+
+export interface CheckConstraintDiffAdded {
+ object: 'checkConstraint';
+ type: 'added';
+ tableId: string;
+ newCheckConstraint: T;
+}
+
+export const createCheckConstraintDiffAddedSchema = (
+ checkConstraintSchema: z.ZodType
+): z.ZodType> => {
+ return z.object({
+ object: z.literal('checkConstraint'),
+ type: z.literal('added'),
+ tableId: z.string(),
+ newCheckConstraint: checkConstraintSchema,
+ }) as z.ZodType>;
+};
+
+export interface CheckConstraintDiffRemoved {
+ object: 'checkConstraint';
+ type: 'removed';
+ checkConstraintId: string;
+ tableId: string;
+}
+
+export const checkConstraintDiffRemovedSchema: z.ZodType =
+ z.object({
+ object: z.literal('checkConstraint'),
+ type: z.literal('removed'),
+ checkConstraintId: z.string(),
+ tableId: z.string(),
+ });
+
+export interface CheckConstraintDiffChanged {
+ object: 'checkConstraint';
+ type: 'changed';
+ checkConstraintId: string;
+ newCheckConstraintId: string;
+ tableId: string;
+ attribute: CheckConstraintDiffAttribute;
+ oldValue?: string | null;
+ newValue?: string | null;
+}
+
+export const checkConstraintDiffChangedSchema: z.ZodType =
+ z.object({
+ object: z.literal('checkConstraint'),
+ type: z.literal('changed'),
+ checkConstraintId: z.string(),
+ newCheckConstraintId: z.string(),
+ tableId: z.string(),
+ attribute: checkConstraintDiffAttributeSchema,
+ oldValue: z.string().nullable().optional(),
+ newValue: z.string().nullable().optional(),
+ });
+
+export type CheckConstraintDiff =
+ | CheckConstraintDiffAdded
+ | CheckConstraintDiffRemoved
+ | CheckConstraintDiffChanged;
+
+export const createCheckConstraintDiffSchema = (
+ checkConstraintSchema: z.ZodType
+): z.ZodType> => {
+ return z.union([
+ createCheckConstraintDiffAddedSchema(checkConstraintSchema),
+ checkConstraintDiffRemovedSchema,
+ checkConstraintDiffChangedSchema,
+ ]) as z.ZodType>;
+};
diff --git a/src/lib/domain/diff/diff-check/__tests__/diff-check.test.ts b/src/lib/domain/diff/diff-check/__tests__/diff-check.test.ts
new file mode 100644
index 000000000..a2eed36b1
--- /dev/null
+++ b/src/lib/domain/diff/diff-check/__tests__/diff-check.test.ts
@@ -0,0 +1,3018 @@
+import { describe, it, expect } from 'vitest';
+import { generateDiff } from '../diff-check';
+import type { Diagram } from '@/lib/domain/diagram';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBField } from '@/lib/domain/db-field';
+import type { DBIndex } from '@/lib/domain/db-index';
+import type { DBRelationship } from '@/lib/domain/db-relationship';
+import type { Area } from '@/lib/domain/area';
+import type { Note } from '@/lib/domain/note';
+import { DatabaseType } from '@/lib/domain/database-type';
+import type { TableDiffChanged } from '../../table-diff';
+import type { FieldDiffChanged } from '../../field-diff';
+import type { AreaDiffChanged } from '../../area-diff';
+import type { NoteDiffChanged } from '../../note-diff';
+import type { IndexDiffChanged } from '../../index-diff';
+import type { RelationshipDiffChanged } from '../../relationship-diff';
+
+// Helper function to create a mock diagram
+function createMockDiagram(overrides?: Partial): Diagram {
+ return {
+ id: 'diagram-1',
+ name: 'Test Diagram',
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [],
+ relationships: [],
+ areas: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ ...overrides,
+ };
+}
+
+// Helper function to create a mock table
+function createMockTable(overrides?: Partial): DBTable {
+ return {
+ id: 'table-1',
+ name: 'users',
+ fields: [],
+ indexes: [],
+ x: 0,
+ y: 0,
+ ...overrides,
+ } as DBTable;
+}
+
+// Helper function to create a mock field
+function createMockField(overrides?: Partial): DBField {
+ return {
+ id: 'field-1',
+ name: 'id',
+ type: { id: 'integer', name: 'integer' },
+ primaryKey: false,
+ nullable: true,
+ unique: false,
+ ...overrides,
+ } as DBField;
+}
+
+// Helper function to create a mock relationship
+function createMockRelationship(
+ overrides?: Partial
+): DBRelationship {
+ return {
+ id: 'rel-1',
+ name: 'fk_default',
+ sourceTableId: 'table-1',
+ targetTableId: 'table-2',
+ sourceFieldId: 'field-1',
+ targetFieldId: 'field-2',
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ createdAt: Date.now(),
+ ...overrides,
+ } as DBRelationship;
+}
+
+// Helper function to create a mock area
+function createMockArea(overrides?: Partial ): Area {
+ return {
+ id: 'area-1',
+ name: 'Main Area',
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ color: 'blue',
+ ...overrides,
+ } as Area;
+}
+
+// Helper function to create a mock note
+function createMockNote(overrides?: Partial): Note {
+ return {
+ id: 'note-1',
+ content: 'Test note content',
+ x: 0,
+ y: 0,
+ width: 200,
+ height: 150,
+ color: '#3b82f6',
+ ...overrides,
+ } as Note;
+}
+
+// Helper function to create a mock index
+function createMockIndex(overrides?: Partial): DBIndex {
+ return {
+ id: 'index-1',
+ name: 'idx_users_email',
+ unique: false,
+ fieldIds: ['field-1'],
+ createdAt: Date.now(),
+ ...overrides,
+ } as DBIndex;
+}
+
+describe('generateDiff', () => {
+ describe('Basic Table Diffing', () => {
+ it('should detect added tables', () => {
+ const oldDiagram = createMockDiagram({ tables: [] });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable()],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('table-table-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('added');
+ expect(result.changedTables.has('table-1')).toBe(true);
+ });
+
+ it('should detect removed tables', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable()],
+ });
+ const newDiagram = createMockDiagram({ tables: [] });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('table-table-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('removed');
+ expect(result.changedTables.has('table-1')).toBe(true);
+ });
+
+ it('should detect table name changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ name: 'users' })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ name: 'customers' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('table-name-table-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as TableDiffChanged)?.attribute).toBe('name');
+ });
+
+ it('should detect table position changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ x: 0, y: 0 })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ x: 100, y: 200 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ tables: ['name', 'comments', 'color', 'x', 'y'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(2);
+ expect(result.diffMap.has('table-x-table-1')).toBe(true);
+ expect(result.diffMap.has('table-y-table-1')).toBe(true);
+ });
+
+ it('should detect table width changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ width: 150 })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ width: 250 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ tables: ['width'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('table-width-table-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as TableDiffChanged)?.attribute).toBe('width');
+ expect((diff as TableDiffChanged)?.oldValue).toBe(150);
+ expect((diff as TableDiffChanged)?.newValue).toBe(250);
+ });
+
+ it('should detect multiple table dimension changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ x: 0, y: 0, width: 100 })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ x: 50, y: 75, width: 200 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ tables: ['x', 'y', 'width'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(3);
+ expect(result.diffMap.has('table-x-table-1')).toBe(true);
+ expect(result.diffMap.has('table-y-table-1')).toBe(true);
+ expect(result.diffMap.has('table-width-table-1')).toBe(true);
+
+ const widthDiff = result.diffMap.get('table-width-table-1');
+ expect(widthDiff?.type).toBe('changed');
+ expect((widthDiff as TableDiffChanged)?.oldValue).toBe(100);
+ expect((widthDiff as TableDiffChanged)?.newValue).toBe(200);
+ });
+ });
+
+ describe('Field Diffing', () => {
+ it('should detect added fields', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ fields: [] })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ fields: [createMockField()],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('field-field-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('added');
+ expect(result.changedFields.has('field-1')).toBe(true);
+ });
+
+ it('should detect removed fields', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ fields: [createMockField()],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ fields: [] })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('field-field-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('removed');
+ });
+
+ it('should detect field type changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ fields: [
+ createMockField({
+ type: { id: 'integer', name: 'integer' },
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ fields: [
+ createMockField({
+ type: { id: 'varchar', name: 'varchar' },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('field-type-field-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as FieldDiffChanged)?.attribute).toBe('type');
+ });
+ });
+
+ describe('Index Diffing', () => {
+ it('should detect added indexes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ indexes: [] })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex()],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('added');
+ expect(result.changedIndexes.has('index-1')).toBe(true);
+ });
+
+ it('should detect removed indexes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex()],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ indexes: [] })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('removed');
+ expect(result.changedIndexes.has('index-1')).toBe(true);
+ });
+
+ it('should detect index name changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ name: 'idx_old_name' })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ name: 'idx_new_name' })],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-name-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as IndexDiffChanged)?.attribute).toBe('name');
+ expect((diff as IndexDiffChanged)?.oldValue).toBe('idx_old_name');
+ expect((diff as IndexDiffChanged)?.newValue).toBe('idx_new_name');
+ expect(result.changedIndexes.has('index-1')).toBe(true);
+ });
+
+ it('should detect index unique changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ unique: false })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ unique: true })],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-unique-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as IndexDiffChanged)?.attribute).toBe('unique');
+ expect((diff as IndexDiffChanged)?.oldValue).toBe(false);
+ expect((diff as IndexDiffChanged)?.newValue).toBe(true);
+ });
+
+ it('should detect index fieldIds changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ fieldIds: ['field-1'] })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ fieldIds: ['field-1', 'field-2'],
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-fieldIds-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as IndexDiffChanged)?.attribute).toBe('fieldIds');
+ expect((diff as IndexDiffChanged)?.oldValue).toEqual(['field-1']);
+ expect((diff as IndexDiffChanged)?.newValue).toEqual([
+ 'field-1',
+ 'field-2',
+ ]);
+ });
+
+ it('should detect index type changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: 'btree' })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: 'hash' })],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-type-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as IndexDiffChanged)?.attribute).toBe('type');
+ expect((diff as IndexDiffChanged)?.oldValue).toBe('btree');
+ expect((diff as IndexDiffChanged)?.newValue).toBe('hash');
+ });
+
+ it('should not detect index type change when both are undefined/null (use database default)', () => {
+ const oldDiagram = createMockDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: undefined })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: null })],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.has('index-type-index-1')).toBe(false);
+ });
+
+ it('should not detect index type change when one is undefined and the other is the database default', () => {
+ const oldDiagram = createMockDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: undefined })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: 'btree' })],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // btree is the default for PostgreSQL, so undefined vs 'btree' should be equal
+ expect(result.diffMap.has('index-type-index-1')).toBe(false);
+ });
+
+ it('should detect index type change when one is undefined and the other differs from database default', () => {
+ const oldDiagram = createMockDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: undefined })],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ databaseType: DatabaseType.POSTGRESQL,
+ tables: [
+ createMockTable({
+ indexes: [createMockIndex({ type: 'hash' })],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // undefined defaults to 'btree' for PostgreSQL, so undefined vs 'hash' should be different
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-type-index-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as IndexDiffChanged)?.attribute).toBe('type');
+ });
+
+ it('should detect multiple index attribute changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ name: 'idx_old',
+ unique: false,
+ type: 'btree',
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ name: 'idx_new',
+ unique: true,
+ type: 'hash',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(3);
+ expect(result.diffMap.has('index-name-index-1')).toBe(true);
+ expect(result.diffMap.has('index-unique-index-1')).toBe(true);
+ expect(result.diffMap.has('index-type-index-1')).toBe(true);
+ });
+
+ it('should only check specified index attributes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ name: 'idx_old',
+ unique: false,
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ name: 'idx_new',
+ unique: true,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ indexes: ['name'], // Only check name changes
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ expect(result.diffMap.has('index-name-index-1')).toBe(true);
+ expect(result.diffMap.has('index-unique-index-1')).toBe(false);
+ });
+
+ it('should match indexes by name when IDs differ', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ id: 'index-old-id',
+ name: 'idx_email',
+ unique: false,
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ id: 'index-new-id',
+ name: 'idx_email',
+ unique: true,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // Should detect as change (matched by name), not add+remove
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('index-unique-index-old-id');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ });
+
+ it('should match indexes by fieldIds when IDs and names differ', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ id: 'index-old-id',
+ name: 'idx_old_name',
+ fieldIds: ['field-1', 'field-2'],
+ unique: false,
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ id: 'index-new-id',
+ name: 'idx_new_name',
+ fieldIds: ['field-1', 'field-2'],
+ unique: true,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // Should detect name and unique as changes (matched by fieldIds)
+ expect(result.diffMap.size).toBe(2);
+ expect(result.diffMap.has('index-name-index-old-id')).toBe(true);
+ expect(result.diffMap.has('index-unique-index-old-id')).toBe(true);
+ });
+
+ it('should prioritize ID matching over name and fieldIds', () => {
+ // Even if name and fieldIds match a different index,
+ // ID matching takes priority
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ id: 'index-1',
+ name: 'idx_email',
+ fieldIds: ['field-1'],
+ unique: false,
+ }),
+ createMockIndex({
+ id: 'index-2',
+ name: 'idx_other',
+ fieldIds: ['field-2'],
+ unique: false,
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ indexes: [
+ createMockIndex({
+ id: 'index-1',
+ name: 'idx_renamed', // Name changed
+ fieldIds: ['field-1'],
+ unique: true, // Unique changed
+ }),
+ createMockIndex({
+ id: 'index-2',
+ name: 'idx_email', // Has old index-1's name
+ fieldIds: ['field-2'],
+ unique: false,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // index-1 should match by ID, detecting name and unique changes
+ expect(result.diffMap.has('index-name-index-1')).toBe(true);
+ expect(result.diffMap.has('index-unique-index-1')).toBe(true);
+
+ // index-2 should match by ID, detecting only name change
+ expect(result.diffMap.has('index-name-index-2')).toBe(true);
+
+ // No added or removed indexes
+ const added = Array.from(result.diffMap.values()).filter(
+ (d) => d.type === 'added' && d.object === 'index'
+ );
+ const removed = Array.from(result.diffMap.values()).filter(
+ (d) => d.type === 'removed' && d.object === 'index'
+ );
+ expect(added.length).toBe(0);
+ expect(removed.length).toBe(0);
+ });
+ });
+
+ describe('Relationship Diffing', () => {
+ it('should detect added relationships', () => {
+ const oldDiagram = createMockDiagram({ relationships: [] });
+ const newDiagram = createMockDiagram({
+ relationships: [createMockRelationship()],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('added');
+ expect(result.changedRelationships.has('rel-1')).toBe(true);
+ });
+
+ it('should detect removed relationships', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [createMockRelationship()],
+ });
+ const newDiagram = createMockDiagram({ relationships: [] });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('removed');
+ expect(result.changedRelationships.has('rel-1')).toBe(true);
+ });
+
+ it('should detect relationship name changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ name: 'fk_old_name' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ name: 'fk_new_name' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-name-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe('name');
+ expect((diff as RelationshipDiffChanged)?.oldValue).toBe(
+ 'fk_old_name'
+ );
+ expect((diff as RelationshipDiffChanged)?.newValue).toBe(
+ 'fk_new_name'
+ );
+ expect(result.changedRelationships.has('rel-1')).toBe(true);
+ });
+
+ it('should detect relationship sourceCardinality changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceCardinality: 'one' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceCardinality: 'many' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get(
+ 'relationship-sourceCardinality-rel-1'
+ );
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'sourceCardinality'
+ );
+ expect((diff as RelationshipDiffChanged)?.oldValue).toBe('one');
+ expect((diff as RelationshipDiffChanged)?.newValue).toBe('many');
+ });
+
+ it('should detect relationship targetCardinality changes', () => {
+ // Use (many, many) -> (many, one) to test targetCardinality change detection
+ // This is NOT equivalent since source is 'many' in both cases
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'many',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'many',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get(
+ 'relationship-targetCardinality-rel-1'
+ );
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'targetCardinality'
+ );
+ expect((diff as RelationshipDiffChanged)?.oldValue).toBe('many');
+ expect((diff as RelationshipDiffChanged)?.newValue).toBe('one');
+ });
+
+ it('should NOT detect cardinality changes between (one,one) and (one,many) as they produce same DDL', () => {
+ // (one,one) -> (one,many): source stays 'one', equivalent from DDL perspective
+ const oldDiagram1 = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+ const newDiagram1 = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+
+ const result1 = generateDiff({
+ diagram: oldDiagram1,
+ newDiagram: newDiagram1,
+ });
+
+ // Should NOT detect any cardinality changes
+ expect(
+ result1.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(false);
+ expect(
+ result1.diffMap.has('relationship-targetCardinality-rel-1')
+ ).toBe(false);
+ expect(result1.diffMap.size).toBe(0);
+
+ // (one,many) -> (one,one): reverse direction, also equivalent
+ const oldDiagram2 = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+ const newDiagram2 = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+
+ const result2 = generateDiff({
+ diagram: oldDiagram2,
+ newDiagram: newDiagram2,
+ });
+
+ expect(
+ result2.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(false);
+ expect(
+ result2.diffMap.has('relationship-targetCardinality-rel-1')
+ ).toBe(false);
+ expect(result2.diffMap.size).toBe(0);
+ });
+
+ it('should still detect cardinality changes that produce different DDL', () => {
+ // (one,one) -> (many,one): source changes from 'one' to 'many', real change
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'one',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ sourceCardinality: 'many',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // Should detect the sourceCardinality change
+ expect(
+ result.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(true);
+ expect(result.diffMap.size).toBe(1);
+ });
+
+ it('should detect multiple relationship attribute changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_old',
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_new',
+ sourceCardinality: 'many',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(3);
+ expect(result.diffMap.has('relationship-name-rel-1')).toBe(true);
+ expect(
+ result.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(true);
+ expect(
+ result.diffMap.has('relationship-targetCardinality-rel-1')
+ ).toBe(true);
+ });
+
+ it('should only check specified relationship attributes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_old',
+ sourceCardinality: 'one',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_new',
+ sourceCardinality: 'many',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ relationships: ['name'], // Only check name changes
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ expect(result.diffMap.has('relationship-name-rel-1')).toBe(true);
+ expect(
+ result.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(false);
+ });
+
+ it('should only check specified relationship change types', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ id: 'rel-1', name: 'fk_old' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ // Use different sourceFieldId to ensure it's a truly different relationship
+ createMockRelationship({
+ id: 'rel-2',
+ name: 'fk_new',
+ sourceFieldId: 'field-3',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ changeTypes: {
+ relationships: ['added'], // Only check for added relationships
+ },
+ },
+ });
+
+ // Should only detect added relationship (rel-2)
+ const addedRelationships = Array.from(
+ result.diffMap.values()
+ ).filter(
+ (diff) =>
+ diff.type === 'added' && diff.object === 'relationship'
+ );
+ expect(addedRelationships.length).toBe(1);
+
+ // Should not detect removed relationship (rel-1)
+ const removedRelationships = Array.from(
+ result.diffMap.values()
+ ).filter(
+ (diff) =>
+ diff.type === 'removed' && diff.object === 'relationship'
+ );
+ expect(removedRelationships.length).toBe(0);
+ });
+
+ it('should use custom relationship matcher', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ id: 'rel-1',
+ name: 'fk_users_orders',
+ sourceCardinality: 'one',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ id: 'rel-2',
+ name: 'fk_users_orders',
+ sourceCardinality: 'many',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ matchers: {
+ relationship: (rel, rels) =>
+ rels.find((r) => r.name === rel.name),
+ },
+ },
+ });
+
+ // With name-based matching, rel-1 should match rel-2 by name
+ // and detect the sourceCardinality change
+ const cardinalityChange = result.diffMap.get(
+ 'relationship-sourceCardinality-rel-1'
+ );
+ expect(cardinalityChange).toBeDefined();
+ expect(cardinalityChange?.type).toBe('changed');
+ expect(
+ (cardinalityChange as RelationshipDiffChanged)?.attribute
+ ).toBe('sourceCardinality');
+ expect(
+ (cardinalityChange as RelationshipDiffChanged)?.oldValue
+ ).toBe('one');
+ expect(
+ (cardinalityChange as RelationshipDiffChanged)?.newValue
+ ).toBe('many');
+ });
+
+ it('should not detect changes when relationships are unchanged', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_users_orders',
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_users_orders',
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ expect(result.changedRelationships.size).toBe(0);
+ });
+
+ it('should detect relationship sourceSchema changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceSchema: 'public' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceSchema: 'private' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-sourceSchema-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'sourceSchema'
+ );
+ expect((diff as RelationshipDiffChanged)?.oldValue).toBe('public');
+ expect((diff as RelationshipDiffChanged)?.newValue).toBe('private');
+ });
+
+ it('should detect relationship targetSchema changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ targetSchema: 'schema_a' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ targetSchema: 'schema_b' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-targetSchema-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'targetSchema'
+ );
+ });
+
+ it('should detect relationship sourceTableId changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceTableId: 'table-1' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceTableId: 'table-3' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-sourceTableId-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'sourceTableId'
+ );
+ expect((diff as RelationshipDiffChanged)?.oldValue).toBe('table-1');
+ expect((diff as RelationshipDiffChanged)?.newValue).toBe('table-3');
+ });
+
+ it('should detect relationship targetTableId changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ targetTableId: 'table-2' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ targetTableId: 'table-4' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-targetTableId-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'targetTableId'
+ );
+ });
+
+ it('should detect relationship sourceFieldId changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceFieldId: 'field-1' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ sourceFieldId: 'field-5' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-sourceFieldId-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'sourceFieldId'
+ );
+ expect((diff as RelationshipDiffChanged)?.oldValue).toBe('field-1');
+ expect((diff as RelationshipDiffChanged)?.newValue).toBe('field-5');
+ });
+
+ it('should detect relationship targetFieldId changes', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ targetFieldId: 'field-2' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({ targetFieldId: 'field-6' }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('relationship-targetFieldId-rel-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as RelationshipDiffChanged)?.attribute).toBe(
+ 'targetFieldId'
+ );
+ });
+
+ it('should detect all relationship attribute changes simultaneously', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_old',
+ sourceSchema: 'public',
+ sourceTableId: 'table-1',
+ targetSchema: 'public',
+ targetTableId: 'table-2',
+ sourceFieldId: 'field-1',
+ targetFieldId: 'field-2',
+ sourceCardinality: 'one',
+ targetCardinality: 'many',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_new',
+ sourceSchema: 'private',
+ sourceTableId: 'table-3',
+ targetSchema: 'private',
+ targetTableId: 'table-4',
+ sourceFieldId: 'field-5',
+ targetFieldId: 'field-6',
+ sourceCardinality: 'many',
+ targetCardinality: 'one',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(9);
+ expect(result.diffMap.has('relationship-name-rel-1')).toBe(true);
+ expect(result.diffMap.has('relationship-sourceSchema-rel-1')).toBe(
+ true
+ );
+ expect(result.diffMap.has('relationship-sourceTableId-rel-1')).toBe(
+ true
+ );
+ expect(result.diffMap.has('relationship-targetSchema-rel-1')).toBe(
+ true
+ );
+ expect(result.diffMap.has('relationship-targetTableId-rel-1')).toBe(
+ true
+ );
+ expect(result.diffMap.has('relationship-sourceFieldId-rel-1')).toBe(
+ true
+ );
+ expect(result.diffMap.has('relationship-targetFieldId-rel-1')).toBe(
+ true
+ );
+ expect(
+ result.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(true);
+ expect(
+ result.diffMap.has('relationship-targetCardinality-rel-1')
+ ).toBe(true);
+ });
+
+ it('should filter relationship attributes correctly', () => {
+ const oldDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_old',
+ sourceSchema: 'public',
+ sourceTableId: 'table-1',
+ sourceCardinality: 'one',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ relationships: [
+ createMockRelationship({
+ name: 'fk_new',
+ sourceSchema: 'private',
+ sourceTableId: 'table-3',
+ sourceCardinality: 'many',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ relationships: ['name', 'sourceCardinality'], // Only check these
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(2);
+ expect(result.diffMap.has('relationship-name-rel-1')).toBe(true);
+ expect(
+ result.diffMap.has('relationship-sourceCardinality-rel-1')
+ ).toBe(true);
+ // Should not detect sourceSchema or sourceTableId changes
+ expect(result.diffMap.has('relationship-sourceSchema-rel-1')).toBe(
+ false
+ );
+ expect(result.diffMap.has('relationship-sourceTableId-rel-1')).toBe(
+ false
+ );
+ });
+ });
+
+ describe('Area Diffing', () => {
+ it('should detect added areas when includeAreas is true', () => {
+ const oldDiagram = createMockDiagram({ areas: [] });
+ const newDiagram = createMockDiagram({
+ areas: [createMockArea()],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeAreas: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('area-area-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('added');
+ expect(result.changedAreas.has('area-1')).toBe(true);
+ });
+
+ it('should not detect area changes when includeAreas is false', () => {
+ const oldDiagram = createMockDiagram({ areas: [] });
+ const newDiagram = createMockDiagram({
+ areas: [createMockArea()],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeAreas: false,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should detect area width changes', () => {
+ const oldDiagram = createMockDiagram({
+ areas: [createMockArea({ width: 100 })],
+ });
+ const newDiagram = createMockDiagram({
+ areas: [createMockArea({ width: 200 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeAreas: true,
+ attributes: {
+ areas: ['width'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('area-width-area-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as AreaDiffChanged)?.attribute).toBe('width');
+ expect((diff as AreaDiffChanged)?.oldValue).toBe(100);
+ expect((diff as AreaDiffChanged)?.newValue).toBe(200);
+ });
+
+ it('should detect area height changes', () => {
+ const oldDiagram = createMockDiagram({
+ areas: [createMockArea({ height: 100 })],
+ });
+ const newDiagram = createMockDiagram({
+ areas: [createMockArea({ height: 300 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeAreas: true,
+ attributes: {
+ areas: ['height'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('area-height-area-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as AreaDiffChanged)?.attribute).toBe('height');
+ expect((diff as AreaDiffChanged)?.oldValue).toBe(100);
+ expect((diff as AreaDiffChanged)?.newValue).toBe(300);
+ });
+
+ it('should detect multiple area dimension changes', () => {
+ const oldDiagram = createMockDiagram({
+ areas: [
+ createMockArea({ x: 0, y: 0, width: 100, height: 100 }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ areas: [
+ createMockArea({ x: 50, y: 50, width: 200, height: 300 }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeAreas: true,
+ attributes: {
+ areas: ['x', 'y', 'width', 'height'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(4);
+ expect(result.diffMap.has('area-x-area-1')).toBe(true);
+ expect(result.diffMap.has('area-y-area-1')).toBe(true);
+ expect(result.diffMap.has('area-width-area-1')).toBe(true);
+ expect(result.diffMap.has('area-height-area-1')).toBe(true);
+ });
+ });
+
+ describe('Note Diffing', () => {
+ it('should detect added notes when includeNotes is true', () => {
+ const oldDiagram = createMockDiagram({ notes: [] });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote()],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('added');
+ expect(result.changedNotes.has('note-1')).toBe(true);
+ });
+
+ it('should not detect note changes when includeNotes is false', () => {
+ const oldDiagram = createMockDiagram({ notes: [] });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote()],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: false,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ expect(result.changedNotes.size).toBe(0);
+ });
+
+ it('should detect removed notes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote()],
+ });
+ const newDiagram = createMockDiagram({ notes: [] });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('removed');
+ expect(result.changedNotes.has('note-1')).toBe(true);
+ });
+
+ it('should detect note content changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote({ content: 'Old content' })],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote({ content: 'New content' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-content-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as NoteDiffChanged)?.attribute).toBe('content');
+ expect((diff as NoteDiffChanged)?.oldValue).toBe('Old content');
+ expect((diff as NoteDiffChanged)?.newValue).toBe('New content');
+ });
+
+ it('should detect note color changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote({ color: '#3b82f6' })],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote({ color: '#ef4444' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-color-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as NoteDiffChanged)?.attribute).toBe('color');
+ expect((diff as NoteDiffChanged)?.oldValue).toBe('#3b82f6');
+ expect((diff as NoteDiffChanged)?.newValue).toBe('#ef4444');
+ });
+
+ it('should detect note position changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote({ x: 0, y: 0 })],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote({ x: 100, y: 200 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ attributes: {
+ notes: ['content', 'color', 'x', 'y'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(2);
+ expect(result.diffMap.has('note-x-note-1')).toBe(true);
+ expect(result.diffMap.has('note-y-note-1')).toBe(true);
+
+ const xDiff = result.diffMap.get('note-x-note-1');
+ expect((xDiff as NoteDiffChanged)?.oldValue).toBe(0);
+ expect((xDiff as NoteDiffChanged)?.newValue).toBe(100);
+ });
+
+ it('should detect note width changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote({ width: 200 })],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote({ width: 300 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ attributes: {
+ notes: ['width'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-width-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as NoteDiffChanged)?.attribute).toBe('width');
+ expect((diff as NoteDiffChanged)?.oldValue).toBe(200);
+ expect((diff as NoteDiffChanged)?.newValue).toBe(300);
+ });
+
+ it('should detect note height changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote({ height: 150 })],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote({ height: 250 })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ attributes: {
+ notes: ['height'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-height-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as NoteDiffChanged)?.attribute).toBe('height');
+ expect((diff as NoteDiffChanged)?.oldValue).toBe(150);
+ expect((diff as NoteDiffChanged)?.newValue).toBe(250);
+ });
+
+ it('should detect multiple note dimension changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({ x: 0, y: 0, width: 200, height: 150 }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({ x: 50, y: 75, width: 300, height: 250 }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ attributes: {
+ notes: ['x', 'y', 'width', 'height'],
+ },
+ },
+ });
+
+ expect(result.diffMap.size).toBe(4);
+ expect(result.diffMap.has('note-x-note-1')).toBe(true);
+ expect(result.diffMap.has('note-y-note-1')).toBe(true);
+ expect(result.diffMap.has('note-width-note-1')).toBe(true);
+ expect(result.diffMap.has('note-height-note-1')).toBe(true);
+
+ const widthDiff = result.diffMap.get('note-width-note-1');
+ expect((widthDiff as NoteDiffChanged)?.oldValue).toBe(200);
+ expect((widthDiff as NoteDiffChanged)?.newValue).toBe(300);
+
+ const heightDiff = result.diffMap.get('note-height-note-1');
+ expect((heightDiff as NoteDiffChanged)?.oldValue).toBe(150);
+ expect((heightDiff as NoteDiffChanged)?.newValue).toBe(250);
+ });
+
+ it('should detect multiple notes with different changes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({ id: 'note-1', content: 'Note 1' }),
+ createMockNote({ id: 'note-2', content: 'Note 2' }),
+ createMockNote({ id: 'note-3', content: 'Note 3' }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'Note 1 Updated',
+ }), // Changed
+ createMockNote({ id: 'note-2', content: 'Note 2' }), // Unchanged
+ // note-3 removed
+ createMockNote({ id: 'note-4', content: 'Note 4' }), // Added
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ // Should detect: 1 content change, 1 removal, 1 addition
+ expect(result.diffMap.has('note-content-note-1')).toBe(true); // Changed
+ expect(result.diffMap.has('note-note-3')).toBe(true); // Removed
+ expect(result.diffMap.has('note-note-4')).toBe(true); // Added
+
+ expect(result.changedNotes.has('note-1')).toBe(true);
+ expect(result.changedNotes.has('note-3')).toBe(true);
+ expect(result.changedNotes.has('note-4')).toBe(true);
+ });
+
+ it('should use custom note matcher', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'Unique content',
+ color: '#3b82f6',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-2',
+ content: 'Unique content',
+ color: '#ef4444',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ matchers: {
+ note: (note, notes) =>
+ notes.find((n) => n.content === note.content),
+ },
+ },
+ });
+
+ // With content-based matching, note-1 should match note-2 by content
+ // and detect the color change
+ const colorChange = result.diffMap.get('note-color-note-1');
+ expect(colorChange).toBeDefined();
+ expect(colorChange?.type).toBe('changed');
+ expect((colorChange as NoteDiffChanged)?.attribute).toBe('color');
+ expect((colorChange as NoteDiffChanged)?.oldValue).toBe('#3b82f6');
+ expect((colorChange as NoteDiffChanged)?.newValue).toBe('#ef4444');
+ });
+
+ it('should only check specified note change types', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [createMockNote({ id: 'note-1', content: 'Note 1' })],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [createMockNote({ id: 'note-2', content: 'Note 2' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ changeTypes: {
+ notes: ['added'], // Only check for added notes
+ },
+ },
+ });
+
+ // Should only detect added note (note-2)
+ const addedNotes = Array.from(result.diffMap.values()).filter(
+ (diff) => diff.type === 'added' && diff.object === 'note'
+ );
+ expect(addedNotes.length).toBe(1);
+
+ // Should not detect removed note (note-1)
+ const removedNotes = Array.from(result.diffMap.values()).filter(
+ (diff) => diff.type === 'removed' && diff.object === 'note'
+ );
+ expect(removedNotes.length).toBe(0);
+ });
+
+ it('should only check specified note attributes', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'Old content',
+ color: '#3b82f6',
+ x: 0,
+ y: 0,
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'New content',
+ color: '#ef4444',
+ x: 100,
+ y: 200,
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ attributes: {
+ notes: ['content'], // Only check content changes
+ },
+ },
+ });
+
+ // Should only detect content change
+ const contentChanges = Array.from(result.diffMap.values()).filter(
+ (diff) =>
+ diff.type === 'changed' &&
+ diff.attribute === 'content' &&
+ diff.object === 'note'
+ );
+ expect(contentChanges.length).toBe(1);
+
+ // Should not detect color or position changes
+ const otherChanges = Array.from(result.diffMap.values()).filter(
+ (diff) =>
+ diff.type === 'changed' &&
+ (diff.attribute === 'color' ||
+ diff.attribute === 'x' ||
+ diff.attribute === 'y') &&
+ diff.object === 'note'
+ );
+ expect(otherChanges.length).toBe(0);
+ });
+ });
+
+ describe('Custom Matchers', () => {
+ it('should use custom table matcher to match by name', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1', name: 'users' })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-2', name: 'users' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ matchers: {
+ table: (table, tables) =>
+ tables.find((t) => t.name === table.name),
+ },
+ },
+ });
+
+ // Should not detect any changes since tables match by name
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should detect changes when custom matcher finds no match', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1', name: 'users' })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-2', name: 'customers' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ matchers: {
+ table: (table, tables) =>
+ tables.find((t) => t.name === table.name),
+ },
+ },
+ });
+
+ // Should detect both added and removed since names don't match
+ expect(result.diffMap.size).toBe(2);
+ expect(result.diffMap.has('table-table-1')).toBe(true); // removed
+ expect(result.diffMap.has('table-table-2')).toBe(true); // added
+ });
+
+ it('should use custom field matcher to match by name', () => {
+ const field1 = createMockField({
+ id: 'field-1',
+ name: 'email',
+ nullable: true,
+ });
+ const field2 = createMockField({
+ id: 'field-2',
+ name: 'email',
+ nullable: false,
+ });
+
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1', fields: [field1] })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1', fields: [field2] })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ matchers: {
+ field: (field, fields) =>
+ fields.find((f) => f.name === field.name),
+ },
+ },
+ });
+
+ // With name-based matching, field-1 should match field-2 by name
+ // and detect the nullable change
+ const nullableChange = result.diffMap.get('field-nullable-field-1');
+ expect(nullableChange).toBeDefined();
+ expect(nullableChange?.type).toBe('changed');
+ expect((nullableChange as FieldDiffChanged)?.attribute).toBe(
+ 'nullable'
+ );
+ });
+
+ it('should use case-insensitive custom matcher', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1', name: 'Users' })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-2', name: 'users' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ matchers: {
+ table: (table, tables) =>
+ tables.find(
+ (t) =>
+ t.name.toLowerCase() ===
+ table.name.toLowerCase()
+ ),
+ },
+ },
+ });
+
+ // With case-insensitive name matching, the tables are matched
+ // but the name case difference is still detected as a change
+ expect(result.diffMap.size).toBe(1);
+ const nameChange = result.diffMap.get('table-name-table-1');
+ expect(nameChange).toBeDefined();
+ expect(nameChange?.type).toBe('changed');
+ expect((nameChange as TableDiffChanged)?.attribute).toBe('name');
+ expect((nameChange as TableDiffChanged)?.oldValue).toBe('Users');
+ expect((nameChange as TableDiffChanged)?.newValue).toBe('users');
+ });
+ });
+
+ describe('Filtering Options', () => {
+ it('should only check specified change types', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1', name: 'users' })],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [createMockTable({ id: 'table-2', name: 'products' })],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ changeTypes: {
+ tables: ['added'], // Only check for added tables
+ },
+ },
+ });
+
+ // Should only detect added table (table-2)
+ const addedTables = Array.from(result.diffMap.values()).filter(
+ (diff) => diff.type === 'added' && diff.object === 'table'
+ );
+ expect(addedTables.length).toBe(1);
+
+ // Should not detect removed table (table-1)
+ const removedTables = Array.from(result.diffMap.values()).filter(
+ (diff) => diff.type === 'removed' && diff.object === 'table'
+ );
+ expect(removedTables.length).toBe(0);
+ });
+
+ it('should only check specified attributes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ name: 'users',
+ color: 'blue',
+ comments: 'old comment',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ name: 'customers',
+ color: 'red',
+ comments: 'new comment',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ attributes: {
+ tables: ['name'], // Only check name changes
+ },
+ },
+ });
+
+ // Should only detect name change
+ const nameChanges = Array.from(result.diffMap.values()).filter(
+ (diff) =>
+ diff.type === 'changed' &&
+ diff.attribute === 'name' &&
+ diff.object === 'table'
+ );
+ expect(nameChanges.length).toBe(1);
+
+ // Should not detect color or comments changes
+ const otherChanges = Array.from(result.diffMap.values()).filter(
+ (diff) =>
+ diff.type === 'changed' &&
+ (diff.attribute === 'color' ||
+ diff.attribute === 'comments') &&
+ diff.object === 'table'
+ );
+ expect(otherChanges.length).toBe(0);
+ });
+
+ it('should respect include flags', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ fields: [createMockField()],
+ indexes: [{ id: 'idx-1', name: 'idx' } as DBIndex],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ fields: [],
+ indexes: [],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeFields: false,
+ includeIndexes: true,
+ },
+ });
+
+ // Should only detect index removal, not field removal
+ expect(result.diffMap.has('index-idx-1')).toBe(true);
+ expect(result.diffMap.has('field-field-1')).toBe(false);
+ });
+ });
+
+ describe('Smart Comment Comparison', () => {
+ describe('Table Comments', () => {
+ it('should not detect change when comments differ only by newlines vs spaces', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments:
+ '| Source: [DB].[schema].[table].[col] | Target: [DW].[reporting].[dim_items].[item_id] | Description: Primary key | Notes: none',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments:
+ '| Source: [DB].[schema].[table].[col]\n| Target: [DW].[reporting].[dim_items].[item_id]\n| Description: Primary key\n| Notes: none',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // Should not detect any changes
+ expect(result.diffMap.size).toBe(0);
+ expect(result.changedTables.size).toBe(0);
+ });
+
+ it('should not detect change when comments differ by multiple spaces', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments:
+ 'Column description with extra spaces',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'Column description with extra spaces',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should not detect change when comments differ by tabs vs spaces', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'Key\tValue\tPairs',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'Key Value Pairs',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should not detect change when comments differ by leading/trailing whitespace', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: ' Table description ',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'Table description',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should detect change when comments have actual content differences', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'Old description',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'New description',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('table-comments-table-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as TableDiffChanged)?.attribute).toBe('comments');
+ });
+
+ it('should detect change when one comment is empty and other has content', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: '',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: 'New comment',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ });
+
+ it('should not detect change when both comments are undefined', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: undefined,
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: undefined,
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should not detect change when one is undefined and other is whitespace-only', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: undefined,
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: ' ',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+ });
+
+ describe('Field Comments', () => {
+ it('should not detect change when field comments differ only by newlines vs spaces', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments:
+ '| Type: FK | Ref: orders.id | Info: Order reference',
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments:
+ '| Type: FK\n| Ref: orders.id\n| Info: Order reference',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ expect(result.changedFields.size).toBe(0);
+ });
+
+ it('should not detect change when field comments differ by CRLF vs LF', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments: 'Line 1\r\nLine 2\r\nLine 3',
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments: 'Line 1\nLine 2\nLine 3',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should detect change when field comments have actual content differences', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments: 'Primary key for users table',
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments: 'Unique identifier for customers',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('field-comments-field-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as FieldDiffChanged)?.attribute).toBe('comments');
+ });
+ });
+
+ describe('Note Content', () => {
+ it('should not detect change when note content differs only by newlines vs spaces', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content:
+ '# Title | Section 1 | Section 2 | Section 3',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content:
+ '# Title\n| Section 1\n| Section 2\n| Section 3',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ expect(result.changedNotes.size).toBe(0);
+ });
+
+ it('should not detect change when note content differs by mixed whitespace', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content:
+ 'Important\t\tnote\n\nwith mixed\r\n\r\nwhitespace',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'Important note with mixed whitespace',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should detect change when note content has actual differences', () => {
+ const oldDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'TODO: Implement feature X',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ content: 'DONE: Feature X implemented',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeNotes: true,
+ },
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('note-content-note-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ expect((diff as NoteDiffChanged)?.attribute).toBe('content');
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle complex multi-line structured comments', () => {
+ const structuredComment1 =
+ '| Column: user_id | Type: INT | Nullable: NO | Default: AUTO_INCREMENT | FK: users.id | Description: Foreign key reference to users table';
+ const structuredComment2 =
+ '| Column: user_id\n| Type: INT\n| Nullable: NO\n| Default: AUTO_INCREMENT\n| FK: users.id\n| Description: Foreign key reference to users table';
+
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments: structuredComment1,
+ }),
+ ],
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ fields: [
+ createMockField({
+ id: 'field-1',
+ comments: structuredComment2,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ });
+
+ it('should preserve detection of actual word changes in multi-line comments', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: '| Status: Active\n| Owner: Team A',
+ }),
+ ],
+ });
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ comments: '| Status: Deprecated\n| Owner: Team B',
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ expect(result.diffMap.size).toBe(1);
+ const diff = result.diffMap.get('table-comments-table-1');
+ expect(diff).toBeDefined();
+ expect(diff?.type).toBe('changed');
+ });
+ });
+ });
+
+ describe('Complex Scenarios', () => {
+ it('should detect all dimensional changes for tables, areas, and notes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ x: 0,
+ y: 0,
+ width: 100,
+ }),
+ ],
+ areas: [
+ createMockArea({
+ id: 'area-1',
+ x: 0,
+ y: 0,
+ width: 200,
+ height: 150,
+ }),
+ ],
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ x: 0,
+ y: 0,
+ width: 300,
+ height: 200,
+ }),
+ ],
+ });
+
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ x: 10,
+ y: 20,
+ width: 120,
+ }),
+ ],
+ areas: [
+ createMockArea({
+ id: 'area-1',
+ x: 25,
+ y: 35,
+ width: 250,
+ height: 175,
+ }),
+ ],
+ notes: [
+ createMockNote({
+ id: 'note-1',
+ x: 40,
+ y: 50,
+ width: 350,
+ height: 225,
+ }),
+ ],
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ options: {
+ includeAreas: true,
+ includeNotes: true,
+ attributes: {
+ tables: ['x', 'y', 'width'],
+ areas: ['x', 'y', 'width', 'height'],
+ notes: ['x', 'y', 'width', 'height'],
+ },
+ },
+ });
+
+ // Table dimensional changes
+ expect(result.diffMap.has('table-x-table-1')).toBe(true);
+ expect(result.diffMap.has('table-y-table-1')).toBe(true);
+ expect(result.diffMap.has('table-width-table-1')).toBe(true);
+
+ // Area dimensional changes
+ expect(result.diffMap.has('area-x-area-1')).toBe(true);
+ expect(result.diffMap.has('area-y-area-1')).toBe(true);
+ expect(result.diffMap.has('area-width-area-1')).toBe(true);
+ expect(result.diffMap.has('area-height-area-1')).toBe(true);
+
+ // Note dimensional changes
+ expect(result.diffMap.has('note-x-note-1')).toBe(true);
+ expect(result.diffMap.has('note-y-note-1')).toBe(true);
+ expect(result.diffMap.has('note-width-note-1')).toBe(true);
+ expect(result.diffMap.has('note-height-note-1')).toBe(true);
+
+ // Verify the correct values
+ const tableWidthDiff = result.diffMap.get('table-width-table-1');
+ expect((tableWidthDiff as TableDiffChanged)?.oldValue).toBe(100);
+ expect((tableWidthDiff as TableDiffChanged)?.newValue).toBe(120);
+
+ const areaWidthDiff = result.diffMap.get('area-width-area-1');
+ expect((areaWidthDiff as AreaDiffChanged)?.oldValue).toBe(200);
+ expect((areaWidthDiff as AreaDiffChanged)?.newValue).toBe(250);
+
+ const areaHeightDiff = result.diffMap.get('area-height-area-1');
+ expect((areaHeightDiff as AreaDiffChanged)?.oldValue).toBe(150);
+ expect((areaHeightDiff as AreaDiffChanged)?.newValue).toBe(175);
+
+ const noteWidthDiff = result.diffMap.get('note-width-note-1');
+ expect((noteWidthDiff as NoteDiffChanged)?.oldValue).toBe(300);
+ expect((noteWidthDiff as NoteDiffChanged)?.newValue).toBe(350);
+
+ const noteHeightDiff = result.diffMap.get('note-height-note-1');
+ expect((noteHeightDiff as NoteDiffChanged)?.oldValue).toBe(200);
+ expect((noteHeightDiff as NoteDiffChanged)?.newValue).toBe(225);
+ });
+
+ it('should handle multiple simultaneous changes', () => {
+ const oldDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ name: 'users',
+ fields: [
+ createMockField({ id: 'field-1', name: 'id' }),
+ createMockField({ id: 'field-2', name: 'email' }),
+ ],
+ }),
+ createMockTable({
+ id: 'table-2',
+ name: 'products',
+ }),
+ ],
+ relationships: [createMockRelationship()],
+ });
+
+ const newDiagram = createMockDiagram({
+ tables: [
+ createMockTable({
+ id: 'table-1',
+ name: 'customers', // Changed name
+ fields: [
+ createMockField({ id: 'field-1', name: 'id' }),
+ // Removed field-2
+ createMockField({ id: 'field-3', name: 'name' }), // Added field
+ ],
+ }),
+ // Removed table-2
+ createMockTable({
+ id: 'table-3',
+ name: 'orders', // Added table
+ }),
+ ],
+ relationships: [], // Removed relationship
+ });
+
+ const result = generateDiff({
+ diagram: oldDiagram,
+ newDiagram,
+ });
+
+ // Verify all changes are detected
+ expect(result.diffMap.has('table-name-table-1')).toBe(true); // Table name change
+ expect(result.diffMap.has('field-field-2')).toBe(true); // Removed field
+ expect(result.diffMap.has('field-field-3')).toBe(true); // Added field
+ expect(result.diffMap.has('table-table-2')).toBe(true); // Removed table
+ expect(result.diffMap.has('table-table-3')).toBe(true); // Added table
+ expect(result.diffMap.has('relationship-rel-1')).toBe(true); // Removed relationship
+ });
+
+ it('should handle empty diagrams', () => {
+ const emptyDiagram1 = createMockDiagram();
+ const emptyDiagram2 = createMockDiagram();
+
+ const result = generateDiff({
+ diagram: emptyDiagram1,
+ newDiagram: emptyDiagram2,
+ });
+
+ expect(result.diffMap.size).toBe(0);
+ expect(result.changedTables.size).toBe(0);
+ expect(result.changedFields.size).toBe(0);
+ expect(result.changedAreas.size).toBe(0);
+ expect(result.changedNotes.size).toBe(0);
+ });
+
+ it('should handle diagrams with undefined collections', () => {
+ const diagram1 = createMockDiagram({
+ tables: undefined,
+ relationships: undefined,
+ areas: undefined,
+ notes: undefined,
+ });
+ const diagram2 = createMockDiagram({
+ tables: [createMockTable({ id: 'table-1' })],
+ relationships: [createMockRelationship({ id: 'rel-1' })],
+ areas: [createMockArea({ id: 'area-1' })],
+ notes: [createMockNote({ id: 'note-1' })],
+ });
+
+ const result = generateDiff({
+ diagram: diagram1,
+ newDiagram: diagram2,
+ options: {
+ includeAreas: true,
+ includeNotes: true,
+ },
+ });
+
+ // Should detect all as added
+ expect(result.diffMap.has('table-table-1')).toBe(true);
+ expect(result.diffMap.has('relationship-rel-1')).toBe(true);
+ expect(result.diffMap.has('area-area-1')).toBe(true);
+ expect(result.diffMap.has('note-note-1')).toBe(true);
+ });
+ });
+});
diff --git a/src/lib/domain/diff/diff-check/diff-check.ts b/src/lib/domain/diff/diff-check/diff-check.ts
index f2d1916d4..6c3e3421a 100644
--- a/src/lib/domain/diff/diff-check/diff-check.ts
+++ b/src/lib/domain/diff/diff-check/diff-check.ts
@@ -1,8 +1,33 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { DBField } from '@/lib/domain/db-field';
-import type { DBIndex } from '@/lib/domain/db-index';
+import {
+ defaultIndexTypeForDatabase,
+ type DBIndex,
+} from '@/lib/domain/db-index';
+import type { DBCheckConstraint } from '@/lib/domain/db-check-constraint';
+import type { DBTable } from '@/lib/domain/db-table';
+import type { DBRelationship } from '@/lib/domain/db-relationship';
+import type { Area } from '@/lib/domain/area';
+import type { Note } from '@/lib/domain/note';
import type { ChartDBDiff, DiffMap, DiffObject } from '@/lib/domain/diff/diff';
-import type { FieldDiffAttribute } from '@/lib/domain/diff/field-diff';
+import type {
+ FieldDiff,
+ FieldDiffAttribute,
+} from '@/lib/domain/diff/field-diff';
+import type { TableDiff, TableDiffAttribute } from '../table-diff';
+import type { AreaDiff, AreaDiffAttribute } from '../area-diff';
+import type { NoteDiff, NoteDiffAttribute } from '../note-diff';
+import type { IndexDiff, IndexDiffAttribute } from '../index-diff';
+import type {
+ CheckConstraintDiff,
+ CheckConstraintDiffAttribute,
+} from '../check-constraint-diff';
+import type {
+ RelationshipDiff,
+ RelationshipDiffAttribute,
+} from '../relationship-diff';
+import { areBooleansEqual } from '@/lib/utils';
+import type { DatabaseType } from '../../database-type';
export function getDiffMapKey({
diffObject,
@@ -18,37 +43,284 @@ export function getDiffMapKey({
: `${diffObject}-${objectId}`;
}
+const isOneOfDefined = (
+ ...values: (string | number | boolean | undefined | null)[]
+): boolean => {
+ return values.some((value) => value !== undefined && value !== null);
+};
+
+const normalizeBoolean = (value: boolean | undefined | null): boolean => {
+ return value === true;
+};
+
+/**
+ * Normalizes a comment/content string for comparison purposes.
+ * This handles cases where the same content differs only in whitespace formatting,
+ * such as newlines vs spaces, multiple spaces, or different line break styles.
+ *
+ * Examples that will be considered equal:
+ * - "| A | B" vs "| A\n| B"
+ * - "hello world" vs "hello world"
+ * - "line1\r\nline2" vs "line1\nline2"
+ */
+const normalizeComment = (
+ value: string | undefined | null
+): string | undefined => {
+ if (value === undefined || value === null) {
+ return undefined;
+ }
+ // Replace all whitespace sequences (newlines, tabs, multiple spaces) with a single space
+ // Then trim leading/trailing whitespace
+ return value.replace(/\s+/g, ' ').trim();
+};
+
+/**
+ * Compares two comment/content strings in a whitespace-insensitive manner.
+ * Returns true if the comments are semantically different (i.e., a real change).
+ */
+const areCommentsDifferent = (
+ oldComment: string | undefined | null,
+ newComment: string | undefined | null
+): boolean => {
+ const normalizedOld = normalizeComment(oldComment);
+ const normalizedNew = normalizeComment(newComment);
+
+ // Both undefined/empty means equal
+ if (!normalizedOld && !normalizedNew) {
+ return false;
+ }
+
+ // One defined, one not means different
+ if (!normalizedOld || !normalizedNew) {
+ return true;
+ }
+
+ // Compare normalized versions
+ return normalizedOld !== normalizedNew;
+};
+
+// Helper to determine if an attribute change should add to the changed map
+// - undefined: always add (current behavior)
+// - empty array: never add
+// - array with values: only add if attribute is in the array
+const shouldAddToChangedMap = (
+ attribute: T,
+ changedAttributes?: T[]
+): boolean => {
+ if (changedAttributes === undefined) {
+ return true;
+ }
+ if (changedAttributes.length === 0) {
+ return false;
+ }
+ return changedAttributes.includes(attribute);
+};
+
+export interface GenerateDiffOptions {
+ includeTables?: boolean;
+ includeFields?: boolean;
+ includeIndexes?: boolean;
+ includeCheckConstraints?: boolean;
+ includeRelationships?: boolean;
+ includeAreas?: boolean;
+ includeNotes?: boolean;
+ attributes?: {
+ tables?: TableDiffAttribute[];
+ fields?: FieldDiffAttribute[];
+ indexes?: IndexDiffAttribute[];
+ checkConstraints?: CheckConstraintDiffAttribute[];
+ relationships?: RelationshipDiffAttribute[];
+ areas?: AreaDiffAttribute[];
+ notes?: NoteDiffAttribute[];
+ };
+ changedMaps?: {
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedFieldsAttributes?: FieldDiffAttribute[];
+ changedIndexesAttributes?: IndexDiffAttribute[];
+ changedCheckConstraintsAttributes?: CheckConstraintDiffAttribute[];
+ changedRelationshipsAttributes?: RelationshipDiffAttribute[];
+ changedAreasAttributes?: AreaDiffAttribute[];
+ changedNotesAttributes?: NoteDiffAttribute[];
+ };
+ changeTypes?: {
+ tables?: TableDiff['type'][];
+ fields?: FieldDiff['type'][];
+ indexes?: IndexDiff['type'][];
+ checkConstraints?: CheckConstraintDiff['type'][];
+ relationships?: RelationshipDiff['type'][];
+ areas?: AreaDiff['type'][];
+ notes?: NoteDiff['type'][];
+ };
+ matchers?: {
+ table?: (table: DBTable, tables: DBTable[]) => DBTable | undefined;
+ field?: (field: DBField, fields: DBField[]) => DBField | undefined;
+ index?: (index: DBIndex, indexes: DBIndex[]) => DBIndex | undefined;
+ checkConstraint?: (
+ constraint: DBCheckConstraint,
+ constraints: DBCheckConstraint[]
+ ) => DBCheckConstraint | undefined;
+ relationship?: (
+ relationship: DBRelationship,
+ relationships: DBRelationship[]
+ ) => DBRelationship | undefined;
+ area?: (area: Area, areas: Area[]) => Area | undefined;
+ note?: (note: Note, notes: Note[]) => Note | undefined;
+ };
+}
+
export function generateDiff({
diagram,
newDiagram,
+ options = {},
}: {
diagram: Diagram;
newDiagram: Diagram;
+ options?: GenerateDiffOptions;
}): {
diffMap: DiffMap;
changedTables: Map;
changedFields: Map;
+ changedIndexes: Map;
+ changedCheckConstraints: Map;
+ changedRelationships: Map;
+ changedAreas: Map;
+ changedNotes: Map;
+ relationshipIdMap: Map;
} {
+ // Merge with default options
+ const mergedOptions: GenerateDiffOptions = {
+ includeTables: options.includeTables ?? true,
+ includeFields: options.includeFields ?? true,
+ includeIndexes: options.includeIndexes ?? true,
+ includeCheckConstraints: options.includeCheckConstraints ?? true,
+ includeRelationships: options.includeRelationships ?? true,
+ includeAreas: options.includeAreas ?? false,
+ includeNotes: options.includeNotes ?? false,
+ attributes: options.attributes ?? {},
+ changedMaps: options.changedMaps,
+ changeTypes: options.changeTypes ?? {},
+ matchers: options.matchers ?? {},
+ };
+
const newDiffs = new Map();
const changedTables = new Map();
const changedFields = new Map();
+ const changedIndexes = new Map();
+ const changedCheckConstraints = new Map();
+ const changedRelationships = new Map();
+ const changedAreas = new Map();
+ const changedNotes = new Map();
+ const relationshipIdMap = new Map();
+
+ // Use provided matchers or default ones
+ const tableMatcher = mergedOptions.matchers?.table ?? defaultTableMatcher;
+ const fieldMatcher = mergedOptions.matchers?.field ?? defaultFieldMatcher;
+ const indexMatcher = mergedOptions.matchers?.index ?? defaultIndexMatcher;
+ const checkConstraintMatcher =
+ mergedOptions.matchers?.checkConstraint ??
+ defaultCheckConstraintMatcher;
+ const relationshipMatcher =
+ mergedOptions.matchers?.relationship ?? defaultRelationshipMatcher;
+ const areaMatcher = mergedOptions.matchers?.area ?? defaultAreaMatcher;
+ const noteMatcher = mergedOptions.matchers?.note ?? defaultNoteMatcher;
// Compare tables
- compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
+ if (mergedOptions.includeTables) {
+ compareTables({
+ diagram,
+ newDiagram,
+ diffMap: newDiffs,
+ changedTables,
+ attributes: mergedOptions.attributes?.tables,
+ changedTablesAttributes:
+ mergedOptions.changedMaps?.changedTablesAttributes,
+ changeTypes: mergedOptions.changeTypes?.tables,
+ tableMatcher,
+ });
+ }
- // Compare fields and indexes for matching tables
+ // Compare fields, indexes, and check constraints for matching tables
compareTableContents({
diagram,
newDiagram,
diffMap: newDiffs,
changedTables,
changedFields,
+ changedIndexes,
+ changedCheckConstraints,
+ options: mergedOptions,
+ changedTablesAttributes:
+ mergedOptions.changedMaps?.changedTablesAttributes,
+ changedFieldsAttributes:
+ mergedOptions.changedMaps?.changedFieldsAttributes,
+ changedIndexesAttributes:
+ mergedOptions.changedMaps?.changedIndexesAttributes,
+ changedCheckConstraintsAttributes:
+ mergedOptions.changedMaps?.changedCheckConstraintsAttributes,
+ tableMatcher,
+ fieldMatcher,
+ indexMatcher,
+ checkConstraintMatcher,
+ databaseType: diagram.databaseType,
});
// Compare relationships
- compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
+ if (mergedOptions.includeRelationships) {
+ compareRelationships({
+ diagram,
+ newDiagram,
+ diffMap: newDiffs,
+ changedRelationships,
+ relationshipIdMap,
+ attributes: mergedOptions.attributes?.relationships,
+ changedRelationshipsAttributes:
+ mergedOptions.changedMaps?.changedRelationshipsAttributes,
+ changeTypes: mergedOptions.changeTypes?.relationships,
+ relationshipMatcher,
+ });
+ }
+
+ // Compare areas if enabled
+ if (mergedOptions.includeAreas) {
+ compareAreas({
+ diagram,
+ newDiagram,
+ diffMap: newDiffs,
+ changedAreas,
+ attributes: mergedOptions.attributes?.areas,
+ changedAreasAttributes:
+ mergedOptions.changedMaps?.changedAreasAttributes,
+ changeTypes: mergedOptions.changeTypes?.areas,
+ areaMatcher,
+ });
+ }
- return { diffMap: newDiffs, changedTables, changedFields };
+ // Compare notes if enabled
+ if (mergedOptions.includeNotes) {
+ compareNotes({
+ diagram,
+ newDiagram,
+ diffMap: newDiffs,
+ changedNotes,
+ attributes: mergedOptions.attributes?.notes,
+ changedNotesAttributes:
+ mergedOptions.changedMaps?.changedNotesAttributes,
+ changeTypes: mergedOptions.changeTypes?.notes,
+ noteMatcher,
+ });
+ }
+
+ return {
+ diffMap: newDiffs,
+ changedTables,
+ changedFields,
+ changedIndexes,
+ changedCheckConstraints,
+ changedRelationships,
+ changedAreas,
+ changedNotes,
+ relationshipIdMap,
+ };
}
// Compare tables between diagrams
@@ -57,156 +329,340 @@ function compareTables({
newDiagram,
diffMap,
changedTables,
+ attributes,
+ changedTablesAttributes,
+ changeTypes,
+ tableMatcher,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedTables: Map;
+ attributes?: TableDiffAttribute[];
+ changedTablesAttributes?: TableDiffAttribute[];
+ changeTypes?: TableDiff['type'][];
+ tableMatcher: (table: DBTable, tables: DBTable[]) => DBTable | undefined;
}) {
const oldTables = diagram.tables || [];
const newTables = newDiagram.tables || [];
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
+
// Check for added tables
- for (const newTable of newTables) {
- if (!oldTables.find((t) => t.id === newTable.id)) {
- diffMap.set(
- getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
- {
- object: 'table',
- type: 'added',
- tableAdded: newTable,
- }
- );
- changedTables.set(newTable.id, true);
+ if (typesToCheck.includes('added')) {
+ for (const newTable of newTables) {
+ if (!tableMatcher(newTable, oldTables)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: newTable.id,
+ }),
+ {
+ object: 'table',
+ type: 'added',
+ tableAdded: newTable,
+ }
+ );
+ changedTables.set(newTable.id, true);
+ }
}
}
// Check for removed tables
- for (const oldTable of oldTables) {
- if (!newTables.find((t) => t.id === oldTable.id)) {
- diffMap.set(
- getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
- {
- object: 'table',
- type: 'removed',
- tableId: oldTable.id,
- }
- );
- changedTables.set(oldTable.id, true);
+ if (typesToCheck.includes('removed')) {
+ for (const oldTable of oldTables) {
+ if (!tableMatcher(oldTable, newTables)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ }),
+ {
+ object: 'table',
+ type: 'removed',
+ tableId: oldTable.id,
+ }
+ );
+ changedTables.set(oldTable.id, true);
+ }
}
}
// Check for table name, comments and color changes
- for (const oldTable of oldTables) {
- const newTable = newTables.find((t) => t.id === oldTable.id);
+ if (typesToCheck.includes('changed')) {
+ for (const oldTable of oldTables) {
+ const newTable = tableMatcher(oldTable, newTables);
- if (!newTable) continue;
+ if (!newTable) continue;
- if (oldTable.name !== newTable.name) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'table',
- objectId: oldTable.id,
- attribute: 'name',
- }),
- {
- object: 'table',
- type: 'changed',
- tableId: oldTable.id,
- attribute: 'name',
- newValue: newTable.name,
- oldValue: oldTable.name,
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: TableDiffAttribute[] = attributes ?? [
+ 'name',
+ 'comments',
+ 'color',
+ ];
+
+ if (
+ attributesToCheck.includes('name') &&
+ oldTable.name !== newTable.name
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ attribute: 'name',
+ }),
+ {
+ object: 'table',
+ type: 'changed',
+ tableId: oldTable.id,
+ newTableId: newTable.id,
+ attribute: 'name',
+ newValue: newTable.name,
+ oldValue: oldTable.name,
+ }
+ );
+
+ if (shouldAddToChangedMap('name', changedTablesAttributes)) {
+ changedTables.set(oldTable.id, true);
}
- );
+ }
- changedTables.set(oldTable.id, true);
- }
+ if (
+ attributesToCheck.includes('comments') &&
+ areCommentsDifferent(oldTable.comments, newTable.comments)
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ attribute: 'comments',
+ }),
+ {
+ object: 'table',
+ type: 'changed',
+ tableId: oldTable.id,
+ newTableId: newTable.id,
+ attribute: 'comments',
+ newValue: newTable.comments,
+ oldValue: oldTable.comments,
+ }
+ );
- if (
- (oldTable.comments || newTable.comments) &&
- oldTable.comments !== newTable.comments
- ) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'table',
- objectId: oldTable.id,
- attribute: 'comments',
- }),
- {
- object: 'table',
- type: 'changed',
- tableId: oldTable.id,
- attribute: 'comments',
- newValue: newTable.comments,
- oldValue: oldTable.comments,
+ if (
+ shouldAddToChangedMap('comments', changedTablesAttributes)
+ ) {
+ changedTables.set(oldTable.id, true);
}
- );
+ }
- changedTables.set(oldTable.id, true);
- }
+ if (
+ attributesToCheck.includes('color') &&
+ oldTable.color !== newTable.color
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ attribute: 'color',
+ }),
+ {
+ object: 'table',
+ type: 'changed',
+ tableId: oldTable.id,
+ newTableId: newTable.id,
+ attribute: 'color',
+ newValue: newTable.color,
+ oldValue: oldTable.color,
+ }
+ );
- if (oldTable.color !== newTable.color) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'table',
- objectId: oldTable.id,
- attribute: 'color',
- }),
- {
- object: 'table',
- type: 'changed',
- tableId: oldTable.id,
- attribute: 'color',
- newValue: newTable.color,
- oldValue: oldTable.color,
+ if (shouldAddToChangedMap('color', changedTablesAttributes)) {
+ changedTables.set(oldTable.id, true);
}
- );
+ }
+
+ if (attributesToCheck.includes('x') && oldTable.x !== newTable.x) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ attribute: 'x',
+ }),
+ {
+ object: 'table',
+ type: 'changed',
+ tableId: oldTable.id,
+ newTableId: newTable.id,
+ attribute: 'x',
+ newValue: newTable.x,
+ oldValue: oldTable.x,
+ }
+ );
+
+ if (shouldAddToChangedMap('x', changedTablesAttributes)) {
+ changedTables.set(oldTable.id, true);
+ }
+ }
+
+ if (attributesToCheck.includes('y') && oldTable.y !== newTable.y) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ attribute: 'y',
+ }),
+ {
+ object: 'table',
+ type: 'changed',
+ tableId: oldTable.id,
+ newTableId: newTable.id,
+ attribute: 'y',
+ newValue: newTable.y,
+ oldValue: oldTable.y,
+ }
+ );
+
+ if (shouldAddToChangedMap('y', changedTablesAttributes)) {
+ changedTables.set(oldTable.id, true);
+ }
+ }
- changedTables.set(oldTable.id, true);
+ if (
+ attributesToCheck.includes('width') &&
+ oldTable.width !== newTable.width
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'table',
+ objectId: oldTable.id,
+ attribute: 'width',
+ }),
+ {
+ object: 'table',
+ type: 'changed',
+ tableId: oldTable.id,
+ newTableId: newTable.id,
+ attribute: 'width',
+ newValue: newTable.width,
+ oldValue: oldTable.width,
+ }
+ );
+
+ if (shouldAddToChangedMap('width', changedTablesAttributes)) {
+ changedTables.set(oldTable.id, true);
+ }
+ }
}
}
}
-// Compare fields and indexes for matching tables
+// Compare fields, indexes, and check constraints for matching tables
function compareTableContents({
diagram,
newDiagram,
diffMap,
changedTables,
changedFields,
+ changedIndexes,
+ changedCheckConstraints,
+ options,
+ changedTablesAttributes,
+ changedFieldsAttributes,
+ changedIndexesAttributes,
+ changedCheckConstraintsAttributes,
+ tableMatcher,
+ fieldMatcher,
+ indexMatcher,
+ checkConstraintMatcher,
+ databaseType,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedTables: Map;
changedFields: Map;
+ changedIndexes: Map;
+ changedCheckConstraints: Map;
+ options?: GenerateDiffOptions;
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedFieldsAttributes?: FieldDiffAttribute[];
+ changedIndexesAttributes?: IndexDiffAttribute[];
+ changedCheckConstraintsAttributes?: CheckConstraintDiffAttribute[];
+ tableMatcher: (table: DBTable, tables: DBTable[]) => DBTable | undefined;
+ fieldMatcher: (field: DBField, fields: DBField[]) => DBField | undefined;
+ indexMatcher: (index: DBIndex, indexes: DBIndex[]) => DBIndex | undefined;
+ checkConstraintMatcher: (
+ constraint: DBCheckConstraint,
+ constraints: DBCheckConstraint[]
+ ) => DBCheckConstraint | undefined;
+ databaseType: DatabaseType;
}) {
const oldTables = diagram.tables || [];
const newTables = newDiagram.tables || [];
// For each table that exists in both diagrams
for (const oldTable of oldTables) {
- const newTable = newTables.find((t) => t.id === oldTable.id);
+ const newTable = tableMatcher(oldTable, newTables);
if (!newTable) continue;
// Compare fields
- compareFields({
- tableId: oldTable.id,
- oldFields: oldTable.fields,
- newFields: newTable.fields,
- diffMap,
- changedTables,
- changedFields,
- });
+ if (options?.includeFields) {
+ compareFields({
+ tableId: oldTable.id,
+ oldFields: oldTable.fields,
+ newFields: newTable.fields,
+ diffMap,
+ changedTables,
+ changedFields,
+ attributes: options?.attributes?.fields,
+ changedTablesAttributes,
+ changedFieldsAttributes,
+ changeTypes: options?.changeTypes?.fields,
+ fieldMatcher,
+ });
+ }
// Compare indexes
- compareIndexes({
- tableId: oldTable.id,
- oldIndexes: oldTable.indexes,
- newIndexes: newTable.indexes,
- diffMap,
- changedTables,
- });
+ if (options?.includeIndexes) {
+ compareIndexes({
+ tableId: oldTable.id,
+ oldIndexes: oldTable.indexes,
+ newIndexes: newTable.indexes,
+ diffMap,
+ changedTables,
+ changedIndexes,
+ attributes: options?.attributes?.indexes,
+ changedTablesAttributes,
+ changedIndexesAttributes,
+ changeTypes: options?.changeTypes?.indexes,
+ indexMatcher,
+ databaseType,
+ });
+ }
+
+ // Compare check constraints
+ if (options?.includeCheckConstraints) {
+ compareCheckConstraints({
+ tableId: oldTable.id,
+ oldCheckConstraints: oldTable.checkConstraints ?? [],
+ newCheckConstraints: newTable.checkConstraints ?? [],
+ diffMap,
+ changedTables,
+ changedCheckConstraints,
+ attributes: options?.attributes?.checkConstraints,
+ changedTablesAttributes,
+ changedCheckConstraintsAttributes,
+ changeTypes: options?.changeTypes?.checkConstraints,
+ checkConstraintMatcher,
+ });
+ }
}
}
@@ -218,6 +674,11 @@ function compareFields({
diffMap,
changedTables,
changedFields,
+ attributes,
+ changedTablesAttributes,
+ changedFieldsAttributes,
+ changeTypes,
+ fieldMatcher,
}: {
tableId: string;
oldFields: DBField[];
@@ -225,62 +686,83 @@ function compareFields({
diffMap: DiffMap;
changedTables: Map;
changedFields: Map;
+ attributes?: FieldDiffAttribute[];
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedFieldsAttributes?: FieldDiffAttribute[];
+ changeTypes?: FieldDiff['type'][];
+ fieldMatcher: (field: DBField, fields: DBField[]) => DBField | undefined;
}) {
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
// Check for added fields
- for (const newField of newFields) {
- if (!oldFields.find((f) => f.id === newField.id)) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'field',
- objectId: newField.id,
- }),
- {
- object: 'field',
- type: 'added',
- newField,
- tableId,
- }
- );
- changedTables.set(tableId, true);
- changedFields.set(newField.id, true);
+ if (typesToCheck.includes('added')) {
+ for (const newField of newFields) {
+ if (!fieldMatcher(newField, oldFields)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'field',
+ objectId: newField.id,
+ }),
+ {
+ object: 'field',
+ type: 'added',
+ newField,
+ tableId,
+ }
+ );
+ changedTables.set(tableId, true);
+ changedFields.set(newField.id, true);
+ }
}
}
// Check for removed fields
- for (const oldField of oldFields) {
- if (!newFields.find((f) => f.id === oldField.id)) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'field',
- objectId: oldField.id,
- }),
- {
- object: 'field',
- type: 'removed',
- fieldId: oldField.id,
- tableId,
- }
- );
+ if (typesToCheck.includes('removed')) {
+ for (const oldField of oldFields) {
+ if (!fieldMatcher(oldField, newFields)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'field',
+ objectId: oldField.id,
+ }),
+ {
+ object: 'field',
+ type: 'removed',
+ fieldId: oldField.id,
+ tableId,
+ }
+ );
- changedTables.set(tableId, true);
- changedFields.set(oldField.id, true);
+ changedTables.set(tableId, true);
+ changedFields.set(oldField.id, true);
+ }
}
}
// Check for field changes
- for (const oldField of oldFields) {
- const newField = newFields.find((f) => f.id === oldField.id);
- if (!newField) continue;
-
- // Compare basic field properties
- compareFieldProperties({
- tableId,
- oldField,
- newField,
- diffMap,
- changedTables,
- changedFields,
- });
+ if (typesToCheck.includes('changed')) {
+ for (const oldField of oldFields) {
+ const newField = fieldMatcher(oldField, newFields);
+ if (!newField) continue;
+
+ // Compare basic field properties
+ compareFieldProperties({
+ tableId,
+ oldField,
+ newField,
+ diffMap,
+ changedTables,
+ changedFields,
+ attributes,
+ changedTablesAttributes,
+ changedFieldsAttributes,
+ });
+ }
}
}
@@ -292,6 +774,9 @@ function compareFieldProperties({
diffMap,
changedTables,
changedFields,
+ attributes,
+ changedTablesAttributes,
+ changedFieldsAttributes,
}: {
tableId: string;
oldField: DBField;
@@ -299,37 +784,68 @@ function compareFieldProperties({
diffMap: DiffMap;
changedTables: Map;
changedFields: Map;
+ attributes?: FieldDiffAttribute[];
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedFieldsAttributes?: FieldDiffAttribute[];
}) {
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: FieldDiffAttribute[] = attributes ?? [
+ 'name',
+ 'type',
+ 'primaryKey',
+ 'unique',
+ 'nullable',
+ 'comments',
+ 'characterMaximumLength',
+ 'scale',
+ 'precision',
+ 'increment',
+ 'isArray',
+ ];
+
const changedAttributes: FieldDiffAttribute[] = [];
- if (oldField.name !== newField.name) {
+ if (attributesToCheck.includes('name') && oldField.name !== newField.name) {
changedAttributes.push('name');
}
- if (oldField.type.id !== newField.type.id) {
+ if (
+ attributesToCheck.includes('type') &&
+ oldField.type.id !== newField.type.id
+ ) {
changedAttributes.push('type');
}
- if (oldField.primaryKey !== newField.primaryKey) {
+ if (
+ attributesToCheck.includes('primaryKey') &&
+ oldField.primaryKey !== newField.primaryKey
+ ) {
changedAttributes.push('primaryKey');
}
- if (oldField.unique !== newField.unique) {
+ if (
+ attributesToCheck.includes('unique') &&
+ oldField.unique !== newField.unique
+ ) {
changedAttributes.push('unique');
}
- if (oldField.nullable !== newField.nullable) {
+ if (
+ attributesToCheck.includes('nullable') &&
+ oldField.nullable !== newField.nullable
+ ) {
changedAttributes.push('nullable');
}
if (
- (newField.comments || oldField.comments) &&
- oldField.comments !== newField.comments
+ attributesToCheck.includes('comments') &&
+ areCommentsDifferent(oldField.comments, newField.comments)
) {
changedAttributes.push('comments');
}
if (
+ attributesToCheck.includes('characterMaximumLength') &&
(newField.characterMaximumLength || oldField.characterMaximumLength) &&
oldField.characterMaximumLength !== newField.characterMaximumLength
) {
@@ -337,6 +853,7 @@ function compareFieldProperties({
}
if (
+ attributesToCheck.includes('scale') &&
(newField.scale || oldField.scale) &&
oldField.scale !== newField.scale
) {
@@ -344,13 +861,37 @@ function compareFieldProperties({
}
if (
+ attributesToCheck.includes('precision') &&
(newField.precision || oldField.precision) &&
oldField.precision !== newField.precision
) {
changedAttributes.push('precision');
}
+ if (
+ attributesToCheck.includes('increment') &&
+ isOneOfDefined(newField.increment, oldField.increment) &&
+ normalizeBoolean(oldField.increment) !==
+ normalizeBoolean(newField.increment)
+ ) {
+ changedAttributes.push('increment');
+ }
+
+ if (
+ attributesToCheck.includes('isArray') &&
+ isOneOfDefined(newField.isArray, oldField.isArray) &&
+ normalizeBoolean(oldField.isArray) !==
+ normalizeBoolean(newField.isArray)
+ ) {
+ changedAttributes.push('isArray');
+ }
+
if (changedAttributes.length > 0) {
+ // Track which attributes should trigger adding to changed maps
+ const attributesThatTriggerChange = changedAttributes.filter((attr) =>
+ shouldAddToChangedMap(attr, changedFieldsAttributes)
+ );
+
for (const attribute of changedAttributes) {
diffMap.set(
getDiffMapKey({
@@ -362,6 +903,7 @@ function compareFieldProperties({
object: 'field',
type: 'changed',
fieldId: oldField.id,
+ newFieldId: newField.id,
tableId,
attribute,
oldValue: oldField[attribute] ?? '',
@@ -369,8 +911,22 @@ function compareFieldProperties({
}
);
}
- changedTables.set(tableId, true);
- changedFields.set(oldField.id, true);
+
+ // Only add to changed maps if at least one attribute should trigger a change
+ if (attributesThatTriggerChange.length > 0) {
+ // For changedTables, we need to check changedTablesAttributes
+ // undefined = always add, empty = never add, array = check if any field attribute qualifies
+ if (changedTablesAttributes === undefined) {
+ changedTables.set(tableId, true);
+ } else if (changedTablesAttributes.length > 0) {
+ // If changedTablesAttributes has values, we only add if explicitly configured
+ // Since these are field changes, we keep current behavior of adding to changedTables
+ changedTables.set(tableId, true);
+ }
+ // If changedTablesAttributes is empty array, don't add to changedTables
+
+ changedFields.set(oldField.id, true);
+ }
}
}
@@ -381,48 +937,371 @@ function compareIndexes({
newIndexes,
diffMap,
changedTables,
+ changedIndexes,
+ attributes,
+ changedTablesAttributes,
+ changedIndexesAttributes,
+ changeTypes,
+ indexMatcher,
+ databaseType,
}: {
tableId: string;
oldIndexes: DBIndex[];
newIndexes: DBIndex[];
diffMap: DiffMap;
changedTables: Map;
+ changedIndexes: Map;
+ attributes?: IndexDiffAttribute[];
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedIndexesAttributes?: IndexDiffAttribute[];
+ changeTypes?: IndexDiff['type'][];
+ indexMatcher: (index: DBIndex, indexes: DBIndex[]) => DBIndex | undefined;
+ databaseType: DatabaseType;
}) {
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
+
+ // For structural changes (added/removed indexes), add to changedTables unless
+ // changedTablesAttributes is explicitly set to empty array
+ const shouldAddToChangedTables =
+ changedTablesAttributes === undefined ||
+ changedTablesAttributes.length > 0;
+
// Check for added indexes
- for (const newIndex of newIndexes) {
- if (!oldIndexes.find((i) => i.id === newIndex.id)) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'index',
- objectId: newIndex.id,
- }),
- {
- object: 'index',
- type: 'added',
- newIndex,
- tableId,
+ if (typesToCheck.includes('added')) {
+ for (const newIndex of newIndexes) {
+ if (!indexMatcher(newIndex, oldIndexes)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'index',
+ objectId: newIndex.id,
+ }),
+ {
+ object: 'index',
+ type: 'added',
+ newIndex,
+ tableId,
+ }
+ );
+ if (shouldAddToChangedTables) {
+ changedTables.set(tableId, true);
}
- );
- changedTables.set(tableId, true);
+ changedIndexes.set(newIndex.id, true);
+ }
}
}
// Check for removed indexes
- for (const oldIndex of oldIndexes) {
- if (!newIndexes.find((i) => i.id === oldIndex.id)) {
+ if (typesToCheck.includes('removed')) {
+ for (const oldIndex of oldIndexes) {
+ if (!indexMatcher(oldIndex, newIndexes)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'index',
+ objectId: oldIndex.id,
+ }),
+ {
+ object: 'index',
+ type: 'removed',
+ indexId: oldIndex.id,
+ tableId,
+ }
+ );
+ if (shouldAddToChangedTables) {
+ changedTables.set(tableId, true);
+ }
+ changedIndexes.set(oldIndex.id, true);
+ }
+ }
+ }
+
+ // Check for index changes
+ if (typesToCheck.includes('changed')) {
+ for (const oldIndex of oldIndexes) {
+ const newIndex = indexMatcher(oldIndex, newIndexes);
+ if (!newIndex) continue;
+
+ compareIndexProperties({
+ tableId,
+ oldIndex,
+ newIndex,
+ diffMap,
+ changedTables,
+ changedIndexes,
+ attributes,
+ changedTablesAttributes,
+ changedIndexesAttributes,
+ databaseType,
+ });
+ }
+ }
+}
+
+// Helper to compare fieldIds arrays
+const areFieldIdsEqual = (
+ oldFieldIds: string[],
+ newFieldIds: string[]
+): boolean => {
+ if (oldFieldIds.length !== newFieldIds.length) {
+ return false;
+ }
+ for (let i = 0; i < oldFieldIds.length; i++) {
+ if (oldFieldIds[i] !== newFieldIds[i]) {
+ return false;
+ }
+ }
+ return true;
+};
+
+// Compare index properties
+function compareIndexProperties({
+ tableId,
+ oldIndex,
+ newIndex,
+ diffMap,
+ changedTables,
+ changedIndexes,
+ attributes,
+ changedTablesAttributes,
+ changedIndexesAttributes,
+ databaseType,
+}: {
+ tableId: string;
+ oldIndex: DBIndex;
+ newIndex: DBIndex;
+ diffMap: DiffMap;
+ changedTables: Map;
+ changedIndexes: Map;
+ attributes?: IndexDiffAttribute[];
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedIndexesAttributes?: IndexDiffAttribute[];
+ databaseType: DatabaseType;
+}) {
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: IndexDiffAttribute[] = attributes ?? [
+ 'name',
+ 'unique',
+ 'fieldIds',
+ 'type',
+ ];
+
+ const changedAttributes: IndexDiffAttribute[] = [];
+
+ if (attributesToCheck.includes('name') && oldIndex.name !== newIndex.name) {
+ changedAttributes.push('name');
+ }
+
+ if (
+ attributesToCheck.includes('unique') &&
+ oldIndex.unique !== newIndex.unique
+ ) {
+ changedAttributes.push('unique');
+ }
+
+ if (
+ attributesToCheck.includes('fieldIds') &&
+ !areFieldIdsEqual(oldIndex.fieldIds, newIndex.fieldIds)
+ ) {
+ changedAttributes.push('fieldIds');
+ }
+
+ if (attributesToCheck.includes('type')) {
+ const oldType =
+ oldIndex.type ?? defaultIndexTypeForDatabase[databaseType];
+ const newType =
+ newIndex.type ?? defaultIndexTypeForDatabase[databaseType];
+
+ // if both null/undefined, consider equal
+ if (oldType !== newType) {
+ changedAttributes.push('type');
+ }
+ }
+
+ if (changedAttributes.length > 0) {
+ // Track which attributes should trigger adding to changed maps
+ const attributesThatTriggerChange = changedAttributes.filter((attr) =>
+ shouldAddToChangedMap(attr, changedIndexesAttributes)
+ );
+
+ for (const attribute of changedAttributes) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: oldIndex.id,
+ attribute,
}),
{
object: 'index',
- type: 'removed',
+ type: 'changed',
indexId: oldIndex.id,
+ newIndexId: newIndex.id,
tableId,
+ attribute,
+ oldValue: oldIndex[attribute],
+ newValue: newIndex[attribute],
+ }
+ );
+ }
+
+ // Only add to changed maps if at least one attribute should trigger a change
+ if (attributesThatTriggerChange.length > 0) {
+ // For changedTables, we need to check changedTablesAttributes
+ if (changedTablesAttributes === undefined) {
+ changedTables.set(tableId, true);
+ } else if (changedTablesAttributes.length > 0) {
+ changedTables.set(tableId, true);
+ }
+
+ changedIndexes.set(oldIndex.id, true);
+ }
+ }
+}
+
+// Compare check constraints between tables
+function compareCheckConstraints({
+ tableId,
+ oldCheckConstraints,
+ newCheckConstraints,
+ diffMap,
+ changedTables,
+ changedCheckConstraints,
+ attributes,
+ changedTablesAttributes,
+ changedCheckConstraintsAttributes,
+ changeTypes,
+ checkConstraintMatcher,
+}: {
+ tableId: string;
+ oldCheckConstraints: DBCheckConstraint[];
+ newCheckConstraints: DBCheckConstraint[];
+ diffMap: DiffMap;
+ changedTables: Map;
+ changedCheckConstraints: Map;
+ attributes?: CheckConstraintDiffAttribute[];
+ changedTablesAttributes?: TableDiffAttribute[];
+ changedCheckConstraintsAttributes?: CheckConstraintDiffAttribute[];
+ changeTypes?: CheckConstraintDiff['type'][];
+ checkConstraintMatcher: (
+ constraint: DBCheckConstraint,
+ constraints: DBCheckConstraint[]
+ ) => DBCheckConstraint | undefined;
+}) {
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
+
+ // For structural changes (added/removed constraints), add to changedTables unless
+ // changedTablesAttributes is explicitly set to empty array
+ const shouldAddToChangedTables =
+ changedTablesAttributes === undefined ||
+ changedTablesAttributes.length > 0;
+
+ // Check for added check constraints
+ if (typesToCheck.includes('added')) {
+ for (const newConstraint of newCheckConstraints) {
+ if (!checkConstraintMatcher(newConstraint, oldCheckConstraints)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'checkConstraint',
+ objectId: newConstraint.id,
+ }),
+ {
+ object: 'checkConstraint',
+ type: 'added',
+ newCheckConstraint: newConstraint,
+ tableId,
+ }
+ );
+ if (shouldAddToChangedTables) {
+ changedTables.set(tableId, true);
+ }
+ changedCheckConstraints.set(newConstraint.id, true);
+ }
+ }
+ }
+
+ // Check for removed check constraints
+ if (typesToCheck.includes('removed')) {
+ for (const oldConstraint of oldCheckConstraints) {
+ if (!checkConstraintMatcher(oldConstraint, newCheckConstraints)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'checkConstraint',
+ objectId: oldConstraint.id,
+ }),
+ {
+ object: 'checkConstraint',
+ type: 'removed',
+ checkConstraintId: oldConstraint.id,
+ tableId,
+ }
+ );
+ if (shouldAddToChangedTables) {
+ changedTables.set(tableId, true);
}
+ changedCheckConstraints.set(oldConstraint.id, true);
+ }
+ }
+ }
+
+ // Check for check constraint changes (expression changes)
+ if (typesToCheck.includes('changed')) {
+ for (const oldConstraint of oldCheckConstraints) {
+ const newConstraint = checkConstraintMatcher(
+ oldConstraint,
+ newCheckConstraints
);
- changedTables.set(tableId, true);
+ if (!newConstraint) continue;
+
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: CheckConstraintDiffAttribute[] =
+ attributes ?? ['expression'];
+
+ if (
+ attributesToCheck.includes('expression') &&
+ oldConstraint.expression !== newConstraint.expression
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'checkConstraint',
+ objectId: oldConstraint.id,
+ attribute: 'expression',
+ }),
+ {
+ object: 'checkConstraint',
+ type: 'changed',
+ checkConstraintId: oldConstraint.id,
+ newCheckConstraintId: newConstraint.id,
+ tableId,
+ attribute: 'expression',
+ oldValue: oldConstraint.expression,
+ newValue: newConstraint.expression,
+ }
+ );
+
+ if (
+ shouldAddToChangedMap(
+ 'expression',
+ changedCheckConstraintsAttributes
+ )
+ ) {
+ if (changedTablesAttributes === undefined) {
+ changedTables.set(tableId, true);
+ } else if (changedTablesAttributes.length > 0) {
+ changedTables.set(tableId, true);
+ }
+ changedCheckConstraints.set(oldConstraint.id, true);
+ }
+ }
}
}
}
@@ -432,45 +1311,831 @@ function compareRelationships({
diagram,
newDiagram,
diffMap,
+ changedRelationships,
+ relationshipIdMap,
+ attributes,
+ changedRelationshipsAttributes,
+ changeTypes,
+ relationshipMatcher,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
+ changedRelationships: Map;
+ relationshipIdMap: Map;
+ attributes?: RelationshipDiffAttribute[];
+ changedRelationshipsAttributes?: RelationshipDiffAttribute[];
+ changeTypes?: RelationshipDiff['type'][];
+ relationshipMatcher: (
+ relationship: DBRelationship,
+ relationships: DBRelationship[]
+ ) => DBRelationship | undefined;
}) {
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
const oldRelationships = diagram.relationships || [];
const newRelationships = newDiagram.relationships || [];
// Check for added relationships
- for (const newRelationship of newRelationships) {
- if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
- diffMap.set(
- getDiffMapKey({
- diffObject: 'relationship',
- objectId: newRelationship.id,
- }),
- {
- object: 'relationship',
- type: 'added',
- newRelationship,
- }
- );
+ if (typesToCheck.includes('added')) {
+ for (const newRelationship of newRelationships) {
+ if (!relationshipMatcher(newRelationship, oldRelationships)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'relationship',
+ objectId: newRelationship.id,
+ }),
+ {
+ object: 'relationship',
+ type: 'added',
+ newRelationship,
+ }
+ );
+ changedRelationships.set(newRelationship.id, true);
+ }
}
}
// Check for removed relationships
- for (const oldRelationship of oldRelationships) {
- if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
+ if (typesToCheck.includes('removed')) {
+ for (const oldRelationship of oldRelationships) {
+ if (!relationshipMatcher(oldRelationship, newRelationships)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'relationship',
+ objectId: oldRelationship.id,
+ }),
+ {
+ object: 'relationship',
+ type: 'removed',
+ relationshipId: oldRelationship.id,
+ }
+ );
+ changedRelationships.set(oldRelationship.id, true);
+ }
+ }
+ }
+
+ // Check for relationship changes
+ if (typesToCheck.includes('changed')) {
+ for (const oldRelationship of oldRelationships) {
+ const newRelationship = relationshipMatcher(
+ oldRelationship,
+ newRelationships
+ );
+ if (!newRelationship) continue;
+
+ compareRelationshipProperties({
+ oldRelationship,
+ newRelationship,
+ diffMap,
+ changedRelationships,
+ relationshipIdMap,
+ attributes,
+ changedRelationshipsAttributes,
+ });
+ }
+ }
+}
+
+// Compare relationship properties
+function compareRelationshipProperties({
+ oldRelationship,
+ newRelationship,
+ diffMap,
+ changedRelationships,
+ relationshipIdMap,
+ attributes,
+ changedRelationshipsAttributes,
+}: {
+ oldRelationship: DBRelationship;
+ newRelationship: DBRelationship;
+ diffMap: DiffMap;
+ changedRelationships: Map;
+ relationshipIdMap: Map;
+ attributes?: RelationshipDiffAttribute[];
+ changedRelationshipsAttributes?: RelationshipDiffAttribute[];
+}) {
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: RelationshipDiffAttribute[] = attributes ?? [
+ 'name',
+ 'sourceSchema',
+ 'sourceTableId',
+ 'targetSchema',
+ 'targetTableId',
+ 'sourceFieldId',
+ 'targetFieldId',
+ 'sourceCardinality',
+ 'targetCardinality',
+ ];
+
+ const changedAttributes: RelationshipDiffAttribute[] = [];
+
+ if (
+ attributesToCheck.includes('name') &&
+ oldRelationship.name !== newRelationship.name
+ ) {
+ changedAttributes.push('name');
+ }
+
+ if (
+ attributesToCheck.includes('sourceSchema') &&
+ oldRelationship.sourceSchema !== newRelationship.sourceSchema
+ ) {
+ changedAttributes.push('sourceSchema');
+ }
+
+ if (
+ attributesToCheck.includes('sourceTableId') &&
+ oldRelationship.sourceTableId !== newRelationship.sourceTableId
+ ) {
+ changedAttributes.push('sourceTableId');
+ }
+
+ if (
+ attributesToCheck.includes('targetSchema') &&
+ oldRelationship.targetSchema !== newRelationship.targetSchema
+ ) {
+ changedAttributes.push('targetSchema');
+ }
+
+ if (
+ attributesToCheck.includes('targetTableId') &&
+ oldRelationship.targetTableId !== newRelationship.targetTableId
+ ) {
+ changedAttributes.push('targetTableId');
+ }
+
+ if (
+ attributesToCheck.includes('sourceFieldId') &&
+ oldRelationship.sourceFieldId !== newRelationship.sourceFieldId
+ ) {
+ changedAttributes.push('sourceFieldId');
+ }
+
+ if (
+ attributesToCheck.includes('targetFieldId') &&
+ oldRelationship.targetFieldId !== newRelationship.targetFieldId
+ ) {
+ changedAttributes.push('targetFieldId');
+ }
+ // Check cardinality changes, but exclude changes that produce the same DDL.
+ // In DDL export, when source is 'one', the relationship direction stays consistent.
+ // Therefore (one, one) ↔ (one, many) are equivalent from DDL perspective.
+ const shouldCheckCardinality =
+ attributesToCheck.includes('sourceCardinality') ||
+ attributesToCheck.includes('targetCardinality');
+
+ if (shouldCheckCardinality) {
+ const oldSource = oldRelationship.sourceCardinality;
+ const oldTarget = oldRelationship.targetCardinality;
+ const newSource = newRelationship.sourceCardinality;
+ const newTarget = newRelationship.targetCardinality;
+
+ // Check if this is an "equivalent" cardinality change that produces the same DDL
+ // Equivalent pairs: (one, one) ↔ (many, one) - both put FK on source table
+ const isEquivalentCardinalityChange =
+ (oldSource === 'one' &&
+ oldTarget === 'one' &&
+ newTarget === 'many' &&
+ newSource === 'one') ||
+ (oldTarget === 'many' &&
+ oldSource === 'one' &&
+ newSource === 'one' &&
+ newTarget === 'one');
+
+ if (!isEquivalentCardinalityChange) {
+ if (
+ attributesToCheck.includes('sourceCardinality') &&
+ oldSource !== newSource
+ ) {
+ changedAttributes.push('sourceCardinality');
+ }
+
+ if (
+ attributesToCheck.includes('targetCardinality') &&
+ oldTarget !== newTarget
+ ) {
+ changedAttributes.push('targetCardinality');
+ }
+ }
+ }
+
+ if (changedAttributes.length > 0) {
+ // Track which attributes should trigger adding to changed maps
+ const attributesThatTriggerChange = changedAttributes.filter((attr) =>
+ shouldAddToChangedMap(attr, changedRelationshipsAttributes)
+ );
+
+ for (const attribute of changedAttributes) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: oldRelationship.id,
+ attribute,
}),
{
object: 'relationship',
- type: 'removed',
+ type: 'changed',
relationshipId: oldRelationship.id,
+ newRelationshipId: newRelationship.id,
+ attribute,
+ oldValue: oldRelationship[attribute],
+ newValue: newRelationship[attribute],
}
);
}
+
+ // Only add to changed maps if at least one attribute should trigger a change
+ if (attributesThatTriggerChange.length > 0) {
+ changedRelationships.set(oldRelationship.id, true);
+ changedRelationships.set(newRelationship.id, true);
+
+ // Store bidirectional mapping between old and new IDs
+ relationshipIdMap.set(oldRelationship.id, newRelationship.id);
+ relationshipIdMap.set(newRelationship.id, oldRelationship.id);
+ }
+ }
+}
+
+// Compare areas between diagrams
+function compareAreas({
+ diagram,
+ newDiagram,
+ diffMap,
+ changedAreas,
+ attributes,
+ changedAreasAttributes,
+ changeTypes,
+ areaMatcher,
+}: {
+ diagram: Diagram;
+ newDiagram: Diagram;
+ diffMap: DiffMap;
+ changedAreas: Map;
+ attributes?: AreaDiffAttribute[];
+ changedAreasAttributes?: AreaDiffAttribute[];
+ changeTypes?: AreaDiff['type'][];
+ areaMatcher: (area: Area, areas: Area[]) => Area | undefined;
+}) {
+ const oldAreas = diagram.areas || [];
+ const newAreas = newDiagram.areas || [];
+
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
+
+ // Check for added areas
+ if (typesToCheck.includes('added')) {
+ for (const newArea of newAreas) {
+ if (!areaMatcher(newArea, oldAreas)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: newArea.id,
+ }),
+ {
+ object: 'area',
+ type: 'added',
+ areaAdded: newArea,
+ }
+ );
+ changedAreas.set(newArea.id, true);
+ }
+ }
+ }
+
+ // Check for removed areas
+ if (typesToCheck.includes('removed')) {
+ for (const oldArea of oldAreas) {
+ if (!areaMatcher(oldArea, newAreas)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ }),
+ {
+ object: 'area',
+ type: 'removed',
+ areaId: oldArea.id,
+ }
+ );
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+ }
+
+ // Check for area name and color changes
+ if (typesToCheck.includes('changed')) {
+ for (const oldArea of oldAreas) {
+ const newArea = areaMatcher(oldArea, newAreas);
+
+ if (!newArea) continue;
+
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: AreaDiffAttribute[] = attributes ?? [
+ 'name',
+ 'color',
+ ];
+
+ if (
+ attributesToCheck.includes('name') &&
+ oldArea.name !== newArea.name
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ attribute: 'name',
+ }),
+ {
+ object: 'area',
+ type: 'changed',
+ areaId: oldArea.id,
+ newAreaId: newArea.id,
+ attribute: 'name',
+ newValue: newArea.name,
+ oldValue: oldArea.name,
+ }
+ );
+ if (shouldAddToChangedMap('name', changedAreasAttributes)) {
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+
+ if (
+ attributesToCheck.includes('color') &&
+ oldArea.color !== newArea.color
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ attribute: 'color',
+ }),
+ {
+ object: 'area',
+ type: 'changed',
+ areaId: oldArea.id,
+ newAreaId: newArea.id,
+ attribute: 'color',
+ newValue: newArea.color,
+ oldValue: oldArea.color,
+ }
+ );
+ if (shouldAddToChangedMap('color', changedAreasAttributes)) {
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+
+ if (attributesToCheck.includes('x') && oldArea.x !== newArea.x) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ attribute: 'x',
+ }),
+ {
+ object: 'area',
+ type: 'changed',
+ areaId: oldArea.id,
+ newAreaId: newArea.id,
+ attribute: 'x',
+ newValue: newArea.x,
+ oldValue: oldArea.x,
+ }
+ );
+ if (shouldAddToChangedMap('x', changedAreasAttributes)) {
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+
+ if (attributesToCheck.includes('y') && oldArea.y !== newArea.y) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ attribute: 'y',
+ }),
+ {
+ object: 'area',
+ type: 'changed',
+ areaId: oldArea.id,
+ newAreaId: newArea.id,
+ attribute: 'y',
+ newValue: newArea.y,
+ oldValue: oldArea.y,
+ }
+ );
+ if (shouldAddToChangedMap('y', changedAreasAttributes)) {
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+
+ if (
+ attributesToCheck.includes('width') &&
+ oldArea.width !== newArea.width
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ attribute: 'width',
+ }),
+ {
+ object: 'area',
+ type: 'changed',
+ areaId: oldArea.id,
+ newAreaId: newArea.id,
+ attribute: 'width',
+ newValue: newArea.width,
+ oldValue: oldArea.width,
+ }
+ );
+ if (shouldAddToChangedMap('width', changedAreasAttributes)) {
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+
+ if (
+ attributesToCheck.includes('height') &&
+ oldArea.height !== newArea.height
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'area',
+ objectId: oldArea.id,
+ attribute: 'height',
+ }),
+ {
+ object: 'area',
+ type: 'changed',
+ areaId: oldArea.id,
+ newAreaId: newArea.id,
+ attribute: 'height',
+ newValue: newArea.height,
+ oldValue: oldArea.height,
+ }
+ );
+ if (shouldAddToChangedMap('height', changedAreasAttributes)) {
+ changedAreas.set(oldArea.id, true);
+ }
+ }
+ }
}
}
+
+// Compare notes between diagrams
+function compareNotes({
+ diagram,
+ newDiagram,
+ diffMap,
+ changedNotes,
+ attributes,
+ changedNotesAttributes,
+ changeTypes,
+ noteMatcher,
+}: {
+ diagram: Diagram;
+ newDiagram: Diagram;
+ diffMap: DiffMap;
+ changedNotes: Map;
+ attributes?: NoteDiffAttribute[];
+ changedNotesAttributes?: NoteDiffAttribute[];
+ changeTypes?: NoteDiff['type'][];
+ noteMatcher: (note: Note, notes: Note[]) => Note | undefined;
+}) {
+ const oldNotes = diagram.notes || [];
+ const newNotes = newDiagram.notes || [];
+
+ // If changeTypes is empty array, don't check any changes
+ if (changeTypes && changeTypes.length === 0) {
+ return;
+ }
+
+ // If changeTypes is undefined, check all types
+ const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
+
+ // Check for added notes
+ if (typesToCheck.includes('added')) {
+ for (const newNote of newNotes) {
+ if (!noteMatcher(newNote, oldNotes)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: newNote.id,
+ }),
+ {
+ object: 'note',
+ type: 'added',
+ noteAdded: newNote,
+ }
+ );
+ changedNotes.set(newNote.id, true);
+ }
+ }
+ }
+
+ // Check for removed notes
+ if (typesToCheck.includes('removed')) {
+ for (const oldNote of oldNotes) {
+ if (!noteMatcher(oldNote, newNotes)) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ }),
+ {
+ object: 'note',
+ type: 'removed',
+ noteId: oldNote.id,
+ }
+ );
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+ }
+
+ // Check for note content and color changes
+ if (typesToCheck.includes('changed')) {
+ for (const oldNote of oldNotes) {
+ const newNote = noteMatcher(oldNote, newNotes);
+
+ if (!newNote) continue;
+
+ // If attributes are specified, only check those attributes
+ const attributesToCheck: NoteDiffAttribute[] = attributes ?? [
+ 'content',
+ 'color',
+ ];
+
+ if (
+ attributesToCheck.includes('content') &&
+ areCommentsDifferent(oldNote.content, newNote.content)
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ attribute: 'content',
+ }),
+ {
+ object: 'note',
+ type: 'changed',
+ noteId: oldNote.id,
+ newNoteId: newNote.id,
+ attribute: 'content',
+ newValue: newNote.content,
+ oldValue: oldNote.content,
+ }
+ );
+ if (shouldAddToChangedMap('content', changedNotesAttributes)) {
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+
+ if (
+ attributesToCheck.includes('color') &&
+ oldNote.color !== newNote.color
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ attribute: 'color',
+ }),
+ {
+ object: 'note',
+ type: 'changed',
+ noteId: oldNote.id,
+ newNoteId: newNote.id,
+ attribute: 'color',
+ newValue: newNote.color,
+ oldValue: oldNote.color,
+ }
+ );
+ if (shouldAddToChangedMap('color', changedNotesAttributes)) {
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+
+ if (attributesToCheck.includes('x') && oldNote.x !== newNote.x) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ attribute: 'x',
+ }),
+ {
+ object: 'note',
+ type: 'changed',
+ noteId: oldNote.id,
+ newNoteId: newNote.id,
+ attribute: 'x',
+ newValue: newNote.x,
+ oldValue: oldNote.x,
+ }
+ );
+ if (shouldAddToChangedMap('x', changedNotesAttributes)) {
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+
+ if (attributesToCheck.includes('y') && oldNote.y !== newNote.y) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ attribute: 'y',
+ }),
+ {
+ object: 'note',
+ type: 'changed',
+ noteId: oldNote.id,
+ newNoteId: newNote.id,
+ attribute: 'y',
+ newValue: newNote.y,
+ oldValue: oldNote.y,
+ }
+ );
+ if (shouldAddToChangedMap('y', changedNotesAttributes)) {
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+
+ if (
+ attributesToCheck.includes('width') &&
+ oldNote.width !== newNote.width
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ attribute: 'width',
+ }),
+ {
+ object: 'note',
+ type: 'changed',
+ noteId: oldNote.id,
+ newNoteId: newNote.id,
+ attribute: 'width',
+ newValue: newNote.width,
+ oldValue: oldNote.width,
+ }
+ );
+ if (shouldAddToChangedMap('width', changedNotesAttributes)) {
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+
+ if (
+ attributesToCheck.includes('height') &&
+ oldNote.height !== newNote.height
+ ) {
+ diffMap.set(
+ getDiffMapKey({
+ diffObject: 'note',
+ objectId: oldNote.id,
+ attribute: 'height',
+ }),
+ {
+ object: 'note',
+ type: 'changed',
+ noteId: oldNote.id,
+ newNoteId: newNote.id,
+ attribute: 'height',
+ newValue: newNote.height,
+ oldValue: oldNote.height,
+ }
+ );
+ if (shouldAddToChangedMap('height', changedNotesAttributes)) {
+ changedNotes.set(oldNote.id, true);
+ }
+ }
+ }
+ }
+}
+
+const defaultTableMatcher = (
+ table: DBTable,
+ tables: DBTable[]
+): DBTable | undefined => {
+ return tables.find((t) => t.id === table.id);
+};
+
+const defaultFieldMatcher = (
+ field: DBField,
+ fields: DBField[]
+): DBField | undefined => {
+ return fields.find((f) => f.id === field.id);
+};
+
+const defaultIndexMatcher = (
+ index: DBIndex,
+ indexes: DBIndex[]
+): DBIndex | undefined => {
+ // Priority 1: Match by ID
+ const byId = indexes.find((i) => i.id === index.id);
+ if (byId) {
+ return byId;
+ }
+
+ // Priority 2: Match by name (only if unique match)
+ if (index.name) {
+ const byName = indexes.filter(
+ (i) =>
+ i.name === index.name &&
+ areBooleansEqual(i.isPrimaryKey, index.isPrimaryKey)
+ );
+ if (byName.length === 1) {
+ return byName[0];
+ }
+ }
+
+ // Priority 3: Match by fieldIds (only if unique match)
+ const byFieldIds = indexes.filter(
+ (i) =>
+ areFieldIdsEqual(i.fieldIds, index.fieldIds) &&
+ areBooleansEqual(i.isPrimaryKey, index.isPrimaryKey)
+ );
+ if (byFieldIds.length === 1) {
+ return byFieldIds[0];
+ }
+
+ return undefined;
+};
+
+const defaultCheckConstraintMatcher = (
+ constraint: DBCheckConstraint,
+ constraints: DBCheckConstraint[]
+): DBCheckConstraint | undefined => {
+ // Priority 1: Match by ID
+ const byId = constraints.find((c) => c.id === constraint.id);
+ if (byId) {
+ return byId;
+ }
+
+ // Priority 2: Match by expression (only if unique match)
+ if (constraint.expression) {
+ const byExpression = constraints.filter(
+ (c) => c.expression === constraint.expression
+ );
+ if (byExpression.length === 1) {
+ return byExpression[0];
+ }
+ }
+
+ return undefined;
+};
+
+const defaultRelationshipMatcher = (
+ relationship: DBRelationship,
+ relationships: DBRelationship[]
+): DBRelationship | undefined => {
+ // Priority 1: Match by ID
+ const byId = relationships.find((r) => r.id === relationship.id);
+ if (byId) {
+ return byId;
+ }
+
+ // Priority 2: Match by name (only if unique match)
+ if (relationship.name) {
+ const byName = relationships.filter(
+ (r) => r.name === relationship.name
+ );
+ if (byName.length === 1) {
+ return byName[0];
+ }
+ }
+
+ // Priority 3: Match by structural identity (source/target table and field IDs)
+ const byStructure = relationships.filter(
+ (r) =>
+ r.sourceTableId === relationship.sourceTableId &&
+ r.targetTableId === relationship.targetTableId &&
+ r.sourceFieldId === relationship.sourceFieldId &&
+ r.targetFieldId === relationship.targetFieldId
+ );
+ if (byStructure.length === 1) {
+ return byStructure[0];
+ }
+
+ return undefined;
+};
+
+const defaultAreaMatcher = (area: Area, areas: Area[]): Area | undefined => {
+ return areas.find((a) => a.id === area.id);
+};
+
+const defaultNoteMatcher = (note: Note, notes: Note[]): Note | undefined => {
+ return notes.find((n) => n.id === note.id);
+};
diff --git a/src/lib/domain/diff/diff.ts b/src/lib/domain/diff/diff.ts
index a93b6d474..60df14b16 100644
--- a/src/lib/domain/diff/diff.ts
+++ b/src/lib/domain/diff/diff.ts
@@ -1,5 +1,7 @@
import { z } from 'zod';
+import type { CheckConstraintDiff } from './check-constraint-diff';
+import { createCheckConstraintDiffSchema } from './check-constraint-diff';
import type { FieldDiff } from './field-diff';
import { createFieldDiffSchema } from './field-diff';
import type { IndexDiff } from './index-diff';
@@ -8,52 +10,188 @@ import type { RelationshipDiff } from './relationship-diff';
import { createRelationshipDiffSchema } from './relationship-diff';
import type { TableDiff } from './table-diff';
import { createTableDiffSchema } from './table-diff';
-import type { DBField, DBIndex, DBRelationship, DBTable } from '..';
+import type { AreaDiff } from './area-diff';
+import { createAreaDiffSchema } from './area-diff';
+import type { NoteDiff } from './note-diff';
+import { createNoteDiffSchema } from './note-diff';
+import type {
+ DBCheckConstraint,
+ DBField,
+ DBIndex,
+ DBRelationship,
+ DBTable,
+ Area,
+ Note,
+} from '..';
export type ChartDBDiff<
TTable = DBTable,
TField = DBField,
TIndex = DBIndex,
+ TCheckConstraint = DBCheckConstraint,
TRelationship = DBRelationship,
+ TArea = Area,
+ TNote = Note,
> =
| TableDiff
| FieldDiff
| IndexDiff
- | RelationshipDiff;
+ | CheckConstraintDiff