nonoumasy
nonoumasy

Reputation: 61

How to insert a subdocument into a mongo collection?

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>
    );
}


Upvotes: 1

Views: 1009

Answers (1)

balexandre
balexandre

Reputation: 75133

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

Upvotes: 1

Related Questions