Remove unused stub backend folder and add web frontend plan
Cleanup project structure by removing duplicate maternal-app-backend/ folder at root level. The working backend is located at maternal-app/maternal-app-backend/. Also added web frontend implementation plan documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1733
docs/maternal-web-frontend-plan.md
Normal file
1733
docs/maternal-web-frontend-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
|||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
export const getDatabaseConfig = (
|
|
||||||
configService: ConfigService,
|
|
||||||
): TypeOrmModuleOptions => ({
|
|
||||||
type: 'postgres',
|
|
||||||
host: configService.get<string>('DATABASE_HOST', 'localhost'),
|
|
||||||
port: configService.get<number>('DATABASE_PORT', 5555),
|
|
||||||
username: configService.get<string>('DATABASE_USER', 'maternal_user'),
|
|
||||||
password: configService.get<string>('DATABASE_PASSWORD'),
|
|
||||||
database: configService.get<string>('DATABASE_NAME', 'maternal_app'),
|
|
||||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
|
||||||
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
|
|
||||||
synchronize: false, // Always use migrations in production
|
|
||||||
logging: configService.get<string>('NODE_ENV') === 'development',
|
|
||||||
ssl: configService.get<string>('NODE_ENV') === 'production',
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { getDatabaseConfig } from '../config/database.config';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forRootAsync({
|
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: getDatabaseConfig,
|
|
||||||
inject: [ConfigService],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class DatabaseModule {}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
Column,
|
|
||||||
PrimaryColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
BeforeInsert,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { Family } from './family.entity';
|
|
||||||
|
|
||||||
@Entity('children')
|
|
||||||
export class Child {
|
|
||||||
@PrimaryColumn({ length: 20 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'family_id', length: 20 })
|
|
||||||
familyId: string;
|
|
||||||
|
|
||||||
@Column({ length: 100 })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ name: 'birth_date', type: 'date' })
|
|
||||||
birthDate: Date;
|
|
||||||
|
|
||||||
@Column({ length: 20, nullable: true })
|
|
||||||
gender?: string;
|
|
||||||
|
|
||||||
@Column({ name: 'photo_url', type: 'text', nullable: true })
|
|
||||||
photoUrl?: string;
|
|
||||||
|
|
||||||
@Column({ name: 'medical_info', type: 'jsonb', default: {} })
|
|
||||||
medicalInfo: Record<string, any>;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@Column({ name: 'deleted_at', type: 'timestamp', nullable: true })
|
|
||||||
deletedAt?: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => Family, (family) => family.children, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'family_id' })
|
|
||||||
family: Family;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
generateId() {
|
|
||||||
if (!this.id) {
|
|
||||||
this.id = `chd_${this.generateNanoId()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateNanoId(): string {
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
Column,
|
|
||||||
PrimaryColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
BeforeInsert,
|
|
||||||
Index,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from './user.entity';
|
|
||||||
|
|
||||||
@Entity('device_registry')
|
|
||||||
@Index(['userId', 'deviceFingerprint'], { unique: true })
|
|
||||||
export class DeviceRegistry {
|
|
||||||
@PrimaryColumn({ length: 20 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'user_id', length: 20 })
|
|
||||||
userId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'device_fingerprint', length: 255 })
|
|
||||||
deviceFingerprint: string;
|
|
||||||
|
|
||||||
@Column({ length: 20 })
|
|
||||||
platform: string;
|
|
||||||
|
|
||||||
@Column({ default: false })
|
|
||||||
trusted: boolean;
|
|
||||||
|
|
||||||
@Column({ name: 'last_seen', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
lastSeen: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.devices, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'user_id' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
generateId() {
|
|
||||||
if (!this.id) {
|
|
||||||
this.id = `dev_${this.generateNanoId()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateNanoId(): string {
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
Column,
|
|
||||||
PrimaryColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from './user.entity';
|
|
||||||
import { Family } from './family.entity';
|
|
||||||
|
|
||||||
export enum FamilyRole {
|
|
||||||
PARENT = 'parent',
|
|
||||||
CAREGIVER = 'caregiver',
|
|
||||||
VIEWER = 'viewer',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FamilyPermissions {
|
|
||||||
canAddChildren: boolean;
|
|
||||||
canEditChildren: boolean;
|
|
||||||
canLogActivities: boolean;
|
|
||||||
canViewReports: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('family_members')
|
|
||||||
export class FamilyMember {
|
|
||||||
@PrimaryColumn({ name: 'user_id', length: 20 })
|
|
||||||
userId: string;
|
|
||||||
|
|
||||||
@PrimaryColumn({ name: 'family_id', length: 20 })
|
|
||||||
familyId: string;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
type: 'varchar',
|
|
||||||
length: 20,
|
|
||||||
enum: FamilyRole,
|
|
||||||
})
|
|
||||||
role: FamilyRole;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
type: 'jsonb',
|
|
||||||
default: {
|
|
||||||
canAddChildren: false,
|
|
||||||
canEditChildren: false,
|
|
||||||
canLogActivities: true,
|
|
||||||
canViewReports: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
permissions: FamilyPermissions;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'joined_at' })
|
|
||||||
joinedAt: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.familyMemberships, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'user_id' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@ManyToOne(() => Family, (family) => family.members, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'family_id' })
|
|
||||||
family: Family;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
Column,
|
|
||||||
PrimaryColumn,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
BeforeInsert,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from './user.entity';
|
|
||||||
import { FamilyMember } from './family-member.entity';
|
|
||||||
import { Child } from './child.entity';
|
|
||||||
|
|
||||||
@Entity('families')
|
|
||||||
export class Family {
|
|
||||||
@PrimaryColumn({ length: 20 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
|
||||||
name?: string;
|
|
||||||
|
|
||||||
@Column({ name: 'share_code', length: 10, unique: true })
|
|
||||||
shareCode: string;
|
|
||||||
|
|
||||||
@Column({ name: 'created_by', length: 20 })
|
|
||||||
createdBy: string;
|
|
||||||
|
|
||||||
@Column({ name: 'subscription_tier', length: 20, default: 'free' })
|
|
||||||
subscriptionTier: string;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
|
||||||
@JoinColumn({ name: 'created_by' })
|
|
||||||
creator: User;
|
|
||||||
|
|
||||||
@OneToMany(() => FamilyMember, (member) => member.family)
|
|
||||||
members: FamilyMember[];
|
|
||||||
|
|
||||||
@OneToMany(() => Child, (child) => child.family)
|
|
||||||
children: Child[];
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
generateId() {
|
|
||||||
if (!this.id) {
|
|
||||||
this.id = `fam_${this.generateNanoId()}`;
|
|
||||||
}
|
|
||||||
if (!this.shareCode) {
|
|
||||||
this.shareCode = this.generateShareCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateNanoId(): string {
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateShareCode(): string {
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export { User } from './user.entity';
|
|
||||||
export { DeviceRegistry } from './device-registry.entity';
|
|
||||||
export { Family } from './family.entity';
|
|
||||||
export { FamilyMember, FamilyRole, FamilyPermissions } from './family-member.entity';
|
|
||||||
export { Child } from './child.entity';
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
Column,
|
|
||||||
PrimaryColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
OneToMany,
|
|
||||||
BeforeInsert,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { DeviceRegistry } from './device-registry.entity';
|
|
||||||
import { FamilyMember } from './family-member.entity';
|
|
||||||
|
|
||||||
@Entity('users')
|
|
||||||
export class User {
|
|
||||||
@PrimaryColumn({ length: 20 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, unique: true })
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@Column({ length: 20, nullable: true })
|
|
||||||
phone?: string;
|
|
||||||
|
|
||||||
@Column({ name: 'password_hash', length: 255 })
|
|
||||||
passwordHash: string;
|
|
||||||
|
|
||||||
@Column({ length: 100 })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ length: 10, default: 'en-US' })
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
@Column({ length: 50, default: 'UTC' })
|
|
||||||
timezone: string;
|
|
||||||
|
|
||||||
@Column({ name: 'email_verified', default: false })
|
|
||||||
emailVerified: boolean;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at' })
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@OneToMany(() => DeviceRegistry, (device) => device.user)
|
|
||||||
devices: DeviceRegistry[];
|
|
||||||
|
|
||||||
@OneToMany(() => FamilyMember, (familyMember) => familyMember.user)
|
|
||||||
familyMemberships: FamilyMember[];
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
generateId() {
|
|
||||||
if (!this.id) {
|
|
||||||
this.id = `usr_${this.generateNanoId()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateNanoId(): string {
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
-- V001_20240110120000_create_core_auth.sql
|
|
||||||
-- Migration V001: Core Authentication Tables
|
|
||||||
|
|
||||||
-- Create extension for generating random IDs
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
-- Users table
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
phone VARCHAR(20),
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
locale VARCHAR(10) DEFAULT 'en-US',
|
|
||||||
timezone VARCHAR(50) DEFAULT 'UTC',
|
|
||||||
email_verified BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Device registry table
|
|
||||||
CREATE TABLE IF NOT EXISTS device_registry (
|
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
|
||||||
user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
device_fingerprint VARCHAR(255) NOT NULL,
|
|
||||||
platform VARCHAR(20) NOT NULL,
|
|
||||||
trusted BOOLEAN DEFAULT FALSE,
|
|
||||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(user_id, device_fingerprint)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_devices_user ON device_registry(user_id);
|
|
||||||
|
|
||||||
-- Update timestamp trigger function
|
|
||||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ language 'plpgsql';
|
|
||||||
|
|
||||||
-- Apply trigger to users table
|
|
||||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
-- V002_20240110130000_create_family_structure.sql
|
|
||||||
-- Migration V002: Family Structure
|
|
||||||
|
|
||||||
-- Families table
|
|
||||||
CREATE TABLE IF NOT EXISTS families (
|
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
|
||||||
name VARCHAR(100),
|
|
||||||
share_code VARCHAR(10) UNIQUE,
|
|
||||||
created_by VARCHAR(20) REFERENCES users(id),
|
|
||||||
subscription_tier VARCHAR(20) DEFAULT 'free',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Family members table (junction table with additional data)
|
|
||||||
CREATE TABLE IF NOT EXISTS family_members (
|
|
||||||
user_id VARCHAR(20) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
family_id VARCHAR(20) REFERENCES families(id) ON DELETE CASCADE,
|
|
||||||
role VARCHAR(20) NOT NULL CHECK (role IN ('parent', 'caregiver', 'viewer')),
|
|
||||||
permissions JSONB DEFAULT '{"canAddChildren": false, "canEditChildren": false, "canLogActivities": true, "canViewReports": true}'::jsonb,
|
|
||||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (user_id, family_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Children table
|
|
||||||
CREATE TABLE IF NOT EXISTS children (
|
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
|
||||||
family_id VARCHAR(20) NOT NULL REFERENCES families(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
birth_date DATE NOT NULL,
|
|
||||||
gender VARCHAR(20),
|
|
||||||
photo_url TEXT,
|
|
||||||
medical_info JSONB DEFAULT '{}'::jsonb,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_families_share_code ON families(share_code);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_family_members_family ON family_members(family_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_children_family ON children(family_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_children_active ON children(deleted_at) WHERE deleted_at IS NULL;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
-- V003_20240110140000_create_refresh_tokens.sql
|
|
||||||
-- Migration V003: Refresh Tokens Table
|
|
||||||
|
|
||||||
-- Refresh tokens table for JWT authentication
|
|
||||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
|
||||||
user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
device_id VARCHAR(20) REFERENCES device_registry(id) ON DELETE CASCADE,
|
|
||||||
token_hash VARCHAR(255) NOT NULL,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
revoked BOOLEAN DEFAULT FALSE,
|
|
||||||
revoked_at TIMESTAMP,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes for refresh token lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_device ON refresh_tokens(device_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_active ON refresh_tokens(expires_at, revoked) WHERE revoked = FALSE;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { Client } from 'pg';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
// Load environment variables
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const client = new Client({
|
|
||||||
host: process.env.DATABASE_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.DATABASE_PORT || '5555'),
|
|
||||||
user: process.env.DATABASE_USER || 'maternal_user',
|
|
||||||
password: process.env.DATABASE_PASSWORD,
|
|
||||||
database: process.env.DATABASE_NAME || 'maternal_app',
|
|
||||||
});
|
|
||||||
|
|
||||||
const MIGRATIONS_DIR = __dirname;
|
|
||||||
|
|
||||||
async function runMigrations() {
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
console.log('Connected to database');
|
|
||||||
|
|
||||||
// Create migrations tracking table
|
|
||||||
await client.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
||||||
version VARCHAR(50) PRIMARY KEY,
|
|
||||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Get list of migration files
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(MIGRATIONS_DIR)
|
|
||||||
.filter((file) => file.startsWith('V') && file.endsWith('.sql'))
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
console.log(`Found ${files.length} migration files`);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const version = file.split('_')[0]; // Extract V001, V002, etc.
|
|
||||||
|
|
||||||
// Check if migration already executed
|
|
||||||
const result = await client.query(
|
|
||||||
'SELECT version FROM schema_migrations WHERE version = $1',
|
|
||||||
[version],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.rows.length > 0) {
|
|
||||||
console.log(`✓ Migration ${version} already executed`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and execute migration
|
|
||||||
const migrationPath = path.join(MIGRATIONS_DIR, file);
|
|
||||||
const sql = fs.readFileSync(migrationPath, 'utf-8');
|
|
||||||
|
|
||||||
console.log(`Running migration ${version}...`);
|
|
||||||
await client.query(sql);
|
|
||||||
|
|
||||||
// Record migration
|
|
||||||
await client.query(
|
|
||||||
'INSERT INTO schema_migrations (version) VALUES ($1)',
|
|
||||||
[version],
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✓ Migration ${version} completed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('All migrations completed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Migration error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
await client.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runMigrations();
|
|
||||||
Reference in New Issue
Block a user