Initial commit of the Mado Motor NestJS API.
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* 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()
|
||||
})
|
||||
Reference in New Issue
Block a user