397 lines
11 KiB
TypeScript
397 lines
11 KiB
TypeScript
/**
|
|
* Import JSON exported from production MySQL (VM) into local Postgres.
|
|
*
|
|
* Usage:
|
|
* npx ts-node -r tsconfig-paths/register scripts/import-legacy-export.ts
|
|
*
|
|
* Optional:
|
|
* --dir scripts/data
|
|
* --replace wipe products/blogs/representations (and non-kept users) before import
|
|
*/
|
|
import { promises as fs } from 'fs'
|
|
import * as path from 'path'
|
|
import {
|
|
PrismaClient,
|
|
RepresentationStatus,
|
|
UserRole,
|
|
} from '@prisma/client'
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
function arg(name: string, fallback?: string): string | undefined {
|
|
const idx = process.argv.indexOf(`--${name}`)
|
|
if (idx >= 0 && process.argv[idx + 1] && !process.argv[idx + 1].startsWith('--')) {
|
|
return process.argv[idx + 1]
|
|
}
|
|
if (process.argv.includes(`--${name}`)) return 'true'
|
|
return fallback
|
|
}
|
|
|
|
function legacyId(prefix: string, id: string | number): string {
|
|
return `legacy-${prefix}-${id}`
|
|
}
|
|
|
|
function normalizePhone(raw: string | null | undefined): string {
|
|
const digits = String(raw ?? '').replace(/\D/g, '')
|
|
if (digits.length === 10 && digits.startsWith('9')) return `0${digits}`
|
|
if (digits.length === 11 && digits.startsWith('09')) return digits
|
|
return digits || '09000000000'
|
|
}
|
|
|
|
function mapRole(role: string | null | undefined, cellNumber: string): UserRole {
|
|
if (cellNumber === '09127004945') return UserRole.superAdmin
|
|
switch (role) {
|
|
case 'Admin':
|
|
return UserRole.Admin
|
|
case 'Agent':
|
|
return UserRole.Agent
|
|
default:
|
|
return UserRole.User
|
|
}
|
|
}
|
|
|
|
function mapRequestStatus(status: number): RepresentationStatus {
|
|
if (status === 2) return RepresentationStatus.active
|
|
if (status === 1) return RepresentationStatus.rejected
|
|
return RepresentationStatus.pending
|
|
}
|
|
|
|
function toDate(value: string | null | undefined): Date | undefined {
|
|
if (!value) return undefined
|
|
const d = new Date(value)
|
|
return Number.isNaN(d.getTime()) ? undefined : d
|
|
}
|
|
|
|
function phpBcryptToNode(hash: string): string {
|
|
// Node bcrypt accepts $2y$ in most versions; normalize to $2b$ just in case.
|
|
if (hash.startsWith('$2y$')) return `$2b$${hash.slice(4)}`
|
|
return hash
|
|
}
|
|
|
|
async function readJson<T>(file: string): Promise<T> {
|
|
const raw = await fs.readFile(file, 'utf8')
|
|
return JSON.parse(raw) as T
|
|
}
|
|
|
|
async function main() {
|
|
const dir = path.resolve(
|
|
process.cwd(),
|
|
arg('dir', 'scripts/data') ?? 'scripts/data',
|
|
)
|
|
const replace = Boolean(arg('replace'))
|
|
|
|
type UserRow = {
|
|
id: string
|
|
first_name: string
|
|
last_name: string
|
|
mobile: string
|
|
password: string
|
|
role: string
|
|
created_at?: string
|
|
updated_at?: string
|
|
}
|
|
type ProductRow = {
|
|
id: string
|
|
title_fa: string
|
|
title_en: string
|
|
price: string
|
|
quantity: string
|
|
category: string
|
|
description: string
|
|
image_url: string | null
|
|
created_at?: string
|
|
updated_at?: string
|
|
}
|
|
type BlogRow = {
|
|
id: string
|
|
title: string
|
|
summary: string
|
|
body: string
|
|
author: string
|
|
published: boolean
|
|
image_url: string | null
|
|
created_at?: string
|
|
updated_at?: string
|
|
}
|
|
type AgentRow = {
|
|
id: string
|
|
name: string
|
|
owner: string
|
|
national_code: string
|
|
mobile: string
|
|
phone: string
|
|
postal_code: string
|
|
address: string
|
|
description: string
|
|
city: string
|
|
province: string
|
|
created_at?: string
|
|
updated_at?: string
|
|
}
|
|
type RequestRow = {
|
|
id: string
|
|
first_name: string
|
|
last_name: string
|
|
national_code: string
|
|
mobile: string
|
|
phone: string
|
|
address: string
|
|
postal_code: string
|
|
description: string
|
|
status: number
|
|
city: string
|
|
province: string
|
|
created_at?: string
|
|
updated_at?: string
|
|
}
|
|
|
|
const users = await readJson<UserRow[]>(path.join(dir, 'users.json'))
|
|
const products = await readJson<ProductRow[]>(path.join(dir, 'products.json'))
|
|
const blogs = await readJson<BlogRow[]>(path.join(dir, 'blogs.json'))
|
|
const agents = await readJson<AgentRow[]>(path.join(dir, 'agents.json'))
|
|
const requests = await readJson<RequestRow[]>(
|
|
path.join(dir, 'agent_requests.json'),
|
|
)
|
|
|
|
if (replace) {
|
|
console.log('Clearing existing catalog / representation / non-superAdmin users…')
|
|
await prisma.orderItem.deleteMany({})
|
|
await prisma.order.deleteMany({})
|
|
await prisma.review.deleteMany({})
|
|
await prisma.callRequest.deleteMany({})
|
|
await prisma.representation.deleteMany({})
|
|
await prisma.blogPost.deleteMany({})
|
|
await prisma.product.deleteMany({})
|
|
await prisma.refreshToken.deleteMany({})
|
|
await prisma.smsOtp.deleteMany({})
|
|
await prisma.user.deleteMany({
|
|
where: { role: { not: UserRole.superAdmin } },
|
|
})
|
|
}
|
|
|
|
let usersUpserted = 0
|
|
for (const u of users) {
|
|
const cellNumber = normalizePhone(u.mobile)
|
|
const id = legacyId('user', u.id)
|
|
const role = mapRole(u.role, cellNumber)
|
|
const passwordHash = phpBcryptToNode(u.password)
|
|
const data = {
|
|
firstName: (u.first_name || '—').slice(0, 190),
|
|
lastName: (u.last_name || '—').slice(0, 190),
|
|
cellNumber,
|
|
passwordHash,
|
|
role,
|
|
disabled: false,
|
|
createdAt: toDate(u.created_at),
|
|
updatedAt: toDate(u.updated_at),
|
|
}
|
|
|
|
const byPhone = await prisma.user.findUnique({ where: { cellNumber } })
|
|
if (byPhone) {
|
|
await prisma.user.update({
|
|
where: { id: byPhone.id },
|
|
data: {
|
|
firstName: data.firstName,
|
|
lastName: data.lastName,
|
|
passwordHash: data.passwordHash,
|
|
// keep local superAdmin elevation
|
|
role:
|
|
byPhone.role === UserRole.superAdmin || role === UserRole.superAdmin
|
|
? UserRole.superAdmin
|
|
: role,
|
|
},
|
|
})
|
|
} else {
|
|
await prisma.user.upsert({
|
|
where: { id },
|
|
create: { id, ...data },
|
|
update: {
|
|
firstName: data.firstName,
|
|
lastName: data.lastName,
|
|
cellNumber: data.cellNumber,
|
|
passwordHash: data.passwordHash,
|
|
role: data.role,
|
|
},
|
|
})
|
|
}
|
|
usersUpserted += 1
|
|
}
|
|
|
|
let productsUpserted = 0
|
|
for (const p of products) {
|
|
const id = legacyId('product', p.id)
|
|
const price = Number.parseInt(String(p.price || '0'), 10) || 0
|
|
const stock = Number.parseInt(String(p.quantity || '0'), 10) || 0
|
|
await prisma.product.upsert({
|
|
where: { id },
|
|
create: {
|
|
id,
|
|
nameFa: p.title_fa || 'بدون عنوان',
|
|
nameEn: p.title_en || '',
|
|
price,
|
|
stock,
|
|
category: p.category || '',
|
|
imageUrl: p.image_url || null,
|
|
createdAt: toDate(p.created_at),
|
|
updatedAt: toDate(p.updated_at),
|
|
},
|
|
update: {
|
|
nameFa: p.title_fa || 'بدون عنوان',
|
|
nameEn: p.title_en || '',
|
|
price,
|
|
stock,
|
|
category: p.category || '',
|
|
imageUrl: p.image_url || null,
|
|
},
|
|
})
|
|
productsUpserted += 1
|
|
}
|
|
|
|
let blogsUpserted = 0
|
|
for (const b of blogs) {
|
|
const id = legacyId('blog', b.id)
|
|
await prisma.blogPost.upsert({
|
|
where: { id },
|
|
create: {
|
|
id,
|
|
title: b.title || 'بدون عنوان',
|
|
summary: b.summary || '',
|
|
body: b.body || '',
|
|
author: (b.author || '').trim(),
|
|
imageUrl: b.image_url || null,
|
|
published: Boolean(b.published),
|
|
createdAt: toDate(b.created_at),
|
|
updatedAt: toDate(b.updated_at),
|
|
},
|
|
update: {
|
|
title: b.title || 'بدون عنوان',
|
|
summary: b.summary || '',
|
|
body: b.body || '',
|
|
author: (b.author || '').trim(),
|
|
imageUrl: b.image_url || null,
|
|
published: Boolean(b.published),
|
|
},
|
|
})
|
|
blogsUpserted += 1
|
|
}
|
|
|
|
let repsUpserted = 0
|
|
for (const a of agents) {
|
|
const id = legacyId('agent', a.id)
|
|
const mobile = normalizePhone(a.mobile)
|
|
const owner = a.owner || a.name || ''
|
|
const nameParts = owner.trim().split(/\s+/)
|
|
await prisma.representation.upsert({
|
|
where: { id },
|
|
create: {
|
|
id,
|
|
name: a.name || owner || `نمایندگی ${a.id}`,
|
|
owner,
|
|
firstName: nameParts[0] || '',
|
|
lastName: nameParts.slice(1).join(' '),
|
|
nationalCode: a.national_code || '',
|
|
mobile,
|
|
province: a.province || '',
|
|
city: a.city || '',
|
|
landline: a.phone || '',
|
|
address: a.address || '',
|
|
postalCode: a.postal_code || '',
|
|
description: a.description || '',
|
|
status: RepresentationStatus.active,
|
|
createdAt: toDate(a.created_at),
|
|
updatedAt: toDate(a.updated_at),
|
|
},
|
|
update: {
|
|
name: a.name || owner || `نمایندگی ${a.id}`,
|
|
owner,
|
|
nationalCode: a.national_code || '',
|
|
mobile,
|
|
province: a.province || '',
|
|
city: a.city || '',
|
|
landline: a.phone || '',
|
|
address: a.address || '',
|
|
postalCode: a.postal_code || '',
|
|
description: a.description || '',
|
|
status: RepresentationStatus.active,
|
|
},
|
|
})
|
|
repsUpserted += 1
|
|
}
|
|
|
|
let requestsUpserted = 0
|
|
for (const r of requests) {
|
|
const id = legacyId('agent-request', r.id)
|
|
const mobile = normalizePhone(r.mobile)
|
|
const status = mapRequestStatus(Number(r.status) || 0)
|
|
const name = `${r.first_name || ''} ${r.last_name || ''}`.trim()
|
|
await prisma.representation.upsert({
|
|
where: { id },
|
|
create: {
|
|
id,
|
|
name: name || `درخواست ${r.id}`,
|
|
owner: name,
|
|
firstName: r.first_name || '',
|
|
lastName: r.last_name || '',
|
|
nationalCode: r.national_code || '',
|
|
mobile,
|
|
province: r.province || '',
|
|
city: r.city || '',
|
|
landline: r.phone || '',
|
|
address: r.address || '',
|
|
postalCode: r.postal_code || '',
|
|
description: r.description || '',
|
|
status,
|
|
createdAt: toDate(r.created_at),
|
|
updatedAt: toDate(r.updated_at),
|
|
},
|
|
update: {
|
|
name: name || `درخواست ${r.id}`,
|
|
owner: name,
|
|
firstName: r.first_name || '',
|
|
lastName: r.last_name || '',
|
|
nationalCode: r.national_code || '',
|
|
mobile,
|
|
province: r.province || '',
|
|
city: r.city || '',
|
|
landline: r.phone || '',
|
|
address: r.address || '',
|
|
postalCode: r.postal_code || '',
|
|
description: r.description || '',
|
|
status,
|
|
},
|
|
})
|
|
requestsUpserted += 1
|
|
}
|
|
|
|
const counts = {
|
|
users: await prisma.user.count(),
|
|
products: await prisma.product.count(),
|
|
blogs: await prisma.blogPost.count(),
|
|
representations: await prisma.representation.count(),
|
|
activeReps: await prisma.representation.count({
|
|
where: { status: RepresentationStatus.active },
|
|
}),
|
|
pendingReps: await prisma.representation.count({
|
|
where: { status: RepresentationStatus.pending },
|
|
}),
|
|
}
|
|
|
|
console.log('Imported:', {
|
|
usersUpserted,
|
|
productsUpserted,
|
|
blogsUpserted,
|
|
agentsAsActive: repsUpserted,
|
|
agentRequests: requestsUpserted,
|
|
})
|
|
console.log('DB totals:', counts)
|
|
}
|
|
|
|
main()
|
|
.catch((err) => {
|
|
console.error(err)
|
|
process.exitCode = 1
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect()
|
|
})
|