@@ -4,6 +4,53 @@ const fs = require('fs');
44const path = require ( 'path' ) ;
55const { match } = require ( 'assert' ) ;
66
7+ function isFiniteNumber ( n ) {
8+ return typeof n === 'number' && Number . isFinite ( n ) ;
9+ }
10+
11+ function toNumberOrZero ( v ) {
12+ if ( isFiniteNumber ( v ) ) return v ;
13+ const n = parseFloat ( String ( v ?? '' ) . trim ( ) ) ;
14+ return Number . isFinite ( n ) ? n : 0 ;
15+ }
16+
17+ function toIntOrZero ( v ) {
18+ if ( isFiniteNumber ( v ) ) return Math . trunc ( v ) ;
19+ const n = parseInt ( String ( v ?? '' ) . trim ( ) , 10 ) ;
20+ return Number . isFinite ( n ) ? n : 0 ;
21+ }
22+
23+ function toBool ( v ) {
24+ if ( typeof v === 'boolean' ) return v ;
25+ const s = String ( v ?? '' ) . trim ( ) . toLowerCase ( ) ;
26+ if ( s === 'yes' || s === 'true' || s === '1' ) return true ;
27+ return false ;
28+ }
29+
30+ function splitFlags ( v , regex , mapper = capitalize ) {
31+ const s = String ( v ?? '' ) ;
32+ if ( ! s . trim ( ) ) return [ ] ;
33+ return s . split ( regex ) . map ( mapper ) . map ( x => x . trim ( ) ) . filter ( flag => flag . length > 0 ) ;
34+ }
35+
36+ function computeFloweringMonthsByNumber ( monthStr ) {
37+ const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
38+ const s = String ( monthStr ?? '' ) . trim ( ) ;
39+ if ( ! s ) return [ ] ;
40+ // Support en dash, em dash, and hyphen ranges (e.g. "May–Jun", "May-Jun", "May—Jun")
41+ const parts = s . split ( / [ – — - ] / ) . map ( p => p . trim ( ) ) . filter ( Boolean ) ;
42+ if ( parts . length === 1 ) {
43+ const idx = months . indexOf ( parts [ 0 ] ) ;
44+ return idx > - 1 ? [ idx ] : [ ] ;
45+ }
46+ const from = months . indexOf ( parts [ 0 ] ) ;
47+ const to = months . indexOf ( parts [ 1 ] ) ;
48+ if ( from === - 1 || to === - 1 ) return [ ] ;
49+ const out = [ ] ;
50+ for ( let i = from ; i <= to ; i ++ ) out . push ( i ) ;
51+ return out ;
52+ }
53+
754// Work around lack of top level await while still reporting errors properly
855go ( ) . then ( ( ) => {
956 // All done
@@ -15,6 +62,25 @@ go().then(() => {
1562
1663async function go ( ) {
1764 const { plants, close } = await db ( ) ;
65+
66+ // Helpful when schema validation is enabled: print details so failures are actionable.
67+ const originalUpdateOne = plants . updateOne . bind ( plants ) ;
68+ plants . updateOne = async ( ...args ) => {
69+ try {
70+ return await originalUpdateOne ( ...args ) ;
71+ } catch ( e ) {
72+ if ( e && e . code === 121 && e . errInfo && e . errInfo . details ) {
73+ console . error ( 'MongoDB schema validation failed during massage.updateOne' ) ;
74+ try {
75+ console . error ( JSON . stringify ( e . errInfo . details , null , 2 ) ) ;
76+ } catch ( jsonErr ) {
77+ console . error ( e . errInfo . details ) ;
78+ }
79+ }
80+ throw e ;
81+ }
82+ } ;
83+
1884 let values = await plants . find ( ) . toArray ( ) ;
1985
2086 // Fix a small set of plants that already had duplicate records
@@ -33,6 +99,57 @@ async function go() {
3399 for ( let i = 0 ; ( i < values . length ) ; i ++ ) {
34100 let plant = values [ i ] ;
35101
102+ // Ensure required fields exist and have valid types BEFORE any other updates.
103+ // This allows us to repair legacy/invalid documents even when validation is strict.
104+ const scientificName = plant [ 'Scientific Name' ] || plant . _id ;
105+ const genus = ( String ( scientificName ?? '' ) . split ( / \s + / ) [ 0 ] || '' ) . trim ( ) ;
106+ const family = plant [ 'Plant Family' ] || plant [ 'Family' ] || '' ;
107+
108+ // Build required flags (empty arrays are acceptable to the schema).
109+ const plantType = plant [ 'Plant Type' ] || '' ;
110+ let plantTypeFlags = [ ] ;
111+ let lifeCycleFlags = [ ] ;
112+ const matches = String ( plantType ) . match ( / ^ ( .* ?) ( \( .* ?\) ) ? $ / ) ;
113+ if ( matches ) {
114+ plantTypeFlags = matches [ 1 ] . split ( / , / ) ;
115+ if ( matches [ 2 ] ) {
116+ plantTypeFlags = [ ...plantTypeFlags , ...matches [ 2 ] . split ( / o r / ) ] ;
117+ }
118+ plantTypeFlags = plantTypeFlags . map ( flag => flag . trim ( ) . replace ( '(' , '' ) . replace ( ')' , '' ) ) . map ( capitalize ) . filter ( Boolean ) ;
119+ const lifeCycles = [ 'Annual' , 'Biennial' , 'Perennial' ] ;
120+ lifeCycleFlags = plantTypeFlags . filter ( pt => lifeCycles . includes ( pt ) ) ;
121+ plantTypeFlags = plantTypeFlags . filter ( pt => ! lifeCycles . includes ( pt ) ) ;
122+ }
123+
124+ const sunExposureFlags = splitFlags ( plant [ 'Sun Exposure' ] , / , \s * / , capitalize ) ;
125+ const soilMoistureFlags = splitFlags ( plant [ 'Soil Moisture' ] , / , \s * / , capitalize ) ;
126+ const pollinatorFlags = splitFlags ( plant [ 'Pollinators' ] , / \s * (?: , | ; ) + \s * / , capitalize ) ;
127+ const flowerColorFlags = splitFlags ( plant [ 'Flower Color' ] , / \s * [ - — – , ] \s * / , capitalize ) ;
128+ const availabilityFlags = [ ] ;
129+ if ( String ( plant [ 'Online Flag' ] ?? '' ) === '1' ) availabilityFlags . push ( 'Online' ) ;
130+ if ( String ( plant [ 'Local Flag' ] ?? '' ) === '1' ) availabilityFlags . push ( 'Local' ) ;
131+
132+ const requiredFixes = {
133+ 'Height (feet)' : toNumberOrZero ( plant [ 'Height (feet)' ] ) ,
134+ 'Spread (feet)' : toNumberOrZero ( plant [ 'Spread (feet)' ] ) ,
135+ 'Recommendation Score' : toIntOrZero ( plant [ 'Recommendation Score' ] ) ,
136+ 'Showy' : toBool ( plant [ 'Showy' ] ) ,
137+ 'Superplant' : toBool ( plant [ 'Superplant' ] ) ,
138+ 'States' : splitFlags ( plant [ 'Distribution in USA' ] , / , \s * / , s => String ( s ) . trim ( ) ) . filter ( Boolean ) ,
139+ 'Genus' : genus ,
140+ 'Family' : String ( family ?? '' ) ,
141+ 'Sun Exposure Flags' : sunExposureFlags ,
142+ 'Soil Moisture Flags' : soilMoistureFlags ,
143+ 'Plant Type Flags' : plantTypeFlags ,
144+ 'Life Cycle Flags' : lifeCycleFlags ,
145+ 'Pollinator Flags' : pollinatorFlags ,
146+ 'Flower Color Flags' : flowerColorFlags ,
147+ 'Availability Flags' : availabilityFlags ,
148+ 'Flowering Months By Number' : computeFloweringMonthsByNumber ( plant [ 'Flowering Months' ] ) ,
149+ } ;
150+
151+ await plants . updateOne ( { _id : plant . _id } , { $set : requiredFixes } ) ;
152+
36153 // Check if image files exist and set hasImage and hasPreview flags
37154 const fullImagePath = `${ __dirname } /images/${ plant . _id } .jpg` ;
38155 const previewImagePath = `${ __dirname } /images/${ plant . _id } .preview.jpg` ;
@@ -79,31 +196,9 @@ async function go() {
79196 }
80197
81198 // Process Flowering Months into an array of flags
82- const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
83- let [ from , to ] = plant [ 'Flowering Months' ] . split ( '–' ) ;
84- from = months . indexOf ( from ) ;
85- to = months . indexOf ( to ) ;
86- if ( ( from > - 1 ) && ( to > - 1 ) ) {
87- const matching = [ ] ;
88- for ( let i = from ; ( i <= to ) ; i ++ ) {
89- matching . push ( i ) ;
90- }
91- await plants . updateOne ( {
92- _id : plant . _id
93- } , {
94- $set : {
95- 'Flowering Months By Number' : matching
96- }
97- } ) ;
98- } else {
99- await plants . updateOne ( {
100- _id : plant . _id
101- } , {
102- $unset : {
103- 'Flowering Months By Number' : 1
104- }
105- } ) ;
106- }
199+ // NOTE: This field is required by strict DB schema. Never $unset it.
200+ const floweringMonthsByNumber = computeFloweringMonthsByNumber ( plant [ 'Flowering Months' ] ) ;
201+ await plants . updateOne ( { _id : plant . _id } , { $set : { 'Flowering Months By Number' : floweringMonthsByNumber } } ) ;
107202 var states = plant [ 'Distribution in USA' ] . split ( ', ' )
108203 await plants . updateOne ( {
109204 _id : plant . _id
@@ -148,12 +243,12 @@ async function go() {
148243 const score = plant [ 'Recommendation Score' ] ;
149244 if ( typeof score === 'string' || score instanceof String ) {
150245 const numScore = parseFloat ( score ) ;
151- if ( numScore != NaN ) {
246+ if ( ! isNaN ( numScore ) ) {
152247 await plants . updateOne ( {
153248 _id : plant . _id
154249 } , {
155250 $set : {
156- 'Recommendation Score' : numScore
251+ 'Recommendation Score' : Math . trunc ( numScore )
157252 }
158253 } ) ;
159254 } else {
@@ -201,93 +296,8 @@ async function go() {
201296 } ) ;
202297 }
203298 }
204- let plantTypeFlags = [ ] ;
205- const plantType = plant [ 'Plant Type' ] ;
206- const matches = plantType . match ( / ^ ( .* ?) ( \( .* ?\) ) ? $ / ) ;
207- if ( ! matches ) {
208- console . error ( `Don't know how to handle ${ plantType } ` ) ;
209- } else {
210- plantTypeFlags = matches [ 1 ] . split ( / , / ) ;
211- if ( matches [ 2 ] ) {
212- plantTypeFlags = [ ...plantTypeFlags , ...matches [ 2 ] . split ( / o r / ) ] ;
213- }
214- plantTypeFlags = plantTypeFlags . map ( flag => flag . trim ( ) . replace ( '(' , '' ) . replace ( ')' , '' ) ) . map ( capitalize ) ;
215- const lifeCycles = [ 'Annual' , 'Biennial' , 'Perennial' ] ;
216- const lifeCycleFlags = plantTypeFlags . filter ( plantType => lifeCycles . includes ( plantType ) ) ;
217- plantTypeFlags = plantTypeFlags . filter ( plantType => ! lifeCycles . includes ( plantType ) ) ;
218- await plants . updateOne ( {
219- _id : plant . _id
220- } , {
221- $set : {
222- 'Plant Type Flags' : plantTypeFlags ,
223- 'Life Cycle Flags' : lifeCycleFlags
224- }
225- } ) ;
226- }
227- const sunExposureFlags = plant [ 'Sun Exposure' ] . split ( ', ' ) . map ( capitalize ) . filter ( flag => flag . length > 0 ) ;
228- await plants . updateOne ( {
229- _id : plant . _id
230- } , {
231- $set : {
232- 'Sun Exposure Flags' : sunExposureFlags
233- }
234- } ) ;
235- const soilMoistureFlags = plant [ 'Soil Moisture' ] . split ( / , \s * / ) . map ( capitalize ) . filter ( flag => flag . length > 0 ) ;
236- if ( soilMoistureFlags . length ) {
237- await plants . updateOne ( {
238- _id : plant . _id
239- } , {
240- $set : {
241- 'Soil Moisture Flags' : soilMoistureFlags
242- }
243- } ) ;
244- } else {
245- await plants . updateOne ( {
246- _id : plant . _id
247- } , {
248- $unset : {
249- 'Soil Moisture Flags' : 1
250- }
251- } ) ;
252- }
253- const pollinatorFlags = plant [ 'Pollinators' ] . split ( / \s * (?: , | ; ) + \s * / ) . map ( capitalize ) . filter ( flag => flag . length > 0 ) ;
254- await plants . updateOne ( {
255- _id : plant . _id
256- } , {
257- $set : {
258- 'Pollinator Flags' : pollinatorFlags
259- }
260- } ) ;
261- const propagationFlags = plant [ 'Propagation' ] . split ( / \s * (?: , | ; ) + \s * / ) . map ( capitalize ) . filter ( flag => flag . length > 0 ) ;
262- await plants . updateOne ( {
263- _id : plant . _id
264- } , {
265- $set : {
266- 'Propagation Flags' : propagationFlags
267- }
268- } ) ;
269- const flowerColorFlags = ( plant [ 'Flower Color' ] || '' ) . split ( / \s * [ - — – , ] \s * / ) . map ( capitalize ) . filter ( flag => flag . length > 0 ) ;
270- await plants . updateOne ( {
271- _id : plant . _id
272- } , {
273- $set : {
274- 'Flower Color Flags' : flowerColorFlags
275- }
276- } ) ;
277- const availabilityFlags = [ ] ;
278- if ( plant [ 'Online Flag' ] === '1' ) {
279- availabilityFlags . push ( 'Online' ) ;
280- }
281- if ( plant [ 'Local Flag' ] === '1' ) {
282- availabilityFlags . push ( 'Local' ) ;
283- }
284- await plants . updateOne ( {
285- _id : plant . _id
286- } , {
287- $set : {
288- 'Availability Flags' : availabilityFlags
289- }
290- } ) ;
299+ // NOTE: "Flags" fields + required fields are computed and fixed at the top of the loop
300+ // in a single `$set` to keep the document schema-valid under strict validation.
291301 }
292302
293303 await close ( ) ;
0 commit comments