GuDingZiChan/backend/scripts/importConfirmedEquipmentToLive.js
2026-03-23 11:54:41 +08:00

772 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let PrismaClient
try {
;({ PrismaClient } = require('@prisma/client'))
} catch {
;({ PrismaClient } = require('/app/node_modules/@prisma/client'))
}
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
const { resolveStorageLocationName } = require('./locationNormalization')
const OUTPUT_ROOT = path.resolve(__dirname, '../../temp_migration_output')
const PLINK_PATH = process.env.FIXED_ASSET_PLINK_PATH || 'C:\\Program Files\\PuTTY\\plink.exe'
const SSH_HOST = process.env.FIXED_ASSET_SSH_HOST || 'root@1.13.22.133'
const SSH_HOSTKEY =
process.env.FIXED_ASSET_SSH_HOSTKEY || 'SHA256:huTZZAYXh4yd5AvE4jo7drXaF/WSFF0SkzYHoddbMLA'
const SSH_PASSWORD = process.env.FIXED_ASSET_SSH_PASSWORD || ''
const REMOTE_NODE_COMMAND = 'docker exec -i equipment-backend node'
const normalizeText = (value) =>
String(value == null ? '' : value)
.replace(/\r/g, ' ')
.replace(/\n/g, ' ')
.replace(/\s+/g, ' ')
.trim()
const parseArgs = () => {
const args = process.argv.slice(2)
const options = {
inputDir: '',
keepExisting: false,
includeOwnerless: false
}
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
if (arg === '--input-dir') {
options.inputDir = args[index + 1] || ''
index += 1
} else if (arg === '--keep-existing') {
options.keepExisting = true
} else if (arg === '--include-ownerless') {
options.includeOwnerless = true
}
}
return options
}
const ensureOutputDir = (prefix) => {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19)
const outputDir = path.join(OUTPUT_ROOT, `${prefix}_${timestamp}`)
fs.mkdirSync(outputDir, { recursive: true })
return outputDir
}
const resolveLatestMigrationDir = () => {
const candidates = fs
.readdirSync(OUTPUT_ROOT, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith('migration_dry_run_'))
.map((entry) => entry.name)
.sort()
if (!candidates.length) {
throw new Error(`未找到 dry-run 输出目录: ${OUTPUT_ROOT}`)
}
return path.join(OUTPUT_ROOT, candidates[candidates.length - 1])
}
const resolveInputDir = (inputDirArg) => {
if (!inputDirArg) {
return resolveLatestMigrationDir()
}
const resolved = path.isAbsolute(inputDirArg)
? inputDirArg
: path.resolve(process.cwd(), inputDirArg)
if (!fs.existsSync(resolved)) {
throw new Error(`指定的输入目录不存在: ${resolved}`)
}
return resolved
}
const writeJson = (filePath, value) => {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`)
}
const readJsonl = (filePath) =>
fs
.readFileSync(filePath, 'utf8')
.split(/\r?\n/)
.filter(Boolean)
.map((line) => JSON.parse(line))
const parseDate = (value) => {
const normalized = normalizeText(value)
if (!normalized || normalized === '#REF!') {
return null
}
const directDate = new Date(normalized)
if (!Number.isNaN(directDate.getTime())) {
return directDate.toISOString()
}
return null
}
const buildDescription = (record) => {
const parts = [
['legacy_asset_code', record.assetCodeRaw || ''],
['source_row', record.sourceRow],
['source_status', record.currentStatusRaw || ''],
['raw_owner', record.ownerRaw || ''],
['raw_department', record.departmentRaw || ''],
['raw_location', record.addressRaw || ''],
['raw_category', record.assetCategoryRaw || '']
]
return parts
.map(([key, value]) => `${key}=${String(value).replace(/;/g, ',')}`)
.join('; ')
}
const collectDuplicateCodeDetails = (records) => {
const grouped = new Map()
for (const record of records) {
const legacyCode = normalizeText(record.assetCodeRaw).toUpperCase()
if (!legacyCode) {
continue
}
if (!grouped.has(legacyCode)) {
grouped.set(legacyCode, [])
}
grouped.get(legacyCode).push({
bucket: normalizeText(record.bucket),
sourceRow: record.sourceRow,
assetName: normalizeText(record.assetNameRaw),
owner: normalizeText(record.ownerRaw),
department: normalizeText(record.departmentRaw),
resolvedType: normalizeText(record.assetTypeResolvedName || record.assetTypeName),
address: normalizeText(record.addressRaw),
purchaseDate: normalizeText(record.purchaseDateParsed),
unitPrice: normalizeText(record.unitPriceParsed),
sourceStatus: normalizeText(record.currentStatusRaw),
statusKind: normalizeText(record.statusKind),
pendingReasons: Array.isArray(record.pendingReasons) ? record.pendingReasons : []
})
}
return [...grouped.entries()]
.filter(([, items]) => items.length > 1)
.map(([legacyCode, items]) => ({ legacyCode, items }))
}
const partitionImportCandidates = (records) => {
const missingCodeRows = []
const validCodeRecords = []
for (const record of records) {
const legacyCode = normalizeText(record.assetCodeRaw).toUpperCase()
if (!legacyCode) {
missingCodeRows.push({
bucket: normalizeText(record.bucket),
sourceRow: record.sourceRow,
assetName: normalizeText(record.assetNameRaw),
owner: normalizeText(record.ownerRaw),
department: normalizeText(record.departmentRaw),
currentStatus: normalizeText(record.currentStatusRaw),
address: normalizeText(record.addressRaw)
})
continue
}
validCodeRecords.push(record)
}
const duplicateDetails = collectDuplicateCodeDetails(validCodeRecords)
const duplicateCodes = new Set(duplicateDetails.map((item) => item.legacyCode))
const filteredRecords = validCodeRecords.filter(
(record) => !duplicateCodes.has(normalizeText(record.assetCodeRaw).toUpperCase())
)
const duplicateRows = validCodeRecords
.filter((record) => duplicateCodes.has(normalizeText(record.assetCodeRaw).toUpperCase()))
.map((record) => ({
bucket: normalizeText(record.bucket),
legacyCode: normalizeText(record.assetCodeRaw).toUpperCase(),
sourceRow: record.sourceRow,
assetName: normalizeText(record.assetNameRaw),
owner: normalizeText(record.ownerRaw),
department: normalizeText(record.departmentRaw),
currentStatus: normalizeText(record.currentStatusRaw)
}))
return {
filteredRecords,
missingCodeRows,
duplicateRows,
duplicateDetails
}
}
const buildImportPayload = (records) => {
const payload = []
for (const record of records) {
const legacyCode = normalizeText(record.assetCodeRaw).toUpperCase()
const importMode = record.bucket === 'import-without-owner' ? 'without_owner' : 'with_owner'
const statusKind = normalizeText(record.statusKind)
const pendingReasons = Array.isArray(record.pendingReasons) ? record.pendingReasons : []
const matchedPerson = record.ownerMatch?.matchedPerson || null
if (importMode === 'with_owner' && !matchedPerson?.id) {
throw new Error(`发现未匹配 owner 的 import-ready 记录,sourceRow=${record.sourceRow} code=${legacyCode}`)
}
if (importMode === 'without_owner' && !['assigned', 'unassigned'].includes(statusKind)) {
throw new Error(
`import-without-owner 记录状态异常,无法安全导入,sourceRow=${record.sourceRow} code=${legacyCode}`
)
}
const descriptionExtras = [`migration_bucket=${record.bucket}`]
if (importMode === 'without_owner') {
descriptionExtras.push(`owner_pending_reasons=${pendingReasons.join('|') || 'none'}`)
descriptionExtras.push(`owner_resolution=manual_followup_required`)
}
payload.push({
legacyCode,
importMode,
sourceStatusKind: statusKind,
unresolvedOwnerReasons: pendingReasons.join('|'),
rawOwnerName: normalizeText(record.ownerRaw),
equipmentName: normalizeText(record.assetNameRaw),
equipmentModel: normalizeText(record.equipment_model || record.modelRaw) || null,
equipmentType: normalizeText(record.assetTypeResolvedName || record.assetTypeName),
departmentName: normalizeText(record.departmentResolvedName || record.departmentNormalized || ''),
storageLocationName: record.locationMatch
? normalizeText(record.locationResolvedName || resolveStorageLocationName(record.addressRaw).normalizedValue)
: '',
unitPrice: Number(record.unitPriceParsed || 0),
purchaseDate: normalizeText(record.purchaseDateParsed) || null,
supplier: '',
description: [buildDescription(record), ...descriptionExtras].join('; '),
ownerPersonId: matchedPerson?.id || null,
ownerNickname: normalizeText(matchedPerson?.nickname || ''),
ownerRealName: normalizeText(matchedPerson?.real_name || ''),
borrowTime: parseDate(record.claimedAtRaw) || parseDate(record.purchaseDateParsed) || null,
sourceRow: record.sourceRow
})
}
return payload
}
const runRemoteNodeScript = (script) => {
if (!fs.existsSync(PLINK_PATH)) {
throw new Error(`未找到 plink.exe: ${PLINK_PATH}`)
}
if (!SSH_PASSWORD) {
throw new Error('缺少 FIXED_ASSET_SSH_PASSWORD 环境变量')
}
const result = spawnSync(
PLINK_PATH,
['-batch', '-ssh', '-hostkey', SSH_HOSTKEY, '-pw', SSH_PASSWORD, SSH_HOST, REMOTE_NODE_COMMAND],
{
encoding: 'utf8',
input: script,
maxBuffer: 64 * 1024 * 1024,
windowsHide: true
}
)
if (result.error) {
throw result.error
}
if (result.status !== 0) {
throw new Error(`远程脚本执行失败: ${result.stderr || result.stdout}`)
}
const stdout = normalizeText(result.stdout)
if (!stdout) {
throw new Error('远程脚本返回为空')
}
return JSON.parse(stdout)
}
const buildRemoteBackupScript = () => `
let PrismaClient
try { ({ PrismaClient } = require('@prisma/client')) } catch { ({ PrismaClient } = require('/app/node_modules/@prisma/client')) }
const prisma = new PrismaClient()
;(async () => {
const [
equipment,
borrowRecords,
maintenanceRecords,
disposalRecords,
mountHistories,
inventoryTasks,
inventoryTaskParticipants,
inventoryTaskItems,
inventoryTaskActions
] = await Promise.all([
prisma.equipment.findMany({ orderBy: { id: 'asc' } }),
prisma.borrowRecord.findMany({ orderBy: { id: 'asc' } }),
prisma.maintenanceRecord.findMany({ orderBy: { id: 'asc' } }),
prisma.disposalRecord.findMany({ orderBy: { id: 'asc' } }),
prisma.mountHistory.findMany({ orderBy: { id: 'asc' } }),
prisma.inventoryTask.findMany({ orderBy: { id: 'asc' } }),
prisma.inventoryTaskParticipant.findMany({ orderBy: { id: 'asc' } }),
prisma.inventoryTaskItem.findMany({ orderBy: { id: 'asc' } }),
prisma.inventoryTaskAction.findMany({ orderBy: { id: 'asc' } })
])
console.log(JSON.stringify({
generatedAt: new Date().toISOString(),
counts: {
equipment: equipment.length,
borrowRecords: borrowRecords.length,
maintenanceRecords: maintenanceRecords.length,
disposalRecords: disposalRecords.length,
mountHistories: mountHistories.length,
inventoryTasks: inventoryTasks.length,
inventoryTaskParticipants: inventoryTaskParticipants.length,
inventoryTaskItems: inventoryTaskItems.length,
inventoryTaskActions: inventoryTaskActions.length
},
equipment,
borrowRecords,
maintenanceRecords,
disposalRecords,
mountHistories,
inventoryTasks,
inventoryTaskParticipants,
inventoryTaskItems,
inventoryTaskActions
}))
await prisma.$disconnect()
})().catch(async (error) => {
console.error(error && error.stack ? error.stack : String(error))
try { await prisma.$disconnect() } catch {}
process.exit(1)
})
`
const buildRemoteWipeScript = () => `
let PrismaClient
try { ({ PrismaClient } = require('@prisma/client')) } catch { ({ PrismaClient } = require('/app/node_modules/@prisma/client')) }
const prisma = new PrismaClient()
;(async () => {
const before = {
equipment: await prisma.equipment.count(),
borrowRecords: await prisma.borrowRecord.count(),
maintenanceRecords: await prisma.maintenanceRecord.count(),
disposalRecords: await prisma.disposalRecord.count(),
mountHistories: await prisma.mountHistory.count(),
inventoryTasks: await prisma.inventoryTask.count(),
inventoryTaskParticipants: await prisma.inventoryTaskParticipant.count(),
inventoryTaskItems: await prisma.inventoryTaskItem.count(),
inventoryTaskActions: await prisma.inventoryTaskAction.count()
}
await prisma.$transaction(async (tx) => {
await tx.inventoryTaskAction.deleteMany({})
await tx.inventoryTaskItem.deleteMany({})
await tx.inventoryTaskParticipant.deleteMany({})
await tx.inventoryTask.deleteMany({})
await tx.maintenanceRecord.deleteMany({})
await tx.disposalRecord.deleteMany({})
await tx.mountHistory.deleteMany({})
await tx.borrowRecord.deleteMany({})
await tx.equipment.deleteMany({})
})
const after = {
equipment: await prisma.equipment.count(),
borrowRecords: await prisma.borrowRecord.count(),
maintenanceRecords: await prisma.maintenanceRecord.count(),
disposalRecords: await prisma.disposalRecord.count(),
mountHistories: await prisma.mountHistory.count(),
inventoryTasks: await prisma.inventoryTask.count(),
inventoryTaskParticipants: await prisma.inventoryTaskParticipant.count(),
inventoryTaskItems: await prisma.inventoryTaskItem.count(),
inventoryTaskActions: await prisma.inventoryTaskAction.count()
}
console.log(JSON.stringify({ before, after }))
await prisma.$disconnect()
})().catch(async (error) => {
console.error(error && error.stack ? error.stack : String(error))
try { await prisma.$disconnect() } catch {}
process.exit(1)
})
`
const buildRemoteImportScript = (payload, keepExisting) => {
const payloadBase64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')
return `
let PrismaClient
try { ({ PrismaClient } = require('@prisma/client')) } catch { ({ PrismaClient } = require('/app/node_modules/@prisma/client')) }
const prisma = new PrismaClient()
const payload = JSON.parse(Buffer.from('${payloadBase64}', 'base64').toString('utf8'))
const keepExisting = ${keepExisting ? 'true' : 'false'}
const normalizeText = (value) => String(value == null ? '' : value).replace(/\\r/g, ' ').replace(/\\n/g, ' ').replace(/\\s+/g, ' ').trim()
const toDateOrNull = (value) => {
const normalized = normalizeText(value)
if (!normalized) return null
const parsed = new Date(normalized)
return Number.isNaN(parsed.getTime()) ? null : parsed
}
const calculateValues = (unitPrice, remainingQuantity) => {
const normalizedUnitPrice = Number(unitPrice || 0)
const totalValue = normalizedUnitPrice
const borrowedQuantity = Math.max(1 - Number(remainingQuantity || 0), 0)
return {
totalValue,
borrowedValue: normalizedUnitPrice * borrowedQuantity,
availableValue: normalizedUnitPrice * Number(remainingQuantity || 0)
}
}
;(async () => {
const [assetTypes, departments, locations] = await Promise.all([
prisma.assetType.findMany({ where: { status: 'active' }, select: { id: true, name: true } }),
prisma.department.findMany({
where: { status: 'active', source_system: 'ding_hr_business' },
select: { id: true, name: true }
}),
prisma.storageLocation.findMany({ where: { status: 'active' }, select: { id: true, name: true } })
])
const assetTypeMap = new Map(assetTypes.map((item) => [item.name, item]))
const departmentMap = new Map(departments.map((item) => [item.name, item]))
const locationMap = new Map(locations.map((item) => [item.name, item]))
const duplicatePayloadCodes = new Set()
const payloadCodes = new Set()
for (const item of payload) {
if (payloadCodes.has(item.legacyCode)) {
duplicatePayloadCodes.add(item.legacyCode)
}
payloadCodes.add(item.legacyCode)
}
if (duplicatePayloadCodes.size > 0) {
throw new Error('导入 payload 中存在重复资产编码: ' + [...duplicatePayloadCodes].join(', '))
}
const existingCodes = await prisma.equipment.findMany({
where: { equipment_code: { in: [...payloadCodes] } },
select: { equipment_code: true }
})
if (!keepExisting && existingCodes.length > 0) {
throw new Error('线上已存在同名资产编码: ' + existingCodes.map((item) => item.equipment_code).join(', '))
}
const existingCodeSet = new Set(existingCodes.map((item) => item.equipment_code))
const results = []
const skippedExisting = []
for (const item of payload) {
if (existingCodeSet.has(item.legacyCode)) {
skippedExisting.push({
legacyCode: item.legacyCode,
importMode: item.importMode,
reason: 'already_exists'
})
continue
}
const assetType = assetTypeMap.get(item.equipmentType)
if (!assetType) {
throw new Error('资产类型不存在: ' + item.equipmentType + ' code=' + item.legacyCode)
}
const department = item.departmentName ? departmentMap.get(item.departmentName) || null : null
if (item.departmentName && !department) {
throw new Error('业务部门不存在: ' + item.departmentName + ' code=' + item.legacyCode)
}
const location = item.storageLocationName ? locationMap.get(item.storageLocationName) || null : null
if (item.storageLocationName && !location) {
throw new Error('存放地点不存在: ' + item.storageLocationName + ' code=' + item.legacyCode)
}
let person = null
if (item.importMode === 'with_owner') {
person = await prisma.person.findFirst({
where: {
id: item.ownerPersonId,
status: 'active',
source_system: 'ding_hr'
},
select: {
id: true,
real_name: true,
nickname: true,
id_number: true
}
})
if (!person) {
throw new Error('使用人不存在或不再是 active: personId=' + item.ownerPersonId + ' code=' + item.legacyCode)
}
}
const unitPrice = Number(item.unitPrice || 0)
const purchaseDate = toDateOrNull(item.purchaseDate)
const borrowTime = toDateOrNull(item.borrowTime) || new Date()
let equipmentStatus = '可用'
let remainingQuantity = 1
let borrowRecordNeeded = false
let borrowNotes = 'Excel迁移导入;在职长期'
if (item.importMode === 'with_owner') {
equipmentStatus = '已借出'
remainingQuantity = 0
borrowRecordNeeded = true
} else if (item.sourceStatusKind === 'assigned') {
equipmentStatus = '已借出'
remainingQuantity = 0
borrowNotes = 'Excel迁移导入;当前使用人待行政补录'
} else if (item.sourceStatusKind === 'unassigned') {
equipmentStatus = '可用'
remainingQuantity = 1
} else {
throw new Error('ownerless 资产状态异常: ' + item.sourceStatusKind + ' code=' + item.legacyCode)
}
const values = calculateValues(unitPrice, remainingQuantity)
const created = await prisma.$transaction(async (tx) => {
const equipment = await tx.equipment.create({
data: {
equipment_code: item.legacyCode,
equipment_name: item.equipmentName,
equipment_model: normalizeText(item.equipmentModel) || null,
equipment_type: assetType.name,
code_generation_mode: 'manual',
status: equipmentStatus,
total_quantity: 1,
remaining_quantity: remainingQuantity,
unit_price: unitPrice,
total_value: values.totalValue,
borrowed_value: values.borrowedValue,
available_value: values.availableValue,
total_value_with_children: values.totalValue,
description: normalizeText(item.description) || null,
purchase_date: purchaseDate,
supplier: normalizeText(item.supplier) || null,
storage_location_id: location?.id,
asset_type_id: assetType.id,
department_id: department?.id,
mount_status: 'none'
}
})
if (borrowRecordNeeded && person) {
await tx.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
borrow_time: borrowTime,
expected_return_time: null,
is_long_term: true,
return_time: null,
status: '已借出',
return_condition: '正常',
notes: borrowNotes
}
})
}
return {
id: equipment.id,
equipment_code: equipment.equipment_code,
importMode: item.importMode,
status: equipment.status,
person_id: person?.id || null,
person_nickname: person?.nickname || null
}
})
results.push(created)
}
const verification = {
equipment: await prisma.equipment.count(),
borrowRecords: await prisma.borrowRecord.count(),
activeBorrow: await prisma.borrowRecord.count({ where: { status: '已借出' } }),
importedCodeCount: await prisma.equipment.count({ where: { equipment_code: { in: [...payloadCodes] } } }),
importedWithOwnerCount: results.filter((item) => item.importMode === 'with_owner').length,
importedWithoutOwnerCount: results.filter((item) => item.importMode === 'without_owner').length,
skippedExistingCount: skippedExisting.length
}
console.log(JSON.stringify({
imported: results.length,
verification,
skippedExisting,
sample: results.slice(0, 10)
}))
await prisma.$disconnect()
})().catch(async (error) => {
console.error(error && error.stack ? error.stack : String(error))
try { await prisma.$disconnect() } catch {}
process.exit(1)
})
`
}
const buildRemoteVerifyScript = (payload) => {
const payloadBase64 = Buffer.from(
JSON.stringify(payload.map((item) => item.legacyCode)),
'utf8'
).toString('base64')
return `
let PrismaClient
try { ({ PrismaClient } = require('@prisma/client')) } catch { ({ PrismaClient } = require('/app/node_modules/@prisma/client')) }
const prisma = new PrismaClient()
const codes = JSON.parse(Buffer.from('${payloadBase64}', 'base64').toString('utf8'))
;(async () => {
const [equipment, borrowRecords, activeBorrow, importedEquipments] = await Promise.all([
prisma.equipment.count(),
prisma.borrowRecord.count(),
prisma.borrowRecord.count({ where: { status: '已借出' } }),
prisma.equipment.findMany({
where: { equipment_code: { in: codes } },
orderBy: { equipment_code: 'asc' },
take: 20,
select: {
equipment_code: true,
equipment_name: true,
equipment_type: true,
status: true,
remaining_quantity: true,
department: { select: { name: true } },
storageLocation: { select: { name: true } }
}
})
])
console.log(JSON.stringify({
equipment,
borrowRecords,
activeBorrow,
importedCodeCount: await prisma.equipment.count({ where: { equipment_code: { in: codes } } }),
sample: importedEquipments
}))
await prisma.$disconnect()
})().catch(async (error) => {
console.error(error && error.stack ? error.stack : String(error))
try { await prisma.$disconnect() } catch {}
process.exit(1)
})
`
}
const run = () => {
const options = parseArgs()
const inputDir = resolveInputDir(options.inputDir)
const normalizedPath = path.join(inputDir, 'normalized_assets.jsonl')
if (!fs.existsSync(normalizedPath)) {
throw new Error(`未找到 normalized_assets.jsonl: ${normalizedPath}`)
}
const normalizedRecords = readJsonl(normalizedPath)
const importReadyRecords = normalizedRecords.filter((record) => record.bucket === 'import-ready')
const importWithoutOwnerRecords = options.includeOwnerless
? normalizedRecords.filter((record) => record.bucket === 'import-without-owner')
: []
const importCandidates = [...importReadyRecords, ...importWithoutOwnerRecords]
if (!importCandidates.length) {
throw new Error('没有可导入的确认记录')
}
const partitioned = partitionImportCandidates(importCandidates)
const payload = buildImportPayload(partitioned.filteredRecords)
const backupDir = ensureOutputDir('live_import_backup')
writeJson(path.join(backupDir, 'excluded_missing_code_rows.json'), {
excludedMissingCodeCount: partitioned.missingCodeRows.length,
rows: partitioned.missingCodeRows
})
writeJson(path.join(backupDir, 'excluded_duplicate_codes.json'), {
excludedDuplicateCodeCount: partitioned.duplicateDetails.length,
excludedDuplicateRecordCount: partitioned.duplicateRows.length,
duplicateDetails: partitioned.duplicateDetails,
excludedRows: partitioned.duplicateRows
})
writeJson(path.join(backupDir, 'planned_import_summary.json'), {
inputDir,
importReadyCount: importReadyRecords.length,
importWithoutOwnerCount: importWithoutOwnerRecords.length,
selectedCandidateCount: importCandidates.length,
excludedMissingCodeCount: partitioned.missingCodeRows.length,
excludedDuplicateRecordCount: partitioned.duplicateRows.length,
excludedDuplicateCodeCount: partitioned.duplicateDetails.length,
finalImportCount: payload.length,
finalImportReadyCount: payload.filter((item) => item.importMode === 'with_owner').length,
finalImportWithoutOwnerCount: payload.filter((item) => item.importMode === 'without_owner').length,
keepExisting: options.keepExisting,
includeOwnerless: options.includeOwnerless
})
writeJson(path.join(backupDir, 'planned_payload.json'), {
payloadCount: payload.length,
payload
})
const backup = runRemoteNodeScript(buildRemoteBackupScript())
writeJson(path.join(backupDir, 'pre_import_live_backup.json'), backup)
let wipeResult = { skipped: true }
if (!options.keepExisting) {
wipeResult = runRemoteNodeScript(buildRemoteWipeScript())
writeJson(path.join(backupDir, 'wipe_result.json'), wipeResult)
}
const importResult = runRemoteNodeScript(buildRemoteImportScript(payload, options.keepExisting))
writeJson(path.join(backupDir, 'import_result.json'), importResult)
const verification = runRemoteNodeScript(buildRemoteVerifyScript(payload))
writeJson(path.join(backupDir, 'post_import_verification.json'), verification)
console.log(
JSON.stringify(
{
backupDir,
inputDir,
importedCount: importResult.imported,
wipeResult,
importResult,
verification
},
null,
2
)
)
}
try {
run()
} catch (error) {
console.error(error && error.stack ? error.stack : String(error))
process.exit(1)
}