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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ CODE_OF_CONDUCT.md
components.json
CONTRIBUTING.md
Dockerfile
compose.yml
.dockerignore
LICENSE
next-env.d.ts
prettier.config.js
Expand Down
29 changes: 8 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ WORKDIR /app
RUN npm i -g corepack@latest && corepack enable
COPY package.json pnpm-lock.yaml prisma/ ./

RUN pnpm install
RUN pnpm install --frozen-lockfile

COPY . .

Expand All @@ -25,34 +25,21 @@ FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS release
ARG APP_VERSION

ENV NODE_ENV=production
ENV DOCKER_OUTPUT=1

WORKDIR /app

RUN apk update \
&& apk add --no-cache libc6-compat \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /var/cache/apk/*

COPY --from=base /app/next.config.js .
COPY --from=base /app/package.json .
COPY --from=base /app/pnpm-lock.yaml .
&& rm -rf /var/cache/apk/*

COPY --from=base /app/.next/standalone ./
COPY --from=base /app/.next/static ./.next/static
COPY --from=base /app/public ./public

COPY --from=base /app/prisma/schema.prisma ./prisma/schema.prisma
COPY --from=base /app/prisma/migrations ./prisma/migrations
COPY --from=base /app/node_modules/prisma ./node_modules/prisma
COPY --from=base /app/node_modules/@prisma ./node_modules/@prisma

RUN npm i -g corepack@latest \
&& corepack enable \
&& mkdir node_modules/.bin \
&& ln -s /app/node_modules/prisma/build/index.js ./node_modules/.bin/prisma
COPY --from=base /app/.next/standalone ./
COPY --from=base /app/.next/static ./.next/static
COPY --from=base /app/public ./public
COPY --from=base /app/prisma/migrations ./migrations

# set this so it throws error where starting server
ENV SKIP_ENV_VALIDATION="false"
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
ENV APP_VERSION=${APP_VERSION}

COPY ./start.sh ./start.sh
Expand Down
3 changes: 0 additions & 3 deletions prisma/migrations/20250920192654_recurrence/migration.sql
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ BEGIN
FROM "Expense"
WHERE id = original_expense_id
RETURNING id INTO new_expense_id;

-- STEP 2: Insert the new expense participants
INSERT INTO "ExpenseParticipant" (
"expenseId", "userId", amount
Expand All @@ -41,12 +40,10 @@ BEGIN
new_expense_id, "userId", amount
FROM "ExpenseParticipant"
WHERE "expenseId" = original_expense_id;

-- STEP 3: Set notified to false in the ExpenseRecurrence table
UPDATE "ExpenseRecurrence"
SET notified = false
WHERE id = (SELECT "recurrenceId" FROM "Expense" WHERE id = original_expense_id);

-- STEP 4: Return the new expense ID
RETURN new_expense_id;
END;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ ALTER TABLE "public"."ExpenseNote" ADD CONSTRAINT "ExpenseNote_expenseUuid_fkey"

-- Migrate data
UPDATE "public"."Expense" SET "uuidId" = gen_random_uuid() WHERE "uuidId" IS NULL;

UPDATE "public"."ExpenseParticipant" SET "expenseUuid" = (SELECT "uuidId" FROM "public"."Expense" WHERE "id" = "expenseId") WHERE "expenseUuid" IS NULL;

UPDATE "public"."ExpenseNote" SET "expenseUuid" = (SELECT "uuidId" FROM "public"."Expense" WHERE "id" = "expenseId") WHERE "expenseUuid" IS NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ WITH "Differences" AS (
FROM "InsertExpenses" ie

UNION ALL

-- Row 2: The Debtor -> Negative Amount
SELECT
ie."id",
Expand All @@ -110,20 +109,16 @@ DECLARE
payer_id INT;
BEGIN
SELECT "paidBy" INTO payer_id FROM "Expense" WHERE id = NEW."expenseId";

-- ONLY update if the array actually contains the ID.
-- This prevents locking the row if the friend is already visible (which is 99% of cases).

UPDATE "User"
SET "hiddenFriendIds" = array_remove("hiddenFriendIds", payer_id)
WHERE id = NEW."userId"
AND "hiddenFriendIds" @> ARRAY[payer_id]; -- Only if array contains payer_id

UPDATE "User"
SET "hiddenFriendIds" = array_remove("hiddenFriendIds", NEW."userId")
WHERE id = payer_id
AND "hiddenFriendIds" @> ARRAY[NEW."userId"]; -- Only if array contains userId

RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Expand Down
2 changes: 2 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const env = createEnv({
'You forgot to change the default URL',
),
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
DOCKER_OUTPUT: z.boolean().default(false),
NEXTAUTH_SECRET: 'production' === process.env.NODE_ENV ? z.string() : z.string().optional(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
Expand Down Expand Up @@ -96,6 +97,7 @@ export const env = createEnv({
process.env.DATABASE_URL ??
`postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT}`,
NODE_ENV: process.env.NODE_ENV,
DOCKER_OUTPUT: !!process.env.DOCKER_OUTPUT,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXTAUTH_URL_INTERNAL: process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL,
Expand Down
7 changes: 7 additions & 0 deletions src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { db } from '~/server/db';

// Import migrations
import { migrateSettlementsToGroups } from './settle_groups';
import { runDbMigrations } from './programmatic-prisma';
import { env } from '~/env';

/**
* Get the current schema version from the database.
Expand Down Expand Up @@ -45,6 +47,11 @@ async function setVersion(version: string): Promise<void> {
* Migrations are run in order and each updates the schema version on success.
*/
export async function runMigrations(): Promise<void> {
if (env.DOCKER_OUTPUT) {
console.log('=== Prisma DB Migrations ===\n');
await runDbMigrations();
}

console.log('=== SplitPro Data Migrations ===\n');

const currentVersion = await getCurrentVersion();
Expand Down
135 changes: 135 additions & 0 deletions src/migrations/programmatic-prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { pipeline } from 'stream/promises';
import { createHash, randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { db } from '~/server/db';
import { env } from '~/env';

const migrationsPath = `${process.cwd()}/migrations`;

export async function runDbMigrations() {
console.info(`Running DB migrations for "${env.NODE_ENV}"`);

// check if any migrations have been applied
const migrationsTable: ({ exists: boolean } | undefined)[] = await db.$queryRaw`
SELECT EXISTS(
SELECT *
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = '_prisma_migrations'
);
`;

const migrationTableExists = migrationsTable[0]?.exists === true;

if (!migrationTableExists) {
await db.$queryRaw`
CREATE TABLE _prisma_migrations (
id VARCHAR(36) PRIMARY KEY NOT NULL,
checksum VARCHAR(64) NOT NULL,
finished_at TIMESTAMPTZ,
migration_name VARCHAR(255) NOT NULL,
logs TEXT,
rolled_back_at TIMESTAMPTZ,
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
applied_steps_count INTEGER NOT NULL DEFAULT 0
);
`;
}

const dbMigrations: {
id: string;
checksum: string;
finished_at: string;
migration_name: string;
logs: string | null;
rolled_back_at: string | null;
started_at: string;
applied_steps_count: number;
}[] = await db.$queryRaw`
SELECT *
FROM _prisma_migrations
`;

const localMigrations = walkMigrationDirectory();

console.info(
`DB migration found (${dbMigrations.length}): ${dbMigrations.map(({ migration_name }) => migration_name).join(', ')}`,
);
console.info(`Local migrations found (${localMigrations.length}): ${localMigrations.join(', ')}`);

let totalMigrationsApplied = 0;
for (const localMigrationName of localMigrations) {
// find local migration in all DB migrations
const existingMigration = dbMigrations.find(
(migration) => migration.migration_name === localMigrationName,
);
if (existingMigration?.rolled_back_at) {
throw new Error(
'Unsupported Prisma migrate feature: Script does not support rolled back migrations',
);
}

if (!existingMigration) {
console.info(`Migration ${localMigrationName} will be executed`);

const migrationContents = readFileSync(
`${migrationsPath}/${localMigrationName}/migration.sql`,
'utf8',
);

// executeRawUnsafe cannot insert multiple commands into a prepared statement
const migrationStatements = migrationContents
.split(';\n\n')
.map((stmt) => stmt.trim() + ';')
.filter((stmt) => stmt.length > 0);

const ops = [
...migrationStatements.map((stmt) => db.$executeRawUnsafe(stmt)),
db.$executeRawUnsafe(`
INSERT INTO _prisma_migrations (
id,
checksum,
migration_name,
finished_at,
started_at,
applied_steps_count,
logs,
rolled_back_at
) VALUES (
'${randomUUID()}',
'${await computeHash(migrationContents)}',
'${localMigrationName}',
now(),
now(),
1,
NULL,
NULL
);
`),
];

await db.$transaction(ops);

console.info(`Migration ${localMigrationName} applied successfully`);

totalMigrationsApplied++;
}
}
console.info(`A total of ${totalMigrationsApplied} migration(s) were applied`);
}

function walkMigrationDirectory() {
return readdirSync(migrationsPath).reduce<string[]>((migrations, file) => {
const dirPath = join(migrationsPath, file);
if (statSync(dirPath).isDirectory()) {
migrations.push(file);
}
return migrations;
}, []);
}

async function computeHash(content: string) {
const hash = createHash('sha256');
await pipeline(content, hash);
return hash.digest('hex');
}
3 changes: 0 additions & 3 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ for file_var in $file_vars; do
echo "Set $base_var from $file_var ($file_path)" >&2
done

echo "Deploying prisma migrations"

pnpx prisma@6 migrate deploy --schema ./prisma/schema.prisma

echo "Starting web server"

Expand Down