1

MERN stack. There are a main storyModel and an eventModel which is an embedded subdocument. I create a story and then add events to it. I've tried several ways, searched SO for the way to insert a subdocument to an existing mongo collection, looked at the mongo and mongoose documentation(very little) and I can't seem to insert. I was able to do it before but after refactoring, the code doesn't work anymore. I tried isolating where the problem is, and it seems to be pointing to route although I'm not 100% on this.

Here is my model/schema(using mongoose):

const mongoose = require('mongoose')

    const GeoSchema = mongoose.Schema({
        type: {
            type: String,
            enum: ['Point', 'LineString', 'Polygon'],
            default: "Point"
        },
        coordinates: {
            type: [Number],
            index: "2dsphere"
        }
    })

    const EventSchema = mongoose.Schema({
        eventDate: {
            type: Date
        },
        eventTitle: {
            type: String,
            required: true,
            minlength: 3
        },
        eventDescription: {
            type: String,
            minlength: 3
        },
        eventImageUrl: {
            type: String
        },
        eventLink: {
            type: String
        },
        eventAudio: {
            type: String
        },
        eventLocation: GeoSchema,
    })

    const StorySchema = mongoose.Schema({
        storyTitle: {
            type: String,
            minlength: 5,
            required: true
        },
        storySummary: {
            type: String
        },
        storyImageUrl: {
            type: String,
            required: true
        },
        storyStatus: {
            type: String,
            default: 'public',
            enum: ['public', 'private']
        },
        storyCreator: {
            type: mongoose.Types.ObjectId,
            // required: true,
            ref: 'User'
        },
        storyReferences: [String],
        storyTags: [String],
        storyMapStyle: {
            type: String,
            default: 'mapbox://styles/mapbox/light-v9',
        },
        likes: [{
            type: mongoose.Types.ObjectId,
            ref: 'User'
        }],
        event: [ EventSchema ]
    }, {timestamps: true})

    module.exports = mongoose.model('Story', StorySchema)
  

Here is my express Route:

// Create a new event for a specific story, used by AddEvent.js
router.put('/:story_id/update', (req, res) => {
    const {
        eventDate,
        eventTitle,
        eventDescription,
        eventImageUrl,
        eventLink,
        eventAudio } = req.body

    console.log('eventDate',eventDate)

    Story.findByIdAndUpdate(
        { _id: req.params.story_id},
        {
            $push:
            { event: 
                {
                eventDate,
                eventTitle,
                eventDescription,
                eventImageUrl,
                eventLink,
                eventAudio
            }}
        })
})

Here is the React code just in case:

import React, {useEffect, useState} from 'react';
import { useForm } from 'react-hook-form'
import { useHistory, useParams } from 'react-router-dom'
import * as yup from 'yup'
import { yupResolver } from "@hookform/resolvers/yup"
import axios from 'axios'
import clsx from 'clsx';

import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
import Collapse from '@material-ui/core/Collapse';
import InfoIcon from '@material-ui/icons/Info';
import InputAdornment from '@material-ui/core/InputAdornment';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';

const schema = yup.object().shape({
    eventDate: yup
        .date(),
    eventTitle: yup
        .string()
        .required('Title is a required field.')
        .min(3),
    eventDescription: yup
        .string(),
    eventImageUrl: yup
        .string(),
    eventLink: yup
        .string(),
    eventAudio: yup
        .string(),
    // eventType: yup
    //     .string(),
    // eventLatitude: yup
    //     .number()
    //     .transform(cv => isNaN(cv) ? undefined : cv).positive()
    //     .nullable()
    //     .lessThan(90)
    //     .moreThan(-90)
    //     .notRequired() ,
    // eventLongitude: yup
    //     .number()
    //     .transform(cv => isNaN(cv) ? undefined : cv).positive()
    //     .nullable()
    //     .lessThan(180)
    //     .moreThan(-180)
    //     .notRequired() ,
})

const useStyles = makeStyles((theme) => ({
    paper: {
        marginTop: theme.spacing(12),
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
    },
    form: {
        width: '100%', // Fix IE 11 issue.
        marginTop: theme.spacing(1),
    },
    submit: {
        margin: theme.spacing(2, 0, 0),
    },
    formControl: {
        marginTop: '1rem',
    },
}));

