From 9f9a1038fae10520dd1b4d83c607b01e502b7580 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 23 Jul 2025 13:21:45 -0700 Subject: [PATCH 1/2] chore: update package versions --- apps/backend/CHANGELOG.md | 9 +++++++++ apps/backend/package.json | 2 +- apps/dashboard/CHANGELOG.md | 11 +++++++++++ apps/dashboard/package.json | 2 +- apps/dev-launchpad/CHANGELOG.md | 2 ++ apps/dev-launchpad/package.json | 2 +- apps/e2e/CHANGELOG.md | 9 +++++++++ apps/e2e/package.json | 2 +- apps/mcp-server/CHANGELOG.md | 6 ++++++ apps/mcp-server/package.json | 2 +- apps/mock-oauth-server/CHANGELOG.md | 2 ++ apps/mock-oauth-server/package.json | 2 +- docs/CHANGELOG.md | 9 +++++++++ docs/package.json | 2 +- examples/cjs-test/CHANGELOG.md | 6 ++++++ examples/cjs-test/package.json | 2 +- examples/demo/CHANGELOG.md | 9 +++++++++ examples/demo/package.json | 2 +- examples/docs-examples/CHANGELOG.md | 9 +++++++++ examples/docs-examples/package.json | 2 +- examples/e-commerce/CHANGELOG.md | 6 ++++++ examples/e-commerce/package.json | 2 +- examples/js-example/CHANGELOG.md | 6 ++++++ examples/js-example/package.json | 2 +- examples/middleware/CHANGELOG.md | 6 ++++++ examples/middleware/package.json | 2 +- examples/partial-prerendering/CHANGELOG.md | 6 ++++++ examples/partial-prerendering/package.json | 2 +- examples/react-example/CHANGELOG.md | 6 ++++++ examples/react-example/package.json | 2 +- examples/supabase/CHANGELOG.md | 6 ++++++ examples/supabase/package.json | 2 +- packages/init-stack/CHANGELOG.md | 7 +++++++ packages/init-stack/package.json | 2 +- packages/js/package.json | 2 +- packages/react/package.json | 2 +- packages/stack-emails/CHANGELOG.md | 8 ++++++++ packages/stack-emails/package.json | 2 +- packages/stack-sc/CHANGELOG.md | 2 ++ packages/stack-sc/package.json | 2 +- packages/stack-shared/CHANGELOG.md | 6 ++++++ packages/stack-shared/package.json | 2 +- packages/stack-ui/CHANGELOG.md | 8 ++++++++ packages/stack-ui/package.json | 2 +- packages/stack/package.json | 2 +- packages/template/CHANGELOG.md | 10 ++++++++++ packages/template/package-template.json | 2 +- packages/template/package.json | 2 +- 48 files changed, 175 insertions(+), 26 deletions(-) diff --git a/apps/backend/CHANGELOG.md b/apps/backend/CHANGELOG.md index 90c5d5548..887a9cbcb 100644 --- a/apps/backend/CHANGELOG.md +++ b/apps/backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @stackframe/stack-backend +## 2.8.24 + +### Patch Changes + +- Various changes +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack-emails@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/apps/backend/package.json b/apps/backend/package.json index fc55c8dfc..fb532e5b3 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-backend", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "clean": "rimraf src/generated && rimraf .next && rimraf node_modules", diff --git a/apps/dashboard/CHANGELOG.md b/apps/dashboard/CHANGELOG.md index 36943ed29..22f35bd00 100644 --- a/apps/dashboard/CHANGELOG.md +++ b/apps/dashboard/CHANGELOG.md @@ -1,5 +1,16 @@ # @stackframe/stack-dashboard +## 2.8.24 + +### Patch Changes + +- Various changes +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack-ui@2.8.24 + - @stackframe/stack@2.8.24 + - @stackframe/stack-emails@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index be4ed476d..d2d809710 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-dashboard", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "clean": "rimraf .next && rimraf node_modules", diff --git a/apps/dev-launchpad/CHANGELOG.md b/apps/dev-launchpad/CHANGELOG.md index ea91885b9..a9aa74beb 100644 --- a/apps/dev-launchpad/CHANGELOG.md +++ b/apps/dev-launchpad/CHANGELOG.md @@ -1,5 +1,7 @@ # @stackframe/dev-launchpad +## 2.8.24 + ## 2.8.23 ## 2.8.22 diff --git a/apps/dev-launchpad/package.json b/apps/dev-launchpad/package.json index 8338af06b..1d6f93c8f 100644 --- a/apps/dev-launchpad/package.json +++ b/apps/dev-launchpad/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/dev-launchpad", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "dev": "serve -p 8100 -s public", diff --git a/apps/e2e/CHANGELOG.md b/apps/e2e/CHANGELOG.md index b40865ed2..41609d67e 100644 --- a/apps/e2e/CHANGELOG.md +++ b/apps/e2e/CHANGELOG.md @@ -1,5 +1,14 @@ # @stackframe/e2e-tests +## 2.8.24 + +### Patch Changes + +- Various changes +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/js@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 9e0ea8300..f4fd5a441 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/e2e-tests", - "version": "2.8.23", + "version": "2.8.24", "private": true, "type": "module", "scripts": { diff --git a/apps/mcp-server/CHANGELOG.md b/apps/mcp-server/CHANGELOG.md index 7ce2f913a..0f8b36e00 100644 --- a/apps/mcp-server/CHANGELOG.md +++ b/apps/mcp-server/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/mcp-server +## 2.8.24 + +### Patch Changes + +- @stackframe/js@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json index 1b91d3b27..564e46df6 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/mcp-server", - "version": "2.8.23", + "version": "2.8.24", "private": true, "type": "module", "bin": { diff --git a/apps/mock-oauth-server/CHANGELOG.md b/apps/mock-oauth-server/CHANGELOG.md index 0e98dacd6..6cf40a5a5 100644 --- a/apps/mock-oauth-server/CHANGELOG.md +++ b/apps/mock-oauth-server/CHANGELOG.md @@ -1,5 +1,7 @@ # @stackframe/mock-oauth-server +## 2.8.24 + ## 2.8.23 ## 2.8.22 diff --git a/apps/mock-oauth-server/package.json b/apps/mock-oauth-server/package.json index 88641dca6..5fbf46ca0 100644 --- a/apps/mock-oauth-server/package.json +++ b/apps/mock-oauth-server/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/mock-oauth-server", - "version": "2.8.23", + "version": "2.8.24", "private": true, "main": "index.js", "scripts": { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3fb7096d1..4479d58c0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,14 @@ # @stackframe/stack-docs +## 2.8.24 + +### Patch Changes + +- Various changes +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/docs/package.json b/docs/package.json index 1b8f0d25a..a95787aaf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-docs", - "version": "2.8.23", + "version": "2.8.24", "description": "", "main": "index.js", "private": true, diff --git a/examples/cjs-test/CHANGELOG.md b/examples/cjs-test/CHANGELOG.md index 38d640cd8..3f9df7991 100644 --- a/examples/cjs-test/CHANGELOG.md +++ b/examples/cjs-test/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/example-cjs-test +## 2.8.24 + +### Patch Changes + +- @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/cjs-test/package.json b/examples/cjs-test/package.json index 35cc6f9d8..e6bea3979 100644 --- a/examples/cjs-test/package.json +++ b/examples/cjs-test/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-cjs-test", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "dev": "next dev --port 8110", diff --git a/examples/demo/CHANGELOG.md b/examples/demo/CHANGELOG.md index 494ae260f..e62310ca0 100644 --- a/examples/demo/CHANGELOG.md +++ b/examples/demo/CHANGELOG.md @@ -1,5 +1,14 @@ # @stackframe/example-demo-app +## 2.8.24 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack-ui@2.8.24 + - @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/demo/package.json b/examples/demo/package.json index 5c98ca0fe..12af2a3de 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-demo-app", - "version": "2.8.23", + "version": "2.8.24", "description": "", "private": true, "scripts": { diff --git a/examples/docs-examples/CHANGELOG.md b/examples/docs-examples/CHANGELOG.md index 03a0b4639..4a673ca56 100644 --- a/examples/docs-examples/CHANGELOG.md +++ b/examples/docs-examples/CHANGELOG.md @@ -1,5 +1,14 @@ # @stackframe/docs-examples +## 2.8.24 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack-ui@2.8.24 + - @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/docs-examples/package.json b/examples/docs-examples/package.json index 788b78893..d1448fbd8 100644 --- a/examples/docs-examples/package.json +++ b/examples/docs-examples/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/docs-examples", - "version": "2.8.23", + "version": "2.8.24", "description": "", "private": true, "scripts": { diff --git a/examples/e-commerce/CHANGELOG.md b/examples/e-commerce/CHANGELOG.md index 6fe478b2e..8e6115e03 100644 --- a/examples/e-commerce/CHANGELOG.md +++ b/examples/e-commerce/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/e-commerce-demo +## 2.8.24 + +### Patch Changes + +- @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/e-commerce/package.json b/examples/e-commerce/package.json index db89ebe50..097d3a517 100644 --- a/examples/e-commerce/package.json +++ b/examples/e-commerce/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/e-commerce-demo", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "dev": "next dev --port 8111", diff --git a/examples/js-example/CHANGELOG.md b/examples/js-example/CHANGELOG.md index 1e146d32f..3e42996be 100644 --- a/examples/js-example/CHANGELOG.md +++ b/examples/js-example/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/js-example +## 2.8.24 + +### Patch Changes + +- @stackframe/js@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/js-example/package.json b/examples/js-example/package.json index e8d50e8df..2e4e27c41 100644 --- a/examples/js-example/package.json +++ b/examples/js-example/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/js-example", - "version": "2.8.23", + "version": "2.8.24", "private": true, "description": "", "main": "index.js", diff --git a/examples/middleware/CHANGELOG.md b/examples/middleware/CHANGELOG.md index 3a904b118..b57280fbd 100644 --- a/examples/middleware/CHANGELOG.md +++ b/examples/middleware/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/example-middleware-demo +## 2.8.24 + +### Patch Changes + +- @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/middleware/package.json b/examples/middleware/package.json index f28f06605..64f643191 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-middleware-demo", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "dev": "next dev --port 8112", diff --git a/examples/partial-prerendering/CHANGELOG.md b/examples/partial-prerendering/CHANGELOG.md index b99625e83..c5069f72a 100644 --- a/examples/partial-prerendering/CHANGELOG.md +++ b/examples/partial-prerendering/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/example-partial-prerendering +## 2.8.24 + +### Patch Changes + +- @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/partial-prerendering/package.json b/examples/partial-prerendering/package.json index e39bd6f65..9dc573a8e 100644 --- a/examples/partial-prerendering/package.json +++ b/examples/partial-prerendering/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-partial-prerendering", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "dev": "next dev --port 8109", diff --git a/examples/react-example/CHANGELOG.md b/examples/react-example/CHANGELOG.md index aaffa2603..ca40b7acd 100644 --- a/examples/react-example/CHANGELOG.md +++ b/examples/react-example/CHANGELOG.md @@ -1,5 +1,11 @@ # react-example +## 2.8.24 + +### Patch Changes + +- @stackframe/react@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/react-example/package.json b/examples/react-example/package.json index 8f70b85ae..79076e914 100644 --- a/examples/react-example/package.json +++ b/examples/react-example/package.json @@ -1,7 +1,7 @@ { "name": "react-example", "private": true, - "version": "2.8.23", + "version": "2.8.24", "type": "module", "scripts": { "dev": "vite --force --port 8120", diff --git a/examples/supabase/CHANGELOG.md b/examples/supabase/CHANGELOG.md index 98578edbc..bd57c8fc2 100644 --- a/examples/supabase/CHANGELOG.md +++ b/examples/supabase/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/example-supabase +## 2.8.24 + +### Patch Changes + +- @stackframe/stack@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/examples/supabase/package.json b/examples/supabase/package.json index d5ef848b6..b9cdfe468 100644 --- a/examples/supabase/package.json +++ b/examples/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-supabase", - "version": "2.8.23", + "version": "2.8.24", "private": true, "scripts": { "dev": "next dev --turbo --port 8115", diff --git a/packages/init-stack/CHANGELOG.md b/packages/init-stack/CHANGELOG.md index a99982c7f..af6db974a 100644 --- a/packages/init-stack/CHANGELOG.md +++ b/packages/init-stack/CHANGELOG.md @@ -1,5 +1,12 @@ # @stackframe/init-stack +## 2.8.24 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/packages/init-stack/package.json b/packages/init-stack/package.json index d58d89eb0..4217ceca3 100644 --- a/packages/init-stack/package.json +++ b/packages/init-stack/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/init-stack", - "version": "2.8.23", + "version": "2.8.24", "description": "The setup wizard for Stack. https://stack-auth.com", "main": "dist/index.js", "type": "module", diff --git a/packages/js/package.json b/packages/js/package.json index d31683dd3..f5bb25b9b 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY", "name": "@stackframe/js", - "version": "2.8.23", + "version": "2.8.24", "sideEffects": false, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/react/package.json b/packages/react/package.json index 8857d5df5..05c110f0f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY", "name": "@stackframe/react", - "version": "2.8.23", + "version": "2.8.24", "sideEffects": false, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/stack-emails/CHANGELOG.md b/packages/stack-emails/CHANGELOG.md index 38664f669..3c329f2f0 100644 --- a/packages/stack-emails/CHANGELOG.md +++ b/packages/stack-emails/CHANGELOG.md @@ -1,5 +1,13 @@ # @stackframe/stack-emails +## 2.8.24 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack-ui@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/packages/stack-emails/package.json b/packages/stack-emails/package.json index e22da10b8..ac21aa347 100644 --- a/packages/stack-emails/package.json +++ b/packages/stack-emails/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-emails", - "version": "2.8.23", + "version": "2.8.24", "main": "./dist/index.js", "types": "./dist/index.d.ts", "private": true, diff --git a/packages/stack-sc/CHANGELOG.md b/packages/stack-sc/CHANGELOG.md index 9d732080c..695f4fecd 100644 --- a/packages/stack-sc/CHANGELOG.md +++ b/packages/stack-sc/CHANGELOG.md @@ -1,5 +1,7 @@ # @stackframe/stack-sc +## 2.8.24 + ## 2.8.23 ## 2.8.22 diff --git a/packages/stack-sc/package.json b/packages/stack-sc/package.json index 61055c524..c0e8e3f3b 100644 --- a/packages/stack-sc/package.json +++ b/packages/stack-sc/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-sc", - "version": "2.8.23", + "version": "2.8.24", "exports": { "./force-react-server": { "types": "./dist/index.react-server.d.ts", diff --git a/packages/stack-shared/CHANGELOG.md b/packages/stack-shared/CHANGELOG.md index 2e1c6d2ea..59491012d 100644 --- a/packages/stack-shared/CHANGELOG.md +++ b/packages/stack-shared/CHANGELOG.md @@ -1,5 +1,11 @@ # @stackframe/stack-shared +## 2.8.24 + +### Patch Changes + +- Various changes + ## 2.8.23 ### Patch Changes diff --git a/packages/stack-shared/package.json b/packages/stack-shared/package.json index 676313868..3187d8e83 100644 --- a/packages/stack-shared/package.json +++ b/packages/stack-shared/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-shared", - "version": "2.8.23", + "version": "2.8.24", "scripts": { "build": "rimraf dist && tsup-node", "typecheck": "tsc --noEmit", diff --git a/packages/stack-ui/CHANGELOG.md b/packages/stack-ui/CHANGELOG.md index 3175d81a6..e300cac1e 100644 --- a/packages/stack-ui/CHANGELOG.md +++ b/packages/stack-ui/CHANGELOG.md @@ -1,5 +1,13 @@ # @stackframe/stack-ui +## 2.8.24 + +### Patch Changes + +- Various changes +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/packages/stack-ui/package.json b/packages/stack-ui/package.json index 092ef0ed5..9cd97fc47 100644 --- a/packages/stack-ui/package.json +++ b/packages/stack-ui/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-ui", - "version": "2.8.23", + "version": "2.8.24", "main": "./dist/index.js", "types": "./dist/index.d.ts", "sideEffects": false, diff --git a/packages/stack/package.json b/packages/stack/package.json index 8313da458..7c9417a70 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY", "name": "@stackframe/stack", - "version": "2.8.23", + "version": "2.8.24", "sideEffects": false, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/template/CHANGELOG.md b/packages/template/CHANGELOG.md index 7c407a9b4..d98d5fe7d 100644 --- a/packages/template/CHANGELOG.md +++ b/packages/template/CHANGELOG.md @@ -1,5 +1,15 @@ # @stackframe/stack +## 2.8.24 + +### Patch Changes + +- Various changes +- Updated dependencies + - @stackframe/stack-shared@2.8.24 + - @stackframe/stack-ui@2.8.24 + - @stackframe/stack-sc@2.8.24 + ## 2.8.23 ### Patch Changes diff --git a/packages/template/package-template.json b/packages/template/package-template.json index 480b180c8..424f7cdb5 100644 --- a/packages/template/package-template.json +++ b/packages/template/package-template.json @@ -11,7 +11,7 @@ "//": "NEXT_LINE_PLATFORM template", "private": true, - "version": "2.8.23", + "version": "2.8.24", "sideEffects": false, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/template/package.json b/packages/template/package.json index be83fb3b4..a79fcbf19 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -2,7 +2,7 @@ "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY", "name": "@stackframe/template", "private": true, - "version": "2.8.23", + "version": "2.8.24", "sideEffects": false, "main": "./dist/index.js", "types": "./dist/index.d.ts", From a7acab4646a76dc01aa711256b17ef60a7822ce7 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Thu, 24 Jul 2025 02:38:37 +0200 Subject: [PATCH 2/2] Auto migration (#526) ---- > [!IMPORTANT] > Introduces an automated database migration system, replacing manual Prisma commands with new scripts and updating workflows, configurations, and tests accordingly. > > - **Auto-Migration System**: > - Introduces `db-migrations.ts` script for handling database migrations automatically. > - Adds utility functions in `utils.tsx` for managing migration files. > - Implements `applyMigrations` and `runMigrationNeeded` in `index.tsx` for executing migrations. > - **Workflow and Scripts**: > - Updates GitHub workflows (`check-prisma-migrations.yaml`, `e2e-api-tests.yaml`) to use new migration commands. > - Replaces `prisma migrate` commands with `db:init`, `db:migrate`, etc., in `package.json` and `README.md`. > - **Testing**: > - Adds `auto-migration.tests.ts` for testing migration logic and concurrency handling. > - **Configuration**: > - Updates `.env.development` and `vitest.config.ts` for new environment variables and paths. > - Modifies `turbo.json` and `package.json` to include new migration tasks and scripts. > > This description was created by [Ellipsis](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral) for 2c241838793f54da34f25b0078f0803ae6770554. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed. --------- Co-authored-by: Konsti Wohlwend Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .../workflows/check-prisma-migrations.yaml | 2 +- .github/workflows/e2e-api-tests.yaml | 3 +- .../e2e-source-of-truth-api-tests.yaml | 10 +- README.md | 4 +- apps/backend/package.json | 15 +- .../migration.sql | 3 +- .../migration.sql | 3 + .../migration.sql | 3 +- .../migration.sql | 22 +- apps/backend/prisma/seed.ts | 6 +- apps/backend/scripts/db-migrations.ts | 107 +++++ .../scripts/generate-migration-imports.ts | 13 + .../app/api/latest/(api-keys)/handlers.tsx | 24 +- .../api/latest/auth/cli/complete/route.tsx | 2 +- .../app/api/latest/auth/cli/poll/route.tsx | 2 +- .../src/app/api/latest/auth/cli/route.tsx | 2 +- .../mfa/sign-in/verification-code-handler.tsx | 2 +- .../oauth/callback/[provider_id]/route.tsx | 2 +- .../otp/sign-in/verification-code-handler.tsx | 2 +- .../register/verification-code-handler.tsx | 2 +- .../sign-in/verification-code-handler.tsx | 2 +- .../auth/password/send-reset-code/route.tsx | 2 +- .../api/latest/auth/password/set/route.tsx | 2 +- .../latest/auth/password/sign-in/route.tsx | 2 +- .../api/latest/auth/password/update/route.tsx | 2 +- .../src/app/api/latest/auth/sessions/crud.tsx | 4 +- .../auth/sessions/current/refresh/route.tsx | 2 +- .../latest/auth/sessions/current/route.tsx | 2 +- .../[provider_id]/access-token/crud.tsx | 2 +- .../send-verification-code/route.tsx | 4 +- .../app/api/latest/contact-channels/crud.tsx | 20 +- .../verify/verification-code-handler.tsx | 6 +- .../emails/notification-preference/crud.tsx | 4 +- .../api/latest/emails/send-email/route.tsx | 4 +- .../latest/emails/unsubscribe-link/route.tsx | 5 +- .../credential-scanning/revoke/route.tsx | 9 +- .../confirm/verification-code-handler.tsx | 6 +- .../confirm/verification-code-handler.tsx | 6 +- .../internal/ai-chat/[threadId]/route.tsx | 4 +- .../app/api/latest/internal/emails/crud.tsx | 4 +- .../app/api/latest/internal/metrics/route.tsx | 8 +- .../latest/internal/projects/current/crud.tsx | 6 +- .../app/api/latest/oauth-providers/crud.tsx | 14 +- .../project-permission-definitions/crud.tsx | 6 +- .../api/latest/project-permissions/crud.tsx | 10 +- .../accept/verification-code-handler.tsx | 4 +- .../app/api/latest/team-invitations/crud.tsx | 6 +- .../team-invitations/send-code/route.tsx | 3 +- .../api/latest/team-member-profiles/crud.tsx | 9 +- .../app/api/latest/team-memberships/crud.tsx | 6 +- .../team-permission-definitions/crud.tsx | 6 +- .../app/api/latest/team-permissions/crud.tsx | 9 +- .../backend/src/app/api/latest/teams/crud.tsx | 19 +- .../backend/src/app/api/latest/users/crud.tsx | 16 +- .../auto-migrations/auto-migration.tests.ts | 394 ++++++++++++++++++ apps/backend/src/auto-migrations/index.tsx | 194 +++++++++ apps/backend/src/auto-migrations/utils.tsx | 31 ++ apps/backend/src/lib/emails.tsx | 4 +- .../src/lib/notification-categories.ts | 5 +- apps/backend/src/lib/projects.tsx | 2 +- apps/backend/src/oauth/model.tsx | 3 +- apps/backend/src/prisma-client.tsx | 45 +- apps/backend/vitest.config.ts | 19 +- apps/dashboard/.env | 2 +- apps/e2e/.env | 1 + apps/e2e/.env.development | 1 + docker/server/.env | 2 +- docs/templates/others/self-host.mdx | 8 +- package.json | 10 +- pnpm-lock.yaml | 114 +++-- turbo.json | 18 +- 71 files changed, 1097 insertions(+), 199 deletions(-) create mode 100644 apps/backend/scripts/db-migrations.ts create mode 100644 apps/backend/scripts/generate-migration-imports.ts create mode 100644 apps/backend/src/auto-migrations/auto-migration.tests.ts create mode 100644 apps/backend/src/auto-migrations/index.tsx create mode 100644 apps/backend/src/auto-migrations/utils.tsx diff --git a/.github/workflows/check-prisma-migrations.yaml b/.github/workflows/check-prisma-migrations.yaml index 36f65db2e..9da74922f 100644 --- a/.github/workflows/check-prisma-migrations.yaml +++ b/.github/workflows/check-prisma-migrations.yaml @@ -39,4 +39,4 @@ jobs: run: docker run -d --name postgres-prisma-diff-shadow -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=PLACEHOLDER-PASSWORD--dfaBC1hm1v -e POSTGRES_DB=postgres -p 5432:5432 postgres:latest - name: Check for differences in Prisma schema and migrations - run: pnpm run prisma migrate diff --from-migrations ./prisma/migrations --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url postgres://postgres:PLACEHOLDER-PASSWORD--dfaBC1hm1v@localhost:5432/postgres --exit-code + run: cd apps/backend && pnpm run prisma migrate diff --from-migrations ./prisma/migrations --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url postgres://postgres:PLACEHOLDER-PASSWORD--dfaBC1hm1v@localhost:5432/postgres --exit-code diff --git a/.github/workflows/e2e-api-tests.yaml b/.github/workflows/e2e-api-tests.yaml index 5f986bbee..b1efdbfe4 100644 --- a/.github/workflows/e2e-api-tests.yaml +++ b/.github/workflows/e2e-api-tests.yaml @@ -17,6 +17,7 @@ jobs: env: NODE_ENV: test STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes + STACK_DIRECT_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/stackframe" strategy: matrix: @@ -96,7 +97,7 @@ jobs: run: npx wait-on tcp:localhost:8113 - name: Initialize database - run: pnpm run prisma -- migrate reset --force + run: pnpm run db:init - name: Start stack-backend in background uses: JarvusInnovations/background-action@v1.0.7 diff --git a/.github/workflows/e2e-source-of-truth-api-tests.yaml b/.github/workflows/e2e-source-of-truth-api-tests.yaml index bb9ee6462..4c970bc3c 100644 --- a/.github/workflows/e2e-source-of-truth-api-tests.yaml +++ b/.github/workflows/e2e-source-of-truth-api-tests.yaml @@ -19,6 +19,7 @@ jobs: STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes STACK_OVERRIDE_SOURCE_OF_TRUTH: '{"type": "postgres", "connectionString": "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/source-of-truth-db?schema=sot-schema"}' STACK_TEST_SOURCE_OF_TRUTH: true + STACK_DIRECT_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/stackframe" strategy: matrix: @@ -97,11 +98,14 @@ jobs: - name: Wait on Svix run: npx wait-on tcp:localhost:8113 - - name: Initialize source of truth database - run: "STACK_DIRECT_DATABASE_CONNECTION_STRING='postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/source-of-truth-db?schema=sot-schema' pnpm run prisma -- migrate reset --force --skip-seed" + - name: Create source-of-truth database and schema + run: | + psql postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/postgres -c "CREATE DATABASE \"source-of-truth-db\";" + psql postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/source-of-truth-db -c "CREATE SCHEMA \"sot-schema\";" - name: Initialize database - run: pnpm run prisma -- migrate reset --force + run: pnpm run db:init + - name: Start stack-backend in background uses: JarvusInnovations/background-action@v1.0.7 with: diff --git a/README.md b/README.md index 8b5af0a58..b86c0b4e7 100644 --- a/README.md +++ b/README.md @@ -163,10 +163,10 @@ pnpm run prisma studio ### Database migrations -If you make changes to the Prisma schema, you need to run the following command to create a migration: +If you make changes to the Prisma schema, you need to run the following command to create a migration file: ```sh -pnpm run prisma migrate dev +pnpm run db:migration-gen ``` ### Chat with the codebase diff --git a/apps/backend/package.json b/apps/backend/package.json index fb532e5b3..4e9450bb4 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -17,12 +17,19 @@ "codegen-prisma:watch": "pnpm run prisma generate --watch", "codegen-route-info": "pnpm run with-env tsx scripts/generate-route-info.ts", "codegen-route-info:watch": "pnpm run with-env tsx watch --clear-screen=false scripts/generate-route-info.ts", - "codegen": "pnpm run codegen-prisma && pnpm run codegen-route-info", + "codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine && pnpm run generate-openapi; else pnpm run codegen-prisma && pnpm run generate-openapi; fi' && pnpm run codegen-route-info", "codegen:watch": "concurrently -n \"prisma,docs,route-info\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run watch-docs\" \"pnpm run codegen-route-info:watch\"", "psql-inner": "psql $STACK_DATABASE_CONNECTION_STRING", "psql": "pnpm run with-env pnpm run psql-inner", - "prisma": "pnpm run with-env prisma", "prisma-studio": "pnpm run with-env prisma studio --port 8106 --browser none", + "prisma": "pnpm run with-env prisma", + "db:migration-gen": "pnpm run with-env tsx scripts/db-migrations.ts generate-migration-file", + "db:reset": "pnpm run with-env tsx scripts/db-migrations.ts reset", + "db:seed": "pnpm run with-env tsx scripts/db-migrations.ts seed", + "db:init": "pnpm run with-env tsx scripts/db-migrations.ts init", + "db:migrate": "pnpm run with-env tsx scripts/db-migrations.ts migrate", + "generate-migration-imports": "pnpm run with-env tsx scripts/generate-migration-imports.ts", + "generate-migration-imports:watch": "chokidar 'prisma/migrations/**/*.sql' -c 'pnpm run generate-migration-imports'", "lint": "next lint", "watch-docs": "pnpm run with-env bash -c 'tsx watch --clear-screen=false scripts/generate-openapi-fumadocs.ts && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs'", "generate-openapi": "pnpm run with-env tsx scripts/generate-openapi.ts", @@ -62,6 +69,8 @@ "@vercel/otel": "^1.10.4", "ai": "^4.3.17", "bcrypt": "^5.1.1", + "chokidar-cli": "^3.0.0", + "dotenv": "^16.4.5", "dotenv-cli": "^7.3.0", "freestyle-sandboxes": "^0.0.92", "jose": "^5.2.2", @@ -70,6 +79,7 @@ "nodemailer": "^6.9.10", "oidc-provider": "^8.5.1", "openid-client": "5.6.4", + "postgres": "^3.4.5", "pg": "^8.16.3", "posthog-node": "^4.1.0", "react": "19.0.0", @@ -77,6 +87,7 @@ "semver": "^7.6.3", "sharp": "^0.32.6", "svix": "^1.25.0", + "vite": "^6.1.0", "yaml": "^2.4.5", "yup": "^1.4.0", "zod": "^3.23.8" diff --git a/apps/backend/prisma/migrations/20240910211533_remove_shared_facebook/migration.sql b/apps/backend/prisma/migrations/20240910211533_remove_shared_facebook/migration.sql index 04d2902ed..cc28e7aaa 100644 --- a/apps/backend/prisma/migrations/20240910211533_remove_shared_facebook/migration.sql +++ b/apps/backend/prisma/migrations/20240910211533_remove_shared_facebook/migration.sql @@ -37,10 +37,9 @@ DELETE FROM "ProxiedOAuthProviderConfig" WHERE "type" = 'FACEBOOK'; -- AlterEnum -BEGIN; +-- SPLIT_STATEMENT_SENTINEL CREATE TYPE "ProxiedOAuthProviderType_new" AS ENUM ('GITHUB', 'GOOGLE', 'MICROSOFT', 'SPOTIFY'); ALTER TABLE "ProxiedOAuthProviderConfig" ALTER COLUMN "type" TYPE "ProxiedOAuthProviderType_new" USING ("type"::text::"ProxiedOAuthProviderType_new"); ALTER TYPE "ProxiedOAuthProviderType" RENAME TO "ProxiedOAuthProviderType_old"; ALTER TYPE "ProxiedOAuthProviderType_new" RENAME TO "ProxiedOAuthProviderType"; DROP TYPE "ProxiedOAuthProviderType_old"; -COMMIT; \ No newline at end of file diff --git a/apps/backend/prisma/migrations/20250304200822_add_project_user_count/migration.sql b/apps/backend/prisma/migrations/20250304200822_add_project_user_count/migration.sql index 103c199db..1d6e3f616 100644 --- a/apps/backend/prisma/migrations/20250304200822_add_project_user_count/migration.sql +++ b/apps/backend/prisma/migrations/20250304200822_add_project_user_count/migration.sql @@ -8,6 +8,8 @@ UPDATE "Project" SET "userCount" = ( ); -- Create function to update userCount +-- SPLIT_STATEMENT_SENTINEL +-- SINGLE_STATEMENT_SENTINEL CREATE OR REPLACE FUNCTION update_project_user_count() RETURNS TRIGGER AS $$ BEGIN @@ -30,6 +32,7 @@ BEGIN RETURN NULL; END; $$ LANGUAGE plpgsql; +-- SPLIT_STATEMENT_SENTINEL -- Create triggers DROP TRIGGER IF EXISTS project_user_insert_trigger ON "ProjectUser"; diff --git a/apps/backend/prisma/migrations/20250325235813_project_user_permissions/migration.sql b/apps/backend/prisma/migrations/20250325235813_project_user_permissions/migration.sql index 0ea8f373c..9db2cdf89 100644 --- a/apps/backend/prisma/migrations/20250325235813_project_user_permissions/migration.sql +++ b/apps/backend/prisma/migrations/20250325235813_project_user_permissions/migration.sql @@ -5,13 +5,12 @@ */ -- AlterEnum -BEGIN; CREATE TYPE "PermissionScope_new" AS ENUM ('PROJECT', 'TEAM'); ALTER TABLE "Permission" ALTER COLUMN "scope" TYPE "PermissionScope_new" USING ("scope"::text::"PermissionScope_new"); ALTER TYPE "PermissionScope" RENAME TO "PermissionScope_old"; ALTER TYPE "PermissionScope_new" RENAME TO "PermissionScope"; DROP TYPE "PermissionScope_old"; -COMMIT; +-- SPLIT_STATEMENT_SENTINEL -- AlterTable ALTER TABLE "Permission" ADD COLUMN "isDefaultProjectPermission" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/backend/prisma/migrations/20250425171311_remove_old_config/migration.sql b/apps/backend/prisma/migrations/20250425171311_remove_old_config/migration.sql index 0ba3471ff..d576ed778 100644 --- a/apps/backend/prisma/migrations/20250425171311_remove_old_config/migration.sql +++ b/apps/backend/prisma/migrations/20250425171311_remove_old_config/migration.sql @@ -167,17 +167,14 @@ ALTER TABLE "AuthMethod" DROP COLUMN "authMethodConfigId", DROP COLUMN "projectConfigId"; -- AlterTable -BEGIN; ALTER TABLE "ConnectedAccount" ADD COLUMN "configOAuthProviderId" TEXT; UPDATE "ConnectedAccount" SET "configOAuthProviderId" = "oauthProviderConfigId"; ALTER TABLE "ConnectedAccount" ALTER COLUMN "configOAuthProviderId" SET NOT NULL; ALTER TABLE "ConnectedAccount" DROP COLUMN "oauthProviderConfigId"; ALTER TABLE "ConnectedAccount" DROP COLUMN "connectedAccountConfigId"; ALTER TABLE "ConnectedAccount" DROP COLUMN "projectConfigId"; -COMMIT; -- AlterTable -BEGIN; ALTER TABLE "EmailTemplate" DROP CONSTRAINT "EmailTemplate_pkey"; ALTER TABLE "EmailTemplate" ADD COLUMN "projectId" TEXT; @@ -189,12 +186,15 @@ JOIN "Project" P ON P."configId" = PC."id" WHERE ET."projectConfigId" = PC."id"; -- Check if we have any null projectId values +-- SPLIT_STATEMENT_SENTINEL +-- SINGLE_STATEMENT_SENTINEL DO $$ BEGIN IF EXISTS (SELECT 1 FROM "EmailTemplate" WHERE "projectId" IS NULL) THEN RAISE EXCEPTION 'Some EmailTemplate records have null projectId values after migration'; END IF; END $$; +-- SPLIT_STATEMENT_SENTINEL -- Now make the column NOT NULL ALTER TABLE "EmailTemplate" ALTER COLUMN "projectId" SET NOT NULL; @@ -204,38 +204,30 @@ ALTER TABLE "EmailTemplate" ADD CONSTRAINT "EmailTemplate_pkey" PRIMARY KEY ("pr -- Drop the old column ALTER TABLE "EmailTemplate" DROP COLUMN "projectConfigId"; -COMMIT; -- AlterTable -BEGIN; ALTER TABLE "OAuthAccessToken" ADD COLUMN "configOAuthProviderId" TEXT; UPDATE "OAuthAccessToken" SET "configOAuthProviderId" = "oAuthProviderConfigId"; ALTER TABLE "OAuthAccessToken" ALTER COLUMN "configOAuthProviderId" SET NOT NULL; ALTER TABLE "OAuthAccessToken" DROP COLUMN "oAuthProviderConfigId"; -COMMIT; -- AlterTable -BEGIN; ALTER TABLE "OAuthAuthMethod" ADD COLUMN "configOAuthProviderId" TEXT; UPDATE "OAuthAuthMethod" SET "configOAuthProviderId" = "oauthProviderConfigId"; ALTER TABLE "OAuthAuthMethod" ALTER COLUMN "configOAuthProviderId" SET NOT NULL; ALTER TABLE "OAuthAuthMethod" DROP COLUMN "oauthProviderConfigId"; ALTER TABLE "OAuthAuthMethod" DROP COLUMN "projectConfigId"; -COMMIT; -- AlterTable -BEGIN; ALTER TABLE "OAuthToken" ADD COLUMN "configOAuthProviderId" TEXT; UPDATE "OAuthToken" SET "configOAuthProviderId" = "oAuthProviderConfigId"; ALTER TABLE "OAuthToken" ALTER COLUMN "configOAuthProviderId" SET NOT NULL; ALTER TABLE "OAuthToken" DROP COLUMN "oAuthProviderConfigId"; -COMMIT; -- AlterTable ALTER TABLE "Project" DROP COLUMN "configId"; -- AlterTable -BEGIN; ALTER TABLE "ProjectUserDirectPermission" ADD COLUMN "permissionId" TEXT; -- Update permissionId with values from Permission table @@ -249,10 +241,8 @@ ALTER TABLE "ProjectUserDirectPermission" ALTER COLUMN "permissionId" SET NOT NU -- Drop the old column ALTER TABLE "ProjectUserDirectPermission" DROP COLUMN "permissionDbId"; -COMMIT; -- AlterTable -BEGIN; ALTER TABLE "ProjectUserOAuthAccount" ADD COLUMN "configOAuthProviderId" TEXT; UPDATE "ProjectUserOAuthAccount" SET "configOAuthProviderId" = "oauthProviderConfigId"; ALTER TABLE "ProjectUserOAuthAccount" ALTER COLUMN "configOAuthProviderId" SET NOT NULL; @@ -260,13 +250,13 @@ ALTER TABLE "ProjectUserOAuthAccount" DROP CONSTRAINT "ProjectUserOAuthAccount_p ALTER TABLE "ProjectUserOAuthAccount" DROP COLUMN "oauthProviderConfigId"; ALTER TABLE "ProjectUserOAuthAccount" DROP COLUMN "projectConfigId"; ALTER TABLE "ProjectUserOAuthAccount" ADD CONSTRAINT "ProjectUserOAuthAccount_pkey" PRIMARY KEY ("tenancyId", "configOAuthProviderId", "providerAccountId"); -COMMIT; -- AlterTable -BEGIN; ALTER TABLE "TeamMemberDirectPermission" ADD COLUMN "permissionId" TEXT; -- Check for rows where both or neither field is populated +-- SPLIT_STATEMENT_SENTINEL +-- SINGLE_STATEMENT_SENTINEL DO $$ BEGIN IF EXISTS ( @@ -277,6 +267,7 @@ BEGIN RAISE EXCEPTION 'Invalid state: Each TeamMemberDirectPermission must have exactly one of permissionDbId or systemPermission set'; END IF; END $$; +-- SPLIT_STATEMENT_SENTINEL -- Update permissionId using systemPermission when available UPDATE "TeamMemberDirectPermission" @@ -295,7 +286,6 @@ ALTER TABLE "TeamMemberDirectPermission" ALTER COLUMN "permissionId" SET NOT NUL -- Then drop the old columns ALTER TABLE "TeamMemberDirectPermission" DROP COLUMN "permissionDbId"; ALTER TABLE "TeamMemberDirectPermission" DROP COLUMN "systemPermission"; -COMMIT; -- DropTable DROP TABLE "AuthMethodConfig"; diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index 4b9fa6031..8a8135d8e 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -58,7 +58,7 @@ async function seed() { } const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID); - const internalPrisma = getPrismaClientForTenancy(internalTenancy); + const internalPrisma = await getPrismaClientForTenancy(internalTenancy); internalProject = await createOrUpdateProject({ projectId: 'internal', @@ -142,7 +142,7 @@ async function seed() { } if (adminGithubId) { - const githubAccount = await getPrismaClientForTenancy(internalTenancy).projectUserOAuthAccount.findFirst({ + const githubAccount = await internalPrisma.projectUserOAuthAccount.findFirst({ where: { tenancyId: internalTenancy.id, configOAuthProviderId: 'github', @@ -153,7 +153,7 @@ async function seed() { if (githubAccount) { console.log(`GitHub account already exists, skipping creation`); } else { - await getPrismaClientForTenancy(internalTenancy).projectUserOAuthAccount.create({ + await internalPrisma.projectUserOAuthAccount.create({ data: { tenancyId: internalTenancy.id, projectUserId: newUser.projectUserId, diff --git a/apps/backend/scripts/db-migrations.ts b/apps/backend/scripts/db-migrations.ts new file mode 100644 index 000000000..a2d4e4ce4 --- /dev/null +++ b/apps/backend/scripts/db-migrations.ts @@ -0,0 +1,107 @@ +import { applyMigrations } from "@/auto-migrations"; +import { MIGRATION_FILES_DIR, getMigrationFiles } from "@/auto-migrations/utils"; +import { globalPrismaClient, globalPrismaSchema } from "@/prisma-client"; +import { execSync } from "child_process"; +import * as readline from 'readline'; + +const dropSchema = async () => { + await globalPrismaClient.$executeRaw`DROP SCHEMA ${globalPrismaSchema} CASCADE`; + await globalPrismaClient.$executeRaw`CREATE SCHEMA ${globalPrismaSchema}`; + await globalPrismaClient.$executeRaw`GRANT ALL ON SCHEMA ${globalPrismaSchema} TO postgres`; + await globalPrismaClient.$executeRaw`GRANT ALL ON SCHEMA ${globalPrismaSchema} TO public`; +}; + +const seed = async () => { + execSync('pnpm run db-seed-script', { stdio: 'inherit' }); +}; + +const promptDropDb = async () => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const answer = await new Promise(resolve => { + rl.question('Are you sure you want to drop everything in the database? This action cannot be undone. (y/N): ', resolve); + }); + rl.close(); + + if (answer.toLowerCase() !== 'y') { + console.log('Operation cancelled'); + process.exit(0); + } +}; + +const migrate = async () => { + await applyMigrations({ + prismaClient: globalPrismaClient, + migrationFiles: getMigrationFiles(MIGRATION_FILES_DIR), + logging: true, + schema: globalPrismaSchema, + }); +}; + +const showHelp = () => { + console.log(`Database Migration Script + +Usage: pnpm db-migrations + +Commands: + reset Drop all data and recreate the database, then apply migrations and seed + generate-migration-file Generate a new migration file using Prisma, then reset and migrate + seed [Advanced] Run database seeding only + init Apply migrations and seed the database + migrate Apply migrations + help Show this help message +`); +}; + +const main = async () => { + const args = process.argv.slice(2); + const command = args[0]; + + switch (command) { + case 'reset': { + await promptDropDb(); + await dropSchema(); + await migrate(); + await seed(); + break; + } + case 'generate-migration-file': { + execSync('pnpm prisma migrate dev --skip-seed', { stdio: 'inherit' }); + await dropSchema(); + await migrate(); + await seed(); + break; + } + case 'seed': { + await seed(); + break; + } + case 'init': { + await migrate(); + await seed(); + break; + } + case 'migrate': { + await migrate(); + break; + } + case 'help': { + showHelp(); + break; + } + default: { + console.error('Unknown command.'); + showHelp(); + process.exit(1); + } + } +}; + +// eslint-disable-next-line no-restricted-syntax +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/apps/backend/scripts/generate-migration-imports.ts b/apps/backend/scripts/generate-migration-imports.ts new file mode 100644 index 000000000..e4dff82bc --- /dev/null +++ b/apps/backend/scripts/generate-migration-imports.ts @@ -0,0 +1,13 @@ +import { writeFileSyncIfChanged } from '@stackframe/stack-shared/dist/utils/fs'; +import fs from 'fs'; +import path from 'path'; +import { MIGRATION_FILES_DIR, getMigrationFiles } from '../src/auto-migrations/utils'; + +const migrationFiles = getMigrationFiles(MIGRATION_FILES_DIR); + +fs.mkdirSync(path.join(process.cwd(), 'src', 'generated'), { recursive: true }); + +writeFileSyncIfChanged( + path.join(process.cwd(), 'src', 'generated', 'migration-files.tsx'), + `export const MIGRATION_FILES = ${JSON.stringify(migrationFiles, null, 2)};\n` +); diff --git a/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx b/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx index b73f36125..01260083d 100644 --- a/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx +++ b/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx @@ -41,6 +41,8 @@ async function ensureUserCanManageApiKeys( throw new StatusError(StatusError.BadRequest, "Cannot provide both userId and teamId"); } + const prisma = await getPrismaClientForTenancy(auth.tenancy); + if (auth.type === "client") { if (!auth.user) { throw new KnownErrors.UserAuthenticationRequired(); @@ -57,7 +59,7 @@ async function ensureUserCanManageApiKeys( // Check team API key permissions if (options.teamId !== undefined) { const userId = auth.user.id; - const hasManageApiKeysPermission = await getPrismaClientForTenancy(auth.tenancy).$transaction(async (tx) => { + const hasManageApiKeysPermission = await prisma.$transaction(async (tx) => { const permissions = await listPermissions(tx, { scope: 'team', tenancy: auth.tenancy, @@ -198,7 +200,9 @@ function createApiKeyHandlers(type: Type) { type, }); - const apiKey = await getPrismaClientForTenancy(auth.tenancy).projectApiKey.create({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const apiKey = await prisma.projectApiKey.create({ data: { id: apiKeyId, description: body.description, @@ -244,8 +248,9 @@ function createApiKeyHandlers(type: Type) { }), handler: async ({ auth, body }) => { await throwIfFeatureDisabled(auth.tenancy.config, type); + const prisma = await getPrismaClientForTenancy(auth.tenancy); - const apiKey = await getPrismaClientForTenancy(auth.tenancy).projectApiKey.findUnique({ + const apiKey = await prisma.projectApiKey.findUnique({ where: { tenancyId: auth.tenancy.id, secretApiKey: body.api_key, @@ -299,7 +304,8 @@ function createApiKeyHandlers(type: Type) { teamId, }); - const apiKeys = await getPrismaClientForTenancy(auth.tenancy).projectApiKey.findMany({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const apiKeys = await prisma.projectApiKey.findMany({ where: { tenancyId: auth.tenancy.id, projectUserId: userId, @@ -319,7 +325,9 @@ function createApiKeyHandlers(type: Type) { onRead: async ({ auth, query, params }) => { await throwIfFeatureDisabled(auth.tenancy.config, type); - const apiKey = await getPrismaClientForTenancy(auth.tenancy).projectApiKey.findUnique({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const apiKey = await prisma.projectApiKey.findUnique({ where: { tenancyId_id: { tenancyId: auth.tenancy.id, @@ -342,7 +350,9 @@ function createApiKeyHandlers(type: Type) { onUpdate: async ({ auth, data, params, query }) => { await throwIfFeatureDisabled(auth.tenancy.config, type); - const existingApiKey = await getPrismaClientForTenancy(auth.tenancy).projectApiKey.findUnique({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const existingApiKey = await prisma.projectApiKey.findUnique({ where: { tenancyId_id: { tenancyId: auth.tenancy.id, @@ -361,7 +371,7 @@ function createApiKeyHandlers(type: Type) { }); // Update the API key - const updatedApiKey = await getPrismaClientForTenancy(auth.tenancy).projectApiKey.update({ + const updatedApiKey = await prisma.projectApiKey.update({ where: { tenancyId_id: { tenancyId: auth.tenancy.id, diff --git a/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx b/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx index 57528ed29..488d334e3 100644 --- a/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx +++ b/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx @@ -24,7 +24,7 @@ export const POST = createSmartRouteHandler({ bodyType: yupString().oneOf(["success"]).defined(), }), async handler({ auth: { tenancy }, body: { login_code, refresh_token } }) { - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); // Find the CLI auth attempt const cliAuth = await prisma.cliAuthAttempt.findUnique({ diff --git a/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx b/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx index 2bcba6b4e..90c5b41af 100644 --- a/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx +++ b/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx @@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({ }).defined(), }), async handler({ auth: { tenancy }, body: { polling_code } }) { - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); // Find the CLI auth attempt const cliAuth = await prisma.cliAuthAttempt.findFirst({ diff --git a/apps/backend/src/app/api/latest/auth/cli/route.tsx b/apps/backend/src/app/api/latest/auth/cli/route.tsx index 8c1ca1815..62028f115 100644 --- a/apps/backend/src/app/api/latest/auth/cli/route.tsx +++ b/apps/backend/src/app/api/latest/auth/cli/route.tsx @@ -33,7 +33,7 @@ export const POST = createSmartRouteHandler({ const expiresAt = new Date(Date.now() + expires_in_millis); // Create a new CLI auth attempt - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const cliAuth = await prisma.cliAuthAttempt.create({ data: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx index cfad408af..f201f6904 100644 --- a/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx @@ -37,7 +37,7 @@ export const mfaVerificationCodeHandler = createVerificationCodeHandler({ body: signInResponseSchema.defined(), }), async validate(tenancy, method, data, body) { - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const user = await prisma.projectUser.findUniqueOrThrow({ where: { tenancyId_projectUserId: { diff --git a/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx b/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx index fe655cf5e..3f8fd3ff4 100644 --- a/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx +++ b/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx @@ -112,7 +112,7 @@ const handler = createSmartRouteHandler({ if (!tenancy) { throw new StackAssertionError("Tenancy in outerInfo not found; has it been deleted?", { tenancyId }); } - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); try { if (outerInfoDB.expiresAt < new Date()) { diff --git a/apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx index 3f5fb7530..8e42f59c3 100644 --- a/apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx @@ -12,7 +12,7 @@ import { usersCrudHandlers } from "../../../users/crud"; import { createMfaRequiredError } from "../../mfa/sign-in/verification-code-handler"; export async function ensureUserForEmailAllowsOtp(tenancy: Tenancy, email: string): Promise { - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const contactChannel = await getAuthContactChannel( prisma, { diff --git a/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx index c38f2eac7..cb93cb25b 100644 --- a/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx @@ -95,7 +95,7 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({ } const registrationInfo = verification.registrationInfo; - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); await retryTransaction(prisma, async (tx) => { const authMethods = await tx.passkeyAuthMethod.findMany({ diff --git a/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx index e1faa25c3..5574e6a2d 100644 --- a/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx @@ -45,7 +45,7 @@ export const passkeySignInVerificationCodeHandler = createVerificationCodeHandle const credentialId = authentication_response.id; - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); // Get passkey from DB with userHandle const passkey = await prisma.passkeyAuthMethod.findFirst({ where: { diff --git a/apps/backend/src/app/api/latest/auth/password/send-reset-code/route.tsx b/apps/backend/src/app/api/latest/auth/password/send-reset-code/route.tsx index 3063e1631..86ec5cccf 100644 --- a/apps/backend/src/app/api/latest/auth/password/send-reset-code/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/send-reset-code/route.tsx @@ -35,7 +35,7 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.PasswordAuthenticationNotEnabled(); } - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); // TODO filter in the query const contactChannel = await getAuthContactChannel( diff --git a/apps/backend/src/app/api/latest/auth/password/set/route.tsx b/apps/backend/src/app/api/latest/auth/password/set/route.tsx index d3d6dc2ac..b44bed2aa 100644 --- a/apps/backend/src/app/api/latest/auth/password/set/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/set/route.tsx @@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({ throw passwordError; } - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); await retryTransaction(prisma, async (tx) => { const authMethods = await tx.passwordAuthMethod.findMany({ where: { diff --git a/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx b/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx index 567d6ae2c..a7f3e16fb 100644 --- a/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx @@ -38,7 +38,7 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.PasswordAuthenticationNotEnabled(); } - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const contactChannel = await getAuthContactChannel( prisma, { diff --git a/apps/backend/src/app/api/latest/auth/password/update/route.tsx b/apps/backend/src/app/api/latest/auth/password/update/route.tsx index 1ad6610a0..54ba28718 100644 --- a/apps/backend/src/app/api/latest/auth/password/update/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/update/route.tsx @@ -40,7 +40,7 @@ export const POST = createSmartRouteHandler({ throw passwordError; } - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); await retryTransaction(prisma, async (tx) => { const authMethods = await tx.passwordAuthMethod.findMany({ where: { diff --git a/apps/backend/src/app/api/latest/auth/sessions/crud.tsx b/apps/backend/src/app/api/latest/auth/sessions/crud.tsx index 1ff304422..94ca99228 100644 --- a/apps/backend/src/app/api/latest/auth/sessions/crud.tsx +++ b/apps/backend/src/app/api/latest/auth/sessions/crud.tsx @@ -17,7 +17,7 @@ export const sessionsCrudHandlers = createLazyProxy(() => createCrudHandlers(ses user_id: userIdOrMeSchema.defined(), }).defined(), onList: async ({ auth, query }) => { - const prisma = getPrismaClientForTenancy(auth.tenancy); + const prisma = await getPrismaClientForTenancy(auth.tenancy); const schema = getPrismaSchemaForTenancy(auth.tenancy); const listImpersonations = auth.type === 'admin'; @@ -86,7 +86,7 @@ export const sessionsCrudHandlers = createLazyProxy(() => createCrudHandlers(ses return result; }, onDelete: async ({ auth, params }: { auth: SmartRequestAuth, params: { id: string }, query: { user_id?: string } }) => { - const prisma = getPrismaClientForTenancy(auth.tenancy); + const prisma = await getPrismaClientForTenancy(auth.tenancy); const session = await globalPrismaClient.projectUserRefreshToken.findFirst({ where: { tenancyId: auth.tenancy.id, diff --git a/apps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsx b/apps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsx index 1f0c0d91e..d23962d22 100644 --- a/apps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsx +++ b/apps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsx @@ -29,7 +29,7 @@ export const POST = createSmartRouteHandler({ async handler({ auth: { tenancy }, headers: { "x-stack-refresh-token": refreshTokenHeaders } }, fullReq) { const refreshToken = refreshTokenHeaders[0]; - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const sessionObj = await globalPrismaClient.projectUserRefreshToken.findFirst({ where: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/auth/sessions/current/route.tsx b/apps/backend/src/app/api/latest/auth/sessions/current/route.tsx index cc5e22977..70834ecc9 100644 --- a/apps/backend/src/app/api/latest/auth/sessions/current/route.tsx +++ b/apps/backend/src/app/api/latest/auth/sessions/current/route.tsx @@ -31,7 +31,7 @@ export const DELETE = createSmartRouteHandler({ } try { - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const result = await globalPrismaClient.projectUserRefreshToken.deleteMany({ where: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsx b/apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsx index e40a1bb9a..905f30047 100644 --- a/apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsx +++ b/apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsx @@ -39,7 +39,7 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() => cre const providerInstance = await getProvider(provider); // ====================== retrieve access token if it exists ====================== - const prisma = getPrismaClientForTenancy(auth.tenancy); + const prisma = await getPrismaClientForTenancy(auth.tenancy); const accessTokens = await prisma.oAuthAccessToken.findMany({ where: { tenancyId: auth.tenancy.id, diff --git a/apps/backend/src/app/api/latest/contact-channels/[user_id]/[contact_channel_id]/send-verification-code/route.tsx b/apps/backend/src/app/api/latest/contact-channels/[user_id]/[contact_channel_id]/send-verification-code/route.tsx index c0bb4aa72..9b5e93dc6 100644 --- a/apps/backend/src/app/api/latest/contact-channels/[user_id]/[contact_channel_id]/send-verification-code/route.tsx +++ b/apps/backend/src/app/api/latest/contact-channels/[user_id]/[contact_channel_id]/send-verification-code/route.tsx @@ -53,7 +53,9 @@ export const POST = createSmartRouteHandler({ } } - const contactChannel = await getPrismaClientForTenancy(auth.tenancy).contactChannel.findUnique({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const contactChannel = await prisma.contactChannel.findUnique({ where: { tenancyId_projectUserId_id: { tenancyId: auth.tenancy.id, diff --git a/apps/backend/src/app/api/latest/contact-channels/crud.tsx b/apps/backend/src/app/api/latest/contact-channels/crud.tsx index 8c0e9c6bd..d044efd2e 100644 --- a/apps/backend/src/app/api/latest/contact-channels/crud.tsx +++ b/apps/backend/src/app/api/latest/contact-channels/crud.tsx @@ -39,7 +39,9 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl } } - const contactChannel = await getPrismaClientForTenancy(auth.tenancy).contactChannel.findUnique({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const contactChannel = await prisma.contactChannel.findUnique({ where: { tenancyId_projectUserId_id: { tenancyId: auth.tenancy.id, @@ -71,7 +73,9 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl } } - const contactChannel = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const contactChannel = await retryTransaction(prisma, async (tx) => { await ensureContactChannelDoesNotExists(tx, { tenancyId: auth.tenancy.id, userId: data.user_id, @@ -166,7 +170,9 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl } } - const updatedContactChannel = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const updatedContactChannel = await retryTransaction(prisma, async (tx) => { const existingContactChannel = await ensureContactChannelExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id, @@ -230,7 +236,9 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl } } - await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + await retryTransaction(prisma, async (tx) => { await ensureContactChannelExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id, @@ -256,7 +264,9 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl } } - const contactChannels = await getPrismaClientForTenancy(auth.tenancy).contactChannel.findMany({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const contactChannels = await prisma.contactChannel.findMany({ where: { tenancyId: auth.tenancy.id, projectUserId: query.user_id, diff --git a/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx b/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx index 0e673fe7e..e1879cca9 100644 --- a/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx @@ -54,7 +54,9 @@ export const contactChannelVerificationCodeHandler = createVerificationCodeHandl }, } as const; - const contactChannel = await getPrismaClientForTenancy(tenancy).contactChannel.findUnique({ + const prisma = await getPrismaClientForTenancy(tenancy); + + const contactChannel = await prisma.contactChannel.findUnique({ where: uniqueKeys, }); @@ -63,7 +65,7 @@ export const contactChannelVerificationCodeHandler = createVerificationCodeHandl throw new StatusError(400, "Contact channel not found. Was your contact channel deleted?"); } - await getPrismaClientForTenancy(tenancy).contactChannel.update({ + await prisma.contactChannel.update({ where: uniqueKeys, data: { isVerified: true, diff --git a/apps/backend/src/app/api/latest/emails/notification-preference/crud.tsx b/apps/backend/src/app/api/latest/emails/notification-preference/crud.tsx index a099e5684..dd9deed63 100644 --- a/apps/backend/src/app/api/latest/emails/notification-preference/crud.tsx +++ b/apps/backend/src/app/api/latest/emails/notification-preference/crud.tsx @@ -29,7 +29,7 @@ export const notificationPreferencesCrudHandlers = createLazyProxy(() => createC throw new StatusError(StatusError.Forbidden, "You can only manage your own notification preferences"); } } - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId }); const notificationPreference = await prismaClient.userNotificationPreference.upsert({ @@ -72,7 +72,7 @@ export const notificationPreferencesCrudHandlers = createLazyProxy(() => createC throw new StatusError(StatusError.Forbidden, "You can only view your own notification preferences"); } } - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId }); const notificationPreferences = await prismaClient.userNotificationPreference.findMany({ diff --git a/apps/backend/src/app/api/latest/emails/send-email/route.tsx b/apps/backend/src/app/api/latest/emails/send-email/route.tsx index ec86e4a73..b892bec3f 100644 --- a/apps/backend/src/app/api/latest/emails/send-email/route.tsx +++ b/apps/backend/src/app/api/latest/emails/send-email/route.tsx @@ -62,7 +62,9 @@ export const POST = createSmartRouteHandler({ } const activeTheme = themeList[auth.tenancy.completeConfig.emails.theme]; - const users = await getPrismaClientForTenancy(auth.tenancy).projectUser.findMany({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const users = await prisma.projectUser.findMany({ where: { tenancyId: auth.tenancy.id, projectUserId: { diff --git a/apps/backend/src/app/api/latest/emails/unsubscribe-link/route.tsx b/apps/backend/src/app/api/latest/emails/unsubscribe-link/route.tsx index 83930c7fa..4ceb83fa0 100644 --- a/apps/backend/src/app/api/latest/emails/unsubscribe-link/route.tsx +++ b/apps/backend/src/app/api/latest/emails/unsubscribe-link/route.tsx @@ -40,7 +40,10 @@ export async function GET(request: NextRequest) { }); const tenancy = await getSoleTenancyFromProjectBranch(verificationCode.projectId, verificationCode.branchId); - await getPrismaClientForTenancy(tenancy).userNotificationPreference.upsert({ + + const prisma = await getPrismaClientForTenancy(tenancy); + + await prisma.userNotificationPreference.upsert({ where: { tenancyId_projectUserId_notificationCategoryId: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx b/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx index 02dcbba2c..9eb976115 100644 --- a/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx @@ -84,7 +84,7 @@ export const POST = createSmartRouteHandler({ throw new StackAssertionError("Tenancy not found"); } - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const projectUser = await prisma.projectUser.findUnique({ where: { tenancyId_projectUserId: { @@ -113,7 +113,10 @@ export const POST = createSmartRouteHandler({ if (!tenancy) { throw new StackAssertionError("Tenancy not found"); } - const userIdsWithManageApiKeysPermission = await getPrismaClientForTenancy(tenancy).$transaction(async (tx) => { + + const prisma = await getPrismaClientForTenancy(tenancy); + + const userIdsWithManageApiKeysPermission = await prisma.$transaction(async (tx) => { if (!updatedApiKey.teamId) { throw new StackAssertionError("Team ID not specified in team API key"); } @@ -129,7 +132,7 @@ export const POST = createSmartRouteHandler({ return permissions.map(p => p.user_id); }); - const usersWithManageApiKeysPermission = await getPrismaClientForTenancy(tenancy).projectUser.findMany({ + const usersWithManageApiKeysPermission = await prisma.projectUser.findMany({ where: { tenancyId: updatedApiKey.tenancyId, projectUserId: { diff --git a/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx b/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx index 7f7d600ee..3a2aeaa41 100644 --- a/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx @@ -53,7 +53,9 @@ export const integrationProjectTransferCodeHandler = createVerificationCodeHandl if (provisionedProject.count === 0) throw new StatusError(400, "The project to transfer was not provisioned or has already been transferred."); - const recentDbUser = await getPrismaClientForTenancy(tenancy).projectUser.findUnique({ + const prisma = await getPrismaClientForTenancy(tenancy); + + const recentDbUser = await prisma.projectUser.findUnique({ where: { tenancyId_projectUserId: { tenancyId: tenancy.id, @@ -63,7 +65,7 @@ export const integrationProjectTransferCodeHandler = createVerificationCodeHandl }) ?? throwErr("Authenticated user not found in transaction. Something went wrong. Did the user delete their account at the wrong time? (Very unlikely.)"); const rduServerMetadata: any = recentDbUser.serverMetadata; - await getPrismaClientForTenancy(tenancy).projectUser.update({ + await prisma.projectUser.update({ where: { tenancyId_projectUserId: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx b/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx index 53f77096c..fa296988a 100644 --- a/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx @@ -54,7 +54,9 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH if (provisionedProject.count === 0) throw new StatusError(400, "The project to transfer was not provisioned by Neon or has already been transferred."); - const recentDbUser = await getPrismaClientForTenancy(tenancy).projectUser.findUnique({ + const prisma = await getPrismaClientForTenancy(tenancy); + + const recentDbUser = await prisma.projectUser.findUnique({ where: { tenancyId_projectUserId: { tenancyId: tenancy.id, @@ -64,7 +66,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH }) ?? throwErr("Authenticated user not found in transaction. Something went wrong. Did the user delete their account at the wrong time? (Very unlikely.)"); const rduServerMetadata: any = recentDbUser.serverMetadata; - await getPrismaClientForTenancy(tenancy).projectUser.update({ + await prisma.projectUser.update({ where: { tenancyId_projectUserId: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx b/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx index 894bb21c9..e07f3f896 100644 --- a/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx +++ b/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx @@ -4,7 +4,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { createOpenAI } from "@ai-sdk/openai"; import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString, yupUnion } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { generateText, ToolResult } from "ai"; +import { ToolResult, generateText } from "ai"; import { InferType } from "yup"; const textContentSchema = yupObject({ @@ -22,7 +22,7 @@ const toolCallContentSchema = yupObject({ }); const contentSchema = yupArray(yupUnion(textContentSchema, toolCallContentSchema)).defined(); -const openai = createOpenAI({ apiKey: getEnvVariable("STACK_OPENAI_API_KEY") }); +const openai = createOpenAI({ apiKey: getEnvVariable("STACK_OPENAI_API_KEY", "MISSING_OPENAI_API_KEY") }); export const POST = createSmartRouteHandler({ metadata: { diff --git a/apps/backend/src/app/api/latest/internal/emails/crud.tsx b/apps/backend/src/app/api/latest/internal/emails/crud.tsx index 58904e8e4..aa5be457d 100644 --- a/apps/backend/src/app/api/latest/internal/emails/crud.tsx +++ b/apps/backend/src/app/api/latest/internal/emails/crud.tsx @@ -31,7 +31,9 @@ export const internalEmailsCrudHandlers = createLazyProxy(() => createCrudHandle emailId: yupString().optional(), }), onList: async ({ auth }) => { - const emails = await getPrismaClientForTenancy(auth.tenancy).sentEmail.findMany({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const emails = await prisma.sentEmail.findMany({ where: { tenancyId: auth.tenancy.id, }, diff --git a/apps/backend/src/app/api/latest/internal/metrics/route.tsx b/apps/backend/src/app/api/latest/internal/metrics/route.tsx index 86700e08e..ef8118add 100644 --- a/apps/backend/src/app/api/latest/internal/metrics/route.tsx +++ b/apps/backend/src/app/api/latest/internal/metrics/route.tsx @@ -46,7 +46,7 @@ async function loadUsersByCountry(tenancy: Tenancy): Promise { const schema = getPrismaSchemaForTenancy(tenancy); - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); return (await prisma.$queryRaw<{date: Date, dailyUsers: bigint, cumUsers: bigint}[]>` WITH date_series AS ( SELECT GENERATE_SERIES( @@ -107,7 +107,7 @@ async function loadDailyActiveUsers(tenancy: Tenancy, now: Date) { async function loadLoginMethods(tenancy: Tenancy): Promise<{method: string, count: number }[]> { const schema = getPrismaSchemaForTenancy(tenancy); - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); return await prisma.$queryRaw<{ method: string, count: number }[]>` WITH tab AS ( SELECT @@ -202,6 +202,8 @@ export const GET = createSmartRouteHandler({ handler: async (req) => { const now = new Date(); + const prisma = await getPrismaClientForTenancy(req.auth.tenancy); + const [ totalUsers, dailyUsers, @@ -211,7 +213,7 @@ export const GET = createSmartRouteHandler({ recentlyActive, loginMethods ] = await Promise.all([ - getPrismaClientForTenancy(req.auth.tenancy).projectUser.count({ + prisma.projectUser.count({ where: { tenancyId: req.auth.tenancy.id, }, }), loadTotalUsers(req.auth.tenancy, now), diff --git a/apps/backend/src/app/api/latest/internal/projects/current/crud.tsx b/apps/backend/src/app/api/latest/internal/projects/current/crud.tsx index 34ce24bb2..3975b4093 100644 --- a/apps/backend/src/app/api/latest/internal/projects/current/crud.tsx +++ b/apps/backend/src/app/api/latest/internal/projects/current/crud.tsx @@ -41,8 +41,10 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro } }); + const prisma = await getPrismaClientForTenancy(auth.tenancy); + // delete managed ids from users - const users = await getPrismaClientForTenancy(auth.tenancy).projectUser.findMany({ + const users = await prisma.projectUser.findMany({ where: { mirroredProjectId: 'internal', serverMetadata: { @@ -57,7 +59,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro (id: any) => id !== auth.project.id ) as string[]; - await getPrismaClientForTenancy(auth.tenancy).projectUser.update({ + await prisma.projectUser.update({ where: { mirroredProjectId_mirroredBranchId_projectUserId: { mirroredProjectId: 'internal', diff --git a/apps/backend/src/app/api/latest/oauth-providers/crud.tsx b/apps/backend/src/app/api/latest/oauth-providers/crud.tsx index d84c4387e..47202bc39 100644 --- a/apps/backend/src/app/api/latest/oauth-providers/crud.tsx +++ b/apps/backend/src/app/api/latest/oauth-providers/crud.tsx @@ -26,7 +26,7 @@ async function checkInputValidity(options: { allowSignIn: boolean, allowConnectedAccounts: boolean, })): Promise { - const prismaClient = getPrismaClientForTenancy(options.tenancy); + const prismaClient = await getPrismaClientForTenancy(options.tenancy); let providerConfigId: string; if (options.type === 'update') { @@ -86,7 +86,7 @@ async function checkInputValidity(options: { } async function ensureProviderExists(tenancy: Tenancy, userId: string, providerId: string) { - const prismaClient = getPrismaClientForTenancy(tenancy); + const prismaClient = await getPrismaClientForTenancy(tenancy); const provider = await prismaClient.projectUserOAuthAccount.findUnique({ where: { tenancyId_id: { @@ -144,7 +144,7 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler } } - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId: params.user_id }); const oauthAccount = await ensureProviderExists(auth.tenancy, params.user_id, params.provider_id); @@ -168,7 +168,7 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler } } - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); if (query.user_id) { await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId: query.user_id }); @@ -210,7 +210,7 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler } } - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId: params.user_id }); const existingOAuthAccount = await ensureProviderExists(auth.tenancy, params.user_id, params.provider_id); @@ -320,7 +320,7 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler } } - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId: params.user_id }); const existingOAuthAccount = await ensureProviderExists(auth.tenancy, params.user_id, params.provider_id); @@ -347,7 +347,7 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler }); }, async onCreate({ auth, data }) { - const prismaClient = getPrismaClientForTenancy(auth.tenancy); + const prismaClient = await getPrismaClientForTenancy(auth.tenancy); const providerConfig = getProviderConfig(auth.tenancy, data.provider_config_id); await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId: data.user_id }); diff --git a/apps/backend/src/app/api/latest/project-permission-definitions/crud.tsx b/apps/backend/src/app/api/latest/project-permission-definitions/crud.tsx index 4492435e1..47eaab18f 100644 --- a/apps/backend/src/app/api/latest/project-permission-definitions/crud.tsx +++ b/apps/backend/src/app/api/latest/project-permission-definitions/crud.tsx @@ -21,9 +21,10 @@ export const projectPermissionDefinitionsCrudHandlers = createLazyProxy(() => cr ); }, async onUpdate({ auth, data, params }) { + const prisma = await getPrismaClientForTenancy(auth.tenancy); return await updatePermissionDefinition( globalPrismaClient, - getPrismaClientForTenancy(auth.tenancy), + prisma, { oldId: params.permission_id, scope: "project", @@ -33,9 +34,10 @@ export const projectPermissionDefinitionsCrudHandlers = createLazyProxy(() => cr ); }, async onDelete({ auth, params }) { + const prisma = await getPrismaClientForTenancy(auth.tenancy); return await deletePermissionDefinition( globalPrismaClient, - getPrismaClientForTenancy(auth.tenancy), + prisma, { scope: "project", tenancy: auth.tenancy, diff --git a/apps/backend/src/app/api/latest/project-permissions/crud.tsx b/apps/backend/src/app/api/latest/project-permissions/crud.tsx index 18a88e7bf..fe9aff673 100644 --- a/apps/backend/src/app/api/latest/project-permissions/crud.tsx +++ b/apps/backend/src/app/api/latest/project-permissions/crud.tsx @@ -21,7 +21,8 @@ export const projectPermissionsCrudHandlers = createLazyProxy(() => createCrudHa permission_id: permissionDefinitionIdSchema.defined(), }), async onCreate({ auth, params }) { - const result = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const result = await retryTransaction(prisma, async (tx) => { await ensureUserExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id }); return await grantProjectPermission(tx, { @@ -42,7 +43,8 @@ export const projectPermissionsCrudHandlers = createLazyProxy(() => createCrudHa return result; }, async onDelete({ auth, params }) { - const result = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const result = await retryTransaction(prisma, async (tx) => { await ensureProjectPermissionExists(tx, { tenancy: auth.tenancy, userId: params.user_id, @@ -77,7 +79,9 @@ export const projectPermissionsCrudHandlers = createLazyProxy(() => createCrudHa } } - return await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + return await retryTransaction(prisma, async (tx) => { return { items: await listPermissions(tx, { scope: 'project', diff --git a/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx b/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx index 3421afc74..4bef3e462 100644 --- a/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx @@ -69,7 +69,9 @@ export const teamInvitationCodeHandler = createVerificationCodeHandler({ async handler(tenancy, {}, data, body, user) { if (!user) throw new KnownErrors.UserAuthenticationRequired; - const oldMembership = await getPrismaClientForTenancy(tenancy).teamMember.findUnique({ + const prisma = await getPrismaClientForTenancy(tenancy); + + const oldMembership = await prisma.teamMember.findUnique({ where: { tenancyId_projectUserId_teamId: { tenancyId: tenancy.id, diff --git a/apps/backend/src/app/api/latest/team-invitations/crud.tsx b/apps/backend/src/app/api/latest/team-invitations/crud.tsx index 53c21a99a..250e89b49 100644 --- a/apps/backend/src/app/api/latest/team-invitations/crud.tsx +++ b/apps/backend/src/app/api/latest/team-invitations/crud.tsx @@ -16,7 +16,8 @@ export const teamInvitationsCrudHandlers = createLazyProxy(() => createCrudHandl id: yupString().uuid().defined(), }), onList: async ({ auth, query }) => { - return await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + return await retryTransaction(prisma, async (tx) => { if (auth.type === 'client') { // Client can only: // - list invitations in their own team if they have the $read_members AND $invite_members permissions @@ -58,7 +59,8 @@ export const teamInvitationsCrudHandlers = createLazyProxy(() => createCrudHandl }); }, onDelete: async ({ auth, query, params }) => { - await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + await retryTransaction(prisma, async (tx) => { if (auth.type === 'client') { // Client can only: // - delete invitations in their own team if they have the $remove_members permissions diff --git a/apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx b/apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx index 06b35782e..fddea5c7c 100644 --- a/apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx +++ b/apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx @@ -32,7 +32,8 @@ export const POST = createSmartRouteHandler({ }).defined(), }), async handler({ auth, body }) { - await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + await retryTransaction(prisma, async (tx) => { if (auth.type === "client") { if (!auth.user) throw new KnownErrors.UserAuthenticationRequired(); diff --git a/apps/backend/src/app/api/latest/team-member-profiles/crud.tsx b/apps/backend/src/app/api/latest/team-member-profiles/crud.tsx index 545f3cdc5..a58e1d1d3 100644 --- a/apps/backend/src/app/api/latest/team-member-profiles/crud.tsx +++ b/apps/backend/src/app/api/latest/team-member-profiles/crud.tsx @@ -31,7 +31,8 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa user_id: userIdOrMeSchema.defined(), }), onList: async ({ auth, query }) => { - return await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + return await retryTransaction(prisma, async (tx) => { if (auth.type === 'client') { // Client can only: // - list users in their own team if they have the $read_members permission @@ -85,7 +86,8 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa }); }, onRead: async ({ auth, params }) => { - return await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + return await retryTransaction(prisma, async (tx) => { if (auth.type === 'client') { const currentUserId = auth.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser()); if (params.user_id !== currentUserId) { @@ -122,7 +124,8 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa }); }, onUpdate: async ({ auth, data, params }) => { - return await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + return await retryTransaction(prisma, async (tx) => { if (auth.type === 'client') { const currentUserId = auth.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser()); if (params.user_id !== currentUserId) { diff --git a/apps/backend/src/app/api/latest/team-memberships/crud.tsx b/apps/backend/src/app/api/latest/team-memberships/crud.tsx index ced194af4..ae11b32ef 100644 --- a/apps/backend/src/app/api/latest/team-memberships/crud.tsx +++ b/apps/backend/src/app/api/latest/team-memberships/crud.tsx @@ -46,7 +46,8 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl user_id: userIdOrMeSchema.defined(), }), onCreate: async ({ auth, params }) => { - const result = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const result = await retryTransaction(prisma, async (tx) => { await ensureUserExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id, @@ -112,7 +113,8 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl return data; }, onDelete: async ({ auth, params }) => { - await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + await retryTransaction(prisma, async (tx) => { // Users are always allowed to remove themselves from a team // Only users with the $remove_members permission can remove other users if (auth.type === 'client') { diff --git a/apps/backend/src/app/api/latest/team-permission-definitions/crud.tsx b/apps/backend/src/app/api/latest/team-permission-definitions/crud.tsx index 34fe48d6b..45b6e5725 100644 --- a/apps/backend/src/app/api/latest/team-permission-definitions/crud.tsx +++ b/apps/backend/src/app/api/latest/team-permission-definitions/crud.tsx @@ -20,9 +20,10 @@ export const teamPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat ); }, async onUpdate({ auth, data, params }) { + const prisma = await getPrismaClientForTenancy(auth.tenancy); return await updatePermissionDefinition( globalPrismaClient, - getPrismaClientForTenancy(auth.tenancy), + prisma, { oldId: params.permission_id, scope: "team", @@ -36,9 +37,10 @@ export const teamPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat ); }, async onDelete({ auth, params }) { + const prisma = await getPrismaClientForTenancy(auth.tenancy); return await deletePermissionDefinition( globalPrismaClient, - getPrismaClientForTenancy(auth.tenancy), + prisma, { scope: "team", tenancy: auth.tenancy, diff --git a/apps/backend/src/app/api/latest/team-permissions/crud.tsx b/apps/backend/src/app/api/latest/team-permissions/crud.tsx index 5c159acb6..bb82f0463 100644 --- a/apps/backend/src/app/api/latest/team-permissions/crud.tsx +++ b/apps/backend/src/app/api/latest/team-permissions/crud.tsx @@ -23,7 +23,8 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl permission_id: permissionDefinitionIdSchema.defined(), }), async onCreate({ auth, params }) { - const result = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const result = await retryTransaction(prisma, async (tx) => { await ensureTeamMembershipExists(tx, { tenancyId: auth.tenancy.id, teamId: params.team_id, userId: params.user_id }); return await grantTeamPermission(tx, { @@ -46,7 +47,8 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl return result; }, async onDelete({ auth, params }) { - const result = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const result = await retryTransaction(prisma, async (tx) => { await ensureUserTeamPermissionExists(tx, { tenancy: auth.tenancy, teamId: params.team_id, @@ -84,7 +86,8 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl } } - return await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + return await retryTransaction(prisma, async (tx) => { return { items: await listPermissions(tx, { scope: 'team', diff --git a/apps/backend/src/app/api/latest/teams/crud.tsx b/apps/backend/src/app/api/latest/teams/crud.tsx index 4d29dfee5..d1d26a3b3 100644 --- a/apps/backend/src/app/api/latest/teams/crud.tsx +++ b/apps/backend/src/app/api/latest/teams/crud.tsx @@ -68,7 +68,9 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC addUserId = auth.user.id; } - const db = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + const db = await retryTransaction(prisma, async (tx) => { const db = await tx.team.create({ data: { displayName: data.display_name, @@ -105,15 +107,17 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC return result; }, onRead: async ({ params, auth }) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + if (auth.type === 'client') { - await ensureTeamMembershipExists(getPrismaClientForTenancy(auth.tenancy), { + await ensureTeamMembershipExists(prisma, { tenancyId: auth.tenancy.id, teamId: params.team_id, userId: auth.user?.id ?? throwErr(new KnownErrors.UserAuthenticationRequired), }); } - const db = await getPrismaClientForTenancy(auth.tenancy).team.findUnique({ + const db = await prisma.team.findUnique({ where: { tenancyId_teamId: { tenancyId: auth.tenancy.id, @@ -129,7 +133,8 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC return teamPrismaToCrud(db); }, onUpdate: async ({ params, auth, data }) => { - const db = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const db = await retryTransaction(prisma, async (tx) => { if (auth.type === 'client' && data.profile_image_url && !validateBase64Image(data.profile_image_url)) { throw new StatusError(400, "Invalid profile image URL"); } @@ -174,7 +179,8 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC return result; }, onDelete: async ({ params, auth }) => { - await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + await retryTransaction(prisma, async (tx) => { if (auth.type === 'client') { await ensureUserTeamPermissionExists(tx, { tenancy: auth.tenancy, @@ -213,7 +219,8 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC } } - const db = await getPrismaClientForTenancy(auth.tenancy).team.findMany({ + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const db = await prisma.team.findMany({ where: { tenancyId: auth.tenancy.id, ...query.user_id ? { diff --git a/apps/backend/src/app/api/latest/users/crud.tsx b/apps/backend/src/app/api/latest/users/crud.tsx index eaf2bebd9..fb5e28dba 100644 --- a/apps/backend/src/app/api/latest/users/crud.tsx +++ b/apps/backend/src/app/api/latest/users/crud.tsx @@ -166,7 +166,7 @@ export const getUsersLastActiveAtMillis = async (projectId: string, branchId: st // Get the tenancy first to determine the source of truth const tenancy = await getSoleTenancyFromProjectBranch(projectId, branchId); - const prisma = getPrismaClientForTenancy(tenancy); + const prisma = await getPrismaClientForTenancy(tenancy); const schema = getPrismaSchemaForTenancy(tenancy); const events = await prisma.$queryRaw>` SELECT data->>'userId' as "userId", MAX("eventStartedAt") as "lastActiveAt" @@ -358,8 +358,9 @@ export async function getUser(options: { userId: string } & ({ projectId: string } const environmentConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ projectId, branchId })); - const prisma = getPrismaClientForSourceOfTruth(environmentConfig.sourceOfTruth, branchId); - const result = await rawQuery(prisma, getUserQuery(projectId, branchId, options.userId, getPrismaSchemaForSourceOfTruth(environmentConfig.sourceOfTruth, branchId))); + const prisma = await getPrismaClientForSourceOfTruth(environmentConfig.sourceOfTruth, branchId); + const schema = getPrismaSchemaForSourceOfTruth(environmentConfig.sourceOfTruth, branchId); + const result = await rawQuery(prisma, getUserQuery(projectId, branchId, options.userId, schema)); return result; } @@ -385,7 +386,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }, onList: async ({ auth, query }) => { const queryWithoutSpecialChars = query.query?.replace(/[^a-zA-Z0-9\-_.]/g, ''); - const prisma = getPrismaClientForTenancy(auth.tenancy); + const prisma = await getPrismaClientForTenancy(auth.tenancy); const where = { tenancyId: auth.tenancy.id, @@ -464,7 +465,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }); const passwordHash = await getPasswordHashFromData(data); - const prisma = getPrismaClientForTenancy(auth.tenancy); + const prisma = await getPrismaClientForTenancy(auth.tenancy); const result = await retryTransaction(prisma, async (tx) => { await checkAuthData(tx, { tenancyId: auth.tenancy.id, @@ -637,7 +638,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC onUpdate: async ({ auth, data, params }) => { const primaryEmail = data.primary_email ? normalizeEmail(data.primary_email) : data.primary_email; const passwordHash = await getPasswordHashFromData(data); - const prisma = getPrismaClientForTenancy(auth.tenancy); + const prisma = await getPrismaClientForTenancy(auth.tenancy); const result = await retryTransaction(prisma, async (tx) => { await ensureUserExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id }); @@ -969,7 +970,8 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC return result; }, onDelete: async ({ auth, params }) => { - const { teams } = await retryTransaction(getPrismaClientForTenancy(auth.tenancy), async (tx) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + const { teams } = await retryTransaction(prisma, async (tx) => { await ensureUserExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id }); const teams = await tx.team.findMany({ diff --git a/apps/backend/src/auto-migrations/auto-migration.tests.ts b/apps/backend/src/auto-migrations/auto-migration.tests.ts new file mode 100644 index 000000000..ee248b30a --- /dev/null +++ b/apps/backend/src/auto-migrations/auto-migration.tests.ts @@ -0,0 +1,394 @@ +import { PrismaClient } from "@prisma/client"; +import postgres from 'postgres'; +import { ExpectStatic } from "vitest"; +import { applyMigrations, runMigrationNeeded } from "./index"; + +const TEST_DB_PREFIX = 'stack_auth_test_db'; + +const getTestDbURL = (testDbName: string) => { + // @ts-ignore - ImportMeta.env is provided by Vite + const base = import.meta.env.STACK_DIRECT_DATABASE_CONNECTION_STRING.replace(/\/[^/]*$/, ''); + return { + full: `${base}/${testDbName}`, + base, + }; +}; + +const applySql = async (options: { sql: string | string[], fullDbURL: string }) => { + const sql = postgres(options.fullDbURL); + + try { + for (const query of Array.isArray(options.sql) ? options.sql : [options.sql]) { + await sql.unsafe(query); + } + + } finally { + await sql.end(); + } +}; + +const setupTestDatabase = async () => { + const randomSuffix = Math.random().toString(16).substring(2, 12); + const testDbName = `${TEST_DB_PREFIX}_${randomSuffix}`; + const dbURL = getTestDbURL(testDbName); + await applySql({ sql: `CREATE DATABASE ${testDbName}`, fullDbURL: dbURL.base }); + + const prismaClient = new PrismaClient({ + datasources: { + db: { + url: dbURL.full, + }, + }, + }); + + await prismaClient.$connect(); + + return { + prismaClient, + testDbName, + dbURL, + }; +}; + +const teardownTestDatabase = async (prismaClient: PrismaClient, testDbName: string) => { + await prismaClient.$disconnect(); + const dbURL = getTestDbURL(testDbName); + await applySql({ + sql: [ + ` + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '${testDbName}' + AND pid <> pg_backend_pid(); + `, + `DROP DATABASE IF EXISTS ${testDbName}` + ], + fullDbURL: dbURL.base + }); + + // Wait a bit to ensure connections are terminated + await new Promise(resolve => setTimeout(resolve, 500)); +}; + +function runTest(fn: (options: { expect: ExpectStatic, prismaClient: PrismaClient, dbURL: { full: string, base: string } }) => Promise) { + return async ({ expect }: { expect: ExpectStatic }) => { + const { prismaClient, testDbName, dbURL } = await setupTestDatabase(); + try { + await fn({ prismaClient, expect, dbURL }); + } finally { + await teardownTestDatabase(prismaClient, testDbName); + } + }; +} + +const exampleMigrationFiles1 = [ + { + migrationName: "001-create-table", + sql: "CREATE TABLE test (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL);", + }, + { + migrationName: "002-update-table", + sql: "ALTER TABLE test ADD COLUMN age INTEGER NOT NULL DEFAULT 0;", + }, +]; + +const examplePrismaBasedInitQueries = [ + // Settings + `SET statement_timeout = 0`, + `SET lock_timeout = 0`, + `SET idle_in_transaction_session_timeout = 0`, + `SET client_encoding = 'UTF8'`, + `SET standard_conforming_strings = on`, + `SELECT pg_catalog.set_config('search_path', '', false)`, + `SET check_function_bodies = false`, + `SET xmloption = content`, + `SET client_min_messages = warning`, + `SET row_security = off`, + `ALTER SCHEMA public OWNER TO postgres`, + `COMMENT ON SCHEMA public IS ''`, + `SET default_tablespace = ''`, + `SET default_table_access_method = heap`, + `CREATE TABLE public."User" ( + id integer NOT NULL, + name text NOT NULL + )`, + `ALTER TABLE public."User" OWNER TO postgres`, + `CREATE TABLE public._prisma_migrations ( + id character varying(36) NOT NULL, + checksum character varying(64) NOT NULL, + finished_at timestamp with time zone, + migration_name character varying(255) NOT NULL, + logs text, + rolled_back_at timestamp with time zone, + started_at timestamp with time zone DEFAULT now() NOT NULL, + applied_steps_count integer DEFAULT 0 NOT NULL + )`, + `ALTER TABLE public._prisma_migrations OWNER TO postgres`, + `INSERT INTO public._prisma_migrations (id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count) + VALUES ('a34e5ccf-c472-44c7-9d9c-0d4580d18ac3', '9785d85f8c5a8b3dbfbbbd8143cc7485bb48dd8bf30ca3eafd3cd2e1ba15a953', '2025-03-14 21:50:26.794721+00', '20250314215026_init', NULL, NULL, '2025-03-14 21:50:26.656161+00', 1)`, + `INSERT INTO public._prisma_migrations (id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count) + VALUES ('7e7f0e5b-f91b-40fa-b061-d8f2edd274ed', '6853f42ae69239976b84d058430774c8faa83488545e84162844dab84b47294d', '2025-03-14 21:50:47.761397+00', '20250314215047_name', NULL, NULL, '2025-03-14 21:50:47.624814+00', 1)`, + `ALTER TABLE ONLY public."User" ADD CONSTRAINT "User_pkey" PRIMARY KEY (id)`, + `ALTER TABLE ONLY public._prisma_migrations ADD CONSTRAINT _prisma_migrations_pkey PRIMARY KEY (id)`, + `REVOKE USAGE ON SCHEMA public FROM PUBLIC` +]; + +const examplePrismaBasedMigrationFiles = [ + { + migrationName: '20250314215026_init', + sql: `CREATE TABLE "User" ("id" INTEGER NOT NULL, CONSTRAINT "User_pkey" PRIMARY KEY ("id"));`, + }, + { + migrationName: '20250314215047_name', + sql: `ALTER TABLE "User" ADD COLUMN "name" TEXT NOT NULL;`, + }, + { + migrationName: '20250314215050_age', + sql: `ALTER TABLE "User" ADD COLUMN "age" INTEGER NOT NULL DEFAULT 0;`, + }, +]; + + +import.meta.vitest?.test("connects to DB", runTest(async ({ expect, prismaClient }) => { + const result = await prismaClient.$executeRaw`SELECT 1`; + expect(result).toBe(1); +})); + +import.meta.vitest?.test("applies migrations", runTest(async ({ expect, prismaClient }) => { + const { newlyAppliedMigrationNames } = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, schema: 'public' }); + + expect(newlyAppliedMigrationNames).toEqual(['001-create-table', '002-update-table']); + + await prismaClient.$executeRaw`INSERT INTO test (name) VALUES ('test_value')`; + + const result = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].name).toBe('test_value'); + + const ageResult = await prismaClient.$queryRaw`SELECT age FROM test WHERE name = 'test_value'` as { age: number }[]; + expect(Array.isArray(ageResult)).toBe(true); + expect(ageResult.length).toBe(1); + expect(ageResult[0].age).toBe(0); +})); + +import.meta.vitest?.test("first apply half of the migrations, then apply the other half", runTest(async ({ expect, prismaClient }) => { + const { newlyAppliedMigrationNames } = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1.slice(0, 1), schema: 'public' }); + expect(newlyAppliedMigrationNames).toEqual(['001-create-table']); + + const { newlyAppliedMigrationNames: newlyAppliedMigrationNames2 } = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, schema: 'public' }); + expect(newlyAppliedMigrationNames2).toEqual(['002-update-table']); + + await prismaClient.$executeRaw`INSERT INTO test (name) VALUES ('test_value')`; + + const result = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].name).toBe('test_value'); + + const ageResult = await prismaClient.$queryRaw`SELECT age FROM test WHERE name = 'test_value'` as { age: number }[]; + expect(Array.isArray(ageResult)).toBe(true); + expect(ageResult.length).toBe(1); + expect(ageResult[0].age).toBe(0); +})); + +import.meta.vitest?.test("applies migrations concurrently", runTest(async ({ expect, prismaClient }) => { + const [result1, result2] = await Promise.all([ + applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, artificialDelayInSeconds: 1, schema: 'public' }), + applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, artificialDelayInSeconds: 1, schema: 'public' }), + ]); + + const l1 = result1.newlyAppliedMigrationNames.length; + const l2 = result2.newlyAppliedMigrationNames.length; + + // One of the two migrations should be applied, but not both + expect((l1 === 2 && l2 === 0) || (l1 === 0 && l2 === 2)).toBe(true); + + await prismaClient.$executeRaw`INSERT INTO test (name) VALUES ('test_value')`; + const result = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].name).toBe('test_value'); +})); + +import.meta.vitest?.test("applies migrations concurrently with 20 concurrent migrations", runTest(async ({ expect, prismaClient }) => { + const promises = Array.from({ length: 20 }, () => + applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, artificialDelayInSeconds: 1, schema: 'public' }) + ); + + const results = await Promise.all(promises); + + // Count how many migrations were applied by each promise + const appliedCounts = results.map(result => result.newlyAppliedMigrationNames.length); + + // Only one of the promises should have applied all migrations, the rest should have applied none + const successfulApplies = appliedCounts.filter(count => count === 2); + const emptyApplies = appliedCounts.filter(count => count === 0); + + expect(successfulApplies.length).toBe(1); + expect(emptyApplies.length).toBe(19); + + await prismaClient.$executeRaw`INSERT INTO test (name) VALUES ('test_value')`; + const result = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].name).toBe('test_value'); +})); + + +import.meta.vitest?.test("applies migration with a DB previously migrated with prisma", runTest(async ({ expect, prismaClient, dbURL }) => { + await applySql({ sql: examplePrismaBasedInitQueries, fullDbURL: dbURL.full }); + const result = await applyMigrations({ prismaClient, migrationFiles: examplePrismaBasedMigrationFiles, schema: 'public' }); + expect(result.newlyAppliedMigrationNames).toEqual(['20250314215050_age']); + + // apply migrations again + const result2 = await applyMigrations({ prismaClient, migrationFiles: examplePrismaBasedMigrationFiles, schema: 'public' }); + expect(result2.newlyAppliedMigrationNames).toEqual([]); +})); + +import.meta.vitest?.test("applies migration while running a query", runTest(async ({ expect, prismaClient, dbURL }) => { + await runMigrationNeeded({ + prismaClient, + migrationFiles: exampleMigrationFiles1, + artificialDelayInSeconds: 1, + schema: 'public', + }); + + await prismaClient.$executeRaw`INSERT INTO test (name) VALUES ('test_value')`; + + const result = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].name).toBe('test_value'); +})); + +import.meta.vitest?.test("applies migration while running concurrent queries", runTest(async ({ expect, prismaClient, dbURL }) => { + const runMigrationAndInsert = async (testValue: string) => { + await runMigrationNeeded({ + prismaClient, + migrationFiles: exampleMigrationFiles1, + schema: 'public', + }); + await prismaClient.$executeRaw`INSERT INTO test (name) VALUES (${testValue})`; + }; + + await Promise.all([ + runMigrationAndInsert('test_value1'), + runMigrationAndInsert('test_value2'), + ]); + + const result1 = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result1)).toBe(true); + expect(result1.length).toBe(2); + expect(result1.some(r => r.name === 'test_value1')).toBe(true); + expect(result1.some(r => r.name === 'test_value2')).toBe(true); +})); + +import.meta.vitest?.test("applies migration while running an interactive transaction", runTest(async ({ expect, prismaClient, dbURL }) => { + return await prismaClient.$transaction(async (tx, ...args) => { + await runMigrationNeeded({ + prismaClient, + migrationFiles: exampleMigrationFiles1, + schema: 'public', + }); + + await tx.$executeRaw`INSERT INTO test (name) VALUES ('test_value')`; + const result = await tx.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].name).toBe('test_value'); + }, { + isolationLevel: undefined, + }); +})); + +import.meta.vitest?.test("applies migration while running concurrent interactive transactions", runTest(async ({ expect, prismaClient, dbURL }) => { + const runTransactionWithMigration = async (testValue: string) => { + return await prismaClient.$transaction(async (tx) => { + await runMigrationNeeded({ + prismaClient, + schema: 'public', + migrationFiles: exampleMigrationFiles1, + artificialDelayInSeconds: 1, + }); + + await tx.$executeRaw`INSERT INTO test (name) VALUES (${testValue})`; + return testValue; + }); + }; + + const results = await Promise.all([ + runTransactionWithMigration('concurrent_tx_1'), + runTransactionWithMigration('concurrent_tx_2'), + ]); + + expect(results).toEqual(['concurrent_tx_1', 'concurrent_tx_2']); + + const result = await prismaClient.$queryRaw`SELECT name FROM test` as { name: string }[]; + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + expect(result.some(r => r.name === 'concurrent_tx_1')).toBe(true); + expect(result.some(r => r.name === 'concurrent_tx_2')).toBe(true); +})); + +import.meta.vitest?.test("does not apply migrations if they are already applied", runTest(async ({ expect, prismaClient, dbURL }) => { + await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, schema: 'public' }); + const result = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, schema: 'public' }); + expect(result.newlyAppliedMigrationNames).toEqual([]); +})); + +import.meta.vitest?.test("does not apply a migration again if all migrations are already applied, and some future migrations are also applied (rollback scenario)", runTest(async ({ expect, prismaClient, dbURL }) => { + // First, apply all migrations + const initialResult = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, schema: 'public' }); + expect(initialResult.newlyAppliedMigrationNames).toEqual(['001-create-table', '002-update-table']); + + // Verify the table structure is complete + await prismaClient.$executeRaw`INSERT INTO test (name, age) VALUES ('test_value', 25)`; + const fullResult = await prismaClient.$queryRaw`SELECT name, age FROM test` as { name: string, age: number }[]; + expect(fullResult.length).toBe(1); + expect(fullResult[0].name).toBe('test_value'); + expect(fullResult[0].age).toBe(25); + + // Now try to apply only a subset of the migrations (simulating a rollback scenario) + // This should not re-apply any migrations since they're already applied + const subsetResult = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1.slice(0, 1), schema: 'public' }); + expect(subsetResult.newlyAppliedMigrationNames).toEqual([]); + + // Verify the data is still intact and no migrations were re-run + const finalResult = await prismaClient.$queryRaw`SELECT name, age FROM test` as { name: string, age: number }[]; + expect(finalResult.length).toBe(1); + expect(finalResult[0].name).toBe('test_value'); + expect(finalResult[0].age).toBe(25); +})); + +import.meta.vitest?.test("a migration that fails for whatever reasons rolls back all statements successfully, and then reapplying a fixed version of the migration is also successful", runTest(async ({ expect, prismaClient, dbURL }) => { + const exampleMigration3 = { + migrationName: '003-create-table', + sql: ` + CREATE TABLE should_exist_after_the_third_migration (id INTEGER); + `, + }; + const failingMigrationFiles = [...exampleMigrationFiles1.slice(0, -1), { + migrationName: exampleMigrationFiles1[exampleMigrationFiles1.length - 1].migrationName, + sql: ` + CREATE TABLE should_not_exist (id INTEGER); + SELECT 1/0; + ` + }, exampleMigration3]; + + await expect(applyMigrations({ prismaClient, migrationFiles: failingMigrationFiles, schema: 'public' })).rejects.toThrow(); + + // Verify that the first part of the migration was applied but rolled back + await expect(prismaClient.$queryRaw`SELECT * FROM test`).resolves.toBeDefined(); + + // Verify that the table from the third migration was also not created due to rollback + await expect(prismaClient.$queryRaw`SELECT * FROM should_exist_after_the_third_migration`).rejects.toThrow(); + + // Verify that the failing table was not created due to rollback + await expect(prismaClient.$queryRaw`SELECT * FROM should_not_exist`).rejects.toThrow(); + + const result = await applyMigrations({ prismaClient, migrationFiles: [...exampleMigrationFiles1, exampleMigration3], schema: 'public' }); + expect(result.newlyAppliedMigrationNames).toEqual(['002-update-table', '003-create-table']); + + await expect(prismaClient.$queryRaw`SELECT * FROM should_exist_after_the_third_migration`).resolves.toBeDefined(); +})); diff --git a/apps/backend/src/auto-migrations/index.tsx b/apps/backend/src/auto-migrations/index.tsx new file mode 100644 index 000000000..07dfdb2d3 --- /dev/null +++ b/apps/backend/src/auto-migrations/index.tsx @@ -0,0 +1,194 @@ +import { sqlQuoteIdent } from '@/prisma-client'; +import { Prisma, PrismaClient } from '@prisma/client'; +import { MIGRATION_FILES } from './../generated/migration-files'; + +// The bigint key for the pg advisory lock +const MIGRATION_LOCK_ID = 59129034; +class MigrationNeededError extends Error { + constructor() { + super('MIGRATION_NEEDED'); + this.name = 'MigrationNeededError'; + } +} + +function getMigrationError(error: unknown): string { + // P2010: Raw query failed error + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2010') { + if (error.meta?.code === 'P0001') { + const errorName = (error.meta as { message: string }).message.split(' ')[1]; + return errorName; + } + } + throw error; +} + +function isMigrationNeededError(error: unknown): boolean { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + // 42P01: relation does not exist error + if (/relation "(?:.*\.)?SchemaMigration" does not exist/.test(error.message) || /No such table: (?:.*\.)?SchemaMigration/.test(error.message)) { + return true; + } + } + if (error instanceof MigrationNeededError) { + return true; + } + return false; +} + +async function getAppliedMigrations(options: { + prismaClient: PrismaClient, + schema: string, +}) { + const [_1, _2, _3, appliedMigrations] = await options.prismaClient.$transaction([ + options.prismaClient.$executeRaw`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID})`, + options.prismaClient.$executeRaw(Prisma.sql` + SET search_path TO ${sqlQuoteIdent(options.schema)}; + `), + options.prismaClient.$executeRaw` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "SchemaMigration" ( + "id" TEXT NOT NULL DEFAULT gen_random_uuid(), + "finishedAt" TIMESTAMP(3) NOT NULL, + "migrationName" TEXT NOT NULL UNIQUE, + CONSTRAINT "SchemaMigration_pkey" PRIMARY KEY ("id") + ); + + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = '_prisma_migrations' + ) THEN + INSERT INTO "SchemaMigration" ("migrationName", "finishedAt") + SELECT + migration_name, + finished_at + FROM _prisma_migrations + WHERE migration_name NOT IN ( + SELECT "migrationName" FROM "SchemaMigration" + ) + AND finished_at IS NOT NULL; + END IF; + END $$; + `, + options.prismaClient.$queryRaw`SELECT "migrationName" FROM "SchemaMigration"`, + ]); + + return (appliedMigrations as { migrationName: string }[]).map((migration) => migration.migrationName); +} + +export async function applyMigrations(options: { + prismaClient: PrismaClient, + migrationFiles?: { migrationName: string, sql: string }[], + artificialDelayInSeconds?: number, + logging?: boolean, + schema: string, +}): Promise<{ + newlyAppliedMigrationNames: string[], +}> { + const migrationFiles = options.migrationFiles ?? MIGRATION_FILES; + const appliedMigrationNames = await getAppliedMigrations({ prismaClient: options.prismaClient, schema: options.schema }); + const newMigrationFiles = migrationFiles.filter(x => !appliedMigrationNames.includes(x.migrationName)); + + const newlyAppliedMigrationNames = []; + for (const migration of newMigrationFiles) { + if (options.logging) { + console.log(`Applying migration ${migration.migrationName}`); + } + + const transaction = []; + + transaction.push(options.prismaClient.$executeRaw` + SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID}); + `); + + transaction.push(options.prismaClient.$executeRaw(Prisma.sql` + SET search_path TO ${sqlQuoteIdent(options.schema)}; + `)); + + transaction.push(options.prismaClient.$executeRaw` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM "SchemaMigration" + WHERE "migrationName" = '${Prisma.raw(migration.migrationName)}' + ) THEN + RAISE EXCEPTION 'MIGRATION_ALREADY_APPLIED'; + END IF; + END + $$; + `); + + for (const statement of migration.sql.split('SPLIT_STATEMENT_SENTINEL')) { + if (statement.includes('SINGLE_STATEMENT_SENTINEL')) { + transaction.push(options.prismaClient.$queryRaw`${Prisma.raw(statement)}`); + } else { + transaction.push(options.prismaClient.$executeRaw` + DO $$ + BEGIN + ${Prisma.raw(statement)} + END + $$; + `); + } + } + + if (options.artificialDelayInSeconds) { + transaction.push(options.prismaClient.$executeRaw` + SELECT pg_sleep(${options.artificialDelayInSeconds}); + `); + } + + transaction.push(options.prismaClient.$executeRaw` + INSERT INTO "SchemaMigration" ("migrationName", "finishedAt") + VALUES (${migration.migrationName}, clock_timestamp()) + `); + try { + await options.prismaClient.$transaction(transaction); + } catch (e) { + const error = getMigrationError(e); + if (error === 'MIGRATION_ALREADY_APPLIED') { + if (options.logging) { + console.log(`Migration ${migration.migrationName} already applied, skipping`); + } + continue; + } + throw e; + } + + newlyAppliedMigrationNames.push(migration.migrationName); + } + + return { newlyAppliedMigrationNames }; +}; + +export async function runMigrationNeeded(options: { + prismaClient: PrismaClient, + schema: string, + migrationFiles?: { migrationName: string, sql: string }[], + artificialDelayInSeconds?: number, +}): Promise { + const migrationFiles = options.migrationFiles ?? MIGRATION_FILES; + + try { + const result = await options.prismaClient.$queryRaw(Prisma.sql` + SELECT * FROM ${sqlQuoteIdent(options.schema)}."SchemaMigration" + ORDER BY "finishedAt" ASC + `); + for (const migration of migrationFiles) { + if (!(result as any).includes(migration.migrationName)) { + throw new MigrationNeededError(); + } + } + } catch (e) { + if (isMigrationNeededError(e)) { + await applyMigrations({ + prismaClient: options.prismaClient, + migrationFiles: options.migrationFiles, + artificialDelayInSeconds: options.artificialDelayInSeconds, + schema: options.schema, + }); + } else { + throw e; + } + } +} diff --git a/apps/backend/src/auto-migrations/utils.tsx b/apps/backend/src/auto-migrations/utils.tsx new file mode 100644 index 000000000..e8b381acc --- /dev/null +++ b/apps/backend/src/auto-migrations/utils.tsx @@ -0,0 +1,31 @@ +import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; +import fs from "fs"; +import path from "path"; + +export const MIGRATION_FILES_DIR = path.join(process.cwd(), 'prisma', 'migrations'); + +export function getMigrationFiles(migrationDir: string): { migrationName: string, sql: string }[] { + const folders = fs.readdirSync(migrationDir).filter(folder => + fs.statSync(path.join(migrationDir, folder)).isDirectory() + ); + + const result: { migrationName: string, sql: string }[] = []; + + for (const folder of folders) { + const folderPath = path.join(migrationDir, folder); + const sqlFiles = fs.readdirSync(folderPath).filter(file => file.endsWith('.sql')); + + for (const sqlFile of sqlFiles) { + const sqlContent = fs.readFileSync(path.join(folderPath, sqlFile), 'utf8'); + result.push({ + migrationName: folder, + sql: sqlContent + }); + } + } + + result.sort((a, b) => stringCompare(a.migrationName, b.migrationName)); + + return result; +} + diff --git a/apps/backend/src/lib/emails.tsx b/apps/backend/src/lib/emails.tsx index 2a40ba24e..0e1d0d51a 100644 --- a/apps/backend/src/lib/emails.tsx +++ b/apps/backend/src/lib/emails.tsx @@ -266,7 +266,9 @@ export async function sendEmailWithoutRetries(options: SendEmailOptions): Promis throw new StackAssertionError("Tenancy not found"); } - await getPrismaClientForTenancy(tenancy).sentEmail.create({ + const prisma = await getPrismaClientForTenancy(tenancy); + + await prisma.sentEmail.create({ data: { tenancyId: options.tenancyId, to: typeof options.to === 'string' ? [options.to] : options.to, diff --git a/apps/backend/src/lib/notification-categories.ts b/apps/backend/src/lib/notification-categories.ts index df7a49eb6..f39ae3a67 100644 --- a/apps/backend/src/lib/notification-categories.ts +++ b/apps/backend/src/lib/notification-categories.ts @@ -31,7 +31,10 @@ export const hasNotificationEnabled = async (tenancy: Tenancy, userId: string, n if (!notificationCategory) { throw new StackAssertionError('Invalid notification category id', { notificationCategoryId }); } - const userNotificationPreference = await getPrismaClientForTenancy(tenancy).userNotificationPreference.findFirst({ + + const prisma = await getPrismaClientForTenancy(tenancy); + + const userNotificationPreference = await prisma.userNotificationPreference.findFirst({ where: { tenancyId: tenancy.id, projectUserId: userId, diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index 08175b1b9..c4af49812 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -232,7 +232,7 @@ export async function createOrUpdateProject( // Update owner metadata const internalEnvironmentConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ projectId: "internal", branchId: DEFAULT_BRANCH_ID })); - const prisma = getPrismaClientForSourceOfTruth(internalEnvironmentConfig.sourceOfTruth, DEFAULT_BRANCH_ID); + const prisma = await getPrismaClientForSourceOfTruth(internalEnvironmentConfig.sourceOfTruth, DEFAULT_BRANCH_ID); await prisma.$transaction(async (tx) => { for (const userId of options.ownerIds ?? []) { const projectUserTx = await tx.projectUser.findUnique({ diff --git a/apps/backend/src/oauth/model.tsx b/apps/backend/src/oauth/model.tsx index 1fc81fa48..3af89542b 100644 --- a/apps/backend/src/oauth/model.tsx +++ b/apps/backend/src/oauth/model.tsx @@ -136,7 +136,8 @@ export class OAuthModel implements AuthorizationCodeModel { async saveToken(token: Token, client: Client, user: User): Promise { if (token.refreshToken) { const tenancy = await getSoleTenancyFromProjectBranch(...getProjectBranchFromClientId(client.id)); - const projectUser = await getPrismaClientForTenancy(tenancy).projectUser.findUniqueOrThrow({ + const prisma = await getPrismaClientForTenancy(tenancy); + const projectUser = await prisma.projectUser.findUniqueOrThrow({ where: { tenancyId_projectUserId: { tenancyId: tenancy.id, diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index 77ecfe32f..13218a24e 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -2,13 +2,14 @@ import { PrismaNeon } from "@prisma/adapter-neon"; import { PrismaPg } from '@prisma/adapter-pg'; import { Prisma, PrismaClient } from "@prisma/client"; import { OrganizationRenderedConfig } from "@stackframe/stack-shared/dist/config/schema"; -import { getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; +import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { globalVar } from "@stackframe/stack-shared/dist/utils/globals"; import { deepPlainEquals, filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects"; import { ignoreUnhandledRejection } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { isPromise } from "util/types"; +import { runMigrationNeeded } from "./auto-migrations"; import { Tenancy } from "./lib/tenancies"; import { traceSpan } from "./utils/telemetry"; @@ -28,6 +29,8 @@ if (getNodeEnvironment().includes('development')) { } export const globalPrismaClient = prismaClientsStore.global; +const dbString = getEnvVariable("STACK_DIRECT_DATABASE_CONNECTION_STRING", ""); +export const globalPrismaSchema = dbString === "" ? "public" : getSchemaFromConnectionString(dbString); function getNeonPrismaClient(connectionString: string) { let neonPrismaClient = prismaClientsStore.neon.get(connectionString); @@ -36,11 +39,16 @@ function getNeonPrismaClient(connectionString: string) { neonPrismaClient = new PrismaClient({ adapter }); prismaClientsStore.neon.set(connectionString, neonPrismaClient); } + return neonPrismaClient; } -export function getPrismaClientForTenancy(tenancy: Tenancy) { - return getPrismaClientForSourceOfTruth(tenancy.completeConfig.sourceOfTruth, tenancy.branchId); +function getSchemaFromConnectionString(connectionString: string) { + return (new URL(connectionString)).searchParams.get('schema') ?? "public"; +} + +export async function getPrismaClientForTenancy(tenancy: Tenancy) { + return await getPrismaClientForSourceOfTruth(tenancy.completeConfig.sourceOfTruth, tenancy.branchId); } export function getPrismaSchemaForTenancy(tenancy: Tenancy) { @@ -50,45 +58,49 @@ export function getPrismaSchemaForTenancy(tenancy: Tenancy) { function getPostgresPrismaClient(connectionString: string) { let postgresPrismaClient = prismaClientsStore.postgres.get(connectionString); if (!postgresPrismaClient) { - const schema = (new URL(connectionString)).searchParams.get('schema'); + const schema = getSchemaFromConnectionString(connectionString); const adapter = new PrismaPg({ connectionString }, schema ? { schema } : undefined); postgresPrismaClient = { client: new PrismaClient({ adapter }), - schema: schema ?? null, + schema, }; prismaClientsStore.postgres.set(connectionString, postgresPrismaClient); } return postgresPrismaClient; } -export function getPrismaClientForSourceOfTruth(sourceOfTruth: OrganizationRenderedConfig["sourceOfTruth"], branchId: string) { +export async function getPrismaClientForSourceOfTruth(sourceOfTruth: OrganizationRenderedConfig["sourceOfTruth"], branchId: string) { switch (sourceOfTruth.type) { case 'neon': { if (!(branchId in sourceOfTruth.connectionStrings)) { throw new Error(`No connection string provided for Neon source of truth for branch ${branchId}`); } - return getNeonPrismaClient(sourceOfTruth.connectionStrings[branchId]); + const connectionString = sourceOfTruth.connectionStrings[branchId]; + const neonPrismaClient = getNeonPrismaClient(connectionString); + await runMigrationNeeded({ prismaClient: neonPrismaClient, schema: getSchemaFromConnectionString(connectionString) }); + return neonPrismaClient; } case 'postgres': { - return getPostgresPrismaClient(sourceOfTruth.connectionString).client; + const postgresPrismaClient = getPostgresPrismaClient(sourceOfTruth.connectionString); + await runMigrationNeeded({ prismaClient: postgresPrismaClient.client, schema: getSchemaFromConnectionString(sourceOfTruth.connectionString) }); + return postgresPrismaClient.client; } case 'hosted': { return globalPrismaClient; } - default: { - // @ts-expect-error sourceOfTruth should be never, otherwise we're missing a switch-case - throw new StackAssertionError(`Unknown source of truth type: ${sourceOfTruth.type}`); - } } } export function getPrismaSchemaForSourceOfTruth(sourceOfTruth: OrganizationRenderedConfig["sourceOfTruth"], branchId: string) { switch (sourceOfTruth.type) { case 'postgres': { - return getPostgresPrismaClient(sourceOfTruth.connectionString).schema ?? 'public'; + return getSchemaFromConnectionString(sourceOfTruth.connectionString); } - default: { - return 'public'; + case 'neon': { + return getSchemaFromConnectionString(sourceOfTruth.connectionStrings[branchId]); + } + case 'hosted': { + return globalPrismaSchema; } } } @@ -280,13 +292,14 @@ async function rawQueryArray[]>(tx: PrismaClientTransact // Prisma does a query for every rawQuery call by default, even if we batch them with transactions // So, instead we combine all queries into one, and then return them as a single JSON result - const combinedQuery = RawQuery.all(queries); + const combinedQuery = RawQuery.all([...queries]); // TODO: check that combinedQuery supports the prisma client that created tx // Supabase's index advisor only analyzes rows that start with "SELECT" (for some reason) // Since ours starts with "WITH", we prepend a SELECT to it const sqlQuery = Prisma.sql`SELECT * FROM (${combinedQuery.sql}) AS _`; + const rawResult = await tx.$queryRaw(sqlQuery); const postProcessed = combinedQuery.postProcess(rawResult as any); diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts index e4c46fede..81122e248 100644 --- a/apps/backend/vitest.config.ts +++ b/apps/backend/vitest.config.ts @@ -1,7 +1,24 @@ +import { resolve } from 'path' +import { loadEnv } from 'vite' import { defineConfig, mergeConfig } from 'vitest/config' import sharedConfig from '../../vitest.shared' export default mergeConfig( sharedConfig, - defineConfig({}), + defineConfig({ + test: { + testTimeout: 20000, + env: { + ...loadEnv('', process.cwd(), ''), + ...loadEnv('development', process.cwd(), ''), + }, + }, + resolve: { + alias: { + '@': resolve(__dirname, './src') + } + }, + envDir: __dirname, + envPrefix: 'STACK_', + }) ) diff --git a/apps/dashboard/.env b/apps/dashboard/.env index d3af72cc3..83c48bc03 100644 --- a/apps/dashboard/.env +++ b/apps/dashboard/.env @@ -1,7 +1,7 @@ # Basic NEXT_PUBLIC_STACK_API_URL=# enter your stack endpoint here, For local development: http://localhost:8102 (no trailing slash) NEXT_PUBLIC_STACK_PROJECT_ID=internal -NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your Stack publishable client key here. For local development, just enter a random string, then run `pnpm prisma migrate reset` +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your Stack publishable client key here. For local development, just enter a random string, then run `pnpm db:reset` STACK_SECRET_SERVER_KEY=# enter your Stack secret client key here. For local development, do the same as above NEXT_PUBLIC_STACK_EXTRA_REQUEST_HEADERS=# a list of extra request headers to add to all Stack Auth API requests, as a JSON record diff --git a/apps/e2e/.env b/apps/e2e/.env index 447fc74f1..9bacd1152 100644 --- a/apps/e2e/.env +++ b/apps/e2e/.env @@ -5,5 +5,6 @@ STACK_INTERNAL_PROJECT_CLIENT_KEY= STACK_INTERNAL_PROJECT_SERVER_KEY= STACK_INTERNAL_PROJECT_ADMIN_KEY= STACK_TEST_SOURCE_OF_TRUTH= +STACK_DIRECT_DATABASE_CONNECTION_STRING= INBUCKET_API_URL= diff --git a/apps/e2e/.env.development b/apps/e2e/.env.development index f7db7d8f2..e83a15f83 100644 --- a/apps/e2e/.env.development +++ b/apps/e2e/.env.development @@ -4,6 +4,7 @@ STACK_INTERNAL_PROJECT_ID=internal STACK_INTERNAL_PROJECT_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only STACK_INTERNAL_PROJECT_SERVER_KEY=this-secret-server-key-is-for-local-development-only STACK_INTERNAL_PROJECT_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only +STACK_DIRECT_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/stackframe INBUCKET_API_URL=http://localhost:8105 STACK_SVIX_SERVER_URL=http://localhost:8113 diff --git a/docker/server/.env b/docker/server/.env index 4d3d79bd3..7e34dc6a0 100644 --- a/docker/server/.env +++ b/docker/server/.env @@ -29,6 +29,6 @@ STACK_SVIX_SERVER_URL=# this is only needed if you self-host the Svix service NEXT_PUBLIC_STACK_SVIX_SERVER_URL=# this is only needed if you are using docker compose and the external and internal urls are different. This is the external url for the Svix service. STACK_SVIX_API_KEY= +STACK_OPENAI_API_KEY=# enter your openai api key if you want to use the openai related features -STACK_SKIP_MIGRATIONS=# true to skip prisma migrations STACK_SKIP_SEED_SCRIPT=# true to skip the seed script diff --git a/docs/templates/others/self-host.mdx b/docs/templates/others/self-host.mdx index e242838a2..7dcf3223b 100644 --- a/docs/templates/others/self-host.mdx +++ b/docs/templates/others/self-host.mdx @@ -146,13 +146,7 @@ pnpm start:dashboard You need to initialize the database with the following command with the backend environment variables set: ```sh -pnpm prisma migrate deploy -``` - -The database is still empty; you need to create a project with the ID "internal" used by the dashboard to authenticate itself. You can do this with the following command: - -```sh -pnpm prisma db seed +pnpm db:init ``` Now you can go to the dashboard (e.g., https://your-dashboard-url.com) and sign up for an account. diff --git a/package.json b/package.json index 82accaa0c..c39ec1d36 100644 --- a/package.json +++ b/package.json @@ -26,16 +26,20 @@ "codegen": "pnpm pre && turbo run codegen && pnpm run generate-sdks && pnpm run generate-openapi && pnpm run generate-openapi-fumadocs", "deps-compose": "docker compose -p stack-dependencies -f docker/dependencies/docker.compose.yaml", "stop-deps": "POSTGRES_DELAY_MS=0 pnpm run deps-compose kill && POSTGRES_DELAY_MS=0 pnpm run deps-compose down -v", - "init-db": "pnpm pre && pnpm run prisma migrate deploy && pnpm run prisma db seed", "wait-until-postgres-is-ready:pg_isready": "until pg_isready -h localhost -p 5432; do sleep 1; done", "wait-until-postgres-is-ready": "command -v pg_isready >/dev/null 2>&1 && pnpm run wait-until-postgres-is-ready:pg_isready || sleep 10 # not everyone has pg_isready installed, so we fallback to sleeping", - "start-deps:no-delay": "pnpm pre && pnpm run deps-compose up --detach --build && pnpm run wait-until-postgres-is-ready && pnpm run init-db && echo \"\\nDependencies started in the background as Docker containers. 'pnpm run stop-deps' to stop them\"n", + "start-deps:no-delay": "pnpm pre && pnpm run deps-compose up --detach --build && pnpm run wait-until-postgres-is-ready && pnpm run db:init && echo \"\\nDependencies started in the background as Docker containers. 'pnpm run stop-deps' to stop them\"n", "start-deps": "POSTGRES_DELAY_MS=${POSTGRES_DELAY_MS:-20} pnpm run start-deps:no-delay", "restart-deps": "pnpm pre && pnpm run stop-deps && pnpm run start-deps", "restart-deps:no-delay": "pnpm pre && pnpm run stop-deps && pnpm run start-deps:no-delay", "psql": "pnpm pre && pnpm run --filter=@stackframe/stack-backend psql", "explain-query": "pnpm pre && echo 'Paste your query (end with Ctrl-D):' && query=$(cat) && echo 'Connecting to Postgres...' && printf \"EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON)\n$query\" | pnpm run --silent psql -qAt | sed -n '/\\[/,$p' > explained-query.untracked.json && echo 'Explained query saved to explained-query.untracked.json. To analyze it, open it in the query analyzer at https://tatiyants.com/pev/#/plans/new'", - "prisma": "pnpm pre && pnpm run --filter=@stackframe/stack-backend prisma", + "db:migration-gen": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:migration-gen", + "db:reset": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:reset", + "db:seed": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:seed", + "db:init": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:init", + "db:migrate": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:migrate", + "fern": "pnpm pre && pnpm run --filter=@stackframe/docs fern", "dev:full": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-docs:watch\" \"turbo run dev --concurrency 99999\"", "dev": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-docs:watch\" \"pnpm run generate-openapi-docs:watch\" \"turbo run dev --concurrency 99999 --filter=./apps/* --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo \"", "dev:basic": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-docs:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/stack-backend --filter=@stackframe/stack-dashboard --filter=@stackframe/mock-oauth-server\"", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c81e809d9..37b1f5b55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,7 +47,7 @@ importers: version: 6.21.0(eslint@8.30.0)(typescript@5.3.3) '@vitejs/plugin-react': specifier: ^4.3.3 - version: 4.3.3(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0)) + version: 4.3.3(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0)) chokidar-cli: specifier: ^3.0.0 version: 3.0.0 @@ -98,10 +98,10 @@ importers: version: 5.3.3 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.3.3)(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0)) + version: 4.3.2(typescript@5.3.3)(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0)) vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.17.6)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.31.1) + version: 1.6.0(@types/node@20.17.6)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.43.1) wait-on: specifier: ^8.0.1 version: 8.0.1 @@ -189,6 +189,12 @@ importers: bcrypt: specifier: ^5.1.1 version: 5.1.1(encoding@0.1.13) + chokidar-cli: + specifier: ^3.0.0 + version: 3.0.0 + dotenv: + specifier: ^16.4.5 + version: 16.4.7 dotenv-cli: specifier: ^7.3.0 version: 7.4.1 @@ -216,6 +222,9 @@ importers: pg: specifier: ^8.16.3 version: 8.16.3 + postgres: + specifier: ^3.4.5 + version: 3.4.5 posthog-node: specifier: ^4.1.0 version: 4.1.0 @@ -234,6 +243,9 @@ importers: svix: specifier: ^1.25.0 version: 1.25.0(encoding@0.1.13) + vite: + specifier: ^6.1.0 + version: 6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.15.5)(yaml@2.4.5) yaml: specifier: ^2.4.5 version: 2.4.5 @@ -378,7 +390,7 @@ importers: version: 0.2.1(next@15.4.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) posthog-js: specifier: ^1.235.0 - version: 1.235.4 + version: 1.255.1 react: specifier: 19.0.0 version: 19.0.0 @@ -612,7 +624,7 @@ importers: version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) posthog-js: specifier: ^1.235.0 - version: 1.235.4 + version: 1.255.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -872,7 +884,7 @@ importers: version: 5.3.3 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0) + version: 6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0) examples/middleware: dependencies: @@ -968,7 +980,7 @@ importers: version: 18.3.1 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0)) + version: 4.3.4(vite@6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0)) eslint: specifier: ^9.19.0 version: 9.21.0(jiti@2.4.2) @@ -989,7 +1001,7 @@ importers: version: 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.3.3) vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0) + version: 6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0) examples/supabase: dependencies: @@ -13609,8 +13621,12 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - posthog-js@1.235.4: - resolution: {integrity: sha512-CcAQpw7oaIoOwyaeqNZoKjciIMygrjgn6+cBSWFQcbo7aEmiO2666BZHZH/GBFmz0g2/w5abSpO7UntAj/69dw==} + postgres@3.4.5: + resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} + engines: {node: '>=12'} + + posthog-js@1.255.1: + resolution: {integrity: sha512-KMh0o9MhORhEZVjXpktXB5rJ8PfDk+poqBoTSoLzWgNjhJf6D8jcyB9jUMA6vVPfn4YeepVX5NuclDRqOwr5Mw==} peerDependencies: '@rrweb/types': 2.0.0-alpha.17 rrweb-snapshot: 2.0.0-alpha.17 @@ -19021,14 +19037,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.15.18 jest-mock: 29.7.0 '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.17.6 + '@types/node': 22.15.18 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -19070,7 +19086,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.6 + '@types/node': 22.15.18 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -22426,7 +22442,7 @@ snapshots: '@babel/core': 7.26.0 '@sentry/babel-plugin-component-annotate': 2.22.6 '@sentry/cli': 2.38.2(encoding@0.1.13) - dotenv: 16.4.5 + dotenv: 16.4.7 find-up: 5.0.0 glob: 9.3.5 magic-string: 0.30.8 @@ -23279,7 +23295,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.17.6 + '@types/node': 22.15.18 '@types/hast@2.3.10': dependencies: @@ -23793,25 +23809,25 @@ snapshots: next: 15.4.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 - '@vitejs/plugin-react@4.3.3(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0))': + '@vitejs/plugin-react@4.3.3(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0) + vite: 6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0))': + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0))': dependencies: '@babel/core': 7.26.9 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0) + vite: 6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0) transitivePeerDependencies: - supports-color @@ -24952,7 +24968,7 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 20.17.6 + '@types/node': 22.15.18 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -24963,7 +24979,7 @@ snapshots: chromium-edge-launcher@0.2.0: dependencies: - '@types/node': 20.17.6 + '@types/node': 22.15.18 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -25743,7 +25759,7 @@ snapshots: dotenv-cli@7.4.1: dependencies: cross-spawn: 7.0.3 - dotenv: 16.4.5 + dotenv: 16.4.7 dotenv-expand: 10.0.0 minimist: 1.2.8 @@ -28528,7 +28544,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.15.18 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -28540,7 +28556,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.17.6 + '@types/node': 22.15.18 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -28567,7 +28583,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.15.18 jest-util: 29.7.0 jest-regex-util@29.6.3: {} @@ -28575,7 +28591,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.15.18 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -28598,7 +28614,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.17.6 + '@types/node': 22.15.18 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -30982,7 +30998,9 @@ snapshots: dependencies: xtend: 4.0.2 - posthog-js@1.235.4: + postgres@3.4.5: {} + + posthog-js@1.255.1: dependencies: core-js: 3.41.0 fflate: 0.4.8 @@ -33927,13 +33945,13 @@ snapshots: replace-ext: 2.0.0 teex: 1.0.1 - vite-node@1.6.0(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.31.1): + vite-node@1.6.0(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.43.1): dependencies: cac: 6.7.14 debug: 4.4.0 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.14(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.31.1) + vite: 5.4.14(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.43.1) transitivePeerDependencies: - '@types/node' - less @@ -33945,18 +33963,18 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0)): + vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.3.3) optionalDependencies: - vite: 6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0) + vite: 6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.14(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.31.1): + vite@5.4.14(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.43.1): dependencies: esbuild: 0.21.5 postcss: 8.5.3 @@ -33965,9 +33983,9 @@ snapshots: '@types/node': 20.17.6 fsevents: 2.3.3 lightningcss: 1.30.1 - terser: 5.31.1 + terser: 5.43.1 - vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0): + vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.15.5)(yaml@2.4.5): dependencies: esbuild: 0.24.2 postcss: 8.5.3 @@ -33977,11 +33995,25 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.31.1 + terser: 5.43.1 + tsx: 4.15.5 + yaml: 2.4.5 + + vite@6.1.0(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.5.3 + rollup: 4.34.8 + optionalDependencies: + '@types/node': 20.17.6 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.43.1 tsx: 4.19.3 yaml: 2.6.0 - vite@6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.31.1)(tsx@4.19.3)(yaml@2.6.0): + vite@6.1.0(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.3)(yaml@2.6.0): dependencies: esbuild: 0.24.2 postcss: 8.5.3 @@ -33991,11 +34023,11 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.31.1 + terser: 5.43.1 tsx: 4.19.3 yaml: 2.6.0 - vitest@1.6.0(@types/node@20.17.6)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.31.1): + vitest@1.6.0(@types/node@20.17.6)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.43.1): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -34014,8 +34046,8 @@ snapshots: strip-literal: 2.1.0 tinybench: 2.9.0 tinypool: 0.8.4 - vite: 5.4.14(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.31.1) - vite-node: 1.6.0(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.31.1) + vite: 5.4.14(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.43.1) + vite-node: 1.6.0(@types/node@20.17.6)(lightningcss@1.30.1)(terser@5.43.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.17.6 diff --git a/turbo.json b/turbo.json index a8fc81c98..72bcb980f 100644 --- a/turbo.json +++ b/turbo.json @@ -94,9 +94,6 @@ "codegen": { "cache": false }, - "prisma": { - "cache": false - }, "typecheck": { "dependsOn": [] }, @@ -105,6 +102,21 @@ }, "generate-keys": { "cache": false + }, + "db:migration-gen": { + "cache": false + }, + "db:reset": { + "cache": false + }, + "db:seed": { + "cache": false + }, + "db:init": { + "cache": false + }, + "db:migrate": { + "cache": false } } }