mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e169b84ade | |||
| 994c797195 | |||
| 42e2bcf5e9 | |||
| 91fe5d6cf2 | |||
| 585f5c7a7f | |||
| d0cd2d8fa4 | |||
| fab51f2d13 | |||
| fe84e0d748 | |||
| ae9e052cf6 | |||
| 557387a4fd | |||
| 4a2f6ae8f5 | |||
| e8838d1f1e | |||
| c1ae2dbf69 | |||
| eb6c80417c |
@@ -0,0 +1,18 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
||||||
@@ -9,7 +9,7 @@ This guide explains how to run StackRender using Docker.
|
|||||||
The easiest way to run StackRender:
|
The easiest way to run StackRender:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The application will be available at `http://localhost:8080`
|
The application will be available at `http://localhost:8080`
|
||||||
@@ -17,7 +17,7 @@ The application will be available at `http://localhost:8080`
|
|||||||
To stop the application:
|
To stop the application:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Docker CLI
|
### Using Docker CLI
|
||||||
@@ -103,6 +103,14 @@ RUN npm config set strict-ssl false && npm install
|
|||||||
|
|
||||||
This is necessary in some build environments with certificate validation issues.
|
This is necessary in some build environments with certificate validation issues.
|
||||||
|
|
||||||
|
### Out of Memory During Build
|
||||||
|
|
||||||
|
The production build is memory-intensive (the app compiles thousands of modules). If `docker compose up` or `docker build` fails with an out-of-memory error such as `Reached heap limit` or `cannot allocate memory` during the `npm run build` step, increase the memory available to Docker:
|
||||||
|
|
||||||
|
- **Docker Desktop**: Settings, then Resources, then Memory. Set it to at least **6-8 GB** and apply.
|
||||||
|
|
||||||
|
The Dockerfile raises the Node heap (`NODE_OPTIONS=--max-old-space-size`) for the build. That heap size must fit inside the memory available to Docker, so raising the Docker memory limit is the fix when the build runs out of memory.
|
||||||
|
|
||||||
### Container Not Starting
|
### Container Not Starting
|
||||||
|
|
||||||
Check the logs:
|
Check the logs:
|
||||||
@@ -114,7 +122,7 @@ docker logs stackrender
|
|||||||
Or with Docker Compose:
|
Or with Docker Compose:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose logs
|
docker compose logs
|
||||||
```
|
```
|
||||||
|
|
||||||
### Port Already in Use
|
### Port Already in Use
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||

|

