feat(db): Initial Prisma setup and PostgreSQL migration script
This commit is contained in:
parent
444c867ae8
commit
320b627a91
|
|
@ -13,6 +13,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.700.0",
|
"@aws-sdk/client-s3": "^3.700.0",
|
||||||
"@google/genai": "^2.4.0",
|
"@google/genai": "^2.4.0",
|
||||||
|
"@prisma/client": "^6.4.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"prisma": "^6.4.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,11 @@ model User {
|
||||||
role Role @default(student)
|
role Role @default(student)
|
||||||
subscriptionStatus SubscriptionStatus @default(TRIAL) @map("subscription_status")
|
subscriptionStatus SubscriptionStatus @default(TRIAL) @map("subscription_status")
|
||||||
asaasCustomerId String? @map("asaas_customer_id")
|
asaasCustomerId String? @map("asaas_customer_id")
|
||||||
|
cpf String?
|
||||||
|
whatsapp String?
|
||||||
|
birthDate String? @map("birth_date")
|
||||||
|
avatarUrl String? @map("avatar_url") @db.Text
|
||||||
|
trialEndsAt DateTime? @map("trial_ends_at")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
notes Note[]
|
notes Note[]
|
||||||
|
|
@ -60,6 +65,7 @@ model User {
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
moduleGrades ModuleGrade[]
|
moduleGrades ModuleGrade[]
|
||||||
certificates Certificate[]
|
certificates Certificate[]
|
||||||
|
unlockedCourses String[] @default([]) @map("unlocked_courses")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +77,8 @@ model Course {
|
||||||
thumbnail String
|
thumbnail String
|
||||||
category String
|
category String
|
||||||
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
|
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
|
||||||
|
isLocked Boolean @default(false) @map("is_locked")
|
||||||
|
price Float?
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
modules Module[]
|
modules Module[]
|
||||||
|
|
@ -147,6 +155,9 @@ model Payment {
|
||||||
invoiceUrl String @map("invoice_url") @db.Text
|
invoiceUrl String @map("invoice_url") @db.Text
|
||||||
dueDate DateTime @map("due_date") @db.Date
|
dueDate DateTime @map("due_date") @db.Date
|
||||||
paidAt DateTime? @map("paid_at")
|
paidAt DateTime? @map("paid_at")
|
||||||
|
planId String? @map("plan_id")
|
||||||
|
courseId String? @map("course_id")
|
||||||
|
description String? @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
@ -205,3 +216,29 @@ model Certificate {
|
||||||
|
|
||||||
@@map("certificates")
|
@@map("certificates")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Plan {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
title String
|
||||||
|
description String @db.Text
|
||||||
|
price Float
|
||||||
|
cycle String // 'MONTHLY', 'YEARLY', 'LIFETIME'
|
||||||
|
active Boolean @default(true)
|
||||||
|
includedCourses String[] @default([]) @map("included_courses")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@map("plans")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SystemSettings {
|
||||||
|
id String @id @default("default")
|
||||||
|
trialDurationDays Int @default(7) @map("trial_duration_days")
|
||||||
|
asaasEnvironment String @default("sandbox") @map("asaas_environment")
|
||||||
|
asaasApiKey String @default("") @map("asaas_api_key")
|
||||||
|
asaasWebhookUrl String @default("") @map("asaas_webhook_url")
|
||||||
|
asaasWebhookSecret String @default("") @map("asaas_webhook_secret")
|
||||||
|
helpText String @db.Text @default("# Central de Ajuda") @map("help_text")
|
||||||
|
|
||||||
|
@@map("system_settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,242 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const DATA_DIR = process.env.DATA_DIR || (fs.existsSync('/app/data') ? '/app/data' : process.cwd());
|
||||||
|
const DB_FILE = path.join(DATA_DIR, 'database.json');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🔄 Iniciando migração de JSON para PostgreSQL...');
|
||||||
|
|
||||||
|
if (!fs.existsSync(DB_FILE)) {
|
||||||
|
console.error('❌ Arquivo database.json não encontrado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8'));
|
||||||
|
|
||||||
|
// 1. Settings
|
||||||
|
console.log('Migrando Settings...');
|
||||||
|
if (data.settings) {
|
||||||
|
await prisma.systemSettings.upsert({
|
||||||
|
where: { id: 'default' },
|
||||||
|
update: {
|
||||||
|
trialDurationDays: data.settings.trialDurationDays || 7,
|
||||||
|
asaasEnvironment: data.settings.asaasEnvironment || 'sandbox',
|
||||||
|
asaasApiKey: data.settings.asaasApiKey || '',
|
||||||
|
asaasWebhookUrl: data.settings.asaasWebhookUrl || '',
|
||||||
|
asaasWebhookSecret: data.settings.asaasWebhookSecret || '',
|
||||||
|
helpText: data.settings.helpText || '# Central de Ajuda'
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: 'default',
|
||||||
|
trialDurationDays: data.settings.trialDurationDays || 7,
|
||||||
|
asaasEnvironment: data.settings.asaasEnvironment || 'sandbox',
|
||||||
|
asaasApiKey: data.settings.asaasApiKey || '',
|
||||||
|
asaasWebhookUrl: data.settings.asaasWebhookUrl || '',
|
||||||
|
asaasWebhookSecret: data.settings.asaasWebhookSecret || '',
|
||||||
|
helpText: data.settings.helpText || '# Central de Ajuda'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Users
|
||||||
|
console.log(`Migrando ${data.users?.length || 0} Users...`);
|
||||||
|
for (const user of data.users || []) {
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: user.id },
|
||||||
|
update: {
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
password: user.password,
|
||||||
|
role: user.role === 'admin' ? 'admin' : 'student',
|
||||||
|
subscriptionStatus: user.subscriptionStatus,
|
||||||
|
asaasCustomerId: user.asaasCustomerId,
|
||||||
|
cpf: user.cpf,
|
||||||
|
whatsapp: user.whatsapp,
|
||||||
|
birthDate: user.birthDate,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
trialEndsAt: user.trialEndsAt ? new Date(user.trialEndsAt) : null,
|
||||||
|
unlockedCourses: user.unlockedCourses || [],
|
||||||
|
createdAt: new Date(user.createdAt)
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
password: user.password,
|
||||||
|
role: user.role === 'admin' ? 'admin' : 'student',
|
||||||
|
subscriptionStatus: user.subscriptionStatus,
|
||||||
|
asaasCustomerId: user.asaasCustomerId,
|
||||||
|
cpf: user.cpf,
|
||||||
|
whatsapp: user.whatsapp,
|
||||||
|
birthDate: user.birthDate,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
trialEndsAt: user.trialEndsAt ? new Date(user.trialEndsAt) : null,
|
||||||
|
unlockedCourses: user.unlockedCourses || [],
|
||||||
|
createdAt: new Date(user.createdAt)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Courses
|
||||||
|
console.log(`Migrando ${data.courses?.length || 0} Courses...`);
|
||||||
|
for (const course of data.courses || []) {
|
||||||
|
await prisma.course.upsert({
|
||||||
|
where: { id: course.id },
|
||||||
|
update: {
|
||||||
|
title: course.title,
|
||||||
|
description: course.description,
|
||||||
|
thumbnail: course.thumbnail,
|
||||||
|
category: course.category,
|
||||||
|
certificateMode: course.certificateMode || 'FULL_COURSE',
|
||||||
|
isLocked: course.isLocked || false,
|
||||||
|
price: course.price || null,
|
||||||
|
createdAt: new Date(course.createdAt)
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: course.id,
|
||||||
|
title: course.title,
|
||||||
|
description: course.description,
|
||||||
|
thumbnail: course.thumbnail,
|
||||||
|
category: course.category,
|
||||||
|
certificateMode: course.certificateMode || 'FULL_COURSE',
|
||||||
|
isLocked: course.isLocked || false,
|
||||||
|
price: course.price || null,
|
||||||
|
createdAt: new Date(course.createdAt)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Modules
|
||||||
|
console.log(`Migrando ${data.modules?.length || 0} Modules...`);
|
||||||
|
for (const mod of data.modules || []) {
|
||||||
|
await prisma.module.upsert({
|
||||||
|
where: { id: mod.id },
|
||||||
|
update: {
|
||||||
|
courseId: mod.courseId,
|
||||||
|
title: mod.title,
|
||||||
|
order: mod.order,
|
||||||
|
createdAt: new Date(mod.createdAt)
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: mod.id,
|
||||||
|
courseId: mod.courseId,
|
||||||
|
title: mod.title,
|
||||||
|
order: mod.order,
|
||||||
|
createdAt: new Date(mod.createdAt)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Lessons
|
||||||
|
console.log(`Migrando ${data.lessons?.length || 0} Lessons...`);
|
||||||
|
for (const lesson of data.lessons || []) {
|
||||||
|
await prisma.lesson.upsert({
|
||||||
|
where: { id: lesson.id },
|
||||||
|
update: {
|
||||||
|
moduleId: lesson.moduleId,
|
||||||
|
title: lesson.title,
|
||||||
|
videoUrl: lesson.videoUrl,
|
||||||
|
videoType: lesson.videoType === 'youtube' ? 'youtube' : 'direct',
|
||||||
|
order: lesson.order,
|
||||||
|
content: lesson.content,
|
||||||
|
createdAt: new Date(lesson.createdAt)
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: lesson.id,
|
||||||
|
moduleId: lesson.moduleId,
|
||||||
|
title: lesson.title,
|
||||||
|
videoUrl: lesson.videoUrl,
|
||||||
|
videoType: lesson.videoType === 'youtube' ? 'youtube' : 'direct',
|
||||||
|
order: lesson.order,
|
||||||
|
content: lesson.content,
|
||||||
|
createdAt: new Date(lesson.createdAt)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Plans
|
||||||
|
console.log(`Migrando ${data.plans?.length || 0} Plans...`);
|
||||||
|
for (const plan of data.plans || []) {
|
||||||
|
await prisma.plan.upsert({
|
||||||
|
where: { id: plan.id },
|
||||||
|
update: {
|
||||||
|
title: plan.title,
|
||||||
|
description: plan.description || '',
|
||||||
|
price: plan.price,
|
||||||
|
cycle: plan.cycle,
|
||||||
|
active: plan.active,
|
||||||
|
includedCourses: plan.includedCourses || [],
|
||||||
|
createdAt: plan.createdAt ? new Date(plan.createdAt) : new Date()
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: plan.id,
|
||||||
|
title: plan.title,
|
||||||
|
description: plan.description || '',
|
||||||
|
price: plan.price,
|
||||||
|
cycle: plan.cycle,
|
||||||
|
active: plan.active,
|
||||||
|
includedCourses: plan.includedCourses || [],
|
||||||
|
createdAt: plan.createdAt ? new Date(plan.createdAt) : new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Payments
|
||||||
|
console.log(`Migrando ${data.payments?.length || 0} Payments...`);
|
||||||
|
for (const p of data.payments || []) {
|
||||||
|
let billingType = 'PIX';
|
||||||
|
if (p.billingType === 'BOLETO') billingType = 'BOLETO';
|
||||||
|
if (p.billingType === 'CREDIT_CARD') billingType = 'CREDIT_CARD';
|
||||||
|
|
||||||
|
let status = 'PENDING';
|
||||||
|
if (p.status === 'RECEIVED') status = 'RECEIVED';
|
||||||
|
if (p.status === 'CONFIRMED') status = 'CONFIRMED';
|
||||||
|
if (p.status === 'OVERDUE') status = 'OVERDUE';
|
||||||
|
if (p.status === 'REFUNDED') status = 'REFUNDED';
|
||||||
|
|
||||||
|
await prisma.payment.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
userId: p.userId,
|
||||||
|
value: p.value,
|
||||||
|
status: status as any,
|
||||||
|
billingType: billingType as any,
|
||||||
|
invoiceUrl: p.invoiceUrl || '',
|
||||||
|
dueDate: p.dueDate ? new Date(p.dueDate) : new Date(),
|
||||||
|
paidAt: p.paidAt ? new Date(p.paidAt) : null,
|
||||||
|
planId: p.planId || null,
|
||||||
|
courseId: p.courseId || null,
|
||||||
|
description: p.description || null,
|
||||||
|
createdAt: p.createdAt ? new Date(p.createdAt) : new Date()
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
userId: p.userId,
|
||||||
|
value: p.value,
|
||||||
|
status: status as any,
|
||||||
|
billingType: billingType as any,
|
||||||
|
invoiceUrl: p.invoiceUrl || '',
|
||||||
|
dueDate: p.dueDate ? new Date(p.dueDate) : new Date(),
|
||||||
|
paidAt: p.paidAt ? new Date(p.paidAt) : null,
|
||||||
|
planId: p.planId || null,
|
||||||
|
courseId: p.courseId || null,
|
||||||
|
description: p.description || null,
|
||||||
|
createdAt: p.createdAt ? new Date(p.createdAt) : new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Migração finalizada com sucesso!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(e => {
|
||||||
|
console.error('❌ Erro na migração:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient();
|
||||||
Loading…
Reference in New Issue