Files
cleanplate_api/bin/migrate.dart
Oracle Public Cloud User 6bd1ab7e9f Initial commit: CleanPlate backend API
Dart Shelf REST API with auth, recipes, AI (Claude), search, and community modules.
PostgreSQL, Redis, Meilisearch. Docker Compose for local dev.
2026-03-04 14:52:13 +00:00

146 lines
4.0 KiB
Dart

import 'dart:io';
import 'package:logging/logging.dart';
import 'package:cleanplate_api/src/config/env.dart';
import 'package:cleanplate_api/src/config/database.dart';
final _log = Logger('Migrate');
/// Database migration runner.
///
/// Run with: dart run bin/migrate.dart
///
/// This applies all migrations in order. Each migration is idempotent
/// (uses IF NOT EXISTS) so it is safe to re-run.
void main(List<String> args) async {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
stderr.writeln(
'${record.time} [${record.level.name}] ${record.loggerName}: ${record.message}');
});
_log.info('Starting migrations (env=${Env.environment})...');
await Database.initialize();
try {
for (var i = 0; i < _migrations.length; i++) {
_log.info('Running migration ${i + 1}/${_migrations.length}: ${_migrationNames[i]}');
await Database.execute(_migrations[i]);
}
_log.info('All migrations applied successfully');
} catch (e, st) {
_log.severe('Migration failed', e, st);
exit(1);
} finally {
await Database.close();
}
}
const _migrationNames = [
'Create users table',
'Create refresh_tokens table',
'Create recipes table',
'Create ingredients table',
'Create steps table',
'Create saved_recipes table',
'Create reviews table',
];
const _migrations = [
// 1. Users
'''
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
bio TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
)
''',
// 2. Refresh tokens
'''
CREATE TABLE IF NOT EXISTS refresh_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
''',
// 3. Recipes
'''
CREATE TABLE IF NOT EXISTS recipes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
prep_time INTEGER,
cook_time INTEGER,
servings INTEGER,
difficulty TEXT,
cuisine TEXT,
tags TEXT[] DEFAULT '{}',
image_url TEXT,
is_ai_generated BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
)
''',
// 4. Ingredients
'''
CREATE TABLE IF NOT EXISTS ingredients (
id TEXT PRIMARY KEY,
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
quantity TEXT,
unit TEXT,
sort_order INTEGER NOT NULL DEFAULT 0
)
''',
// 5. Steps
'''
CREATE TABLE IF NOT EXISTS steps (
id TEXT PRIMARY KEY,
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
step_number INTEGER NOT NULL,
instruction TEXT NOT NULL,
duration_minutes INTEGER
)
''',
// 6. Saved recipes (bookmarks)
'''
CREATE TABLE IF NOT EXISTS saved_recipes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, recipe_id)
)
''',
// 7. Reviews
'''
CREATE TABLE IF NOT EXISTS reviews (
id TEXT PRIMARY KEY,
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
''',
];