Repository navigation
Expand file tree
/
Copy pathdownload.js
More file actions
213 lines (179 loc) · 6.62 KB
/
Copy pathdownload.js
File metadata and controls
213 lines (179 loc) · 6.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
require('dotenv').config();
const VERSION = '1.2.0';
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const parse = require('csv-parse/lib/sync');
const db = require('./lib/db');
// Logging function
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `${timestamp}: ${message}\n`;
// Log to console
console.log(message);
// Log to file
const logPath = path.join(__dirname, 'download.log');
fs.appendFileSync(logPath, logMessage);
}
// Clear log file at start
fs.writeFileSync(path.join(__dirname, 'download.log'), `Starting download.js version ${VERSION}\n`);
// Log version at startup
log(`Running download.js version ${VERSION}`);
// Ensure required environment variables are set
// Ensure required environment variables are set
const requiredEnvVars = [
'MASTER_CSV_URL',
'ARTICLES_CSV_URL'
];
requiredEnvVars.forEach((envVar) => {
if (!process.env[envVar]) {
console.error(`Missing required environment variable: ${envVar}`);
process.exit(1);
}
});
// Start script execution
go();
let plants = null;
let close = null;
async function go() {
try {
const info = await db();
plants = info.plants;
close = info.close;
// Get initial count
const initialCount = await plants.countDocuments({});
log(`Initial database count: ${initialCount} plants`);
await downloadMain();
// Get final count
const finalCount = await plants.countDocuments({});
log(`Final database count: ${finalCount} plants`);
await close();
} catch (e) {
console.error(e);
process.exit(1);
}
}
async function downloadMain() {
log('Fetching spreadsheet data...');
const body = await get(process.env.MASTER_CSV_URL);
const articlesBody = await get(process.env.ARTICLES_CSV_URL);
// Parse master CSV
const records = parse(body, {
columns: true,
skip_empty_lines: true
});
// Parse articles CSV
const articleRecords = parse(articlesBody, {
columns: true,
skip_empty_lines: true
});
// Remove plants that are not in the latest CSV
await plants.deleteMany({
_id: { $nin: records.map(record => record['Scientific Name']) }
});
log(`Processing ${records.length} plants from master CSV`);
let i = 0;
for (const record of records) {
i++;
// Clean up data by trimming whitespace
const clean = Object.fromEntries(
Object.entries(record).map(([key, value]) => {
return [key.trim(), value.trim()];
})
);
let name = clean['Scientific Name'];
log(`${name} (${i} of ${records.length})`);
// Set _id to the scientific name
clean._id = name;
// Handle superplant status
let sp = (clean['Super Plant'] && clean['Super Plant'].trim() === 'Yes');
// Get existing plant record if available
const existing = await plants.findOne({
_id: name
});
// Set Superplant and Showy flags
clean.Superplant = sp;
clean.Showy = clean.Showy === 'Yes';
// Convert Recommendation Score to an integer and ALWAYS populate.
// DB schema requires this field to exist and be an integer.
const scoreRaw = clean['Recommendation Score'];
const scoreNum = parseInt(scoreRaw, 10);
clean['Recommendation Score'] = Number.isFinite(scoreNum) ? scoreNum : 0;
// Convert height and spread to numbers and ALWAYS populate.
// DB schema requires these fields to exist and be numeric.
const heightNum = parseFloat(clean['Height (feet)']);
clean['Height (feet)'] = Number.isFinite(heightNum) ? heightNum : 0;
const spreadNum = parseFloat(clean['Spread (feet)']);
clean['Spread (feet)'] = Number.isFinite(spreadNum) ? spreadNum : 0;
// Process articles for this plant
clean.Articles = [];
for (const record of articleRecords) {
if (record['Scientific Name'] === clean['Scientific Name']) {
const sources = record['Source'].split(/\s*,\s*/);
const sourceUrls = record['Source URL'].split(/\s*,\s*/);
for (let i = 0; (i < sources.length); i++) {
clean.Articles.push({
'Source': sources[i],
'Source URL': sourceUrls[i]
});
}
}
}
// IMPORTANT:
// - `massage.js` / `optimize-plants-db` add computed fields that our DB schema requires
// (e.g. Flags arrays, Flowering Months By Number, States, Genus, Family, etc).
// - If we blindly replace documents using only the CSV fields, we will both:
// (a) fail schema validation, and (b) lose those computed fields.
//
// So when a record already exists, merge it and only overwrite with the latest CSV fields.
const merged = existing ? { ...existing, ...clean } : clean;
// Update the plant in the database
await update(plants, merged);
}
}
async function update(plants, clean) {
try {
await plants.replaceOne({
_id: clean._id
}, clean, {
upsert: true
});
} catch (e) {
// When MongoDB schema validation fails, expose details (paths/types) to help debug quickly.
// Example: e.errInfo.details.schemaRulesNotSatisfied
if (e && e.code === 121 && e.errInfo && e.errInfo.details) {
console.error('MongoDB schema validation failed for plant:', clean && clean._id);
try {
console.error(JSON.stringify(e.errInfo.details, null, 2));
} catch (jsonErr) {
console.error(e.errInfo.details);
}
}
throw e;
}
}
async function get(url, type = 'text') {
let lastError;
for (let attempt = 0; attempt < 5; attempt++) {
try {
log(`Attempting to fetch ${url} (attempt ${attempt + 1})`);
const response = await fetch(url, { redirect: 'follow' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response[type]();
log(`Successfully fetched ${url}`);
return result;
} catch (e) {
log(`Fetch error (attempt ${attempt + 1}): ${e.message}`);
lastError = e;
await pause();
}
}
throw lastError;
}
function pause() {
return new Promise((resolve) => {
setTimeout(resolve, 5000);
});
}