export default function AddEvent(props) {
    const classes = useStyles();
    const history = useHistory()
    const { register, handleSubmit, errors } = useForm({ 
        resolver: yupResolver(schema)
    })
    const { story_id } = useParams()
    
    const [data, setData] = useState('')
    const [expanded, setExpanded] = useState(false);
    
    const { 
        eventDate,
        eventTitle,
        eventDescription,
        eventImageUrl,
        eventLink,
        eventAudio,
        eventLocation
    } = data

    useEffect(() => {
            axios.put(`http://localhost:5000/story/${story_id}/update`, {
                eventDate,
                eventTitle,
                eventDescription,
                eventImageUrl,
                eventLink,
                eventAudio,
                eventLocation
            })
            .then(() => history.goBack() )
            .catch(err => console.log(err))
    }, [data])

    const handleExpandClick = () => {
        setExpanded(!expanded);
    };

    const handleCancel = () => {
        history.goBack()
    };

    const onSubmit = (data) => {
        console.log(data);
        setData(data) 
    }


    return (
        <Container component="main" maxWidth="xs">
            <div className={classes.paper}>
                <Typography>
                    Add New Event
                </Typography>
                <form className={classes.form} noValidate onSubmit={handleSubmit(onSubmit)}>
                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        id="eventDate"
                        label="eventDate"
                        name="eventDate"
                        autoComplete="eventDate"
                        type="text"
                        autoFocus
                        inputRef={register}
                        error={!!errors.eventDate}
                        helperText={errors?.eventDate?.message}
                    />
                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        required
                        id="eventTitle"
                        label="eventTitle"
                        name="eventTitle"
                        autoComplete="eventTitle"
                        type="text"
                        inputRef={register}
                        error={!!errors.eventTitle}
                        helperText={errors?.eventTitle?.message}
                    />
                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        multiline
                        rows={4}
                        name="eventDescription"
                        label="eventDescription"
                        type="text"
                        id="eventDescription"
                        inputRef={register}
                        error={!!errors.eventDescription}
                        helperText={errors?.eventDescription?.message}
                    />
                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        name="eventImageUrl"
                        label="eventImageUrl"
                        type="text"
                        id="eventImageUrl"
                        InputProps={{
                            endAdornment: (
                                <InputAdornment position="end">
                                    <Button
                                        className={clsx(classes.expand, {
                                            [classes.expandOpen]: expanded,
                                        })}
                                        onClick={handleExpandClick}
                                        aria-expanded={expanded}
                                        aria-label="show more"
                                    >
                                        <InfoIcon size='sm' />
                                    </Button>
                                </InputAdornment>
                            ),
                        }}
                        inputRef={register}
                        error={!!errors.eventImageUrl}
                        helperText={errors?.eventImageUrl?.message}
                    /> 
                    <Collapse in={expanded} timeout="auto" unmountOnExit>
                        <p>Pls paste either an image or video url link here.</p>
                        <p>If you are using a YouTube link: Navigate to the video you wish to embed. Click the Share link below the video, then click the Embed link. The embed link will be highlighted in blue. Copy and paste this link here.
                        </p>
                    </Collapse>
                    
                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        name="eventLink"
                        label="eventLink"
                        type="text"
                        id="eventLink"
                        inputRef={register}
                        error={!!errors.eventLink}
                        helperText={errors?.eventLink?.message}
                    />

                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        name="eventAudio"
                        label="eventAudio"
                        type="text"
                        id="eventAudio"
                        inputRef={register}
                        error={!!errors.eventAudio}
                        helperText={errors?.eventAudio?.message}
                    />
                    {/* <FormControl fullWidth variant="outlined" className={classes.formControl}>
                        <InputLabel id="demo-simple-select-outlined-label">eventType</InputLabel>
                        <Select
                            labelId="demo-simple-select-outlined-label"
                            id="demo-simple-select-outlined"
                            value={eventType}
                            onChange={handleChange}
                            defaultValue={'Point'}
                            label="eventType"
                            className={classes.selectEmpty}
                        >
                            <MenuItem value={'Point'}>Point</MenuItem>
                            <MenuItem value={'LineString'}>LineString</MenuItem>
                            <MenuItem value={'Polygon'}>Polygon</MenuItem>
                        </Select>
                    </FormControl> */}
                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        name="eventLatitude"
                        label="eventLatitude"
                        type="number"
                        id="eventLatitude"
                        inputRef={register}
                        error={!!errors.eventLatitude}
                        helperText={errors?.eventLatitude?.message}
                    />

                    <TextField
                        variant="outlined"
                        margin="normal"
                        fullWidth
                        name="eventLongitude"
                        label="eventLongitude"
                        type="number"
                        id="eventLongitude"
                        inputRef={register}
                        error={!!errors.eventLongitude}
                        helperText={errors?.eventLongitude?.message}
                    />

                    <Button
                        type="submit"
                        fullWidth
                        variant="contained"
                        color="primary"
                        className={classes.submit}
                    >
                        Submit
                    </Button>

                    <Button
                        fullWidth
                        color="default"
                        onClick={handleCancel}
                        className={classes.submit}
                    >
                        Cancel
                    </Button>
                </form>
            </div>
        </Container>
    );
}


nonoumasy
  • 61
  • 1
  • 8
  • Some of the research I gathered: - To edit a nested document from a collection, use positional and set operator - Remember that in MongoDB the subdocument is saved only when the parent document is saved. - To add/insert a nested document into a collection, use the push operator to delete a nested document from a collection, use the pull operator. ``` db.timelines.update( { _id: 1}, {$push: { event: { title: "something", year: 1980} } } ) ``` – nonoumasy Dec 20 '20 at 23:03

1 Answers1

1

in a simple answer, you should call Story.updateOne and not Story.findByIdAndUpdate, but I would suggest a bit more (look at this as a code review thingy) ...

  • make use of pluralism, so the property should be called events and not event as it will have more than one
  • remove property prefixes, so StoryTitle would simply be title (we already know it's a Story, as it's in a Story collection), and EventTitle would be simply `event) (we already know it's an event as it's in an "events" array)
  • why set storyReferences as a string, should be easier manipulated if an array of string, same for storyTags

I took the liberty, just to help in your MongoDB API development, to create a very simple NodeJs project in GitHub with very simple REST calls, where you can go through and make calls to create documents and query them...

GitHub project https://github.com/balexandre/so65351733

balexandre
  • 73,608
  • 45
  • 233
  • 342
  • Obrigado @balexandre! I will dissect each line of your answer. As for the property prefixes, it was mostly for code readibility. So, summary belongs to Story and description belongs to Event. I did read somewhere not to be verbose with names and simpler the better. I will try with shorter names. And you are right, the storyReferences and storyTags should be arrays. I casted them to strings temporarily as I had issues with the array types. I will take a look at your docker container tomorrow. I might have more questions. – nonoumasy Dec 22 '20 at 09:57