From 262471e7eddd63030a1ad8586eaab99d919c5510 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Wed, 8 Jul 2026 12:18:45 -0700 Subject: [PATCH] SQLite support for tg generate + dialect-neutral table codegen Fixes surfaced along the way: - Cardinality inference over-reported "one" for FK columns inside composite PKs/UNIQUEs (both dialects); SQLite rowid-alias FK columns were wrongly nullable ("maybe" instead of "one"). - PG stored-generated columns (`GENERATED ALWAYS AS ... STORED`) missed `generated: true`; ColumnInfo is now codegen-shaped (nullable/default/generated) instead of information_schema-ese. - INSERT prunes columns no row provides so DB defaults apply natively; SQLite raises on heterogeneous rows (it has no per-row DEFAULT spelling) instead of silently inserting NULL. - Examples now run codegen output as-is: SWC decorator transform in their vitest configs, tables regenerated with @expose(). --- examples/basic/package-lock.json | 431 +++- examples/basic/package.json | 2 + examples/basic/src/tables/collars.ts | 11 +- examples/basic/src/tables/dogs.ts | 29 +- examples/basic/src/tables/microchips.ts | 11 +- examples/basic/src/tables/teams.ts | 9 +- examples/basic/src/tables/toys.ts | 11 +- examples/basic/tsconfig.json | 1 + examples/basic/typegres.config.ts | 1 + examples/basic/vitest.config.ts | 17 +- examples/sqlite/.gitignore | 4 + examples/sqlite/migrate.ts | 26 + examples/sqlite/migrations/001_schema.sql | 15 + examples/sqlite/package-lock.json | 2188 +++++++++++++++++++++ examples/sqlite/package.json | 23 + examples/sqlite/src/db.ts | 7 + examples/sqlite/src/dogs.test.ts | 111 ++ examples/sqlite/src/tables/dogs.ts | 15 + examples/sqlite/src/tables/teams.ts | 13 + examples/sqlite/tsconfig.json | 17 + examples/sqlite/typegres.config.ts | 8 + examples/sqlite/vitest.config.ts | 18 + site/.gitignore | 2 + site/typegres.config.ts | 1 + src/builder/insert.test.ts | 114 ++ src/builder/insert.ts | 43 +- src/config.ts | 13 +- src/index.ts | 28 +- src/tables/generate.test.ts | 97 +- src/tables/generate.ts | 315 +-- src/tables/postgres.test.ts | 210 ++ src/tables/postgres.ts | 115 ++ src/tables/sqlite.test.ts | 240 +++ src/tables/sqlite.ts | 193 ++ src/tables/validate-generated.ts | 42 + src/types/sqlite/base.ts | 16 + tsdown.config.ts | 5 + 37 files changed, 4127 insertions(+), 275 deletions(-) create mode 100644 examples/sqlite/.gitignore create mode 100644 examples/sqlite/migrate.ts create mode 100644 examples/sqlite/migrations/001_schema.sql create mode 100644 examples/sqlite/package-lock.json create mode 100644 examples/sqlite/package.json create mode 100644 examples/sqlite/src/db.ts create mode 100644 examples/sqlite/src/dogs.test.ts create mode 100644 examples/sqlite/src/tables/dogs.ts create mode 100644 examples/sqlite/src/tables/teams.ts create mode 100644 examples/sqlite/tsconfig.json create mode 100644 examples/sqlite/typegres.config.ts create mode 100644 examples/sqlite/vitest.config.ts create mode 100644 src/tables/postgres.test.ts create mode 100644 src/tables/postgres.ts create mode 100644 src/tables/sqlite.test.ts create mode 100644 src/tables/sqlite.ts create mode 100644 src/tables/validate-generated.ts diff --git a/examples/basic/package-lock.json b/examples/basic/package-lock.json index 35bd4ec3..6036dd0d 100644 --- a/examples/basic/package-lock.json +++ b/examples/basic/package-lock.json @@ -12,35 +12,58 @@ "typegres": "file:../.." }, "devDependencies": { + "@swc/core": "^1.15.43", "@types/pg": "^8.20.0", + "unplugin-swc": "^1.5.9", "vitest": "^4.1.5" } }, "../..": { - "version": "0.0.1", + "version": "0.2.0", "dependencies": { - "@electric-sql/pglite": "^0.4.4", - "camelcase": "^9.0.0", - "pg": "^8.20.0" + "camelcase": "^9.0.0" }, "bin": { - "tg": "src/cli.ts" + "tg": "dist/cli.mjs" }, "devDependencies": { + "@electric-sql/pglite": "^0.4.4", "@eslint/js": "^10.0.1", + "@standard-schema/spec": "^1.1.0", + "@swc/core": "^1.15.32", + "@types/acorn": "^4.0.6", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.6.0", "@types/pg": "^8.20.0", "@typescript-eslint/eslint-plugin": "^8.59.0", "@typescript-eslint/parser": "^8.59.0", "@typescript/native-preview": "^7.0.0-dev.20260422.1", + "acorn": "^8.16.0", + "better-sqlite3": "^12.11.1", "eslint": "^10.2.1", + "pg": "^8.20.0", "prettier": "^3.8.3", + "secure-json-parse": "^4.1.0", "tsdown": "^0.21.10", "typescript": "^6.0.3", - "vitest": "^4.1.5" + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.5", + "zod": "^4.3.6" }, "engines": { "node": ">=22" + }, + "peerDependencies": { + "@electric-sql/pglite": "^0.4.4", + "pg": "^8.20.0" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "pg": { + "optional": true + } } }, "node_modules/@emnapi/core": { @@ -77,6 +100,38 @@ "tslib": "^2.4.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -84,6 +139,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -377,6 +443,36 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -384,6 +480,268 @@ "dev": true, "license": "MIT" }, + "node_modules/@swc/core": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -555,6 +913,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -913,6 +1284,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1274,6 +1655,37 @@ "dev": true, "license": "MIT" }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-swc": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.9.tgz", + "integrity": "sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^2.3.11" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, "node_modules/vite": { "version": "8.0.9", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", @@ -1442,6 +1854,13 @@ } } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "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", diff --git a/examples/basic/package.json b/examples/basic/package.json index 5b762187..42b1eaad 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -15,7 +15,9 @@ "typegres": "file:../.." }, "devDependencies": { + "@swc/core": "^1.15.43", "@types/pg": "^8.20.0", + "unplugin-swc": "^1.5.9", "vitest": "^4.1.5" } } diff --git a/examples/basic/src/tables/collars.ts b/examples/basic/src/tables/collars.ts index e5a5aa1d..b7f481ce 100644 --- a/examples/basic/src/tables/collars.ts +++ b/examples/basic/src/tables/collars.ts @@ -1,13 +1,14 @@ -import { Int8, Text } from "typegres/postgres"; import { db } from "../db"; +import { expose } from "typegres"; +import { Int8, Text } from "typegres/postgres"; import { Dogs } from "./dogs"; export class Collars extends db.Table("collars") { // @generated-start - id = (Int8<1>).column({ nonNull: true, generated: true }); - color = (Text<1>).column({ nonNull: true }); - dog_id = (Int8<1>).column({ nonNull: true }); + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() color = (Text<1>).column({ nonNull: true }); + @expose() dog_id = (Int8<1>).column({ nonNull: true }); // relations - dog() { return Dogs.from().where(({ dogs }) => dogs.id["="](this.dog_id)).cardinality("one"); } + @expose() dog() { return Dogs.scope(Collars.contextOf(this)).where(({ dogs }) => dogs.id.eq(this.dog_id)).cardinality("one"); } // @generated-end } diff --git a/examples/basic/src/tables/dogs.ts b/examples/basic/src/tables/dogs.ts index 0177643c..66d74489 100644 --- a/examples/basic/src/tables/dogs.ts +++ b/examples/basic/src/tables/dogs.ts @@ -1,24 +1,25 @@ -import { sql } from "typegres"; -import { Int8, Text, Timestamptz } from "typegres/postgres"; import { db } from "../db"; +import { expose, sql } from "typegres"; +import { Int8, Text, Timestamptz } from "typegres/postgres"; import { Teams } from "./teams"; import { Collars } from "./collars"; import { Microchips } from "./microchips"; import { Toys } from "./toys"; + export class Dogs extends db.Table("dogs") { // @generated-start - id = (Int8<1>).column({ nonNull: true, generated: true }); - name = (Text<1>).column({ nonNull: true }); - breed = (Text<0 | 1>).column(); - created_at = (Timestamptz<1>).column({ nonNull: true, default: sql`now()` }); - team_id = (Int8<1>).column({ nonNull: true }); - rival_id = (Int8<0 | 1>).column(); + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + @expose() breed = (Text<0 | 1>).column(); + @expose() created_at = (Timestamptz<1>).column({ nonNull: true, default: sql`now()` }); + @expose() team_id = (Int8<1>).column({ nonNull: true }); + @expose() rival_id = (Int8<0 | 1>).column(); // relations - rival() { return Dogs.from().where(({ dogs }) => dogs.id["="](this.rival_id)).cardinality("maybe"); } - team() { return Teams.from().where(({ teams }) => teams.id["="](this.team_id)).cardinality("one"); } - collars() { return Collars.from().where(({ collars }) => collars.dog_id["="](this.id)).cardinality("one"); } - dogs() { return Dogs.from().where(({ dogs }) => dogs.rival_id["="](this.id)).cardinality("many"); } - microchips() { return Microchips.from().where(({ microchips }) => microchips.dog_id["="](this.id)).cardinality("maybe"); } - toys() { return Toys.from().where(({ toys }) => toys.dog_id["="](this.id)).cardinality("many"); } + @expose() rival() { return Dogs.scope(Dogs.contextOf(this)).where(({ dogs }) => dogs.id.eq(this.rival_id)).cardinality("maybe"); } + @expose() team() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } + @expose() collars() { return Collars.scope(Dogs.contextOf(this)).where(({ collars }) => collars.dog_id.eq(this.id)).cardinality("one"); } + @expose() dogs() { return Dogs.scope(Dogs.contextOf(this)).where(({ dogs }) => dogs.rival_id.eq(this.id)).cardinality("many"); } + @expose() microchips() { return Microchips.scope(Dogs.contextOf(this)).where(({ microchips }) => microchips.dog_id.eq(this.id)).cardinality("maybe"); } + @expose() toys() { return Toys.scope(Dogs.contextOf(this)).where(({ toys }) => toys.dog_id.eq(this.id)).cardinality("many"); } // @generated-end } diff --git a/examples/basic/src/tables/microchips.ts b/examples/basic/src/tables/microchips.ts index 9db62d2f..28cf356c 100644 --- a/examples/basic/src/tables/microchips.ts +++ b/examples/basic/src/tables/microchips.ts @@ -1,13 +1,14 @@ -import { Int8, Text } from "typegres/postgres"; import { db } from "../db"; +import { expose } from "typegres"; +import { Int8, Text } from "typegres/postgres"; import { Dogs } from "./dogs"; export class Microchips extends db.Table("microchips") { // @generated-start - id = (Int8<1>).column({ nonNull: true, generated: true }); - serial = (Text<1>).column({ nonNull: true }); - dog_id = (Int8<0 | 1>).column(); + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() serial = (Text<1>).column({ nonNull: true }); + @expose() dog_id = (Int8<0 | 1>).column(); // relations - dog() { return Dogs.from().where(({ dogs }) => dogs.id["="](this.dog_id)).cardinality("maybe"); } + @expose() dog() { return Dogs.scope(Microchips.contextOf(this)).where(({ dogs }) => dogs.id.eq(this.dog_id)).cardinality("maybe"); } // @generated-end } diff --git a/examples/basic/src/tables/teams.ts b/examples/basic/src/tables/teams.ts index f9a2e25b..03ae0285 100644 --- a/examples/basic/src/tables/teams.ts +++ b/examples/basic/src/tables/teams.ts @@ -1,12 +1,13 @@ -import { Int8, Text } from "typegres/postgres"; import { db } from "../db"; +import { expose } from "typegres"; +import { Int8, Text } from "typegres/postgres"; import { Dogs } from "./dogs"; export class Teams extends db.Table("teams") { // @generated-start - id = (Int8<1>).column({ nonNull: true, generated: true }); - name = (Text<1>).column({ nonNull: true }); + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); // relations - dogs() { return Dogs.from().where(({ dogs }) => dogs.team_id["="](this.id)).cardinality("many"); } + @expose() dogs() { return Dogs.scope(Teams.contextOf(this)).where(({ dogs }) => dogs.team_id.eq(this.id)).cardinality("many"); } // @generated-end } diff --git a/examples/basic/src/tables/toys.ts b/examples/basic/src/tables/toys.ts index c5fe1a64..7596be4b 100644 --- a/examples/basic/src/tables/toys.ts +++ b/examples/basic/src/tables/toys.ts @@ -1,13 +1,14 @@ -import { Int8, Text } from "typegres/postgres"; import { db } from "../db"; +import { expose } from "typegres"; +import { Int8, Text } from "typegres/postgres"; import { Dogs } from "./dogs"; export class Toys extends db.Table("toys") { // @generated-start - id = (Int8<1>).column({ nonNull: true, generated: true }); - name = (Text<1>).column({ nonNull: true }); - dog_id = (Int8<1>).column({ nonNull: true }); + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + @expose() dog_id = (Int8<1>).column({ nonNull: true }); // relations - dog() { return Dogs.from().where(({ dogs }) => dogs.id["="](this.dog_id)).cardinality("one"); } + @expose() dog() { return Dogs.scope(Toys.contextOf(this)).where(({ dogs }) => dogs.id.eq(this.dog_id)).cardinality("one"); } // @generated-end } diff --git a/examples/basic/tsconfig.json b/examples/basic/tsconfig.json index 79708242..37409833 100644 --- a/examples/basic/tsconfig.json +++ b/examples/basic/tsconfig.json @@ -4,6 +4,7 @@ "module": "ESNext", "moduleResolution": "bundler", "strict": true, + "skipLibCheck": true, "noEmit": true }, "include": ["src", "*.ts"] diff --git a/examples/basic/typegres.config.ts b/examples/basic/typegres.config.ts index 2f2a03ef..da7a2552 100644 --- a/examples/basic/typegres.config.ts +++ b/examples/basic/typegres.config.ts @@ -1,6 +1,7 @@ import type { Config } from "typegres"; export default { + dialect: "postgres", db: process.env["DATABASE_URL"] ?? "postgres://localhost/postgres", tables: "src/tables", dbImport: "../db", diff --git a/examples/basic/vitest.config.ts b/examples/basic/vitest.config.ts index 8fb6f2dc..f4187b55 100644 --- a/examples/basic/vitest.config.ts +++ b/examples/basic/vitest.config.ts @@ -1,3 +1,18 @@ import { defineConfig } from "vitest/config"; +import swc from "unplugin-swc"; -export default defineConfig({}); +// SWC handles the TC39 stage-3 decorators (`@expose()`) that `tg +// generate` emits on columns and relations. Vite's default transform +// leaves them as-is, which trips the Node runtime with a SyntaxError. +export default defineConfig({ + oxc: false, + plugins: [ + swc.vite({ + jsc: { + target: "es2022", + parser: { syntax: "typescript", decorators: true }, + transform: { decoratorVersion: "2022-03" }, + }, + }), + ], +}); diff --git a/examples/sqlite/.gitignore b/examples/sqlite/.gitignore new file mode 100644 index 00000000..4bb2143d --- /dev/null +++ b/examples/sqlite/.gitignore @@ -0,0 +1,4 @@ +dev.db +dev.db-journal +dev.db-wal +dev.db-shm diff --git a/examples/sqlite/migrate.ts b/examples/sqlite/migrate.ts new file mode 100644 index 00000000..798e3c07 --- /dev/null +++ b/examples/sqlite/migrate.ts @@ -0,0 +1,26 @@ +// Drops + recreates the SQLite schema at ./dev.db by running every +// migration file under ./migrations/*.sql. `tg generate` reads +// ./dev.db to introspect the resulting schema. + +import Database from "better-sqlite3"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +// Fresh file each run so DROP TABLE isn't needed inside the SQL. +const dbPath = "./dev.db"; +if (fs.existsSync(dbPath)) { fs.unlinkSync(dbPath); } + +const db = new Database(dbPath); +db.pragma("foreign_keys = ON"); + +const migrationsDir = path.resolve(import.meta.dirname, "migrations"); +const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort(); + +for (const file of files) { + const sqlText = fs.readFileSync(path.join(migrationsDir, file), "utf-8"); + console.log(`Running ${file}...`); + db.exec(sqlText); +} + +db.close(); +console.log(`Migrated ${dbPath}`); diff --git a/examples/sqlite/migrations/001_schema.sql b/examples/sqlite/migrations/001_schema.sql new file mode 100644 index 00000000..5c6987ef --- /dev/null +++ b/examples/sqlite/migrations/001_schema.sql @@ -0,0 +1,15 @@ +-- SQLite schema. Read by `migrate.ts` (runs against ./dev.db so +-- `tg generate` has something to introspect) and by `dogs.test.ts` +-- (runs against :memory: so each test run is hermetic). + +CREATE TABLE teams ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE dogs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + breed TEXT, + team_id INTEGER NOT NULL REFERENCES teams(id) +); diff --git a/examples/sqlite/package-lock.json b/examples/sqlite/package-lock.json new file mode 100644 index 00000000..b119e734 --- /dev/null +++ b/examples/sqlite/package-lock.json @@ -0,0 +1,2188 @@ +{ + "name": "typegres-example-sqlite", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typegres-example-sqlite", + "version": "0.0.1", + "dependencies": { + "better-sqlite3": "^12.11.1", + "typegres": "file:../.." + }, + "devDependencies": { + "@swc/core": "^1.15.43", + "@types/better-sqlite3": "^7.6.13", + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.5" + } + }, + "../..": { + "version": "0.2.0", + "dependencies": { + "camelcase": "^9.0.0" + }, + "bin": { + "tg": "dist/cli.mjs" + }, + "devDependencies": { + "@electric-sql/pglite": "^0.4.4", + "@eslint/js": "^10.0.1", + "@standard-schema/spec": "^1.1.0", + "@swc/core": "^1.15.32", + "@types/acorn": "^4.0.6", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^25.6.0", + "@types/pg": "^8.20.0", + "@typescript-eslint/eslint-plugin": "^8.59.0", + "@typescript-eslint/parser": "^8.59.0", + "@typescript/native-preview": "^7.0.0-dev.20260422.1", + "acorn": "^8.16.0", + "better-sqlite3": "^12.11.1", + "eslint": "^10.2.1", + "pg": "^8.20.0", + "prettier": "^3.8.3", + "secure-json-parse": "^4.1.0", + "tsdown": "^0.21.10", + "typescript": "^6.0.3", + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.5", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@electric-sql/pglite": "^0.4.4", + "pg": "^8.20.0" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "pg": { + "optional": true + } + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, + "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/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typegres": { + "resolved": "../..", + "link": true + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-swc": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.9.tgz", + "integrity": "sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^2.3.11" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/examples/sqlite/package.json b/examples/sqlite/package.json new file mode 100644 index 00000000..e931247f --- /dev/null +++ b/examples/sqlite/package.json @@ -0,0 +1,23 @@ +{ + "name": "typegres-example-sqlite", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "migrate": "node --experimental-strip-types migrate.ts", + "generate": "tg generate", + "typecheck": "tsgo --noEmit", + "test": "vitest run", + "check": "npm run typecheck && npm run test" + }, + "dependencies": { + "better-sqlite3": "^12.11.1", + "typegres": "file:../.." + }, + "devDependencies": { + "@swc/core": "^1.15.43", + "@types/better-sqlite3": "^7.6.13", + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.5" + } +} diff --git a/examples/sqlite/src/db.ts b/examples/sqlite/src/db.ts new file mode 100644 index 00000000..85eb4a30 --- /dev/null +++ b/examples/sqlite/src/db.ts @@ -0,0 +1,7 @@ +import { typegres } from "typegres"; + +// `typegres({ type: "sqlite" })` opens a SqliteDriver against the given +// file (or `:memory:` if omitted). The tests use `:memory:` so each +// vitest run is hermetic; the `tg generate` CLI reads schema from the +// `./dev.db` file produced by `npm run migrate`. +export const { db, conn } = await typegres({ type: "sqlite" }); diff --git a/examples/sqlite/src/dogs.test.ts b/examples/sqlite/src/dogs.test.ts new file mode 100644 index 00000000..ee79be3f --- /dev/null +++ b/examples/sqlite/src/dogs.test.ts @@ -0,0 +1,111 @@ +// End-to-end integration test for the codegen'd SQLite tables. +// Runs the migration DDL against `:memory:` (via `db` from ./db) +// then exercises INSERT / SELECT / WHERE / JOIN paths. + +import { test, expect, expectTypeOf, beforeAll, afterAll } from "vitest"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { sql } from "typegres"; +import { db, conn } from "./db"; +import { Dogs } from "./tables/dogs"; +import { Teams } from "./tables/teams"; + +// Run the same migrations file the CLI uses. Keeps the DDL in one +// place — if `001_schema.sql` changes and `tg generate` re-runs, the +// tests come along. +beforeAll(async () => { + const migrationsDir = path.resolve(import.meta.dirname, "..", "migrations"); + const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort(); + for (const file of files) { + const stmts = fs.readFileSync(path.join(migrationsDir, file), "utf-8"); + // conn.execute prepares a single statement per call; split on `;` + // and skip empty fragments (last one is empty after trailing `;`). + for (const stmt of stmts.split(";")) { + const trimmed = stmt.trim(); + if (trimmed) { await conn.execute(sql.raw(trimmed)); } + } + } + + await Teams.insert({ name: "Alpha" }, { name: "Beta" }).execute(conn); + // Explicit `breed: null` — SQLite inserts can't mix "column set" and + // "column omitted" across rows in one call (typegres raises), since + // SQLite has no per-row DEFAULT spelling. + await Dogs.insert( + { name: "Rex", breed: "Labrador", team_id: 1 }, + { name: "Fido", breed: null, team_id: 1 }, + { name: "Buddy", breed: "Poodle", team_id: 2 }, + ).execute(conn); +}); + +afterAll(async () => { + await conn.close(); +}); + +test("basic select: rows come back with column types matching the codegen'd shape", async () => { + const rows = await Dogs.from() + .select(({ dogs }) => ({ id: dogs.id, name: dogs.name, breed: dogs.breed })) + .orderBy(({ dogs }) => dogs.id) + .execute(conn); + + expectTypeOf(rows).toEqualTypeOf<{ id: number; name: string; breed: string | null }[]>(); + expect(rows).toEqual([ + { id: 1, name: "Rex", breed: "Labrador" }, + { id: 2, name: "Fido", breed: null }, + { id: 3, name: "Buddy", breed: "Poodle" }, + ]); +}); + +test("WHERE filters rows and preserves nullability in the returned type", async () => { + const rows = await Dogs.from() + .where(({ dogs }) => dogs.team_id.eq(1)) + .select(({ dogs }) => ({ name: dogs.name })) + .orderBy(({ dogs }) => dogs.name) + .execute(conn); + + expectTypeOf(rows).toEqualTypeOf<{ name: string }[]>(); + expect(rows.map((r) => r.name)).toEqual(["Fido", "Rex"]); +}); + +test("JOIN via .from().join() — dogs × teams matched on FK", async () => { + const rows = await Dogs.from() + .join(Teams, ({ dogs, teams }) => dogs.team_id.eq(teams.id)) + .select(({ dogs, teams }) => ({ + dog: dogs.name, + team: teams.name, + })) + .orderBy(({ dogs }) => dogs.name) + .execute(conn); + + expectTypeOf(rows).toEqualTypeOf<{ dog: string; team: string }[]>(); + expect(rows).toEqual([ + { dog: "Buddy", team: "Beta" }, + { dog: "Fido", team: "Alpha" }, + { dog: "Rex", team: "Alpha" }, + ]); +}); + +test("INSERT ... RETURNING gets back the auto-assigned rowid", async () => { + const [row] = await Dogs.insert({ name: "Nova", team_id: 2 }) + .returning(({ dogs }) => ({ id: dogs.id, name: dogs.name })) + .execute(conn); + + expectTypeOf(row).toEqualTypeOf<{ id: number; name: string } | undefined>(); + expect(row?.name).toBe("Nova"); + expect(typeof row?.id).toBe("number"); +}); + +test("UPDATE ... RETURNING breed on Nova", async () => { + const [row] = await Dogs.update() + .where(({ dogs }) => dogs.name.eq("Nova")) + .set(() => ({ breed: "Husky" })) + .returning(({ dogs }) => ({ id: dogs.id, breed: dogs.breed })) + .execute(conn); + + expectTypeOf(row).toEqualTypeOf<{ id: number; breed: string | null } | undefined>(); + expect(row?.breed).toBe("Husky"); +}); + +test("db.Table provenance: Dogs.database === Teams.database === db", () => { + expect(Dogs.database).toBe(db); + expect(Teams.database).toBe(db); +}); diff --git a/examples/sqlite/src/tables/dogs.ts b/examples/sqlite/src/tables/dogs.ts new file mode 100644 index 00000000..dd03971f --- /dev/null +++ b/examples/sqlite/src/tables/dogs.ts @@ -0,0 +1,15 @@ +import { db } from "../db"; +import { expose } from "typegres"; +import { Integer, Text } from "typegres/sqlite"; +import { Teams } from "./teams"; + +export class Dogs extends db.Table("dogs") { + // @generated-start + @expose() id = (Integer<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + @expose() breed = (Text<0 | 1>).column(); + @expose() team_id = (Integer<1>).column({ nonNull: true }); + // relations + @expose() team() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } + // @generated-end +} diff --git a/examples/sqlite/src/tables/teams.ts b/examples/sqlite/src/tables/teams.ts new file mode 100644 index 00000000..dd17b7e2 --- /dev/null +++ b/examples/sqlite/src/tables/teams.ts @@ -0,0 +1,13 @@ +import { db } from "../db"; +import { expose } from "typegres"; +import { Integer, Text } from "typegres/sqlite"; +import { Dogs } from "./dogs"; + +export class Teams extends db.Table("teams") { + // @generated-start + @expose() id = (Integer<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + // relations + @expose() dogs() { return Dogs.scope(Teams.contextOf(this)).where(({ dogs }) => dogs.team_id.eq(this.id)).cardinality("many"); } + // @generated-end +} diff --git a/examples/sqlite/tsconfig.json b/examples/sqlite/tsconfig.json new file mode 100644 index 00000000..4dc0c733 --- /dev/null +++ b/examples/sqlite/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "types": ["node"] + }, + "include": ["src", "migrate.ts", "typegres.config.ts"] +} diff --git a/examples/sqlite/typegres.config.ts b/examples/sqlite/typegres.config.ts new file mode 100644 index 00000000..6e3fbdd3 --- /dev/null +++ b/examples/sqlite/typegres.config.ts @@ -0,0 +1,8 @@ +import type { Config } from "typegres"; + +export default { + dialect: "sqlite", + db: "./dev.db", + tables: "src/tables", + dbImport: "../db", +} satisfies Config; diff --git a/examples/sqlite/vitest.config.ts b/examples/sqlite/vitest.config.ts new file mode 100644 index 00000000..f4187b55 --- /dev/null +++ b/examples/sqlite/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; +import swc from "unplugin-swc"; + +// SWC handles the TC39 stage-3 decorators (`@expose()`) that `tg +// generate` emits on columns and relations. Vite's default transform +// leaves them as-is, which trips the Node runtime with a SyntaxError. +export default defineConfig({ + oxc: false, + plugins: [ + swc.vite({ + jsc: { + target: "es2022", + parser: { syntax: "typescript", decorators: true }, + transform: { decoratorVersion: "2022-03" }, + }, + }), + ], +}); diff --git a/site/.gitignore b/site/.gitignore index ee00aa29..052abb99 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -8,3 +8,5 @@ public/typegres.js public/typegres.d.ts public/demo.ts.static public/events-*.js +public/chunk-*.js +public/lib-*.js diff --git a/site/typegres.config.ts b/site/typegres.config.ts index ba1bc4ef..8b279544 100644 --- a/site/typegres.config.ts +++ b/site/typegres.config.ts @@ -12,6 +12,7 @@ import type { Config } from "typegres"; export default { + dialect: "postgres", db: process.env["DATABASE_URL"] ?? "postgres://localhost/postgres", tables: "src/demo/schema", dbImport: "../runtime", diff --git a/src/builder/insert.test.ts b/src/builder/insert.test.ts index d7dced2f..b1f49f44 100644 --- a/src/builder/insert.test.ts +++ b/src/builder/insert.test.ts @@ -1,7 +1,9 @@ import { test, expect, expectTypeOf } from "vitest"; import { Int8, Text } from "../types/postgres"; +import * as sqlite from "../types/sqlite"; import type { InsertRow } from "../types/runtime"; import { sql } from "./sql"; +import { typegres } from "../index"; import { setupDb, db, withinTransaction } from "../test-helpers"; setupDb(); @@ -55,3 +57,115 @@ test("insert returning", async () => { ]); }); }); + +test("columns no row provides are pruned so DB defaults apply", async () => { + await withinTransaction(async (tx) => { + await tx.execute(sql`CREATE TABLE tagged ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + label text NOT NULL, + status text NOT NULL DEFAULT 'new' + )`); + + class Tagged extends db.Table("tagged") { + id = (Int8<1>).column({ nonNull: true, generated: true }); label = (Text<1>).column({ nonNull: true }); status = (Text<1>).column({ nonNull: true, default: sql`'new'` }); } + + // `status` appears in no row → pruned from the column list → the + // DB's DEFAULT 'new' applies (not NULL, not an error). + const rows = await tx.execute( + Tagged.insert({ label: "A" }, { label: "B" }) + .returning(({ tagged }) => ({ label: tagged.label, status: tagged.status })), + ); + expect(rows).toEqual([ + { label: "A", status: "new" }, + { label: "B", status: "new" }, + ]); + }); +}); + +test("postgres: column provided in some rows but not others → DEFAULT keyword per gap", async () => { + await withinTransaction(async (tx) => { + await tx.execute(sql`CREATE TABLE mixed ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + label text NOT NULL, + status text NOT NULL DEFAULT 'new' + )`); + + class Mixed extends db.Table("mixed") { + id = (Int8<1>).column({ nonNull: true, generated: true }); label = (Text<1>).column({ nonNull: true }); status = (Text<1>).column({ nonNull: true, default: sql`'new'` }); } + + const rows = await tx.execute( + Mixed.insert({ label: "A" }, { label: "B", status: "old" }) + .returning(({ mixed }) => ({ label: mixed.label, status: mixed.status })), + ); + expect(rows).toEqual([ + { label: "A", status: "new" }, + { label: "B", status: "old" }, + ]); + }); +}); + +test("sqlite: pruning defers to rowid autoincrement and declared defaults", async () => { + const { db: sdb, conn } = await typegres({ type: "sqlite" }); + try { + await conn.execute(sql.raw(`CREATE TABLE tagged ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'new' + )`)); + + class Tagged extends sdb.Table("tagged") { + id = (sqlite.Integer<1>).column({ nonNull: true, generated: true }); label = (sqlite.Text<1>).column({ nonNull: true }); status = (sqlite.Text<1>).column({ nonNull: true, default: sql`'new'` }); } + + // Previously this inserted NULL for id (ok, rowid quirk) AND for + // status (NOT NULL violation). Pruning makes both work natively. + const rows = await Tagged.insert({ label: "A" }, { label: "B" }) + .returning(({ tagged }) => ({ id: tagged.id, label: tagged.label, status: tagged.status })) + .execute(conn); + expect(rows).toEqual([ + { id: 1, label: "A", status: "new" }, + { id: 2, label: "B", status: "new" }, + ]); + } finally { + await conn.close(); + } +}); + +test("sqlite: heterogeneous rows raise instead of silently inserting NULL", async () => { + const { db: sdb, conn } = await typegres({ type: "sqlite" }); + try { + await conn.execute(sql.raw(`CREATE TABLE mixed ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'new' + )`)); + + class Mixed extends sdb.Table("mixed") { + id = (sqlite.Integer<1>).column({ nonNull: true, generated: true }); label = (sqlite.Text<1>).column({ nonNull: true }); status = (sqlite.Text<1>).column({ nonNull: true, default: sql`'new'` }); } + + await expect( + Mixed.insert({ label: "A" }, { label: "B", status: "old" }).execute(conn), + ).rejects.toThrow(/'status' is set in some rows but not others/); + } finally { + await conn.close(); + } +}); + +test("all-default single row uses DEFAULT VALUES; multi-row raises", async () => { + await withinTransaction(async (tx) => { + await tx.execute(sql`CREATE TABLE counters ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY + )`); + + class Counters extends db.Table("counters") { + id = (Int8<1>).column({ nonNull: true, generated: true }); } + + const rows = await tx.execute( + Counters.insert({}).returning(({ counters }) => ({ id: counters.id })), + ); + expect(rows).toEqual([{ id: "1" }]); + + expect(() => Counters.insert({}, {}).finalize().bind()).toThrow( + /multi-row insert with no columns provided/, + ); + }); +}); diff --git a/src/builder/insert.ts b/src/builder/insert.ts index c76925d0..f6371255 100644 --- a/src/builder/insert.ts +++ b/src/builder/insert.ts @@ -38,18 +38,53 @@ export class FinalizedInsert tableCls.database.scopedIdent(k)); + // Columns no row provides are omitted from the column list entirely, + // so the DB applies its own semantics (identity/rowid autoincrement, + // declared DEFAULTs, NULL) — what a hand-written INSERT would do. + // `column()` keeps no runtime opts, so this is also the only way to + // defer to per-column defaults without knowing them. + const usedColumns = columnNames.filter((k) => rows.some((row) => row[k] !== undefined)); const rowSqls = rows.map((row) => { - const vals = columnNames.map((k) => { + const vals = usedColumns.map((k) => { const v = row[k]; - if (v === undefined) { return sql`DEFAULT`; } + if (v === undefined) { + // Some other row provides this column, this one doesn't. + // Only PG is known to spell that `DEFAULT`; for any other + // dialect (SQLite has no per-row spelling for it), silently + // inserting NULL would diverge from what omitting the column + // means (rowid auto-fill, declared DEFAULT) — make the + // caller decide. + if (tableCls.database.dialect === "postgres") { + return sql`DEFAULT`; + } + throw new Error( + `Insert into '${tableName}': column '${k}' is set in some rows but not others. ` + + `The '${tableCls.database.dialect}' dialect cannot express "use the column default" per row — ` + + `provide '${k}' in every row or in none.`, + ); + } const col = getColumn(instance, k); return col[meta].__class.from(v).toSql(); }); return sql`(${sql.join(vals)})`; }); + // Zero provided columns can't be spelled `(cols) VALUES (...)`; + // both dialects use `DEFAULT VALUES`, which is single-row only. + let body: Sql; + if (usedColumns.length === 0) { + if (rows.length > 1) { + throw new Error( + `Insert into '${tableName}': multi-row insert with no columns provided. ` + + `Use one .insert({}) call per row for all-default rows.`, + ); + } + body = sql`DEFAULT VALUES`; + } else { + const columns = usedColumns.map((k) => tableCls.database.scopedIdent(k)); + body = sql`(${sql.join(columns)}) VALUES ${sql.join(rowSqls)}`; + } const inner = sql.join([ - sql`INSERT INTO ${tableCls.ident(tableName)} AS ${alias} (${sql.join(columns)}) VALUES ${sql.join(rowSqls)}`, + sql`INSERT INTO ${tableCls.ident(tableName)} AS ${alias} ${body}`, returning && sql`RETURNING ${compileSelectList(returning)}`, ], sql` `); return sql.withScope([alias], inner); diff --git a/src/config.ts b/src/config.ts index b6b4f53c..0d69d635 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,7 @@ -export interface Config { - db: string; - tables: string; - /** Import path for the Database instance, e.g. "../db" */ - dbImport: string; -} +// Discriminated union — `dialect` picks the connection + introspection +// strategy at codegen time. `db` is a PG connection string for +// `postgres` and a file path (or `":memory:"`) for `sqlite`. Everything +// else (tables output dir, dbImport) is shared. +export type Config = + | { dialect: "postgres"; db: string; tables: string; dbImport: string } + | { dialect: "sqlite"; db: string; tables: string; dbImport: string }; diff --git a/src/index.ts b/src/index.ts index 6f843528..d82f32c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export type { TransactionIsolation, TransactionOptions } from "./database"; export { Table } from "./table"; export { sql, Sql } from "./builder/sql"; export { QueryBuilder } from "./builder/query"; -export { PgDriver, PgliteDriver } from "./driver"; +export { PgDriver, PgliteDriver, SqliteDriver } from "./driver"; export { TypegresLiveEvents } from "./live/events"; export { expose } from "./exoeval/tool"; export type { ToolFunction } from "./exoeval/tool"; @@ -14,19 +14,33 @@ export type { Driver } from "./driver"; import type { Connection } from "./database"; import { Database } from "./database"; -import { PgDriver, PgliteDriver } from "./driver"; +import { PgDriver, PgliteDriver, SqliteDriver } from "./driver"; +import type { Driver } from "./driver"; +import type { DialectName } from "./builder/sql"; // Convenience factory for quick scripts and the playground. Returns // `{ db, conn }` — the immutable metadata handle and its attached // runtime Connection. Real apps typically build these separately at // module load / bootstrap. export const typegres = async ( - opts: { type: "pglite" } | { type: "pg"; connectionString: string }, + opts: + | { type: "pglite" } + | { type: "pg"; connectionString: string } + | { type: "sqlite"; filename?: string }, ): Promise<{ db: Database; conn: Connection }> => { - const driver = opts.type === "pglite" - ? await PgliteDriver.create() - : await PgDriver.create(opts.connectionString); - const db = new Database({ dialect: "postgres" }); + let driver: Driver; + let dialect: DialectName; + if (opts.type === "pglite") { + driver = await PgliteDriver.create(); + dialect = "postgres"; + } else if (opts.type === "pg") { + driver = await PgDriver.create(opts.connectionString); + dialect = "postgres"; + } else { + driver = await SqliteDriver.create(opts.filename ?? ":memory:"); + dialect = "sqlite"; + } + const db = new Database({ dialect }); const conn = db.attach(driver); return { db, conn }; }; diff --git a/src/tables/generate.test.ts b/src/tables/generate.test.ts index 508de4fe..e44c0607 100644 --- a/src/tables/generate.test.ts +++ b/src/tables/generate.test.ts @@ -1,57 +1,18 @@ import { describe, test, expect } from "vitest"; -import * as swc from "@swc/core"; import { generateTable, type ColumnInfo, type Relation } from "./generate"; - -// Validate generated code: (1) syntactically valid TS that swc can -// transform with the same decorator config typegres' build uses; -// (2) every `@` decorator referenced in the body has a matching -// import. swc alone catches syntax/transform errors but happily emits -// `(0, expose)()` even when `expose` isn't imported — the import -// check closes that gap (caught a rename-drift bug otherwise invisible -// to inline snapshot tests). -const validate = async (out: string): Promise => { - await swc.transform(out, { - filename: "generated.ts", - jsc: { - target: "es2022", - parser: { syntax: "typescript", decorators: true }, - transform: { decoratorVersion: "2022-03" }, - }, - module: { type: "es6" }, - isModule: true, - }); - - const decorators = new Set( - [...out.matchAll(/@(\w+)(?:\.\w+)?\(/g)].map((m) => m[1]!), - ); - const imported = new Set(); - for (const m of out.matchAll(/import\s*\{([^}]*)\}/g)) { - for (const raw of m[1]!.split(",")) { - const id = raw.trim().split(/\s+as\s+/)[0]!.replace(/^type\s+/, "").trim(); - if (id) {imported.add(id);} - } - } - for (const d of decorators) { - if (!imported.has(d)) { - throw new Error( - `generated code uses @${d}() but doesn't import \`${d}\`. Imports: [${[...imported].join(", ")}]`, - ); - } - } -}; +import { validateGenerated as validate } from "./validate-generated"; // Helpers — terse fixtures so each test reads as a single intent. const col = ( - column_name: string, - udt_name: string, + name: string, + className: string, overrides: Partial = {}, ): ColumnInfo => ({ - column_name, - udt_name, - is_nullable: "NO", - column_default: null, - is_generated: "NEVER", - identity_generation: null, + name, + className, + nullable: false, + default: null, + generated: false, ...overrides, }); @@ -69,11 +30,11 @@ describe("generateTable — new file", async () => { const out = generateTable( "dogs", [ - col("id", "int8", { identity_generation: "ALWAYS" }), - col("name", "text"), + col("id", "Int8", { generated: true }), + col("name", "Text"), ], [rel("teams", "teams", { cardinality: "one", fromColumn: "team_id", toColumn: "id" })], - { dbImport: "../db" }, + { typeImportPath: "typegres/postgres", dbImport: "../db" }, ); await validate(out); expect(out).toMatchInlineSnapshot(` @@ -87,7 +48,7 @@ describe("generateTable — new file", async () => { @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); @expose() name = (Text<1>).column({ nonNull: true }); // relations - @expose() teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id["="](this.team_id)).cardinality("one"); } + @expose() teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } // @generated-end } " @@ -98,12 +59,12 @@ describe("generateTable — new file", async () => { const out = generateTable( "dogs", [ - col("id", "int8", { identity_generation: "ALWAYS" }), - col("breed", "text", { is_nullable: "YES" }), - col("created_at", "timestamptz", { column_default: "now()" }), + col("id", "Int8", { generated: true }), + col("breed", "Text", { nullable: true }), + col("created_at", "Timestamptz", { default: "now()" }), ], [], - { dbImport: "../db" }, + { typeImportPath: "typegres/postgres", dbImport: "../db" }, ); await validate(out); expect(out).toMatchInlineSnapshot(` @@ -124,7 +85,7 @@ describe("generateTable — new file", async () => { }); describe("generateTable — update mode preserves @expose() state", async () => { - const cols: ColumnInfo[] = [col("id", "int8", { identity_generation: "ALWAYS" }), col("name", "text")]; + const cols: ColumnInfo[] = [col("id", "Int8", { generated: true }), col("name", "Text")]; const rels: Relation[] = [rel("teams", "teams", { cardinality: "one", fromColumn: "team_id", toColumn: "id" })]; test("entries the user stripped stay stripped on regen", async () => { @@ -140,7 +101,7 @@ export class Dogs extends db.Table("dogs") { // @generated-end } `; - const out = generateTable("dogs", cols, rels, { dbImport: "../db", existing }); + const out = generateTable("dogs", cols, rels, { typeImportPath: "typegres/postgres", dbImport: "../db", existing }); await validate(out); expect(out).toMatchInlineSnapshot(` "import { db } from "../db"; @@ -151,7 +112,7 @@ export class Dogs extends db.Table("dogs") { id = (Int8<1>).column({ nonNull: true, generated: true }); name = (Text<1>).column({ nonNull: true }); // relations - teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id["="](this.team_id)).cardinality("one"); } + teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } // @generated-end } " @@ -172,7 +133,7 @@ export class Dogs extends db.Table("dogs") { // @generated-end } `; - const out = generateTable("dogs", cols, rels, { dbImport: "../db", existing }); + const out = generateTable("dogs", cols, rels, { typeImportPath: "typegres/postgres", dbImport: "../db", existing }); await validate(out); expect(out).toMatchInlineSnapshot(` "import { db } from "../db"; @@ -184,7 +145,7 @@ export class Dogs extends db.Table("dogs") { @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); @expose() name = (Text<1>).column({ nonNull: true }); // relations - @expose() teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id["="](this.team_id)).cardinality("one"); } + @expose() teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } // @generated-end } " @@ -204,7 +165,7 @@ export class Dogs extends db.Table("dogs") { // @generated-end } `; - const out = generateTable("dogs", cols, rels, { dbImport: "../db", existing }); + const out = generateTable("dogs", cols, rels, { typeImportPath: "typegres/postgres", dbImport: "../db", existing }); await validate(out); expect(out).toMatchInlineSnapshot(` "import { db } from "../db"; @@ -215,7 +176,7 @@ export class Dogs extends db.Table("dogs") { @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); name = (Text<1>).column({ nonNull: true }); // relations - teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id["="](this.team_id)).cardinality("one"); } + teams() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } // @generated-end } " @@ -234,7 +195,8 @@ export class Dogs extends db.Table("dogs") { // @generated-end } `; - const out = generateTable("dogs", [col("id", "int8", { identity_generation: "ALWAYS" }), col("name", "text")], [], { + const out = generateTable("dogs", [col("id", "Int8", { generated: true }), col("name", "Text")], [], { + typeImportPath: "typegres/postgres", dbImport: "../db", existing, }); @@ -265,9 +227,9 @@ export class Dogs extends db.Table("dogs") { `; const out = generateTable( "dogs", - [col("id", "int8", { identity_generation: "ALWAYS" }), col("breed", "text", { is_nullable: "YES" })], + [col("id", "Int8", { generated: true }), col("breed", "Text", { nullable: true })], [], - { dbImport: "../db", existing: existingNoTool }, + { typeImportPath: "typegres/postgres", dbImport: "../db", existing: existingNoTool }, ); await validate(out); expect(out).toMatchInlineSnapshot(` @@ -295,7 +257,8 @@ export class Dogs extends db.Table("dogs") { // @generated-end } `; - const out = generateTable("dogs", [col("id", "int8", { identity_generation: "ALWAYS" })], [], { + const out = generateTable("dogs", [col("id", "Int8", { generated: true })], [], { + typeImportPath: "typegres/postgres", dbImport: "../db", existing, }); @@ -316,7 +279,7 @@ export class Dogs extends db.Table("dogs") { test("missing markers throws", async () => { const existing = `export class Dogs extends db.Table("dogs") {}\n`; - expect(() => generateTable("dogs", cols, rels, { dbImport: "../db", existing })).toThrow( + expect(() => generateTable("dogs", cols, rels, { typeImportPath: "typegres/postgres", dbImport: "../db", existing })).toThrow( /Missing @generated-start/, ); }); diff --git a/src/tables/generate.ts b/src/tables/generate.ts index 132cba00..0e7a4fce 100644 --- a/src/tables/generate.ts +++ b/src/tables/generate.ts @@ -1,66 +1,42 @@ -import pg from "pg"; +// Dialect-neutral table codegen orchestrator. Introspection + type-name +// mapping live in per-dialect modules (`postgres.ts`, `sqlite.ts`); this +// file owns config loading, per-entry `@expose()` preservation, the +// `@generated-start` / `@generated-end` marker handling, and the file- +// emission shape. + import * as fs from "node:fs"; import * as path from "node:path"; +import { pathToFileURL } from "node:url"; import type { Config } from "../config.ts"; import { pgNameToClassName } from "../types/postgres/introspect.ts"; -// --- Config --- - -const findConfig = async (): Promise => { - // Walk up from cwd looking for typegres.config.ts - let dir = process.cwd(); - while (true) { - const configPath = path.join(dir, "typegres.config.ts"); - if (fs.existsSync(configPath)) { - // eslint-disable-next-line no-restricted-syntax -- runtime config loaded from user's project - const config = await import(configPath); - return { db: config.default.db, tables: path.resolve(dir, config.default.tables), dbImport: config.default.dbImport }; - } - const parent = path.dirname(dir); - if (parent === dir) { - throw new Error("typegres.config.ts not found"); - } - dir = parent; - } -}; - -// --- PG type name → TS class name (shared with types/generate.ts) --- -const pgTypeToClass = pgNameToClassName; - -// --- Introspect --- +// --- Public types --- +// Shared column shape — the contract between introspectors and +// codegen, expressed in codegen's vocabulary (exactly the facts +// `generateColumnLine` needs), not any dialect's catalog-ese. Each +// introspector translates its native facts at the boundary: PG maps +// information_schema rows (`is_nullable === "YES"`, identity / +// stored-generated columns); SQLite maps PRAGMA `table_info` rows +// (`notnull`, rowid-alias detection). export interface ColumnInfo { - column_name: string; - udt_name: string; - is_nullable: string; - column_default: string | null; - is_generated: string; - identity_generation: string | null; + name: string; + // Resolved TS class name for the type (e.g. "Int8", "Text", + // "Integer", "Bool", "SqliteValue"). The introspector resolves this + // at introspection time — shared codegen doesn't know how PG + // typnames vs SQLite affinity resolve. + className: string; + nullable: boolean; + // Raw SQL default expression, spliced into `default: sql`...``. + default: string | null; + // DB assigns the value (identity, stored generated, SQLite rowid + // alias). PG's ALWAYS vs BY DEFAULT distinction is deliberately + // collapsed — both emit `generated: true`; widen to a union if + // typegres ever wants "insertable but defaulted" semantics. + generated: boolean; } -const introspectTable = async (client: pg.Client, tableName: string): Promise => { - const { rows } = await client.query( - `SELECT column_name, udt_name, is_nullable, column_default, is_generated, identity_generation - FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = $1 - ORDER BY ordinal_position`, - [tableName], - ); - return rows; -}; - -const getTables = async (client: pg.Client): Promise => { - const { rows } = await client.query<{ table_name: string }>( - `SELECT table_name FROM information_schema.tables - WHERE table_schema = 'public' AND table_type = 'BASE TABLE' - ORDER BY table_name`, - ); - return rows.map((r) => r.table_name); -}; - -// --- FK introspection --- - -interface FkInfo { +export interface FkInfo { from_table: string; from_column: string; to_table: string; @@ -69,32 +45,6 @@ interface FkInfo { is_nullable: boolean; } -const introspectFks = async (client: pg.Client): Promise => { - const { rows } = await client.query(` - SELECT - cl_from.relname AS from_table, - att_from.attname AS from_column, - cl_to.relname AS to_table, - att_to.attname AS to_column, - EXISTS ( - SELECT 1 FROM pg_index - WHERE indrelid = con.conrelid - AND indkey::int[] @> ARRAY[att_from.attnum::int] - AND indisunique - ) AS is_unique, - NOT att_from.attnotnull AS is_nullable - FROM pg_constraint con - JOIN pg_class cl_from ON con.conrelid = cl_from.oid - JOIN pg_class cl_to ON con.confrelid = cl_to.oid - JOIN pg_attribute att_from ON att_from.attrelid = con.conrelid AND att_from.attnum = ANY(con.conkey) - JOIN pg_attribute att_to ON att_to.attrelid = con.confrelid AND att_to.attnum = ANY(con.confkey) - JOIN pg_namespace ns ON cl_from.relnamespace = ns.oid - WHERE con.contype = 'f' AND ns.nspname = 'public' - ORDER BY cl_from.relname, att_from.attname - `); - return rows; -}; - export type Cardinality = "one" | "maybe" | "many"; export interface Relation { @@ -105,7 +55,50 @@ export interface Relation { toColumn: string; } -const deriveRelations = (tableName: string, allFks: FkInfo[]): Relation[] => { +// A snapshot of the schema — pure data, no methods, no DB handle. +export interface TableIntrospection { + columns: ColumnInfo[]; + fks: FkInfo[]; +} + +export interface Introspection { + tables: Map; +} + +// Small factory that knows how to connect + read the schema for its +// dialect and produce the pure-data `Introspection`. `typeImportPath` +// lives here because it's a codegen concern tied to the dialect, not +// a schema fact. +export interface Introspector { + typeImportPath: string; + introspect(): Promise; +} + +// --- Config --- + +const findConfig = async (): Promise => { + let dir = process.cwd(); + while (true) { + const configPath = path.join(dir, "typegres.config.ts"); + if (fs.existsSync(configPath)) { + // file URL, not the raw path — Windows paths (`C:\...`) aren't + // valid ESM specifiers. + // eslint-disable-next-line no-restricted-syntax -- runtime config loaded from user's project + const config = await import(pathToFileURL(configPath).href); + const c = config.default; + return { dialect: c.dialect, db: c.db, tables: path.resolve(dir, c.tables), dbImport: c.dbImport }; + } + const parent = path.dirname(dir); + if (parent === dir) { + throw new Error("typegres.config.ts not found"); + } + dir = parent; + } +}; + +// --- Relations --- + +export const deriveRelations = (tableName: string, allFks: FkInfo[]): Relation[] => { const relations: Relation[] = []; const usedNames = new Set(); @@ -114,7 +107,6 @@ const deriveRelations = (tableName: string, allFks: FkInfo[]): Relation[] => { usedNames.add(base); return base; } - // Disambiguate: e.g., dogs_rival_id let candidate = base; let i = 2; while (usedNames.has(candidate)) { @@ -153,42 +145,39 @@ const deriveRelations = (tableName: string, allFks: FkInfo[]): Relation[] => { return relations; }; -// --- Generate --- +// --- Column / relation emission --- const generateColumnLine = (col: ColumnInfo, withTool: boolean): string => { - const cls = pgTypeToClass(col.udt_name); - const nullable = col.is_nullable === "YES" ? "0 | 1" : "1"; + const cls = col.className; + const nullable = col.nullable ? "0 | 1" : "1"; const opts: string[] = []; - if (col.is_nullable === "NO") { + if (!col.nullable) { opts.push("nonNull: true"); } - if (col.column_default !== null) { - opts.push(`default: sql\`${col.column_default}\``); + if (col.default !== null) { + opts.push(`default: sql\`${col.default}\``); } - if (col.identity_generation !== null) { + if (col.generated) { opts.push("generated: true"); } const optsArg = opts.length > 0 ? `{ ${opts.join(", ")} }` : ""; const prefix = withTool ? "@expose() " : ""; - return ` ${prefix}${col.column_name} = (${cls}<${nullable}>).column(${optsArg});`; + return ` ${prefix}${col.name} = (${cls}<${nullable}>).column(${optsArg});`; }; const generateRelationLine = (rel: Relation, currentTable: string, withTool: boolean): string => { const targetClass = pgNameToClassName(rel.targetTable); const currentClass = pgNameToClassName(currentTable); const prefix = withTool ? "@expose() " : ""; - // `Target.scope(Current.contextOf(this))` propagates the row's - // scope tag through every relation traversal — joins n-deep stay - // bound to the same principal. For unscoped rows, contextOf - // returns undefined and scope(undefined) behaves like from(). - return ` ${prefix}${rel.name}() { return ${targetClass}.scope(${currentClass}.contextOf(this)).where(({ ${rel.targetTable} }) => ${rel.targetTable}.${rel.toColumn}["="](this.${rel.fromColumn})).cardinality("${rel.cardinality}"); }`; + // `Target.scope(Current.contextOf(this))` propagates the row's scope + // tag through every relation traversal — joins n-deep stay bound to + // the same principal. Dialect-neutral (uses names + cardinality only). + return ` ${prefix}${rel.name}() { return ${targetClass}.scope(${currentClass}.contextOf(this)).where(({ ${rel.targetTable} }) => ${rel.targetTable}.${rel.toColumn}.eq(this.${rel.fromColumn})).cardinality("${rel.cardinality}"); }`; }; -// Scan the existing @generated block to learn which columns/relations the -// user opted out of `@expose()` decoration on. Default for new entries is -// decorated; existing entries keep whatever decoration state they had so -// hand-removed `@expose()`s aren't re-added on regenerate (same spirit as -// `// @generated-start` preserving the file's surrounding code). +// Scan the existing @generated block to learn which columns/relations +// the user opted out of `@expose()` on. New entries default decorated; +// existing entries preserve their state. const parseExistingDecorations = ( block: string, ): { cols: Map; rels: Map } => { @@ -229,39 +218,43 @@ export const generateTable = ( tableName: string, columns: ColumnInfo[], relations: Relation[], - opts: { dbImport: string; existing?: string }, + opts: { typeImportPath: string; dbImport: string; existing?: string }, ): string => { if (opts.existing !== undefined) { return updateBlock(opts.existing, tableName, columns, relations); } - return newFile(tableName, columns, relations, opts.dbImport); + return newFile(tableName, columns, relations, opts.typeImportPath, opts.dbImport); }; -const newFile = (tableName: string, columns: ColumnInfo[], relations: Relation[], dbImport: string): string => { - // Collect unique type imports - const typeClasses = [...new Set(columns.map((c) => pgTypeToClass(c.udt_name)))]; - const hasDefault = columns.some((c) => c.column_default !== null); +const newFile = ( + tableName: string, + columns: ColumnInfo[], + relations: Relation[], + typeImportPath: string, + dbImport: string, +): string => { + const typeClasses = [...new Set(columns.map((c) => c.className))]; + const hasDefault = columns.some((c) => c.default !== null); - // Collect relation target imports const relationTargets = [...new Set(relations.map((r) => r.targetTable))]; const relImports = relationTargets .filter((t) => t !== tableName) .map((t) => `import { ${pgNameToClassName(t)} } from "./${t}";`); - // Split imports: PG-specific type classes come from `typegres/postgres`; - // dialect-agnostic runtime helpers (`expose`, `sql`) come from the top - // `typegres` barrel. Symbols within each group sorted for stable diffs. + // Split imports: dialect-specific type classes come from + // `typeImportPath`; dialect-agnostic runtime helpers (`expose`, + // `sql`) come from the top `typegres` barrel. Symbols within each + // group sorted for stable diffs. const runtimeSyms = ["expose", ...(hasDefault ? ["sql"] : [])].sort(); const typeSyms = [...typeClasses].sort(); const imports = [ `import { db } from "${dbImport}";`, `import { ${runtimeSyms.join(", ")} } from "typegres";`, - `import { ${typeSyms.join(", ")} } from "typegres/postgres";`, + `import { ${typeSyms.join(", ")} } from "${typeImportPath}";`, ...relImports, ]; - // New file: every column/relation gets `@expose()` by default. Users can - // strip individual decorators in-place; updateBlock will respect that. + // New file: every column/relation gets `@expose()` by default. const colLines = columns.map((c) => generateColumnLine(c, true)); const relLines = relations.map((r) => generateRelationLine(r, tableName, true)); const allLines = relLines.length > 0 @@ -278,7 +271,12 @@ ${allLines.join("\n")} `; }; -const updateBlock = (existing: string, tableName: string, columns: ColumnInfo[], relations: Relation[]): string => { +const updateBlock = ( + existing: string, + tableName: string, + columns: ColumnInfo[], + relations: Relation[], +): string => { const startIdx = existing.indexOf(START_MARKER); const endIdx = existing.indexOf(END_MARKER); @@ -286,13 +284,10 @@ const updateBlock = (existing: string, tableName: string, columns: ColumnInfo[], throw new Error("Missing @generated-start or @generated-end markers"); } - // Preserve per-entry decoration state from the existing block. New - // entries (introduced by a schema migration) default to `@expose()`; - // entries the user has stripped stay stripped. const blockContent = existing.slice(startIdx + START_MARKER.length, endIdx); const prior = parseExistingDecorations(blockContent); - const colLines = columns.map((c) => generateColumnLine(c, prior.cols.get(c.column_name) ?? true)); + const colLines = columns.map((c) => generateColumnLine(c, prior.cols.get(c.name) ?? true)); const relLines = relations.map((r) => generateRelationLine(r, tableName, prior.rels.get(r.name) ?? true)); const allLines = relLines.length > 0 ? [...colLines, " // relations", ...relLines] @@ -305,41 +300,59 @@ const updateBlock = (existing: string, tableName: string, columns: ColumnInfo[], // --- Main --- -export const main = async () => { - const config = await findConfig(); - const client = new pg.Client(config.db); - await client.connect(); - - try { - const tables = await getTables(client); - if (tables.length === 0) { - console.log("No tables found in public schema."); - return; - } +const runGeneration = async ( + introspector: Introspector, + tablesDir: string, + dbImport: string, +): Promise => { + const intro = await introspector.introspect(); + if (intro.tables.size === 0) { + console.log("No tables found."); + return; + } + fs.mkdirSync(tablesDir, { recursive: true }); - const allFks = await introspectFks(client); - fs.mkdirSync(config.tables, { recursive: true }); - - for (const tableName of tables) { - const columns = await introspectTable(client, tableName); - const relations = deriveRelations(tableName, allFks); - const filePath = path.join(config.tables, `${tableName}.ts`); - - const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined; - if (existing !== undefined && !existing.includes(START_MARKER)) { - console.log(`Skipped ${filePath} (no @generated markers)`); - continue; - } - const content = generateTable(tableName, columns, relations, - existing === undefined - ? { dbImport: config.dbImport } - : { dbImport: config.dbImport, existing }, - ); - fs.writeFileSync(filePath, content); - console.log(`${existing ? "Updated" : "Created"} ${filePath}`); + // FK list flattened once so `deriveRelations` can scan + // outbound + inbound in the shared shape. + const allFks = [...intro.tables.values()].flatMap((t) => t.fks); + + for (const [tableName, tableData] of intro.tables) { + const relations = deriveRelations(tableName, allFks); + const filePath = path.join(tablesDir, `${tableName}.ts`); + + const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined; + if (existing !== undefined && !existing.includes(START_MARKER)) { + console.log(`Skipped ${filePath} (no @generated markers)`); + continue; } - } finally { - await client.end(); + const content = generateTable(tableName, tableData.columns, relations, + existing === undefined + ? { typeImportPath: introspector.typeImportPath, dbImport } + : { typeImportPath: introspector.typeImportPath, dbImport, existing }, + ); + fs.writeFileSync(filePath, content); + console.log(`${existing ? "Updated" : "Created"} ${filePath}`); + } +}; + +export const main = async (): Promise => { + const config = await findConfig(); + if (config.dialect === "postgres") { + // eslint-disable-next-line no-restricted-syntax -- lazy so SQLite users don't pay the `pg` peer-dep cost + const { postgresIntrospector } = await import("./postgres.ts"); + await runGeneration(postgresIntrospector(config.db), config.tables, config.dbImport); + } else if (config.dialect === "sqlite") { + // eslint-disable-next-line no-restricted-syntax -- lazy so PG users don't pay the `better-sqlite3` peer-dep cost + const { sqliteIntrospector } = await import("./sqlite.ts"); + await runGeneration(await sqliteIntrospector(config.db), config.tables, config.dbImport); + } else { + // Exhaustive over Config["dialect"]. Extend when a new dialect + // lands in the config union. + const _exhaustive: never = config; + throw new Error(`Unknown dialect: ${String((_exhaustive as { dialect: string }).dialect)}`); } }; +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(); +} diff --git a/src/tables/postgres.test.ts b/src/tables/postgres.test.ts new file mode 100644 index 00000000..54904b85 --- /dev/null +++ b/src/tables/postgres.test.ts @@ -0,0 +1,210 @@ +// Postgres table codegen tests, end-to-end against the dev Postgres +// (`bin/startpg`, via DATABASE_URL — same server the rest of the +// pg-backed suites use): real DDL into a scratch database, through +// postgresIntrospector → deriveRelations → generateTable, snapshot +// the generated TypeScript. Mirrors sqlite.test.ts. +// +// Isolation: postgresIntrospector reads the `public` schema of +// whatever database its URL points at, so each vitest worker gets its +// own scratch *database* (the per-worker schemas in test-helpers.ts +// don't help here). `generate()` resets `public` per call, so each +// test is hermetic. + +import { describe, test, expect, beforeAll, afterAll } from "vitest"; +import pg from "pg"; +import { requireDatabaseUrl } from "../pg"; +import { generateTable, deriveRelations } from "./generate"; +import { postgresIntrospector } from "./postgres"; +import { validateGenerated } from "./validate-generated"; + +const scratchName = `tg_introspect_w${process.env["VITEST_WORKER_ID"] ?? "1"}`; +const scratchUrl = (): string => { + const u = new URL(requireDatabaseUrl()); + u.pathname = `/${scratchName}`; + return u.toString(); +}; + +beforeAll(async () => { + const admin = new pg.Client(requireDatabaseUrl()); + await admin.connect(); + try { + await admin.query(`DROP DATABASE IF EXISTS ${scratchName} WITH (FORCE)`); + await admin.query(`CREATE DATABASE ${scratchName}`); + } finally { + await admin.end(); + } +}); + +afterAll(async () => { + const admin = new pg.Client(requireDatabaseUrl()); + await admin.connect(); + try { + await admin.query(`DROP DATABASE IF EXISTS ${scratchName} WITH (FORCE)`); + } finally { + await admin.end(); + } +}); + +// Full pipeline: DDL → scratch DB public schema → introspect → +// relations → emit. Mirrors runGeneration's composition minus the fs +// side. Every generated file is swc-validated before being returned. +const generate = async (ddl: string): Promise> => { + const client = new pg.Client(scratchUrl()); + await client.connect(); + try { + await client.query("DROP SCHEMA public CASCADE; CREATE SCHEMA public;"); + await client.query(ddl); + } finally { + await client.end(); + } + const introspector = postgresIntrospector(scratchUrl()); + const intro = await introspector.introspect(); + const allFks = [...intro.tables.values()].flatMap((t) => t.fks); + const out = new Map(); + for (const [name, data] of intro.tables) { + const src = generateTable(name, data.columns, deriveRelations(name, allFks), { + typeImportPath: introspector.typeImportPath, + dbImport: "../db", + }); + await validateGenerated(src); + out.set(name, src); + } + return out; +}; + +describe("postgres codegen e2e — DDL in, generated TypeScript out", () => { + test("dogs/teams: identity PK generated + nonNull, nullable column, default, relations both directions", async () => { + const files = await generate(` + CREATE TABLE teams ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text NOT NULL + ); + CREATE TABLE dogs ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text NOT NULL, + breed text, + team_id int8 NOT NULL REFERENCES teams(id), + created_at timestamptz NOT NULL DEFAULT now(), + name_upper text GENERATED ALWAYS AS (upper(name)) STORED + ); + `); + expect([...files.keys()]).toEqual(["dogs", "teams"]); + expect(files.get("dogs")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose, sql } from "typegres"; + import { Int8, Text, Timestamptz } from "typegres/postgres"; + import { Teams } from "./teams"; + + export class Dogs extends db.Table("dogs") { + // @generated-start + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + @expose() breed = (Text<0 | 1>).column(); + @expose() team_id = (Int8<1>).column({ nonNull: true }); + @expose() created_at = (Timestamptz<1>).column({ nonNull: true, default: sql\`now()\` }); + @expose() name_upper = (Text<0 | 1>).column({ generated: true }); + // relations + @expose() team() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } + // @generated-end + } + " + `); + expect(files.get("teams")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose } from "typegres"; + import { Int8, Text } from "typegres/postgres"; + import { Dogs } from "./dogs"; + + export class Teams extends db.Table("teams") { + // @generated-start + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + // relations + @expose() dogs() { return Dogs.scope(Teams.contextOf(this)).where(({ dogs }) => dogs.team_id.eq(this.id)).cardinality("many"); } + // @generated-end + } + " + `); + }); + + test("partial unique index doesn't count as unique", async () => { + // UNIQUE ... WHERE gives no global uniqueness guarantee, so the + // inbound relation must stay "many". + const files = await generate(` + CREATE TABLE parent_t (id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY); + CREATE TABLE partial_child ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + parent_id int8 NOT NULL REFERENCES parent_t(id), + archived boolean NOT NULL DEFAULT false + ); + CREATE UNIQUE INDEX partial_uniq ON partial_child (parent_id) WHERE NOT archived; + `); + expect(files.get("parent_t")).toContain(`.cardinality("many")`); + expect(files.get("parent_t")).not.toContain(`.cardinality("one")`); + }); + + test("cardinality inference: composite PK/UNIQUE don't count as unique; single-column PK/UNIQUE do", async () => { + // All four inbound cardinality cases hang off one parent: + // - joiner: FK inside a composite PK → many + // - tagged: FK inside a composite UNIQUE → many + // - profile: FK is the single-column PK → one + // - badge: nullable FK with its own UNIQUE → maybe + const files = await generate(` + CREATE TABLE parent_t (id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY); + CREATE TABLE joiner ( + a int8 NOT NULL REFERENCES parent_t(id), + b int8 NOT NULL, + PRIMARY KEY (a, b) + ); + CREATE TABLE tagged ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + parent_id int8 NOT NULL REFERENCES parent_t(id), + tag text NOT NULL, + UNIQUE (parent_id, tag) + ); + CREATE TABLE profile ( + parent_id int8 PRIMARY KEY REFERENCES parent_t(id) + ); + CREATE TABLE badge ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + parent_id int8 UNIQUE REFERENCES parent_t(id) + ); + `); + expect(files.get("parent_t")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose } from "typegres"; + import { Int8 } from "typegres/postgres"; + import { Badge } from "./badge"; + import { Joiner } from "./joiner"; + import { Profile } from "./profile"; + import { Tagged } from "./tagged"; + + export class ParentT extends db.Table("parent_t") { + // @generated-start + @expose() id = (Int8<1>).column({ nonNull: true, generated: true }); + // relations + @expose() badge() { return Badge.scope(ParentT.contextOf(this)).where(({ badge }) => badge.parent_id.eq(this.id)).cardinality("maybe"); } + @expose() joiner() { return Joiner.scope(ParentT.contextOf(this)).where(({ joiner }) => joiner.a.eq(this.id)).cardinality("many"); } + @expose() profile() { return Profile.scope(ParentT.contextOf(this)).where(({ profile }) => profile.parent_id.eq(this.id)).cardinality("one"); } + @expose() tagged() { return Tagged.scope(ParentT.contextOf(this)).where(({ tagged }) => tagged.parent_id.eq(this.id)).cardinality("many"); } + // @generated-end + } + " + `); + expect(files.get("profile")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose } from "typegres"; + import { Int8 } from "typegres/postgres"; + import { ParentT } from "./parent_t"; + + export class Profile extends db.Table("profile") { + // @generated-start + @expose() parent_id = (Int8<1>).column({ nonNull: true }); + // relations + @expose() parent() { return ParentT.scope(Profile.contextOf(this)).where(({ parent_t }) => parent_t.id.eq(this.parent_id)).cardinality("one"); } + // @generated-end + } + " + `); + }); +}); diff --git a/src/tables/postgres.ts b/src/tables/postgres.ts new file mode 100644 index 00000000..0c1dced2 --- /dev/null +++ b/src/tables/postgres.ts @@ -0,0 +1,115 @@ +// Postgres introspection: information_schema + pg_catalog, mapped +// onto the shared `Introspection` shape. + +import pg from "pg"; +import { pgNameToClassName } from "../types/postgres/introspect.ts"; +import type { ColumnInfo, FkInfo, Introspection, Introspector, TableIntrospection } from "./generate.ts"; + +interface PgColumnRow { + column_name: string; + udt_name: string; + is_nullable: string; + column_default: string | null; + is_generated: string; + identity_generation: string | null; +} + +const queryColumns = async (client: pg.Client, tableName: string): Promise => { + const { rows } = await client.query( + `SELECT column_name, udt_name, is_nullable, column_default, is_generated, identity_generation + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 + ORDER BY ordinal_position`, + [tableName], + ); + return rows; +}; + +const queryTables = async (client: pg.Client): Promise => { + const { rows } = await client.query<{ table_name: string }>( + `SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' AND table_type = 'BASE TABLE' + ORDER BY table_name`, + ); + return rows.map((r) => r.table_name); +}; + +const FK_QUERY = ` + SELECT + cl_from.relname AS from_table, + att_from.attname AS from_column, + cl_to.relname AS to_table, + att_to.attname AS to_column, + EXISTS ( + -- Single-column unique index over exactly this FK column. + -- \`indkey\` is int2vector (0-indexed); casting to int[] preserves + -- the 0 lower bound, so array-equality against ARRAY[x] (1- + -- indexed) always fails. Compare length + first element instead + -- to avoid the composite-index over-report (UNIQUE (a, b) + -- would otherwise mark \`a\` alone as unique via @>). + SELECT 1 FROM pg_index + WHERE indrelid = con.conrelid + AND indisunique + -- Partial unique indexes don't guarantee global uniqueness. + AND indpred IS NULL + AND array_length(indkey::int[], 1) = 1 + AND indkey[0] = att_from.attnum + ) AS is_unique, + NOT att_from.attnotnull AS is_nullable + FROM pg_constraint con + JOIN pg_class cl_from ON con.conrelid = cl_from.oid + JOIN pg_class cl_to ON con.confrelid = cl_to.oid + JOIN pg_attribute att_from ON att_from.attrelid = con.conrelid AND att_from.attnum = ANY(con.conkey) + JOIN pg_attribute att_to ON att_to.attrelid = con.confrelid AND att_to.attnum = ANY(con.confkey) + JOIN pg_namespace ns ON cl_from.relnamespace = ns.oid + WHERE con.contype = 'f' AND ns.nspname = 'public' + ORDER BY cl_from.relname, att_from.attname + `; + +const queryFks = async (client: pg.Client): Promise => { + const { rows } = await client.query(FK_QUERY); + return rows; +}; + +const toColumnInfo = (row: PgColumnRow): ColumnInfo => ({ + name: row.column_name, + // PG type-name → codegen class name is snake→PascalCase — the + // pg_catalog names (int4, text, timestamptz) map 1:1 onto our + // codegen'd classes. + className: pgNameToClassName(row.udt_name), + nullable: row.is_nullable === "YES", + default: row.column_default, + // Identity columns (ALWAYS or BY DEFAULT) and stored generated + // columns (`GENERATED ALWAYS AS (...) STORED`, flagged by + // is_generated) are all DB-assigned. + generated: row.identity_generation !== null || row.is_generated === "ALWAYS", +}); + +export const postgresIntrospector = (dbUrl: string): Introspector => ({ + typeImportPath: "typegres/postgres", + introspect: async (): Promise => { + const client = new pg.Client(dbUrl); + await client.connect(); + try { + const tableNames = await queryTables(client); + const allFks = await queryFks(client); + const fksByTable = new Map(); + for (const fk of allFks) { + const arr = fksByTable.get(fk.from_table) ?? []; + arr.push(fk); + fksByTable.set(fk.from_table, arr); + } + const tables = new Map(); + for (const name of tableNames) { + const rows = await queryColumns(client, name); + tables.set(name, { + columns: rows.map(toColumnInfo), + fks: fksByTable.get(name) ?? [], + }); + } + return { tables }; + } finally { + await client.end(); + } + }, +}); diff --git a/src/tables/sqlite.test.ts b/src/tables/sqlite.test.ts new file mode 100644 index 00000000..397dd30a --- /dev/null +++ b/src/tables/sqlite.test.ts @@ -0,0 +1,240 @@ +// SQLite table codegen tests. +// +// Two altitudes on purpose: +// - `affinityToClass` stays unit-tested: it's a pure string→string rule +// table where a failure should name the exact rule that broke. +// - Everything downstream is tested end-to-end: real DDL into a real +// SQLite file, through sqliteIntrospector → deriveRelations → +// generateTable, snapshot the generated TypeScript. This pins tests +// to the actual contract (DDL in, code out) rather than intermediate +// shapes like ColumnInfo/FkInfo, which are expected to change (see +// flags.md). The marker/@expose() preservation logic is +// generateTable's own API and stays unit-tested in generate.test.ts. + +import { describe, test, expect } from "vitest"; +import * as os from "node:os"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { generateTable, deriveRelations } from "./generate"; +import { affinityToClass, sqliteIntrospector } from "./sqlite"; +import { validateGenerated } from "./validate-generated"; + +describe("affinityToClass — SQLite type affinity rules", () => { + test("INTEGER → Integer", () => { expect(affinityToClass("INTEGER")).toBe("Integer"); }); + test("INT → Integer", () => { expect(affinityToClass("INT")).toBe("Integer"); }); + test("TINYINT → Integer (contains 'INT')", () => { expect(affinityToClass("TINYINT")).toBe("Integer"); }); + test("TEXT → Text", () => { expect(affinityToClass("TEXT")).toBe("Text"); }); + test("VARCHAR(80) → Text", () => { expect(affinityToClass("VARCHAR(80)")).toBe("Text"); }); + test("BLOB → Blob", () => { expect(affinityToClass("BLOB")).toBe("Blob"); }); + test("empty declared type → Blob", () => { expect(affinityToClass("")).toBe("Blob"); }); + test("REAL → Real", () => { expect(affinityToClass("REAL")).toBe("Real"); }); + test("DOUBLE PRECISION → Real", () => { expect(affinityToClass("DOUBLE PRECISION")).toBe("Real"); }); + + test("BOOLEAN → Bool (overlay before affinity)", () => { expect(affinityToClass("BOOLEAN")).toBe("Bool"); }); + test("bool → Bool (case-insensitive)", () => { expect(affinityToClass("bool")).toBe("Bool"); }); + + // NUMERIC affinity fall-through → SqliteValue (no narrow view). + test("DATE → SqliteValue (NUMERIC affinity, no domain type)", () => { + expect(affinityToClass("DATE")).toBe("SqliteValue"); + }); + test("DATETIME → SqliteValue", () => { expect(affinityToClass("DATETIME")).toBe("SqliteValue"); }); + test("DECIMAL(10,2) → SqliteValue", () => { expect(affinityToClass("DECIMAL(10,2)")).toBe("SqliteValue"); }); + test("NUMERIC → SqliteValue", () => { expect(affinityToClass("NUMERIC")).toBe("SqliteValue"); }); + + // Classic SQLite affinity gotchas — we match SQLite's own behavior. + test("FLOATING POINT → Integer (rule 1 catches 'POINT' → 'INT')", () => { + expect(affinityToClass("FLOATING POINT")).toBe("Integer"); + }); + test("CHARINT → Integer (rule 1 beats rule 2)", () => { + expect(affinityToClass("CHARINT")).toBe("Integer"); + }); + test("STRING → SqliteValue (no matching affinity rule)", () => { + expect(affinityToClass("STRING")).toBe("SqliteValue"); + }); +}); + +// --- End-to-end: DDL in, generated TypeScript out --- + +// Full pipeline: DDL → temp .db file → introspect → relations → emit. +// Mirrors runGeneration's composition minus the fs side (existing-file +// handling is generate.test.ts territory). Every generated file is +// swc-validated before being returned. +const generate = async (ddl: string): Promise> => { + // eslint-disable-next-line no-restricted-syntax -- optional peer, test-scoped + const mod = (await import("better-sqlite3")).default; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tg-sqlite-")); + try { + const file = path.join(dir, "test.db"); + const db = new mod(file); + try { + db.exec(ddl); + } finally { + db.close(); + } + const introspector = await sqliteIntrospector(file); + const intro = await introspector.introspect(); + const allFks = [...intro.tables.values()].flatMap((t) => t.fks); + const out = new Map(); + for (const [name, data] of intro.tables) { + const src = generateTable(name, data.columns, deriveRelations(name, allFks), { + typeImportPath: introspector.typeImportPath, + dbImport: "../db", + }); + await validateGenerated(src); + out.set(name, src); + } + return out; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}; + +describe("sqlite codegen e2e — DDL in, generated TypeScript out", () => { + test("dogs/teams: rowid PK generated + nonNull, nullable column, BOOLEAN default, NUMERIC-affinity column, relations both directions", async () => { + const files = await generate(` + CREATE TABLE teams ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + ); + CREATE TABLE dogs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + breed TEXT, + team_id INTEGER NOT NULL REFERENCES teams(id), + active BOOLEAN NOT NULL DEFAULT 0, + seen_at DATETIME + ); + `); + expect([...files.keys()]).toEqual(["dogs", "teams"]); + expect(files.get("dogs")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose, sql } from "typegres"; + import { Bool, Integer, SqliteValue, Text } from "typegres/sqlite"; + import { Teams } from "./teams"; + + export class Dogs extends db.Table("dogs") { + // @generated-start + @expose() id = (Integer<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + @expose() breed = (Text<0 | 1>).column(); + @expose() team_id = (Integer<1>).column({ nonNull: true }); + @expose() active = (Bool<1>).column({ nonNull: true, default: sql\`0\` }); + @expose() seen_at = (SqliteValue<0 | 1>).column(); + // relations + @expose() team() { return Teams.scope(Dogs.contextOf(this)).where(({ teams }) => teams.id.eq(this.team_id)).cardinality("one"); } + // @generated-end + } + " + `); + expect(files.get("teams")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose } from "typegres"; + import { Integer, Text } from "typegres/sqlite"; + import { Dogs } from "./dogs"; + + export class Teams extends db.Table("teams") { + // @generated-start + @expose() id = (Integer<1>).column({ nonNull: true, generated: true }); + @expose() name = (Text<1>).column({ nonNull: true }); + // relations + @expose() dogs() { return Dogs.scope(Teams.contextOf(this)).where(({ dogs }) => dogs.team_id.eq(this.id)).cardinality("many"); } + // @generated-end + } + " + `); + }); + + test("partial/expression unique indexes don't count as unique", async () => { + // - partial_child: UNIQUE ... WHERE — no global uniqueness + // guarantee, so inbound stays "many". + // - expr_child: unique index over an expression — index_info + // reports name=NULL for the member; must not poison the + // unique-column set (or crash on a null name). + const files = await generate(` + CREATE TABLE parent_t (id INTEGER PRIMARY KEY); + CREATE TABLE partial_child ( + id INTEGER PRIMARY KEY, + parent_id INTEGER NOT NULL REFERENCES parent_t(id), + archived INTEGER NOT NULL DEFAULT 0 + ); + CREATE UNIQUE INDEX partial_uniq ON partial_child (parent_id) WHERE archived = 0; + CREATE TABLE expr_child ( + id INTEGER PRIMARY KEY, + parent_id INTEGER NOT NULL REFERENCES parent_t(id), + name TEXT NOT NULL + ); + CREATE UNIQUE INDEX expr_uniq ON expr_child (lower(name)); + `); + const parent = files.get("parent_t")!; + expect(parent).toContain(`partial_child() { return PartialChild.scope(ParentT.contextOf(this)).where(({ partial_child }) => partial_child.parent_id.eq(this.id)).cardinality("many"); }`); + expect(parent).toContain(`expr_child() { return ExprChild.scope(ParentT.contextOf(this)).where(({ expr_child }) => expr_child.parent_id.eq(this.id)).cardinality("many"); }`); + }); + + test("cardinality inference: composite PK/UNIQUE don't count as unique; single-column PK/UNIQUE do", async () => { + // All four inbound cardinality cases hang off one parent: + // - joiner: FK inside a composite PK → many + // - tagged: FK inside a composite UNIQUE → many + // - profile: FK is the single-column PK → one + // - badge: nullable FK with its own UNIQUE → maybe + const files = await generate(` + CREATE TABLE parent_t (id INTEGER PRIMARY KEY); + CREATE TABLE joiner ( + a INTEGER NOT NULL REFERENCES parent_t(id), + b INTEGER NOT NULL, + PRIMARY KEY (a, b) + ); + CREATE TABLE tagged ( + id INTEGER PRIMARY KEY, + parent_id INTEGER NOT NULL REFERENCES parent_t(id), + tag TEXT NOT NULL, + UNIQUE (parent_id, tag) + ); + CREATE TABLE profile ( + parent_id INTEGER PRIMARY KEY REFERENCES parent_t(id) + ); + CREATE TABLE badge ( + id INTEGER PRIMARY KEY, + parent_id INTEGER UNIQUE REFERENCES parent_t(id) + ); + `); + expect(files.get("parent_t")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose } from "typegres"; + import { Integer } from "typegres/sqlite"; + import { Badge } from "./badge"; + import { Joiner } from "./joiner"; + import { Profile } from "./profile"; + import { Tagged } from "./tagged"; + + export class ParentT extends db.Table("parent_t") { + // @generated-start + @expose() id = (Integer<1>).column({ nonNull: true, generated: true }); + // relations + @expose() badge() { return Badge.scope(ParentT.contextOf(this)).where(({ badge }) => badge.parent_id.eq(this.id)).cardinality("maybe"); } + @expose() joiner() { return Joiner.scope(ParentT.contextOf(this)).where(({ joiner }) => joiner.a.eq(this.id)).cardinality("many"); } + @expose() profile() { return Profile.scope(ParentT.contextOf(this)).where(({ profile }) => profile.parent_id.eq(this.id)).cardinality("one"); } + @expose() tagged() { return Tagged.scope(ParentT.contextOf(this)).where(({ tagged }) => tagged.parent_id.eq(this.id)).cardinality("many"); } + // @generated-end + } + " + `); + // profile's outbound side: its FK column is an INTEGER PRIMARY KEY + // rowid alias — PRAGMA reports notnull=0, but it's semantically + // non-null, so the outbound relation must be "one", not "maybe". + expect(files.get("profile")).toMatchInlineSnapshot(` + "import { db } from "../db"; + import { expose } from "typegres"; + import { Integer } from "typegres/sqlite"; + import { ParentT } from "./parent_t"; + + export class Profile extends db.Table("profile") { + // @generated-start + @expose() parent_id = (Integer<1>).column({ nonNull: true, generated: true }); + // relations + @expose() parent() { return ParentT.scope(Profile.contextOf(this)).where(({ parent_t }) => parent_t.id.eq(this.parent_id)).cardinality("one"); } + // @generated-end + } + " + `); + }); +}); diff --git a/src/tables/sqlite.ts b/src/tables/sqlite.ts new file mode 100644 index 00000000..d169f57f --- /dev/null +++ b/src/tables/sqlite.ts @@ -0,0 +1,193 @@ +// SQLite introspection: PRAGMAs (sqlite_master, table_info, +// foreign_key_list, index_list, index_info) mapped onto the shared +// `Introspection` shape. Type-name → class-name resolution (BOOLEAN +// overlay, SQLite affinity rules) happens here at introspection time. + +import type BetterSqlite3 from "better-sqlite3"; +import type { ColumnInfo, FkInfo, Introspection, Introspector, TableIntrospection } from "./generate.ts"; + +// --- PRAGMA row shapes --- + +interface TableInfoRow { + cid: number; + name: string; + type: string; // declared type (may be empty string) + notnull: 0 | 1; + dflt_value: string | null; + pk: number; // 0 = not PK, 1..N = position in PK +} + +interface FkListRow { + id: number; + seq: number; + table: string; + from: string; + to: string; +} + +interface IndexListRow { + seq: number; + name: string; + unique: 0 | 1; + origin: string; + partial: 0 | 1; +} + +interface IndexInfoRow { + seqno: number; + cid: number; // -1 = rowid, -2 = expression + name: string | null; // NULL for rowid / expression index members +} + +// --- Class-name resolution --- + +// SQLite lets you declare any string as a column type and computes an +// "affinity" via ordered substring rules (§ 3.1 of the SQLite type +// docs). We follow those rules 1:1 plus a small overlay for BOOLEAN +// (a strong-enough convention to surface as our `Bool`). NUMERIC +// affinity resolves to `SqliteValue` — the base class with no narrow +// method surface, which honestly reflects SQLite's dynamic typing. +export const affinityToClass = (declaredType: string): string => { + // BOOL/BOOLEAN overlay first — affinity rules would send this to + // NUMERIC otherwise (no "BOOL" match term), losing the intent. + if (/^BOOL(EAN)?$/i.test(declaredType.trim())) { return "Bool"; } + + // Ordered affinity rules. Substring match, case-insensitive, first + // match wins. Order matters — `FLOATING POINT` contains both "FLOA" + // and "INT" (via "POINT"); rule 1 fires first and yields INTEGER, + // matching SQLite's own behavior. + const t = declaredType.toUpperCase(); + if (t.includes("INT")) { return "Integer"; } + if (/CHAR|CLOB|TEXT/.test(t)) { return "Text"; } + if (t.includes("BLOB") || declaredType === "") { return "Blob"; } + if (/REAL|FLOA|DOUB/.test(t)) { return "Real"; } + + // NUMERIC fallback — DATE, DATETIME, TIMESTAMP, DECIMAL, MONEY, and + // custom names all land here. Bare SqliteValue: no narrow + // methods, `unknown` on hydration. User asserts intent at the call + // site when treating it as a specific type. + return "SqliteValue"; +}; + +// `INTEGER PRIMARY KEY` (that exact declared type, not "INT") in a +// rowid table aliases the rowid — auto-increments, nulls are replaced. +// SQLite's own docs are strict about the literal string match. +const isRowidAlias = (declaredType: string, pk: number): boolean => + pk === 1 && /^\s*INTEGER\s*$/i.test(declaredType); + +// --- PRAGMA helpers --- + +const quoteIdent = (name: string): string => `"${name.replace(/"/g, '""')}"`; + +const readTables = (db: BetterSqlite3.Database): string[] => { + const rows = db + .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`) + .all() as { name: string }[]; + return rows.map((r) => r.name); +}; + +const readTableInfo = (db: BetterSqlite3.Database, tableName: string): TableInfoRow[] => + db.prepare(`PRAGMA table_info(${quoteIdent(tableName)})`).all() as TableInfoRow[]; + +const toColumnInfo = (r: TableInfoRow): ColumnInfo => { + const rowid = isRowidAlias(r.type, r.pk); + return { + name: r.name, + className: affinityToClass(r.type), + // INTEGER PRIMARY KEY reports notnull=0 in PRAGMA (SQLite treats + // NULL inserts as "please auto-fill from rowid"), but it's + // semantically non-null — the auto-fill guarantees a value. + nullable: !rowid && !r.notnull, + default: r.dflt_value, + // Rowid aliases are SQLite's only DB-assigned column concept. + generated: rowid, + }; +}; + +const readSingleColUniqueSet = (db: BetterSqlite3.Database, tableName: string, info: TableInfoRow[]): Set => { + const uniq = new Set(); + // PRAGMA table_info's `pk` is 1-based ordinal within the PK, not a + // boolean — for `PRIMARY KEY (a, b)` we get a.pk=1, b.pk=2. Only add + // to the unique set if the PK is single-column. + const pkCols = info.filter((c) => c.pk > 0); + if (pkCols.length === 1) { uniq.add(pkCols[0]!.name); } + const indexes = db.prepare(`PRAGMA index_list(${quoteIdent(tableName)})`).all() as IndexListRow[]; + for (const idx of indexes) { + // Partial unique indexes (`... WHERE expr`) don't guarantee global + // uniqueness — a non-matching row can duplicate the column freely, + // so they must not upgrade inbound cardinality to "one". + if (!idx.unique || idx.partial) { continue; } + const cols = db.prepare(`PRAGMA index_info(${quoteIdent(idx.name)})`).all() as IndexInfoRow[]; + // name is NULL for expression members (`UNIQUE (lower(col))`) — + // those aren't single-column uniqueness either. + if (cols.length === 1 && cols[0]!.name !== null) { uniq.add(cols[0]!.name); } + } + return uniq; +}; + +const readFksFor = (db: BetterSqlite3.Database, tableName: string, info: TableInfoRow[], uniqueCols: Set): FkInfo[] => { + const fks = db.prepare(`PRAGMA foreign_key_list(${quoteIdent(tableName)})`).all() as FkListRow[]; + + // A composite FK repeats the same `id` across multiple rows. Group + // by id — if any group has more than one column, skip with a warning. + const byId = new Map(); + for (const fk of fks) { + const arr = byId.get(fk.id) ?? []; + arr.push(fk); + byId.set(fk.id, arr); + } + const out: FkInfo[] = []; + for (const [, group] of byId) { + if (group.length > 1) { + console.warn( + `Skipping composite FK on ${tableName} (columns: ${group.map((g) => g.from).join(", ")}); ` + + `multi-column FKs aren't emitted as relations.`, + ); + continue; + } + const fk = group[0]!; + const localCol = info.find((c) => c.name === fk.from); + // Same rowid-alias exception as toColumnInfo: `INTEGER PRIMARY KEY` + // reports notnull=0 but is semantically non-null. Without this, + // a self-referencing single-col PK-as-FK is wrongly flagged + // nullable and cardinality drops from "one" to "maybe". + const isRowid = localCol ? isRowidAlias(localCol.type, localCol.pk) : false; + out.push({ + from_table: tableName, + from_column: fk.from, + to_table: fk.table, + to_column: fk.to, + is_unique: uniqueCols.has(fk.from), + is_nullable: !isRowid && (!localCol || localCol.notnull === 0), + }); + } + return out; +}; + +// --- Introspector --- + +export const sqliteIntrospector = async (filename: string): Promise => { + // eslint-disable-next-line no-restricted-syntax -- optional peer, matches other SQLite call sites + const mod = (await import("better-sqlite3")).default; + return { + typeImportPath: "typegres/sqlite", + introspect: async (): Promise => { + const db = new mod(filename, { readonly: true }); + try { + const tableNames = readTables(db); + const tables = new Map(); + for (const name of tableNames) { + const info = readTableInfo(db, name); + const uniqueCols = readSingleColUniqueSet(db, name, info); + tables.set(name, { + columns: info.map(toColumnInfo), + fks: readFksFor(db, name, info, uniqueCols), + }); + } + return { tables }; + } finally { + db.close(); + } + }, + }; +}; diff --git a/src/tables/validate-generated.ts b/src/tables/validate-generated.ts new file mode 100644 index 00000000..5ae629db --- /dev/null +++ b/src/tables/validate-generated.ts @@ -0,0 +1,42 @@ +// Test-only helper shared by the codegen suites (generate.test.ts, +// sqlite.test.ts, postgres.test.ts). +// +// Validates generated code: (1) syntactically valid TS that swc can +// transform with the same decorator config typegres' build uses; +// (2) every `@` decorator referenced in the body has a matching +// import. swc alone catches syntax/transform errors but happily emits +// `(0, expose)()` even when `expose` isn't imported — the import +// check closes that gap (caught a rename-drift bug otherwise invisible +// to inline snapshot tests). +import * as swc from "@swc/core"; + +export const validateGenerated = async (out: string): Promise => { + await swc.transform(out, { + filename: "generated.ts", + jsc: { + target: "es2022", + parser: { syntax: "typescript", decorators: true }, + transform: { decoratorVersion: "2022-03" }, + }, + module: { type: "es6" }, + isModule: true, + }); + + const decorators = new Set( + [...out.matchAll(/@(\w+)(?:\.\w+)?\(/g)].map((m) => m[1]!), + ); + const imported = new Set(); + for (const m of out.matchAll(/import\s*\{([^}]*)\}/g)) { + for (const raw of m[1]!.split(",")) { + const id = raw.trim().split(/\s+as\s+/)[0]!.replace(/^type\s+/, "").trim(); + if (id) { imported.add(id); } + } + } + for (const d of decorators) { + if (!imported.has(d)) { + throw new Error( + `generated code uses @${d}() but doesn't import \`${d}\`. Imports: [${[...imported].join(", ")}]`, + ); + } + } +}; diff --git a/src/types/sqlite/base.ts b/src/types/sqlite/base.ts index 97278a11..e6d7713d 100644 --- a/src/types/sqlite/base.ts +++ b/src/types/sqlite/base.ts @@ -11,6 +11,7 @@ // without resolving the cyclic references at import time. Property // access happens at method-call time. // +import z from "zod"; import { SqlValue, type Dialect } from "../sql-value"; import { meta } from "../sql-value"; import * as types from "./index"; @@ -41,6 +42,21 @@ export class SqliteValue extends SqlValue { return types.Bool.from(sql`(${this.toSql()} IS NOT NULL)`) as types.Bool<1>; } + // Equality against another SqliteValue instance or a JS primitive. + // SQLite is manifest-typed at runtime, so no per-class overload + // narrowing — the shared surface takes any comparable value. + // Primitives get parameterized as-is via sql.param; instances + // splice their .toSql(). + // + // @expose so exoeval-sandboxed predicates (RPC/tool-driven .where() + // callbacks) can call it — parity with PG's codegen'd operator + // methods, which are all exposed. + @expose(z.union([z.custom>((v) => v instanceof SqliteValue), z.boolean(), z.number(), z.string()])) + eq(other: SqliteValue | boolean | number | string): types.Bool { + const rhs = other instanceof SqliteValue ? other.toSql() : sql.param(other); + return types.Bool.from(sql`(${this.toSql()} = ${rhs})`) as types.Bool; + } + // Non-generic `.in()` — SQLite's shallow class hierarchy (SqlValue → // SqliteValue → concrete) triggers TS2589 with the PG-style // `, Vs>(this: T, ...)` signature, because diff --git a/tsdown.config.ts b/tsdown.config.ts index 9f868503..a45d0cd2 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -15,10 +15,15 @@ const swcPlugin = () => export default defineConfig([ // Library: multi-entry ESM + per-entry .d.ts for npm consumers. + // Optional peer deps (`pg`, `@electric-sql/pglite`, `better-sqlite3`) + // stay external so the bundler doesn't try to include their native + // loaders — those use `require` / `__filename` and would trip Node's + // CJS/ESM mixed-mode check when a consumer runs the CLI. { entry: ["src/index.ts", "src/config.ts", "src/builder/sql.ts", "src/types/postgres/index.ts", "src/types/sqlite/index.ts", "src/cli.ts", "src/exoeval/index.ts"], format: ["esm"], clean: true, + deps: { neverBundle: ["pg", "@electric-sql/pglite", "better-sqlite3"] }, plugins: [swcPlugin()], }, // Playground single-file bundle for the site's Monaco + esbuild-wasm