From 756ec5c002e626e965db8fdfff94f727d19e30ad Mon Sep 17 00:00:00 2001 From: DanielS Date: Tue, 23 Jun 2026 03:14:40 +0200 Subject: [PATCH] feat: add automatic database migration and seeding on docker startup --- Dockerfile | 5 +- shop/migrate.js | 93 ++++++++++++++++++++++++++ shop/package-lock.json | 147 +++++++++++++++++++++++++++++++++++++++++ shop/package.json | 1 + 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 shop/migrate.js diff --git a/Dockerfile b/Dockerfile index 4d6ac64..4411e7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,9 @@ COPY --from=builder /app/.next/static ./.next/static # Den fertigen Standalone-Server kopieren COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/supabase ./supabase +COPY --from=builder /app/migrate.js ./migrate.js # Pass Supabase variables to runtime image ARG NEXT_PUBLIC_SUPABASE_URL @@ -45,4 +48,4 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +CMD ["sh", "-c", "node migrate.js && node server.js"] diff --git a/shop/migrate.js b/shop/migrate.js new file mode 100644 index 0000000..309927c --- /dev/null +++ b/shop/migrate.js @@ -0,0 +1,93 @@ +const { Client } = require('pg'); +const fs = require('fs'); +const path = require('path'); + +async function run() { + const connectionString = process.env.DATABASE_URL; + if (!connectionString) { + console.log('DATABASE_URL not set – skipping migrations.'); + return; + } + + console.log('Starting database migrations...'); + const client = new Client({ + connectionString, + connectionTimeoutMillis: 10000, + }); + await client.connect(); + + // Create schema_migrations table if not exists + await client.query(` + CREATE TABLE IF NOT EXISTS public.schema_migrations ( + version VARCHAR(255) PRIMARY KEY, + inserted_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL + ); + `); + + const migrationsDir = path.join(__dirname, 'supabase', 'migrations'); + + if (!fs.existsSync(migrationsDir)) { + console.warn(`Migrations directory not found at: ${migrationsDir}`); + await client.end(); + return; + } + + const files = fs.readdirSync(migrationsDir).sort(); + + for (const file of files) { + if (!file.endsWith('.sql')) continue; + + const { rows } = await client.query('SELECT 1 FROM public.schema_migrations WHERE version = $1', [file]); + if (rows.length === 0) { + console.log(`Running migration: ${file}`); + const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8'); + + // We run the migration in a single transaction + await client.query('BEGIN'); + try { + await client.query(sql); + await client.query('INSERT INTO public.schema_migrations (version) VALUES ($1)', [file]); + await client.query('COMMIT'); + console.log(`Migration ${file} completed successfully.`); + } catch (err) { + await client.query('ROLLBACK'); + console.error(`Migration ${file} failed, rolled back.`); + throw err; + } + } else { + console.log(`Skipping migration: ${file} (already run)`); + } + } + + // Also check if we should run seed.sql (only if we run it for the first time or if products are empty) + const seedFile = path.join(__dirname, 'supabase', 'seed.sql'); + if (fs.existsSync(seedFile)) { + try { + const { rows: productRows } = await client.query('SELECT 1 FROM public.products LIMIT 1'); + if (productRows.length === 0) { + console.log('No products found. Seeding database with default products...'); + const seedSql = fs.readFileSync(seedFile, 'utf8'); + await client.query('BEGIN'); + try { + await client.query(seedSql); + await client.query('COMMIT'); + console.log('Database seeded successfully.'); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Seeding failed, rolled back.', err); + } + } + } catch (e) { + // In case products table check fails (e.g. initial_schema wasn't run or failed) + console.error('Failed to run seed check/seed SQL', e); + } + } + + await client.end(); + console.log('Database migrations completed.'); +} + +run().catch(err => { + console.error('Migration execution failed:', err); + process.exit(1); +}); diff --git a/shop/package-lock.json b/shop/package-lock.json index 17870bf..33e6ee2 100644 --- a/shop/package-lock.json +++ b/shop/package-lock.json @@ -27,6 +27,7 @@ "next": "latest", "next-themes": "^0.4.6", "nodemailer": "^9.0.1", + "pg": "^8.11.3", "radix-ui": "^1.4.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -7186,6 +7187,95 @@ "dev": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7427,6 +7517,45 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8198,6 +8327,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -9121,6 +9259,15 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/shop/package.json b/shop/package.json index b40c727..89a43ad 100644 --- a/shop/package.json +++ b/shop/package.json @@ -29,6 +29,7 @@ "next": "latest", "next-themes": "^0.4.6", "nodemailer": "^9.0.1", + "pg": "^8.11.3", "radix-ui": "^1.4.3", "react": "^19.0.0", "react-dom": "^19.0.0",