|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<a href="https://www.stackrender.io">
|
<a href="https://www.stackrender.io">
|
||||||
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
|
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
|
||||||
@@ -15,16 +15,16 @@
|
|||||||
|
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
# Welcom to StackRender ⚡
|
# Welcome to StackRender ⚡
|
||||||
StackRender was born from the need to automate backend development, covering everything from the database to the final API endpoint. Our first step toward this long-term vision is to provide a **next-generation, free, open-source database schema diagram generator**.
|
StackRender was born from the need to automate backend development, covering everything from the database to the final API endpoint. Our first step toward this long-term vision is to provide a **next-generation, free, open-source database schema diagram generator**.
|
||||||
|
|
||||||
StackRender helps you go from **specifications** to a **fully functional, production-ready database** that can be exported in your preferred SQL dialect: **MySQL, PostgreSQL, MariaDB, or SQLite**.
|
StackRender helps you go from **specifications** to a **fully functional, production-ready database** that can be exported in your preferred SQL dialect: **MySQL, PostgreSQL, MariaDB, SQLite, Oracle, or SQL Server**.
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
|
|
||||||
- **Interactive Diagram UI** – Visually design and manage your database schemas with an intuitive drag-and-drop interface.
|
- **Interactive Diagram UI** – Visually design and manage your database schemas with an intuitive drag-and-drop interface.
|
||||||
- **In-Depth Tables & Columns Control** – Fully customize tables, columns, types, and constraints.
|
- **In-Depth Tables & Columns Control** – Fully customize tables, columns, types, and constraints.
|
||||||
- **Indices Suggestions** – Receive recommendations to optimize database performance.
|
- **Index Suggestions** – Receive recommendations to optimize database performance.
|
||||||
- **Import / Export SQL DDL** – Easily import existing schemas or export your design as SQL scripts.
|
- **Import / Export SQL DDL** – Easily import existing schemas or export your design as SQL scripts.
|
||||||
- **Foreign Key Cycle Detection** – Identify and resolve circular dependencies in relationships.
|
- **Foreign Key Cycle Detection** – Identify and resolve circular dependencies in relationships.
|
||||||
- **AI-Powered Database Assistant** – (Cloud version) Generate database diagrams from specifications and perform additional operations such as schema enrichment, soft-delete implementation, and automatic documentation generation.
|
- **AI-Powered Database Assistant** – (Cloud version) Generate database diagrams from specifications and perform additional operations such as schema enrichment, soft-delete implementation, and automatic documentation generation.
|
||||||
@@ -38,9 +38,10 @@ StackRender is currently in **Public Beta**. Star and watch this repository to g
|
|||||||
- ✅ PostgreSQL
|
- ✅ PostgreSQL
|
||||||
- ✅ MySQL
|
- ✅ MySQL
|
||||||
- ✅ MariaDB
|
- ✅ MariaDB
|
||||||
- ✅ SQLite
|
- ✅ SQLite
|
||||||
|
- ✅ Oracle
|
||||||
|
- ✅ SQL Server (MSSQL)
|
||||||
|
|
||||||
And more coming very soon!
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
Use the [cloud version](https://www.stackrender.io) or deploy locally to start designing your database schemas in minutes.
|
Use the [cloud version](https://www.stackrender.io) or deploy locally to start designing your database schemas in minutes.
|
||||||
@@ -52,7 +53,7 @@ The easiest way to run StackRender locally is using Docker:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build and run using Docker Compose
|
# Build and run using Docker Compose
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
# Or build and run using Docker directly
|
# Or build and run using Docker directly
|
||||||
docker build -t stackrender .
|
docker build -t stackrender .
|
||||||
@@ -68,6 +69,8 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then visit `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
### How to Build
|
### How to Build
|
||||||
Install dependencies and create a production build:
|
Install dependencies and create a production build:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ server {
|
|||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
|
||||||
|
# Putting these at the server level ensures BOTH index.html AND your cached .js/.wasm assets use them!
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||||
|
|
||||||
# Handle client-side routing
|
# Handle client-side routing
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
@@ -24,5 +29,7 @@ server {
|
|||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|wasm)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|wasm)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+416
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "StackRender",
|
"name": "StackRender",
|
||||||
"version": "1.3.3",
|
"version": "1.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "StackRender",
|
"name": "StackRender",
|
||||||
"version": "1.3.3",
|
"version": "1.4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-sql": "^6.9.0",
|
"@codemirror/lang-sql": "^6.9.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
"@typescript-eslint/parser": "8.11.0",
|
"@typescript-eslint/parser": "8.11.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
@@ -102,7 +103,8 @@
|
|||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/react-spectrum": {
|
"node_modules/@adobe/react-spectrum": {
|
||||||
@@ -7634,6 +7636,119 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "2.1.9",
|
||||||
|
"@vitest/utils": "2.1.9",
|
||||||
|
"chai": "^5.1.2",
|
||||||
|
"tinyrainbow": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "2.1.9",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "2.1.9",
|
||||||
|
"pathe": "^1.1.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "2.1.9",
|
||||||
|
"magic-string": "^0.30.12",
|
||||||
|
"pathe": "^1.1.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyspy": "^3.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "2.1.9",
|
||||||
|
"loupe": "^3.1.2",
|
||||||
|
"tinyrainbow": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xyflow/react": {
|
"node_modules/@xyflow/react": {
|
||||||
"version": "12.11.0",
|
"version": "12.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz",
|
||||||
@@ -7930,6 +8045,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types-flow": {
|
"node_modules/ast-types-flow": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||||
@@ -8107,6 +8232,16 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||||
@@ -8188,6 +8323,23 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"dependencies": {
|
||||||
|
"assertion-error": "^2.0.1",
|
||||||
|
"check-error": "^2.1.1",
|
||||||
|
"deep-eql": "^5.0.1",
|
||||||
|
"loupe": "^3.1.0",
|
||||||
|
"pathval": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -8205,6 +8357,16 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/check-error": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/class-variance-authority": {
|
"node_modules/class-variance-authority": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
@@ -8363,6 +8525,25 @@
|
|||||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "src/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -8608,6 +8789,16 @@
|
|||||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -9002,6 +9193,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
@@ -9763,6 +9961,16 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
@@ -9779,6 +9987,16 @@
|
|||||||
"integrity": "sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==",
|
"integrity": "sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -11387,6 +11605,13 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -11877,6 +12102,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pathval": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pgsql-ast-parser": {
|
"node_modules/pgsql-ast-parser": {
|
||||||
"version": "12.0.2",
|
"version": "12.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pgsql-ast-parser/-/pgsql-ast-parser-12.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pgsql-ast-parser/-/pgsql-ast-parser-12.0.2.tgz",
|
||||||
@@ -12891,6 +13133,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/simple-swizzle": {
|
"node_modules/simple-swizzle": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||||
@@ -12932,6 +13181,20 @@
|
|||||||
"sql-formatter": "bin/sql-formatter-cli.cjs"
|
"sql-formatter": "bin/sql-formatter-cli.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"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/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -13216,6 +13479,50 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinypool": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyspy": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -13688,6 +13995,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-node": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cac": "^6.7.14",
|
||||||
|
"debug": "^4.3.7",
|
||||||
|
"es-module-lexer": "^1.5.4",
|
||||||
|
"pathe": "^1.1.2",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite-node": "vite-node.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite-plugin-top-level-await": {
|
"node_modules/vite-plugin-top-level-await": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz",
|
||||||
@@ -14145,6 +14475,72 @@
|
|||||||
"@esbuild/win32-x64": "0.21.5"
|
"@esbuild/win32-x64": "0.21.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
|
||||||
|
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "2.1.9",
|
||||||
|
"@vitest/mocker": "2.1.9",
|
||||||
|
"@vitest/pretty-format": "^2.1.9",
|
||||||
|
"@vitest/runner": "2.1.9",
|
||||||
|
"@vitest/snapshot": "2.1.9",
|
||||||
|
"@vitest/spy": "2.1.9",
|
||||||
|
"@vitest/utils": "2.1.9",
|
||||||
|
"chai": "^5.1.2",
|
||||||
|
"debug": "^4.3.7",
|
||||||
|
"expect-type": "^1.1.0",
|
||||||
|
"magic-string": "^0.30.12",
|
||||||
|
"pathe": "^1.1.2",
|
||||||
|
"std-env": "^3.8.0",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^0.3.1",
|
||||||
|
"tinypool": "^1.0.1",
|
||||||
|
"tinyrainbow": "^1.2.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vite-node": "2.1.9",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
|
"@vitest/browser": "2.1.9",
|
||||||
|
"@vitest/ui": "2.1.9",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/void-elements": {
|
"node_modules/void-elements": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
@@ -14265,6 +14661,23 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
+8
-4
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "StackRender",
|
"name": "StackRender",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.3",
|
"version": "1.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
||||||
"lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix",
|
"lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-sql": "^6.9.0",
|
"@codemirror/lang-sql": "^6.9.0",
|
||||||
@@ -88,6 +90,7 @@
|
|||||||
"@typescript-eslint/parser": "8.11.0",
|
"@typescript-eslint/parser": "8.11.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
@@ -104,6 +107,7 @@
|
|||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-17
@@ -7,7 +7,7 @@ import { DataTypes, TimeDefaultValues } from "@/lib/field";
|
|||||||
|
|
||||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { now } from "@internationalized/date";
|
import { now } from "@internationalized/date";
|
||||||
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -16,6 +16,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
|
|
||||||
import { MultiSelect } from "@/components/multi-select";
|
import { MultiSelect } from "@/components/multi-select";
|
||||||
import { DatePicker } from "@/components/date-picker";
|
import { DatePicker } from "@/components/date-picker";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
|
||||||
interface DefaultValueType {
|
interface DefaultValueType {
|
||||||
number?: boolean;
|
number?: boolean;
|
||||||
@@ -23,7 +24,8 @@ interface DefaultValueType {
|
|||||||
boolean?: boolean;
|
boolean?: boolean;
|
||||||
time?: boolean;
|
time?: boolean;
|
||||||
select?: boolean;
|
select?: boolean;
|
||||||
multiSelect?: boolean
|
multiSelect?: boolean;
|
||||||
|
uuid?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FieldDefaultValueProps {
|
interface FieldDefaultValueProps {
|
||||||
@@ -64,8 +66,8 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
|
|
||||||
const [defaultDateTime, setDefaultDateTime] = useState<any>(() => {
|
const [defaultDateTime, setDefaultDateTime] = useState<any>(() => {
|
||||||
try {
|
try {
|
||||||
if (field.type.name == "time" ) {
|
if (field.type.name == "time") {
|
||||||
return field.defaultValue ;
|
return field.defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.defaultValue) {
|
if (field.defaultValue) {
|
||||||
@@ -78,7 +80,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValueType: DefaultValueType = useMemo(() => {
|
const defaultValueType: DefaultValueType = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
|
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
|
||||||
@@ -86,7 +88,8 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
boolean: field.type?.type == DataTypes.BOOLEAN,
|
boolean: field.type?.type == DataTypes.BOOLEAN,
|
||||||
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
|
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
|
||||||
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
|
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
|
||||||
multiSelect: field.type?.type == DataTypes.ENUM && field.type?.name == "set"
|
multiSelect: field.type?.type == DataTypes.ENUM && field.type?.name == "set",
|
||||||
|
uuid: field.type.name == "uuid" || field.type.name == "uniqueidentifier"
|
||||||
}
|
}
|
||||||
}, [field]);
|
}, [field]);
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
|
|
||||||
const [selectedValues, setSelectedValues] = useState<string[] | string>(() => {
|
const [selectedValues, setSelectedValues] = useState<string[] | string>(() => {
|
||||||
|
|
||||||
if (defaultValueType.select) {
|
if (defaultValueType.select || defaultValueType.uuid) {
|
||||||
if (!field.defaultValue)
|
if (!field.defaultValue)
|
||||||
return "none";
|
return "none";
|
||||||
else
|
else
|
||||||
@@ -204,7 +207,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
|
|
||||||
let value: string | undefined;
|
let value: string | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (field.type.name != "time" && date) {
|
if (field.type.name != "time" && date) {
|
||||||
setDefaultDateTime(date);
|
setDefaultDateTime(date);
|
||||||
@@ -214,8 +217,8 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
else if (field.type.name == "datetime" || field.type.name == "timestamp" || field.type.name == "timestamptz")
|
else if (field.type.name == "datetime" || field.type.name == "timestamp" || field.type.name == "timestamptz")
|
||||||
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
||||||
}
|
}
|
||||||
else if ( field.type.name == "time") {
|
else if (field.type.name == "time") {
|
||||||
value = defaultDateTime ;
|
value = defaultDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -227,7 +230,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
|
|
||||||
const enumValueChange = useCallback((selection: any) => {
|
const enumValueChange = useCallback((selection: any) => {
|
||||||
|
|
||||||
if (defaultValueType.select) {
|
if (defaultValueType.select || defaultValueType.uuid) {
|
||||||
setSelectedValues(selection);
|
setSelectedValues(selection);
|
||||||
editField({
|
editField({
|
||||||
id: field.id,
|
id: field.id,
|
||||||
@@ -256,11 +259,11 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
!defaultValueType.boolean &&
|
!defaultValueType.boolean &&
|
||||||
|
|
||||||
<Label htmlFor="default_value">
|
<Label htmlFor="default_value">
|
||||||
{t("db_controller.field_settings.default_value")}
|
{t("db_controller.field_settings.default_value")}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -317,7 +320,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={"none"} >
|
<SelectItem value={"none"} >
|
||||||
No Default value
|
{t("db_controller.field_settings.no_default")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -333,6 +336,34 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
(defaultValueType.uuid) &&
|
||||||
|
|
||||||
|
<Select
|
||||||
|
aria-label="value"
|
||||||
|
value={selectedValues as any}
|
||||||
|
onValueChange={enumValueChange as any}
|
||||||
|
|
||||||
|
>
|
||||||
|
<SelectTrigger id="charset" className="w-full flex ">
|
||||||
|
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"none"} >
|
||||||
|
{t("db_controller.field_settings.no_default")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"random"} >
|
||||||
|
{t("db_controller.field_settings.random_uuid")}
|
||||||
|
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
}
|
||||||
{
|
{
|
||||||
(defaultValueType.multiSelect) &&
|
(defaultValueType.multiSelect) &&
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
@@ -364,7 +395,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
|
<SelectItem value={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
|
||||||
{
|
{
|
||||||
(field.type.name == "datetime" || field.type.name?.includes("timestamp")) ?
|
(field.type.name?.includes("datetime") || field.type.name?.includes("timestamp") || ((field.type.dialect == DatabaseDialect.ORACLE || field.type.dialect == DatabaseDialect.MSSQL) && field.type.name == "date")) ?
|
||||||
<SelectItem value={TimeDefaultValues.NOW}>{t("db_controller.field_settings.time_default_value.now")}</SelectItem> : null
|
<SelectItem value={TimeDefaultValues.NOW}>{t("db_controller.field_settings.time_default_value.now")}</SelectItem> : null
|
||||||
}
|
}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -389,12 +420,11 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
|||||||
<Input
|
<Input
|
||||||
id="time"
|
id="time"
|
||||||
type="time"
|
type="time"
|
||||||
step={1}
|
step={1}
|
||||||
defaultValue={defaultDateTime}
|
defaultValue={defaultDateTime}
|
||||||
onChange={(event) => setDefaultDateTime( event.target.value)}
|
onChange={(event) => setDefaultDateTime(event.target.value)}
|
||||||
onBlur={saveDefaultDateTime as any}
|
onBlur={saveDefaultDateTime as any}
|
||||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -411,3 +441,5 @@ export default React.memo(fieldDefautlValue);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export const ar = {
|
|||||||
type_enter: "اكتب واضغط Enter",
|
type_enter: "اكتب واضغط Enter",
|
||||||
precision_def: "إجمالي الأرقام المسموحة (قبل + بعد الفاصلة).",
|
precision_def: "إجمالي الأرقام المسموحة (قبل + بعد الفاصلة).",
|
||||||
scale_def: "عدد الأرقام بعد الفاصلة.",
|
scale_def: "عدد الأرقام بعد الفاصلة.",
|
||||||
|
no_default: "لا توجد قيمة افتراضية",
|
||||||
|
random_uuid: "معرّف UUID عشوائي",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "لا توجد قيمة",
|
no_value: "لا توجد قيمة",
|
||||||
custom: "وقت مخصص",
|
custom: "وقت مخصص",
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export const de = {
|
|||||||
type_enter: "Tippen und Enter drücken",
|
type_enter: "Tippen und Enter drücken",
|
||||||
precision_def: "Zulässige Gesamtanzahl an Ziffern (vor + nach dem Dezimalpunkt).",
|
precision_def: "Zulässige Gesamtanzahl an Ziffern (vor + nach dem Dezimalpunkt).",
|
||||||
scale_def: "Ziffern nach dem Dezimalpunkt.",
|
scale_def: "Ziffern nach dem Dezimalpunkt.",
|
||||||
|
no_default: "Kein Standardwert",
|
||||||
|
random_uuid: "Zufällige UUID",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "Kein Wert",
|
no_value: "Kein Wert",
|
||||||
custom: "Benutzerdefinierte Zeit",
|
custom: "Benutzerdefinierte Zeit",
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export const en = {
|
|||||||
sidebar: {
|
sidebar: {
|
||||||
tables: "Tables",
|
tables: "Tables",
|
||||||
relationships: "Relationships",
|
relationships: "Relationships",
|
||||||
database: "Database",
|
database: "Database",
|
||||||
documentation : "Docs"
|
documentation: "Docs"
|
||||||
},
|
},
|
||||||
|
|
||||||
color_picker: {
|
color_picker: {
|
||||||
@@ -104,6 +104,8 @@ export const en = {
|
|||||||
type_enter: "Type and press enter",
|
type_enter: "Type and press enter",
|
||||||
precision_def: "Total digits allowed (before + after the decimal).",
|
precision_def: "Total digits allowed (before + after the decimal).",
|
||||||
scale_def: "Digits allowed after the decimal.",
|
scale_def: "Digits allowed after the decimal.",
|
||||||
|
no_default: "No Default value",
|
||||||
|
random_uuid: "Random UUID",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "No value",
|
no_value: "No value",
|
||||||
custom: "Custom time",
|
custom: "Custom time",
|
||||||
@@ -209,7 +211,7 @@ export const en = {
|
|||||||
import_database: {
|
import_database: {
|
||||||
title: "Import your Database",
|
title: "Import your Database",
|
||||||
import: "Import",
|
import: "Import",
|
||||||
view_docs : "View Docs" ,
|
view_docs: "View Docs",
|
||||||
import_options: "Would you like to import using :",
|
import_options: "Would you like to import using :",
|
||||||
import_error: "SQL Parsing Error",
|
import_error: "SQL Parsing Error",
|
||||||
import_error_description: "We couldn't import your SQL because it contains invalid syntax.",
|
import_error_description: "We couldn't import your SQL because it contains invalid syntax.",
|
||||||
@@ -277,7 +279,7 @@ export const en = {
|
|||||||
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
ssms: {
|
ssms: {
|
||||||
"step1": "Open <bold>SQL Server Management Studio (SSMS)</bold>.",
|
"step1": "Open <bold>SQL Server Management Studio (SSMS)</bold>.",
|
||||||
"step2": "Right-click your database, then select <bold>Tasks → Generate Scripts</bold> from the context menu.",
|
"step2": "Right-click your database, then select <bold>Tasks → Generate Scripts</bold> from the context menu.",
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export const es = {
|
|||||||
type_enter: "Escribe y presiona enter",
|
type_enter: "Escribe y presiona enter",
|
||||||
precision_def: "Total de dígitos permitidos (antes + después del decimal).",
|
precision_def: "Total de dígitos permitidos (antes + después del decimal).",
|
||||||
scale_def: "Dígitos permitidos después del decimal.",
|
scale_def: "Dígitos permitidos después del decimal.",
|
||||||
|
no_default: "Sin valor predeterminado",
|
||||||
|
random_uuid: "UUID aleatorio",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "Sin valor",
|
no_value: "Sin valor",
|
||||||
custom: "Hora personalizada",
|
custom: "Hora personalizada",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const fr = {
|
|||||||
tables: "Tables",
|
tables: "Tables",
|
||||||
relationships: "Relations",
|
relationships: "Relations",
|
||||||
database: "Base de données",
|
database: "Base de données",
|
||||||
documentation : "Docs"
|
documentation: "Docs"
|
||||||
|
|
||||||
},
|
},
|
||||||
color_picker: {
|
color_picker: {
|
||||||
@@ -33,7 +33,7 @@ export const fr = {
|
|||||||
note: "Note",
|
note: "Note",
|
||||||
name: "Nom",
|
name: "Nom",
|
||||||
type: "Type",
|
type: "Type",
|
||||||
required: "Obligatoire",
|
required: "Obligatoire",
|
||||||
|
|
||||||
nullable: "Nullable",
|
nullable: "Nullable",
|
||||||
select_fields: "Sélectionner les champs",
|
select_fields: "Sélectionner les champs",
|
||||||
@@ -97,6 +97,8 @@ export const fr = {
|
|||||||
type_enter: "Tapez et appuyez sur Entrée",
|
type_enter: "Tapez et appuyez sur Entrée",
|
||||||
precision_def: "Nombre total de chiffres autorisés (avant + après la virgule).",
|
precision_def: "Nombre total de chiffres autorisés (avant + après la virgule).",
|
||||||
scale_def: "Nombre de chiffres autorisés après la virgule.",
|
scale_def: "Nombre de chiffres autorisés après la virgule.",
|
||||||
|
no_default: "Aucune valeur par défaut",
|
||||||
|
random_uuid: "UUID aléatoire",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "Aucune valeur",
|
no_value: "Aucune valeur",
|
||||||
custom: "Heure personnalisée",
|
custom: "Heure personnalisée",
|
||||||
@@ -173,7 +175,7 @@ export const fr = {
|
|||||||
theme: "Thème",
|
theme: "Thème",
|
||||||
light: "Clair",
|
light: "Clair",
|
||||||
dark: "Sombre",
|
dark: "Sombre",
|
||||||
system: "Système",
|
system: "Système",
|
||||||
|
|
||||||
help: "Aide",
|
help: "Aide",
|
||||||
show_docs: "Afficher la documentation",
|
show_docs: "Afficher la documentation",
|
||||||
@@ -199,7 +201,7 @@ export const fr = {
|
|||||||
import_database: {
|
import_database: {
|
||||||
title: "Importer votre base de données",
|
title: "Importer votre base de données",
|
||||||
import: "Importer",
|
import: "Importer",
|
||||||
view_docs : "Voir la documentation" ,
|
view_docs: "Voir la documentation",
|
||||||
import_options: "Souhaitez-vous importer en utilisant :",
|
import_options: "Souhaitez-vous importer en utilisant :",
|
||||||
import_error: "Erreur d’analyse SQL",
|
import_error: "Erreur d’analyse SQL",
|
||||||
import_error_description: "Nous n'avons pas pu importer votre SQL car il contient une syntaxe invalide.",
|
import_error_description: "Nous n'avons pas pu importer votre SQL car il contient une syntaxe invalide.",
|
||||||
@@ -263,7 +265,7 @@ export const fr = {
|
|||||||
step4: "Dans la fenêtre, sélectionnez <bold>Exporter uniquement le schéma</bold> et cliquez sur <bold>Enregistrer</bold>.",
|
step4: "Dans la fenêtre, sélectionnez <bold>Exporter uniquement le schéma</bold> et cliquez sur <bold>Enregistrer</bold>.",
|
||||||
step5: "Enfin, copiez le contenu du fichier <code>.sql</code> dans l’éditeur de code ci-dessous."
|
step5: "Enfin, copiez le contenu du fichier <code>.sql</code> dans l’éditeur de code ci-dessous."
|
||||||
},
|
},
|
||||||
ssms: {
|
ssms: {
|
||||||
"step1": "Ouvrez SQL Server Management Studio (SSMS).",
|
"step1": "Ouvrez SQL Server Management Studio (SSMS).",
|
||||||
"step2": "Faites un clic droit sur votre base de données, puis sélectionnez Tâches → Générer des scripts dans le menu contextuel.",
|
"step2": "Faites un clic droit sur votre base de données, puis sélectionnez Tâches → Générer des scripts dans le menu contextuel.",
|
||||||
"step3": "Dans l’étape Choisir les objets, sélectionnez Choisir des objets spécifiques de la base de données, puis cochez toutes les tables.",
|
"step3": "Dans l’étape Choisir les objets, sélectionnez Choisir des objets spécifiques de la base de données, puis cochez toutes les tables.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export const hi = {
|
|||||||
type_enter: "टाइप करें और एंटर दबाएं",
|
type_enter: "टाइप करें और एंटर दबाएं",
|
||||||
precision_def: "कुल अनुमत अंक (दशमलव से पहले और बाद)।",
|
precision_def: "कुल अनुमत अंक (दशमलव से पहले और बाद)।",
|
||||||
scale_def: "दशमलव के बाद अनुमत अंक।",
|
scale_def: "दशमलव के बाद अनुमत अंक।",
|
||||||
|
no_default: "कोई डिफ़ॉल्ट मान नहीं",
|
||||||
|
random_uuid: "रैंडम UUID",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "कोई मान नहीं",
|
no_value: "कोई मान नहीं",
|
||||||
custom: "कस्टम समय",
|
custom: "कस्टम समय",
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export const pt = {
|
|||||||
type_enter: "Digite e pressione enter",
|
type_enter: "Digite e pressione enter",
|
||||||
precision_def: "Dígitos totais permitidos (antes + depois do decimal).",
|
precision_def: "Dígitos totais permitidos (antes + depois do decimal).",
|
||||||
scale_def: "Dígitos permitidos após o decimal.",
|
scale_def: "Dígitos permitidos após o decimal.",
|
||||||
|
no_default: "Sem valor padrão",
|
||||||
|
random_uuid: "UUID aleatório",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "Sem valor",
|
no_value: "Sem valor",
|
||||||
custom: "Hora personalizada",
|
custom: "Hora personalizada",
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export const ru = {
|
|||||||
type_enter: "Введите и нажмите Enter",
|
type_enter: "Введите и нажмите Enter",
|
||||||
precision_def: "Общее количество допустимых цифр (до и после запятой).",
|
precision_def: "Общее количество допустимых цифр (до и после запятой).",
|
||||||
scale_def: "Допустимое количество цифр после запятой.",
|
scale_def: "Допустимое количество цифр после запятой.",
|
||||||
|
no_default: "Нет значения по умолчанию",
|
||||||
|
random_uuid: "Случайный UUID",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "Нет значения",
|
no_value: "Нет значения",
|
||||||
custom: "Произвольное время",
|
custom: "Произвольное время",
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export const zh = {
|
|||||||
type_enter: "输入并按回车",
|
type_enter: "输入并按回车",
|
||||||
precision_def: "允许的总位数(小数点前+后)。",
|
precision_def: "允许的总位数(小数点前+后)。",
|
||||||
scale_def: "小数点后的位数。",
|
scale_def: "小数点后的位数。",
|
||||||
|
no_default: "无默认值",
|
||||||
|
random_uuid: "随机 UUID",
|
||||||
time_default_value: {
|
time_default_value: {
|
||||||
no_value: "无值",
|
no_value: "无值",
|
||||||
custom: "自定义时间",
|
custom: "自定义时间",
|
||||||
|
|||||||
Vendored
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { DataInsertType, DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { MysqlDataType } from "@/lib/data_types/mysql_data_types";
|
||||||
|
import { PostgresDataType } from "@/lib/data_types/postgres_data_types";
|
||||||
|
import { SqliteDataTypes } from "@/lib/data_types/sqlite_data_types";
|
||||||
|
import { MariaDbDataType } from "@/lib/data_types/mariadb_data_types";
|
||||||
|
import { OracleDataType } from "@/lib/data_types/oracle_data_types";
|
||||||
|
import { MSSQLDataType } from "@/lib/data_types/mssql_data_types";
|
||||||
|
|
||||||
|
// Build a DataType[] fixture straight from the seed arrays, applying the exact
|
||||||
|
// same transform as seedDataTypes() -> mapToDataType() in the app (see
|
||||||
|
// src/lib/data_types/seed_datatypes.ts). The importer reads `modifiers` and
|
||||||
|
// `synonyms` as JSON strings, so they must be stringified here just like the
|
||||||
|
// production seeding does. This lets the tests exercise the real supported-type
|
||||||
|
// tables without booting the WASM SQLite database.
|
||||||
|
const mapToDataType = (
|
||||||
|
dataTypes: Partial<DataInsertType>[],
|
||||||
|
dialect: DatabaseDialect,
|
||||||
|
): DataType[] =>
|
||||||
|
dataTypes.map(
|
||||||
|
(dataType) =>
|
||||||
|
({
|
||||||
|
...dataType,
|
||||||
|
dialect,
|
||||||
|
modifiers: dataType.modifiers
|
||||||
|
? JSON.stringify(dataType.modifiers)
|
||||||
|
: null,
|
||||||
|
synonyms: dataType.synonyms ? JSON.stringify(dataType.synonyms) : null,
|
||||||
|
}) as DataType,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataTypesByDialect: Record<DatabaseDialect, DataType[]> = {
|
||||||
|
[DatabaseDialect.MYSQL]: mapToDataType(MysqlDataType, DatabaseDialect.MYSQL),
|
||||||
|
[DatabaseDialect.POSTGRES]: mapToDataType(
|
||||||
|
PostgresDataType,
|
||||||
|
DatabaseDialect.POSTGRES,
|
||||||
|
),
|
||||||
|
[DatabaseDialect.SQLITE]: mapToDataType(
|
||||||
|
SqliteDataTypes,
|
||||||
|
DatabaseDialect.SQLITE,
|
||||||
|
),
|
||||||
|
[DatabaseDialect.MARIADB]: mapToDataType(
|
||||||
|
MariaDbDataType,
|
||||||
|
DatabaseDialect.MARIADB,
|
||||||
|
),
|
||||||
|
[DatabaseDialect.ORACLE]: mapToDataType(
|
||||||
|
OracleDataType,
|
||||||
|
DatabaseDialect.ORACLE,
|
||||||
|
),
|
||||||
|
[DatabaseDialect.MSSQL]: mapToDataType(MSSQLDataType, DatabaseDialect.MSSQL),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDataTypes = (dialect: DatabaseDialect): DataType[] =>
|
||||||
|
dataTypesByDialect[dialect];
|
||||||
Vendored
+178
@@ -0,0 +1,178 @@
|
|||||||
|
import { getDataTypes } from "./data-types";
|
||||||
|
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { ForeignKeyActions } from "@/lib/field";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import {
|
||||||
|
Cardinality,
|
||||||
|
RelationshipType,
|
||||||
|
} from "@/lib/schemas/relationship-schema";
|
||||||
|
|
||||||
|
// Pick a data type by preferred name, falling back to a category predicate.
|
||||||
|
// The seed arrays list types in dialect-specific order, so selecting by
|
||||||
|
// category alone would grab e.g. CHAR before VARCHAR or TINYINT before INTEGER;
|
||||||
|
// naming the canonical type keeps the rendered DDL representative per dialect.
|
||||||
|
const pick = (
|
||||||
|
types: DataType[],
|
||||||
|
names: string[],
|
||||||
|
fallback: (t: DataType) => boolean,
|
||||||
|
): DataType => {
|
||||||
|
for (const name of names) {
|
||||||
|
const match = types.find((t) => t.name?.toLowerCase() === name);
|
||||||
|
|
||||||
|
if (match) return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.find(fallback) as DataType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve a representative type per category for a dialect, straight from the
|
||||||
|
// seed data types, so the fixture uses real type ids the renderer can hydrate.
|
||||||
|
const resolveTypes = (dialect: DatabaseDialect) => {
|
||||||
|
const types = getDataTypes(dialect);
|
||||||
|
const integer = pick(
|
||||||
|
types,
|
||||||
|
["integer", "int", "number"],
|
||||||
|
(t) => t.type === "integer" || t.type === "numeric",
|
||||||
|
);
|
||||||
|
const varchar = pick(
|
||||||
|
types,
|
||||||
|
["varchar", "varchar2", "nvarchar", "character varying"],
|
||||||
|
(t) => t.type === "text",
|
||||||
|
);
|
||||||
|
|
||||||
|
return { integer, varchar };
|
||||||
|
};
|
||||||
|
|
||||||
|
const field = (over: Partial<FieldType>): FieldType =>
|
||||||
|
({
|
||||||
|
isPrimary: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
autoIncrement: false,
|
||||||
|
sequence: 0,
|
||||||
|
...over,
|
||||||
|
}) as FieldType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A minimal but representative two-table schema (users, posts) with a
|
||||||
|
* primary key, an auto-increment column, a NOT NULL UNIQUE text column with a
|
||||||
|
* length, a DEFAULT, and a posts -> users foreign key. Ids are stable strings
|
||||||
|
* so assertions and failures are readable. Relationship source/target objects
|
||||||
|
* are left for prepareForMigration/optimizeOps to hydrate from the ids, exactly
|
||||||
|
* as the app does before rendering.
|
||||||
|
*/
|
||||||
|
export const buildSampleDatabase = (dialect: DatabaseDialect): DatabaseType => {
|
||||||
|
const { integer, varchar } = resolveTypes(dialect);
|
||||||
|
|
||||||
|
const usersFields: FieldType[] = [
|
||||||
|
field({
|
||||||
|
id: "users.id",
|
||||||
|
tableId: "users",
|
||||||
|
name: "id",
|
||||||
|
typeId: integer.id,
|
||||||
|
isPrimary: true,
|
||||||
|
nullable: false,
|
||||||
|
autoIncrement: true,
|
||||||
|
}),
|
||||||
|
field({
|
||||||
|
id: "users.email",
|
||||||
|
tableId: "users",
|
||||||
|
name: "email",
|
||||||
|
typeId: varchar.id,
|
||||||
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
|
maxLength: 255,
|
||||||
|
}),
|
||||||
|
field({
|
||||||
|
id: "users.status",
|
||||||
|
tableId: "users",
|
||||||
|
name: "status",
|
||||||
|
typeId: varchar.id,
|
||||||
|
nullable: false,
|
||||||
|
maxLength: 20,
|
||||||
|
defaultValue: "active",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const postsFields: FieldType[] = [
|
||||||
|
field({
|
||||||
|
id: "posts.id",
|
||||||
|
tableId: "posts",
|
||||||
|
name: "id",
|
||||||
|
typeId: integer.id,
|
||||||
|
isPrimary: true,
|
||||||
|
nullable: false,
|
||||||
|
autoIncrement: true,
|
||||||
|
}),
|
||||||
|
field({
|
||||||
|
id: "posts.user_id",
|
||||||
|
tableId: "posts",
|
||||||
|
name: "user_id",
|
||||||
|
typeId: integer.id,
|
||||||
|
nullable: false,
|
||||||
|
}),
|
||||||
|
field({
|
||||||
|
id: "posts.title",
|
||||||
|
tableId: "posts",
|
||||||
|
name: "title",
|
||||||
|
typeId: varchar.id,
|
||||||
|
nullable: false,
|
||||||
|
maxLength: 255,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const users = {
|
||||||
|
id: "users",
|
||||||
|
name: "users",
|
||||||
|
fields: usersFields,
|
||||||
|
indices: [] as IndexType[],
|
||||||
|
sequence: 0,
|
||||||
|
} as TableType;
|
||||||
|
|
||||||
|
const posts = {
|
||||||
|
id: "posts",
|
||||||
|
name: "posts",
|
||||||
|
fields: postsFields,
|
||||||
|
indices: [] as IndexType[],
|
||||||
|
sequence: 1,
|
||||||
|
} as TableType;
|
||||||
|
|
||||||
|
const tables: TableType[] = [users, posts];
|
||||||
|
|
||||||
|
// Embed the source/target table and field objects the way the app's data
|
||||||
|
// layer hands them to the renderer. The SQLite renderer orders tables up
|
||||||
|
// front by reading relationship.sourceTable / targetTable off the original
|
||||||
|
// database (before prepareForMigration re-hydrates), so these must be present.
|
||||||
|
const relationships: RelationshipType[] = [
|
||||||
|
{
|
||||||
|
id: "rel_posts_users",
|
||||||
|
name: null,
|
||||||
|
sourceTableId: "users",
|
||||||
|
targetTableId: "posts",
|
||||||
|
sourceFieldId: "users.id",
|
||||||
|
targetFieldId: "posts.user_id",
|
||||||
|
sourceTable: users,
|
||||||
|
targetTable: posts,
|
||||||
|
sourceField: usersFields[0],
|
||||||
|
targetField: postsFields[1],
|
||||||
|
cardinality: Cardinality.one_to_many,
|
||||||
|
onDelete: ForeignKeyActions.CASCADE,
|
||||||
|
databaseId: "db",
|
||||||
|
} as RelationshipType,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "db",
|
||||||
|
name: "testdb",
|
||||||
|
dialect,
|
||||||
|
numOfTables: tables.length,
|
||||||
|
createdAt: null,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
} as DatabaseType;
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { cloneField, getNextSequence } from "./field";
|
||||||
|
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
|
||||||
|
// Minimal field factory. FieldType is a Drizzle-inferred row type with many
|
||||||
|
// columns; these helpers only read `sequence` (and copy the rest), so a partial
|
||||||
|
// cast keeps the fixtures focused on what is under test.
|
||||||
|
const makeField = (overrides: Partial<FieldType> = {}): FieldType =>
|
||||||
|
({
|
||||||
|
id: "f1",
|
||||||
|
tableId: "t1",
|
||||||
|
name: "col",
|
||||||
|
sequence: 0,
|
||||||
|
...overrides,
|
||||||
|
}) as FieldType;
|
||||||
|
|
||||||
|
describe("getNextSequence", () => {
|
||||||
|
it("returns 0 for an empty field list", () => {
|
||||||
|
expect(getNextSequence([])).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns max sequence + 1", () => {
|
||||||
|
const fields = [
|
||||||
|
makeField({ sequence: 0 }),
|
||||||
|
makeField({ sequence: 5 }),
|
||||||
|
makeField({ sequence: 2 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getNextSequence(fields)).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a single field", () => {
|
||||||
|
expect(getNextSequence([makeField({ sequence: 4 })])).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cloneField", () => {
|
||||||
|
it("assigns a fresh id and preserves other properties", () => {
|
||||||
|
const original = makeField({
|
||||||
|
id: "original-id",
|
||||||
|
name: "email",
|
||||||
|
sequence: 3,
|
||||||
|
});
|
||||||
|
const clone = cloneField(original);
|
||||||
|
|
||||||
|
expect(clone.id).not.toBe(original.id);
|
||||||
|
expect(clone.name).toBe("email");
|
||||||
|
expect(clone.sequence).toBe(3);
|
||||||
|
expect(clone.tableId).toBe(original.tableId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mutate the source field", () => {
|
||||||
|
const original = makeField({ id: "original-id" });
|
||||||
|
|
||||||
|
cloneField(original);
|
||||||
|
expect(original.id).toBe("original-id");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
|
import { init } from "@guanmingchiu/sqlparser-ts";
|
||||||
|
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { getImporter } from "@/utils/import/import-utils";
|
||||||
|
import { getDataTypes } from "@/test/fixtures/data-types";
|
||||||
|
import { PostgresSqlExample } from "@/lib/import/import_db";
|
||||||
|
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||||
|
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||||
|
|
||||||
|
// The parser is a WASM module whose init() is async. BaseSqlImporter's
|
||||||
|
// constructor kicks it off but does not await it (fine in the browser, where
|
||||||
|
// the promise resolves long before a user imports SQL). In tests we must await
|
||||||
|
// it once up front; init() caches the module globally, so every subsequent
|
||||||
|
// synchronous parseSql() call across all dialects then works.
|
||||||
|
beforeAll(async () => {
|
||||||
|
await init();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getField = (table: TableInsertType, name: string): FieldInsertType =>
|
||||||
|
(table.fields ?? []).find((f) => f.name === name) as FieldInsertType;
|
||||||
|
|
||||||
|
interface DialectCase {
|
||||||
|
name: string;
|
||||||
|
dialect: DatabaseDialect;
|
||||||
|
sql: string;
|
||||||
|
// MySQL / MariaDB / SQL Server surface AUTO_INCREMENT / IDENTITY; the others do not.
|
||||||
|
idAutoIncrement: boolean;
|
||||||
|
// SQLite uses TEXT (no length); the rest carry a VARCHAR length.
|
||||||
|
varcharMaxLength: number | undefined;
|
||||||
|
// Only the MySQL and PostgreSQL fixtures include a CREATE INDEX statement.
|
||||||
|
indexCount: number;
|
||||||
|
// Only the fixtures that spell out ON DELETE CASCADE carry an onDelete action.
|
||||||
|
fkOnDelete: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases: DialectCase[] = [
|
||||||
|
{
|
||||||
|
name: "MySQL",
|
||||||
|
dialect: DatabaseDialect.MYSQL,
|
||||||
|
idAutoIncrement: true,
|
||||||
|
varcharMaxLength: 255,
|
||||||
|
indexCount: 1,
|
||||||
|
fkOnDelete: "cascade",
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_posts_user_id ON posts (user_id);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MariaDB",
|
||||||
|
dialect: DatabaseDialect.MARIADB,
|
||||||
|
idAutoIncrement: true,
|
||||||
|
varcharMaxLength: 255,
|
||||||
|
indexCount: 0,
|
||||||
|
fkOnDelete: "cascade",
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PostgreSQL",
|
||||||
|
dialect: DatabaseDialect.POSTGRES,
|
||||||
|
idAutoIncrement: false,
|
||||||
|
varcharMaxLength: 255,
|
||||||
|
indexCount: 1,
|
||||||
|
fkOnDelete: "cascade",
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id integer PRIMARY KEY,
|
||||||
|
email varchar(255) NOT NULL UNIQUE,
|
||||||
|
status varchar(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id integer PRIMARY KEY,
|
||||||
|
user_id integer NOT NULL,
|
||||||
|
title varchar(255) NOT NULL,
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_posts_user_id ON posts (user_id);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQLite",
|
||||||
|
dialect: DatabaseDialect.SQLITE,
|
||||||
|
idAutoIncrement: false,
|
||||||
|
varcharMaxLength: undefined,
|
||||||
|
indexCount: 0,
|
||||||
|
fkOnDelete: "cascade",
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oracle",
|
||||||
|
dialect: DatabaseDialect.ORACLE,
|
||||||
|
idAutoIncrement: false,
|
||||||
|
varcharMaxLength: 255,
|
||||||
|
indexCount: 0,
|
||||||
|
fkOnDelete: undefined,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id NUMBER PRIMARY KEY,
|
||||||
|
email VARCHAR2(255) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR2(20) DEFAULT 'active' NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id NUMBER PRIMARY KEY,
|
||||||
|
user_id NUMBER NOT NULL,
|
||||||
|
title VARCHAR2(255) NOT NULL,
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQL Server",
|
||||||
|
dialect: DatabaseDialect.MSSQL,
|
||||||
|
idAutoIncrement: true,
|
||||||
|
varcharMaxLength: 255,
|
||||||
|
indexCount: 0,
|
||||||
|
fkOnDelete: undefined,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
email NVARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
status NVARCHAR(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
title NVARCHAR(255) NOT NULL,
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("getImporter().parseSql - CREATE TABLE across dialects", () => {
|
||||||
|
for (const c of cases) {
|
||||||
|
describe(c.name, () => {
|
||||||
|
const parse = () =>
|
||||||
|
getImporter(c.dialect, getDataTypes(c.dialect)).parseSql(c.sql);
|
||||||
|
const typeById = new Map(
|
||||||
|
getDataTypes(c.dialect).map((dt) => [dt.id, dt]),
|
||||||
|
);
|
||||||
|
|
||||||
|
it("parses both tables with their columns and no errors", () => {
|
||||||
|
const r = parse();
|
||||||
|
|
||||||
|
expect(r.errors).toHaveLength(0);
|
||||||
|
expect(r.tables.map((t) => t.name).sort()).toEqual(["posts", "users"]);
|
||||||
|
const users = r.tables.find(
|
||||||
|
(t) => t.name === "users",
|
||||||
|
) as TableInsertType;
|
||||||
|
const posts = r.tables.find(
|
||||||
|
(t) => t.name === "posts",
|
||||||
|
) as TableInsertType;
|
||||||
|
|
||||||
|
expect(users.fields).toHaveLength(3);
|
||||||
|
expect(posts.fields).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps the primary key column", () => {
|
||||||
|
const users = parse().tables.find(
|
||||||
|
(t) => t.name === "users",
|
||||||
|
) as TableInsertType;
|
||||||
|
const id = getField(users, "id");
|
||||||
|
|
||||||
|
expect(id.isPrimary).toBe(true);
|
||||||
|
expect(id.nullable).toBe(false);
|
||||||
|
expect(id.autoIncrement).toBe(c.idAutoIncrement);
|
||||||
|
// id resolves to a numeric family type (integer, or numeric for Oracle NUMBER)
|
||||||
|
expect(["integer", "numeric"]).toContain(
|
||||||
|
typeById.get(id.typeId as string)?.type,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps a NOT NULL UNIQUE text column with its length", () => {
|
||||||
|
const users = parse().tables.find(
|
||||||
|
(t) => t.name === "users",
|
||||||
|
) as TableInsertType;
|
||||||
|
const email = getField(users, "email");
|
||||||
|
|
||||||
|
expect(email.unique).toBe(true);
|
||||||
|
expect(email.nullable).toBe(false);
|
||||||
|
expect(email.isPrimary).toBe(false);
|
||||||
|
expect(typeById.get(email.typeId as string)?.type).toBe("text");
|
||||||
|
expect(email.maxLength ?? undefined).toBe(c.varcharMaxLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses the DEFAULT value", () => {
|
||||||
|
const users = parse().tables.find(
|
||||||
|
(t) => t.name === "users",
|
||||||
|
) as TableInsertType;
|
||||||
|
const status = getField(users, "status");
|
||||||
|
|
||||||
|
expect(status.defaultValue).toBe("active");
|
||||||
|
expect(status.nullable).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures the foreign key from posts to users", () => {
|
||||||
|
const r = parse();
|
||||||
|
const byName = new Map(r.tables.map((t) => [t.name, t.id]));
|
||||||
|
|
||||||
|
expect(r.relationships).toHaveLength(1);
|
||||||
|
const rel = r.relationships[0];
|
||||||
|
|
||||||
|
// convention: source is the referenced (parent) table, target holds the FK
|
||||||
|
expect(rel.sourceTableId).toBe(byName.get("users"));
|
||||||
|
expect(rel.targetTableId).toBe(byName.get("posts"));
|
||||||
|
expect(rel.cardinality).toBe("one_to_many");
|
||||||
|
expect(rel.onDelete).toBe(c.fkOnDelete);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses CREATE INDEX statements", () => {
|
||||||
|
const r = parse();
|
||||||
|
|
||||||
|
expect(r.indexes).toHaveLength(c.indexCount);
|
||||||
|
if (c.indexCount > 0) {
|
||||||
|
const posts = r.tables.find(
|
||||||
|
(t) => t.name === "posts",
|
||||||
|
) as TableInsertType;
|
||||||
|
|
||||||
|
expect(r.indexes[0].name).toBe("idx_posts_user_id");
|
||||||
|
expect(r.indexes[0].tableId).toBe(posts.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getImporter().parseSql - realistic and malformed input", () => {
|
||||||
|
it("parses the bundled PostgreSQL dump", () => {
|
||||||
|
const r = getImporter(
|
||||||
|
DatabaseDialect.POSTGRES,
|
||||||
|
getDataTypes(DatabaseDialect.POSTGRES),
|
||||||
|
).parseSql(PostgresSqlExample);
|
||||||
|
|
||||||
|
expect(r.tables.length).toBe(4);
|
||||||
|
expect(r.relationships.length).toBe(3);
|
||||||
|
expect(r.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collects errors from a malformed statement without dropping valid tables", () => {
|
||||||
|
const r = getImporter(
|
||||||
|
DatabaseDialect.POSTGRES,
|
||||||
|
getDataTypes(DatabaseDialect.POSTGRES),
|
||||||
|
).parseSql(
|
||||||
|
"CREATE TABLE good (id integer PRIMARY KEY); CREATE TABLE bad (;",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(r.tables.map((t) => t.name)).toContain("good");
|
||||||
|
expect(r.errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when there is nothing parseable", () => {
|
||||||
|
const importer = getImporter(
|
||||||
|
DatabaseDialect.POSTGRES,
|
||||||
|
getDataTypes(DatabaseDialect.POSTGRES),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => importer.parseSql("this is not sql at all")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
fixCharsetPlacement,
|
||||||
|
fixSQLiteColumnOrder,
|
||||||
|
getPostgresEnumName,
|
||||||
|
} from "./render-uttils";
|
||||||
|
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
|
||||||
|
describe("fixCharsetPlacement", () => {
|
||||||
|
it("moves CHARACTER SET / COLLATE directly after the column type", () => {
|
||||||
|
const sql = [
|
||||||
|
"CREATE TABLE users (",
|
||||||
|
"name VARCHAR(255) NOT NULL CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,",
|
||||||
|
"email TEXT",
|
||||||
|
")",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const out = fixCharsetPlacement(sql);
|
||||||
|
|
||||||
|
expect(out).toContain(
|
||||||
|
"name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL",
|
||||||
|
);
|
||||||
|
// charset/collation must sit before the remaining attributes
|
||||||
|
const line = out.split("\n").find((l) => l.includes("CHARACTER SET")) ?? "";
|
||||||
|
|
||||||
|
expect(line.indexOf("CHARACTER SET")).toBeLessThan(
|
||||||
|
line.indexOf("NOT NULL"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves columns without charset/collation untouched", () => {
|
||||||
|
const sql = ["CREATE TABLE t (", "email TEXT", ")"].join("\n");
|
||||||
|
|
||||||
|
expect(fixCharsetPlacement(sql)).toContain("email TEXT");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fixSQLiteColumnOrder", () => {
|
||||||
|
it("reorders INTEGER PK attributes to PRIMARY KEY / AUTOINCREMENT / NOT NULL", () => {
|
||||||
|
const sql = [
|
||||||
|
"CREATE TABLE t (",
|
||||||
|
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT",
|
||||||
|
")",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
expect(fixSQLiteColumnOrder(sql)).toContain(
|
||||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves non-primary-key INTEGER columns unchanged", () => {
|
||||||
|
const sql = ["CREATE TABLE t (", "age INTEGER DEFAULT 0", ")"].join("\n");
|
||||||
|
|
||||||
|
expect(fixSQLiteColumnOrder(sql)).toContain("age INTEGER DEFAULT 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPostgresEnumName", () => {
|
||||||
|
it("builds <table>_<lowercased field>_enum", () => {
|
||||||
|
const table = { name: "Order" } as TableType;
|
||||||
|
const field = { name: "Status" } as FieldType;
|
||||||
|
|
||||||
|
expect(getPostgresEnumName(table, field)).toBe("Order_status_enum");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { getRenderer } from "@/utils/render/render-uttils";
|
||||||
|
import { getDataTypes } from "@/test/fixtures/data-types";
|
||||||
|
import { buildSampleDatabase } from "@/test/fixtures/database";
|
||||||
|
|
||||||
|
// getRenderer().renderDDL() takes a DatabaseType and emits dialect DDL. It runs
|
||||||
|
// the migration diff (empty database -> the fixture), so the output is the full
|
||||||
|
// CREATE for every table plus the foreign key. Assertions match on substrings
|
||||||
|
// and quote-agnostic patterns rather than exact strings, since formatting and
|
||||||
|
// identifier quoting differ per dialect and are not what these tests pin down.
|
||||||
|
|
||||||
|
interface RenderCase {
|
||||||
|
name: string;
|
||||||
|
dialect: DatabaseDialect;
|
||||||
|
// Dialect-specific spelling of an auto-increment / identity column.
|
||||||
|
autoIncrement: RegExp;
|
||||||
|
// Dialect-specific spelling of a variable-length string type.
|
||||||
|
varchar: RegExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases: RenderCase[] = [
|
||||||
|
{
|
||||||
|
name: "MySQL",
|
||||||
|
dialect: DatabaseDialect.MYSQL,
|
||||||
|
autoIncrement: /AUTO_INCREMENT/,
|
||||||
|
varchar: /VARCHAR\s*\(\s*255\s*\)/i,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MariaDB",
|
||||||
|
dialect: DatabaseDialect.MARIADB,
|
||||||
|
autoIncrement: /AUTO_INCREMENT/,
|
||||||
|
varchar: /VARCHAR\s*\(\s*255\s*\)/i,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PostgreSQL",
|
||||||
|
dialect: DatabaseDialect.POSTGRES,
|
||||||
|
// auto-increment integers become SERIAL in Postgres
|
||||||
|
autoIncrement: /SERIAL/,
|
||||||
|
varchar: /VARCHAR\s*\(\s*255\s*\)/i,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQLite",
|
||||||
|
dialect: DatabaseDialect.SQLITE,
|
||||||
|
autoIncrement: /AUTOINCREMENT/,
|
||||||
|
// SQLite has no VARCHAR; the text column renders as TEXT
|
||||||
|
varchar: /TEXT\s*\(\s*255\s*\)/i,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oracle",
|
||||||
|
dialect: DatabaseDialect.ORACLE,
|
||||||
|
autoIncrement: /IDENTITY/,
|
||||||
|
varchar: /VARCHAR2\s*\(\s*255\s*\)/i,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQL Server",
|
||||||
|
dialect: DatabaseDialect.MSSQL,
|
||||||
|
autoIncrement: /IDENTITY/,
|
||||||
|
varchar: /VARCHAR\s*\(\s*255\s*\)/i,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const render = (dialect: DatabaseDialect): Promise<string> =>
|
||||||
|
getRenderer(dialect, getDataTypes(dialect))!.renderDDL(
|
||||||
|
buildSampleDatabase(dialect),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("getRenderer().renderDDL - emitted DDL across dialects", () => {
|
||||||
|
for (const c of cases) {
|
||||||
|
describe(c.name, () => {
|
||||||
|
it("creates both tables with their columns", async () => {
|
||||||
|
const sql = await render(c.dialect);
|
||||||
|
|
||||||
|
expect(sql).toMatch(/CREATE TABLE\s+[`"]?users[`"]?/i);
|
||||||
|
expect(sql).toMatch(/CREATE TABLE\s+[`"]?posts[`"]?/i);
|
||||||
|
for (const col of ["id", "email", "status", "user_id", "title"]) {
|
||||||
|
expect(sql).toContain(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits primary key, unique and auto-increment", async () => {
|
||||||
|
const sql = await render(c.dialect);
|
||||||
|
|
||||||
|
expect(sql).toMatch(/PRIMARY KEY/i);
|
||||||
|
expect(sql).toMatch(/UNIQUE/i);
|
||||||
|
expect(sql).toMatch(c.autoIncrement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits the text column type with its length", async () => {
|
||||||
|
const sql = await render(c.dialect);
|
||||||
|
|
||||||
|
expect(sql).toMatch(c.varchar);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits the DEFAULT value", async () => {
|
||||||
|
const sql = await render(c.dialect);
|
||||||
|
|
||||||
|
expect(sql).toMatch(/DEFAULT\s+'active'/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits the foreign key from posts to users with ON DELETE CASCADE", async () => {
|
||||||
|
const sql = await render(c.dialect);
|
||||||
|
|
||||||
|
expect(sql).toMatch(/FOREIGN KEY\s*\(\s*user_id\s*\)/i);
|
||||||
|
expect(sql).toMatch(/REFERENCES\s+[`"]?users[`"]?\s*\(\s*id\s*\)/i);
|
||||||
|
expect(sql).toMatch(/ON DELETE CASCADE/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
|
import { init } from "@guanmingchiu/sqlparser-ts";
|
||||||
|
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { getImporter } from "@/utils/import/import-utils";
|
||||||
|
import { getRenderer } from "@/utils/render/render-uttils";
|
||||||
|
import { getDataTypes } from "@/test/fixtures/data-types";
|
||||||
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
|
||||||
|
// Round-trip: parse DDL -> model -> render DDL -> parse again, and assert the
|
||||||
|
// two models are equal. This is the strongest integration check of the
|
||||||
|
// import/render pipeline. We compare a normalized model (names, resolved type
|
||||||
|
// names, key/constraint flags, relationships), not raw SQL, since formatting
|
||||||
|
// and identifier quoting legitimately differ.
|
||||||
|
|
||||||
|
type ParseResult = ReturnType<ReturnType<typeof getImporter>["parseSql"]>;
|
||||||
|
|
||||||
|
// Adapter: assemble a DatabaseType (what renderDDL consumes) from parseSql
|
||||||
|
// output (tables + id-based relationships). The app normally round-trips this
|
||||||
|
// through the SQLite database; here we build it in memory. Relationship
|
||||||
|
// source/target objects are embedded because the SQLite renderer reads them off
|
||||||
|
// the raw database when ordering tables.
|
||||||
|
const toDatabase = (
|
||||||
|
dialect: DatabaseDialect,
|
||||||
|
result: ParseResult,
|
||||||
|
): DatabaseType => {
|
||||||
|
const tables = result.tables.map(
|
||||||
|
(t) => ({ ...t, indices: [] as IndexType[] }) as TableType,
|
||||||
|
);
|
||||||
|
const table = (id: string) => tables.find((t) => t.id === id);
|
||||||
|
const relationships = result.relationships.map((r) => {
|
||||||
|
const source = table(r.sourceTableId);
|
||||||
|
const target = table(r.targetTableId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
databaseId: "db",
|
||||||
|
sourceTable: source,
|
||||||
|
targetTable: target,
|
||||||
|
sourceField: source?.fields?.find((f) => f.id === r.sourceFieldId),
|
||||||
|
targetField: target?.fields?.find((f) => f.id === r.targetFieldId),
|
||||||
|
} as RelationshipType;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "db",
|
||||||
|
name: "roundtrip",
|
||||||
|
dialect,
|
||||||
|
numOfTables: tables.length,
|
||||||
|
createdAt: null,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
} as DatabaseType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const byName = (a: { name?: string | null }, b: { name?: string | null }) =>
|
||||||
|
(a.name ?? "").localeCompare(b.name ?? "");
|
||||||
|
|
||||||
|
const normalize = (dialect: DatabaseDialect, result: ParseResult) => {
|
||||||
|
const types = getDataTypes(dialect);
|
||||||
|
const typeName = (id?: string | null) =>
|
||||||
|
types.find((t) => t.id === id)?.name ?? null;
|
||||||
|
const tableName = (id: string) =>
|
||||||
|
result.tables.find((t) => t.id === id)?.name ?? id;
|
||||||
|
const fieldName = (tableId: string, fieldId: string) =>
|
||||||
|
result.tables
|
||||||
|
.find((t) => t.id === tableId)
|
||||||
|
?.fields?.find((f: FieldType) => f.id === fieldId)?.name ?? fieldId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tables: [...result.tables].sort(byName).map((t) => ({
|
||||||
|
name: t.name,
|
||||||
|
columns: [...(t.fields ?? [])].sort(byName).map((f: FieldType) => ({
|
||||||
|
name: f.name,
|
||||||
|
type: typeName(f.typeId),
|
||||||
|
isPrimary: !!f.isPrimary,
|
||||||
|
// a primary key is non-nullable in every SQL dialect; canonicalize it.
|
||||||
|
// (SQL Server renders the PK as a table constraint, and the importer
|
||||||
|
// only forces NOT NULL for inline primary keys, so without this the
|
||||||
|
// round-tripped nullable flag would spuriously differ for MSSQL.)
|
||||||
|
nullable: f.isPrimary ? false : !!f.nullable,
|
||||||
|
unique: !!f.unique,
|
||||||
|
autoIncrement: !!f.autoIncrement,
|
||||||
|
maxLength: f.maxLength ?? null,
|
||||||
|
defaultValue: f.defaultValue ?? null,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
relationships: result.relationships
|
||||||
|
.map((r) => ({
|
||||||
|
source: `${tableName(r.sourceTableId)}.${fieldName(
|
||||||
|
r.sourceTableId,
|
||||||
|
r.sourceFieldId,
|
||||||
|
)}`,
|
||||||
|
target: `${tableName(r.targetTableId)}.${fieldName(
|
||||||
|
r.targetTableId,
|
||||||
|
r.targetFieldId,
|
||||||
|
)}`,
|
||||||
|
cardinality: r.cardinality,
|
||||||
|
onDelete: r.onDelete ?? null,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => (a.source + a.target).localeCompare(b.source + b.target)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RoundTripCase {
|
||||||
|
name: string;
|
||||||
|
dialect: DatabaseDialect;
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases: RoundTripCase[] = [
|
||||||
|
{
|
||||||
|
name: "MySQL",
|
||||||
|
dialect: DatabaseDialect.MYSQL,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MariaDB",
|
||||||
|
dialect: DatabaseDialect.MARIADB,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PostgreSQL",
|
||||||
|
dialect: DatabaseDialect.POSTGRES,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id integer PRIMARY KEY,
|
||||||
|
email varchar(255) NOT NULL UNIQUE,
|
||||||
|
status varchar(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id integer PRIMARY KEY,
|
||||||
|
user_id integer NOT NULL,
|
||||||
|
title varchar(255) NOT NULL,
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQLite",
|
||||||
|
dialect: DatabaseDialect.SQLITE,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oracle",
|
||||||
|
dialect: DatabaseDialect.ORACLE,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id NUMBER PRIMARY KEY,
|
||||||
|
email VARCHAR2(255) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR2(20) DEFAULT 'active' NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id NUMBER PRIMARY KEY,
|
||||||
|
user_id NUMBER NOT NULL,
|
||||||
|
title VARCHAR2(255) NOT NULL,
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQL Server",
|
||||||
|
dialect: DatabaseDialect.MSSQL,
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
email NVARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
status NVARCHAR(20) NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
title NVARCHAR(255) NOT NULL,
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("import -> render -> import round-trip", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await init();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const c of cases) {
|
||||||
|
it(`${c.name} model is stable`, async () => {
|
||||||
|
const first = getImporter(c.dialect, getDataTypes(c.dialect)).parseSql(
|
||||||
|
c.sql,
|
||||||
|
);
|
||||||
|
|
||||||
|
// sanity: the seed actually produced the model we intend to round-trip
|
||||||
|
expect(first.errors).toHaveLength(0);
|
||||||
|
expect(first.tables).toHaveLength(2);
|
||||||
|
expect(first.relationships).toHaveLength(1);
|
||||||
|
|
||||||
|
const rendered = await getRenderer(
|
||||||
|
c.dialect,
|
||||||
|
getDataTypes(c.dialect),
|
||||||
|
)!.renderDDL(toDatabase(c.dialect, first));
|
||||||
|
|
||||||
|
const second = getImporter(c.dialect, getDataTypes(c.dialect)).parseSql(
|
||||||
|
rendered,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(second.errors).toHaveLength(0);
|
||||||
|
|
||||||
|
expect(normalize(c.dialect, second)).toEqual(normalize(c.dialect, first));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { orderTables } from "./tables";
|
||||||
|
|
||||||
|
import { SortableTable } from "@/lib/table";
|
||||||
|
|
||||||
|
// `relationships` holds the ids of the tables this table depends on (its foreign
|
||||||
|
// keys / parents), so a valid topological order lists dependencies first.
|
||||||
|
|
||||||
|
describe("orderTables", () => {
|
||||||
|
it("orders a dependency chain parent-first", () => {
|
||||||
|
const tables: SortableTable[] = [
|
||||||
|
{ tableId: "C", relationships: ["B"] },
|
||||||
|
{ tableId: "A", relationships: [] },
|
||||||
|
{ tableId: "B", relationships: ["A"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const order = orderTables(tables);
|
||||||
|
|
||||||
|
expect(order).toHaveLength(3);
|
||||||
|
expect(order.indexOf("A")).toBeLessThan(order.indexOf("B"));
|
||||||
|
expect(order.indexOf("B")).toBeLessThan(order.indexOf("C"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("places a table after every dependency it references", () => {
|
||||||
|
const tables: SortableTable[] = [
|
||||||
|
{ tableId: "orders", relationships: ["users", "products"] },
|
||||||
|
{ tableId: "users", relationships: [] },
|
||||||
|
{ tableId: "products", relationships: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const order = orderTables(tables);
|
||||||
|
|
||||||
|
expect(order.indexOf("users")).toBeLessThan(order.indexOf("orders"));
|
||||||
|
expect(order.indexOf("products")).toBeLessThan(order.indexOf("orders"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws a CircularDependencyError describing the cycle", () => {
|
||||||
|
const tables: SortableTable[] = [
|
||||||
|
{ tableId: "A", relationships: ["B"] },
|
||||||
|
{ tableId: "B", relationships: ["A"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
let caught: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
orderTables(tables);
|
||||||
|
} catch (error) {
|
||||||
|
caught = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(caught).toBeDefined();
|
||||||
|
const err = caught as {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
cycle: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(err.success).toBe(false);
|
||||||
|
expect(err.message).toBe("Cycle detected");
|
||||||
|
expect(Array.isArray(err.cycle)).toBe(true);
|
||||||
|
// the cycle is reported as a closed loop (first node repeated at the end)
|
||||||
|
expect(err.cycle[0]).toBe(err.cycle[err.cycle.length - 1]);
|
||||||
|
expect(err.cycle).toContain("A");
|
||||||
|
expect(err.cycle).toContain("B");
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -8,5 +8,5 @@
|
|||||||
|
|
||||||
"strict": true
|
"strict": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts", "vitest.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
// Standalone test config: the app's vite.config.ts pulls in Tailwind, React and
|
||||||
|
// WASM/worker plugins that pure-logic tests do not need. We only need the `@/`
|
||||||
|
// alias to resolve, which vite-tsconfig-paths provides from tsconfig.json.
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tsconfigPaths()],
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user