Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .github/workflows/check-prisma-migrations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,5 @@ jobs:
- name: Initialize database
run: pnpm run db:init

- name: Remove auto-migration metadata
run: |
cd apps/backend
pnpm run prisma db execute --url postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe --stdin <<'SQL'
DROP TABLE IF EXISTS "SchemaMigration";
SQL

- name: Check for differences in Prisma schema and current DB
run: cd apps/backend && pnpm run prisma migrate diff --from-url postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/shadow_db --exit-code
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "CacheEntry" (
"id" UUID NOT NULL,
"namespace" TEXT NOT NULL,
"cacheKey" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "CacheEntry_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "CacheEntry_namespace_cacheKey_key" ON "CacheEntry"("namespace", "cacheKey");

22 changes: 22 additions & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ datasource db {
directUrl = env("STACK_DIRECT_DATABASE_CONNECTION_STRING")
}

model SchemaMigration {
id String @id @default(dbgenerated("gen_random_uuid()"))
finishedAt DateTime
migrationName String @unique

@@ignore
}



model Project {
// Note that the project with ID `internal` is handled as a special case. All other project IDs are UUIDs.
id String @id
Expand Down Expand Up @@ -828,3 +838,15 @@ model DataVaultEntry {
@@unique([tenancyId, storeId, hashedKey])
@@index([tenancyId, storeId])
}

model CacheEntry {
id String @id @default(uuid()) @db.Uuid
namespace String
cacheKey String
payload Json
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([namespace, cacheKey])
}
106 changes: 95 additions & 11 deletions apps/backend/scripts/db-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { applyMigrations } from "@/auto-migrations";
import { MIGRATION_FILES_DIR, getMigrationFiles } from "@/auto-migrations/utils";
import { globalPrismaClient, globalPrismaSchema, sqlQuoteIdent } from "@/prisma-client";
import { Prisma } from "@prisma/client";
import { execSync } from "child_process";
import * as readline from 'readline';
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import * as readline from "readline";
import { seed } from "../prisma/seed";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";

const dropSchema = async () => {
await globalPrismaClient.$executeRaw(Prisma.sql`DROP SCHEMA ${sqlQuoteIdent(globalPrismaSchema)} CASCADE`);
Expand All @@ -13,26 +16,108 @@ const dropSchema = async () => {
await globalPrismaClient.$executeRaw(Prisma.sql`GRANT ALL ON SCHEMA ${sqlQuoteIdent(globalPrismaSchema)} TO public`);
};

const promptDropDb = async () => {

const askQuestion = (question: string) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
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);
return new Promise<string>((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer);
});
});
rl.close();
};

const promptDropDb = async () => {
const answer = (await askQuestion(
'Are you sure you want to drop everything in the database? This action cannot be undone. (y/N): ',
)).trim();

if (answer.toLowerCase() !== 'y') {
console.log('Operation cancelled');
process.exit(0);
}
};

const migrate = async () => {
const formatMigrationName = (input: string) =>
input
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');

const promptMigrationName = async () => {
while (true) {
const rawName = (await askQuestion('Enter a migration name: ')).trim();
const formattedName = formatMigrationName(rawName);

if (!formattedName) {
console.log('Migration name cannot be empty. Please try again.');
continue;
}

if (formattedName !== rawName) {
console.log(`Using sanitized migration name: ${formattedName}`);
}

return formattedName;
}
};

const timestampPrefix = () => new Date().toISOString().replace(/\D/g, '').slice(0, 14);

const generateMigrationFile = async () => {
const migrationName = await promptMigrationName();
const folderName = `${timestampPrefix()}_${migrationName}`;
const migrationDir = path.join(MIGRATION_FILES_DIR, folderName);
const migrationSqlPath = path.join(migrationDir, 'migration.sql');
const diffUrl = getEnvVariable('STACK_DIRECT_DATABASE_CONNECTION_STRING');

console.log(`Generating migration ${folderName}...`);
const diffResult = spawnSync(
'pnpm',
[
'-s',
'prisma',
'migrate',
'diff',
'--from-url',
diffUrl,
'--to-schema-datamodel',
'prisma/schema.prisma',
'--script',
],
{
cwd: process.cwd(),
encoding: 'utf8',
},
);

if (diffResult.error || diffResult.status !== 0) {
console.error(diffResult.stdout);
console.error(diffResult.stderr);
throw diffResult.error ?? new Error(`Failed to generate migration (exit code ${diffResult.status})`);
}

const sql = diffResult.stdout;

if (!sql.trim()) {
console.log('No schema changes detected. Migration file was not created.');
} else {
fs.mkdirSync(migrationDir, { recursive: true });
fs.writeFileSync(migrationSqlPath, sql, 'utf8');
console.log(`Migration written to ${path.relative(process.cwd(), migrationSqlPath)}`);
console.log('Applying migration...');
await migrate([{ migrationName: folderName, sql }]);
}
};

const migrate = async (selectedMigrationFiles?: { migrationName: string, sql: string }[]) => {
const startTime = performance.now();
const migrationFiles = getMigrationFiles(MIGRATION_FILES_DIR);
const migrationFiles = selectedMigrationFiles ?? getMigrationFiles(MIGRATION_FILES_DIR);
const totalMigrations = migrationFiles.length;

const result = await applyMigrations({
Expand Down Expand Up @@ -98,10 +183,9 @@ const main = async () => {
}
case 'generate-migration-file': {
await promptDropDb();
execSync('pnpm prisma migrate reset --force --skip-seed', { stdio: 'inherit' });
execSync('pnpm prisma migrate dev --skip-seed', { stdio: 'inherit' });
await dropSchema();
await migrate();
await generateMigrationFile();
await seed();
break;
}
Expand Down
Loading