mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Auto migration (#526)
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md
-->
<!-- ELLIPSIS_HIDDEN -->
----
> [!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.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 2c24183879. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>
<!-- ELLIPSIS_HIDDEN -->
---------
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
parent
9f9a1038fa
commit
a7acab4646
@ -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
|
||||
|
||||
3
.github/workflows/e2e-api-tests.yaml
vendored
3
.github/workflows/e2e-api-tests.yaml
vendored
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
107
apps/backend/scripts/db-migrations.ts
Normal file
107
apps/backend/scripts/db-migrations.ts
Normal file
@ -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<string>(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 <command>
|
||||
|
||||
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);
|
||||
});
|
||||
13
apps/backend/scripts/generate-migration-imports.ts
Normal file
13
apps/backend/scripts/generate-migration-imports.ts
Normal file
@ -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`
|
||||
);
|
||||
@ -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 extends "user" | "team">(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 extends "user" | "team">(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 extends "user" | "team">(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 extends "user" | "team">(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 extends "user" | "team">(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 extends "user" | "team">(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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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<UsersCrud["Admin"]["Read"] | null> {
|
||||
const prisma = getPrismaClientForTenancy(tenancy);
|
||||
const prisma = await getPrismaClientForTenancy(tenancy);
|
||||
const contactChannel = await getAuthContactChannel(
|
||||
prisma,
|
||||
{
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
{
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -46,7 +46,7 @@ async function loadUsersByCountry(tenancy: Tenancy): Promise<Record<string, numb
|
||||
|
||||
async function loadTotalUsers(tenancy: Tenancy, now: Date): Promise<DataPoints> {
|
||||
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),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -26,7 +26,7 @@ async function checkInputValidity(options: {
|
||||
allowSignIn: boolean,
|
||||
allowConnectedAccounts: boolean,
|
||||
})): Promise<void> {
|
||||
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 });
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 ? {
|
||||
|
||||
@ -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<Array<{ userId: string, lastActiveAt: Date }>>`
|
||||
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({
|
||||
|
||||
394
apps/backend/src/auto-migrations/auto-migration.tests.ts
Normal file
394
apps/backend/src/auto-migrations/auto-migration.tests.ts
Normal file
@ -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<void>) {
|
||||
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();
|
||||
}));
|
||||
194
apps/backend/src/auto-migrations/index.tsx
Normal file
194
apps/backend/src/auto-migrations/index.tsx
Normal file
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
apps/backend/src/auto-migrations/utils.tsx
Normal file
31
apps/backend/src/auto-migrations/utils.tsx
Normal file
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -136,7 +136,8 @@ export class OAuthModel implements AuthorizationCodeModel {
|
||||
async saveToken(token: Token, client: Client, user: User): Promise<Token | Falsey> {
|
||||
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,
|
||||
|
||||
@ -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<Q extends RawQuery<any>[]>(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);
|
||||
|
||||
@ -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_',
|
||||
})
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
8
docs/templates/others/self-host.mdx
vendored
8
docs/templates/others/self-host.mdx
vendored
@ -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.
|
||||
|
||||
10
package.json
10
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\"",
|
||||
|
||||
114
pnpm-lock.yaml
114
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
|
||||
|
||||
18
turbo.json
18
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user