|
2 | 2 | * Seed table data in database |
3 | 3 | */ |
4 | 4 | const _ = require('lodash') |
| 5 | +const fs = require('fs') |
| 6 | +const path = require('path') |
| 7 | +const { v4: uuid } = require('uuid') |
5 | 8 | const logger = require('../common/logger') |
6 | 9 | const prisma = require('../common/prisma').getClient() |
7 | 10 |
|
@@ -40,36 +43,97 @@ async function importResourceRolePhaseDependency () { |
40 | 43 | } |
41 | 44 |
|
42 | 45 | async function importResource () { |
43 | | - let data = require('./seed/Resource.json') |
44 | | - let memberData = {} |
45 | | - data = _.map(data, d => { |
46 | | - const r = _.omit(d, ['created', 'rating']) |
47 | | - r.createdAt = d.created |
48 | | - // collect member data |
49 | | - const memberId = _.parseInt(d.memberId) |
50 | | - if (!_.keys(memberData).includes(memberId)) { |
51 | | - memberData[memberId] = { |
52 | | - userId: memberId, |
53 | | - handle: d.memberHandle, |
54 | | - handleLower: d.memberHandle.toLowerCase(), |
55 | | - maxRating: _.get(d, 'rating', null), |
56 | | - createdBy |
| 46 | + const logPath = path.join(__dirname, 'seed', 'resource-import-skipped.log') |
| 47 | + // truncate existing log |
| 48 | + try { fs.writeFileSync(logPath, '') } catch (e) { /* ignore */ } |
| 49 | + |
| 50 | + const writeSkip = (reason, rec) => { |
| 51 | + const line = JSON.stringify({ reason, record: rec }) + '\n' |
| 52 | + fs.appendFileSync(logPath, line) |
| 53 | + } |
| 54 | + |
| 55 | + const data = require('./seed/Resource.json') |
| 56 | + const total = data.length |
| 57 | + |
| 58 | + const memberDataMap = {} |
| 59 | + let success = 0 |
| 60 | + let skipped = 0 |
| 61 | + const start = Date.now() |
| 62 | + const progressTimer = setInterval(() => { |
| 63 | + const elapsed = (Date.now() - start) / 1000 |
| 64 | + const processed = success + skipped |
| 65 | + const rate = processed > 0 ? (processed / elapsed) : 0 |
| 66 | + const pct = total > 0 ? ((processed / total) * 100) : 0 |
| 67 | + const remaining = Math.max(total - processed, 0) |
| 68 | + const eta = rate > 0 ? (remaining / rate) : Infinity |
| 69 | + logger.info(`Resource import progress: ${processed}/${total} (${pct.toFixed(1)}%), success=${success}, skipped=${skipped}, rate=${rate.toFixed(2)} rec/s, elapsed=${elapsed.toFixed(1)}s, ETA=${Number.isFinite(eta) ? eta.toFixed(1) : '∞'}s`) |
| 70 | + }, 5000) |
| 71 | + |
| 72 | + for (const d of data) { |
| 73 | + // Validate required fields |
| 74 | + const missing = [] |
| 75 | + for (const f of ['challengeId', 'memberId', 'memberHandle', 'roleId']) { |
| 76 | + if (_.isNil(d[f]) || _.toString(d[f]).trim() === '') missing.push(f) |
| 77 | + } |
| 78 | + if (missing.length) { |
| 79 | + skipped += 1 |
| 80 | + writeSkip(`Missing required field(s): ${missing.join(', ')}`, d) |
| 81 | + continue |
| 82 | + } |
| 83 | + |
| 84 | + // Build record for Prisma. Remove fields not in schema or that conflict. |
| 85 | + const r = _.omit(d, ['created', 'updated', 'rating', 'legacyId']) |
| 86 | + // Ensure id exists; generate if absent |
| 87 | + if (!r.id) r.id = uuid() |
| 88 | + // Normalize timestamps |
| 89 | + if (d.created) r.createdAt = new Date(d.created) |
| 90 | + if (d.updated) r.updatedAt = new Date(d.updated) |
| 91 | + // Ensure strings for string columns |
| 92 | + r.challengeId = _.toString(r.challengeId) |
| 93 | + r.memberId = _.toString(r.memberId) |
| 94 | + r.memberHandle = _.toString(r.memberHandle) |
| 95 | + r.roleId = _.toString(r.roleId) |
| 96 | + if (!r.createdBy) r.createdBy = createdBy |
| 97 | + |
| 98 | + try { |
| 99 | + await prisma.resource.create({ data: r }) |
| 100 | + success += 1 |
| 101 | + // collect member data only for successfully inserted resources |
| 102 | + const mid = _.parseInt(d.memberId) |
| 103 | + if (!_.has(memberDataMap, mid)) { |
| 104 | + memberDataMap[mid] = { |
| 105 | + userId: mid, |
| 106 | + handle: d.memberHandle, |
| 107 | + handleLower: _.toString(d.memberHandle).toLowerCase(), |
| 108 | + maxRating: _.get(d, 'rating', null), |
| 109 | + createdBy |
| 110 | + } |
57 | 111 | } |
| 112 | + } catch (err) { |
| 113 | + skipped += 1 |
| 114 | + writeSkip(`Insert failed: ${err.message}`, d) |
58 | 115 | } |
59 | | - return r |
60 | | - }) |
61 | | - await prisma.resource.createMany({ data }) |
62 | | - logger.info('Imported ResourceRole data') |
63 | | - // import memberProfile and memberStats |
64 | | - memberData = _.values(memberData) |
65 | | - await prisma.memberStats.createMany({ data: memberData }) |
66 | | - await prisma.memberProfile.createMany({ data: _.map(memberData, d => _.omit(d, 'maxRating')) }) |
67 | | - logger.info('Imported Member data') |
| 116 | + } |
| 117 | + |
| 118 | + clearInterval(progressTimer) |
| 119 | + const elapsed = (Date.now() - start) / 1000 |
| 120 | + const rate = (success + skipped) > 0 ? ((success + skipped) / elapsed).toFixed(2) : '0.00' |
| 121 | + logger.info(`Imported Resource data: success=${success}, skipped=${skipped}, total=${total}, elapsed=${elapsed.toFixed(1)}s, rate=${rate} rec/s`) |
| 122 | + |
| 123 | + // import memberProfile and memberStats for members associated with successfully inserted resources |
| 124 | + const memberData = _.values(memberDataMap) |
| 125 | + if (memberData.length) { |
| 126 | + await prisma.memberStats.createMany({ data: memberData }) |
| 127 | + await prisma.memberProfile.createMany({ data: _.map(memberData, (m) => _.omit(m, 'maxRating')) }) |
| 128 | + logger.info(`Imported Member data: count=${memberData.length}`) |
| 129 | + } else { |
| 130 | + logger.info('No member data to import') |
| 131 | + } |
68 | 132 | } |
69 | 133 |
|
70 | 134 | async function main () { |
71 | | - await importResourceRole() |
72 | | - await importResourceRolePhaseDependency() |
| 135 | + //await importResourceRole() |
| 136 | + //await importResourceRolePhaseDependency() |
73 | 137 | await importResource() |
74 | 138 | } |
75 | 139 |
|
|
0 commit comments