2

I am using MEAN stack. I created a CRUD module using meanjs.org yeoman generator. I'm extending the functionality to save a document with a few sub documents. My approach is basically to build JSON client-side, add it to the main document, then save the main document. This works great, except for when I try to build a sub-document for storing image data.

I use HTML5 FileReader's readAsDataURL to get a base64 encoded version of the image data, and store it in a subdocument called Slides. This seems to work, as I can upload images and see them in my object with ng-repeat. When I try to update the main document, however, it doesn't work. Firefox debug console shows something like:

PUT XHR http://localhost:3000/galleries/55e888e0cec8658491097f12

When I click that for details:

Request URL: http://localhost:3000/galleries/55e888e0cec8658491097f12 Request Method: PUT Status Code: HTTP/1.1 500 Internal Server Error

After I get an error like this, nothing saves, even adding a new subdocument without images.

My images are all around 300K or less, not super big, but I'm wondering if there's some problem with JSON holding so much data? Or maybe it's a problem with Mongoose? I don't really know how to go about debugging the backend, it's all been auto-generated with yeoman.

Here's my mongoose schema:

'use strict';

/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

//CommentSchema
var CommentSchema = new Schema({
    user: String,
    comment: String
});

//SlideSchema
var SlideSchema = new Schema({
    image: Buffer,
    order: Number
});

//AuthorSchema
var AuthorSchema = new Schema({
    fname: String,
    lname: String,
    title: String
});

//PosterSchema
var PosterSchema = new Schema({
    title: String,
    authors: [AuthorSchema],
    slides: [SlideSchema],
    comments: [CommentSchema]
});

//Gallery Schema
var GallerySchema = new Schema({
    conference_name: {
        type: String,
        default: '',
        required: 'Please enter Conference Name',
        trim: true
    },
    access_code: {
        type: String,
        index: { unique: true },
        required: 'Please enter a unique Access Code',
        trim: true
    },
    created: {
        type: Date,
        default: Date.now
    },
    user: {
        type: Schema.ObjectId,
        ref: 'User'
    },
    posters: [PosterSchema]
});

mongoose.model('Gallery', GallerySchema);
mongoose.model('Poster', PosterSchema);
mongoose.model('Author', AuthorSchema);
mongoose.model('Slide', SlideSchema);

The CRUD module was generate for Galleries. In 'view-gallery-client.view.html', I added a control to load a modal window to 'add posters'. Add posters builds the necessary JSON object, pushes it onto the gallery's 'posters' subdocument, and then updates the gallery. The modal window also constructs 'authors' and 'slides' subdocuments.

Here's the controller and view for 'add posters' modal:

controller('AddPosterModalController', ['$scope','Upload','$modalInstance','$timeout',
    function ($scope, Upload, $modalInstance, $timeout) {
        $scope.poster = {};
        $scope.poster.authors = [];
        $scope.poster.slides = [];
        $scope.images = [];
        $scope.dynamic = 50;

        $scope.showAuthors = true;

        $scope.add_author = function () {
            $scope.poster.title = $scope.title;

            var author = {
                fname: $scope.fname,
                lname: $scope.lname,
                title: $scope.atitle
            };

            $scope.poster.authors.push(author);

            $scope.fname = '';
            $scope.lname = '';
            $scope.atitle = '';
        };

        $scope.remove_author = function(author) {
            for(var i=0; i<$scope.poster.authors.length; i++)
            {
                if(author === $scope.poster.authors[i])
                    $scope.poster.authors.splice(i,1);
            }
        };

        $scope.add = function() {
            alert('closing modal and returning poster: ' + JSON.stringify($scope.poster));
            $modalInstance.close($scope.poster);
        };

        $scope.remove_slide = function(slide)
        {
            for(var i=0; i<$scope.poster.slides.length; i++)
            {
                if($scope.poster.slides[i] === slide)
                    $scope.poster.slides.splice(i,1);
            }
        };

        $scope.change_slide_order = function(oldorder, amount)
        {
            var neworder = parseInt(oldorder + amount);
            oldorder = parseInt(oldorder);

            //we won't swap indices, but
            if(neworder >= 0 && neworder < $scope.poster.slides.length)
            {
                var slide1 = -1;
                var slide2 = -2;

                for(var i=0; i<$scope.poster.slides.length; i++)
                {
                    if($scope.poster.slides[i].order === oldorder)
                        slide1 = i;
                    else if($scope.poster.slides[i].order === neworder)
                        slide2 = i;
                }

                //swap the slides, not the index, but the order value
                $scope.poster.slides[slide1].order = neworder;
                $scope.poster.slides[slide2].order = oldorder;
            }
        };

        //handle slide uploads
        $scope.add_slides = function(event) {

            var files = $scope.files = event.target.files;


            for(var i=0; i<files.length; i++)
            {
                var reader = new FileReader();

                var file = files[i];
                reader.onload = $scope.imageLoaded;
                reader.readAsDataURL(file);
            }
        };

        $scope.imageLoaded = function(e)
        {
            //timeout should solve the $digest error
            //safer than $apply
            $timeout(function(){
                var data = e.target.result;

                //data = window.btoa(data);

                var order = $scope.poster.slides.length;

                //build a JSON slide object
                var slide = {
                    image: data,
                    order: order
                };

                //add the image to the poster
                $scope.poster.slides.push(slide);
            });
        };
    }
]);

and view:

<div class="modal-header">
    <h3>Add a Poster to the Gallery</h3>
</div>
<div class="modal-body">
    <form class="form-horizontal" data-ng-submit="add_poster()" novalidate>
        <fieldset>
            <div class="row">
                <div class="form-group col-md-12">
                    <label class="control-label" for="title">Poster Title</label>
                    <div class="controls">
                        <input type="text" data-ng-model="title" id="title" class="form-control" placeholder="Poster Title" required>
                    </div>
                </div>
            </div>
            <div class="row"><h3 ng-click="showAuthors = !showAuthors">Authors ({{poster.authors.length}})</h3></div>
            <div class="row" ng-repeat="author in poster.authors" collapse="!showAuthors">
                <a class="btn btn-primary" data-ng-click="remove_author(author)">
                    <i class="glyphicon glyphicon-minus"></i>
                </a>
                <span>{{author.title}} {{author.fname}} {{author.lname}}</span>
            </div>
            <div class="row">
                <div class="form-group col-md-4">
                    <label class="control-label" for="fname">First Name</label>
                    <div class="controls">
                        <input type="text" data-ng-model="fname" id="fname" class="form-control" placeholder="First Name" required>
                    </div>
                </div>
                <div class="form-group col-md-4">
                    <label class="control-label" for="lname">Last Name</label>
                    <div class="controls">
                        <input type="text" data-ng-model="lname" id="lname" class="form-control" placeholder="Last Name" required>
                    </div>
                </div>
                <div class="form-group col-md-3">
                    <label class="control-label" for="atitle">Title</label>
                    <div class="controls">
                        <input type="text" data-ng-model="atitle" id="atitle" class="form-control" placeholder="Title" required>
                    </div>
                </div>
                <div class="form-group col-md-1">
                    <label class="control-label">Add</label>
                    <div class="controls">
                        <a class="btn btn-primary" data-ng-click="add_author()">
                            <i class="glyphicon glyphicon-plus"></i>
                        </a>
                    </div>
                </div>
            </div>
            <div class="row">
                <h3>
                    Slides ({{poster.slides.length}})
                    <label for="files" class="btn btn-primary glyphicon glyphicon-plus"></label>
                    <input type="file" id="files" ng-model="mFiles" multiple collapse="true" accept="image/*" onchange="angular.element(this).scope().add_slides(event)"/>
                </h3>

                <div class="col-md-3" ng-repeat="slide in poster.slides | orderBy:'order'">
                    <img width="160" height="95" src="{{slide.image}}"/> ({{slide.order}})
                    <a class="btn btn-primary" data-ng-click="remove_slide(slide);">
                        <i class="glyphicon glyphicon-minus"></i>
                    </a>
                    <a class="btn btn-primary" data-ng-click="change_slide_order(slide.order,-1);">
                        <i class="glyphicon glyphicon-arrow-left"></i>
                    </a>
                    <a class="btn btn-primary" data-ng-click="change_slide_order(slide.order,1);">
                        <i class="glyphicon glyphicon-arrow-right"></i>
                    </a>
                </div>
            </div>
        </fieldset>
    </form>
</div>
<div class="modal-footer">
    <button class="btn-primary" ng-click="add()" class="btn btn-default">Add Poster</button>
</div>

Any suggestions at all are appreciated. I am trying to keep everything database-only, as the images are small, and this is for use by only a few people without much need to scale at this point. I don't want to even think about messing around with keeping track of stuff on the file system for now.

EDIT - Solved

I have solved this problem. Stupidly, I was not paying enough attention to the node/express console log. I was getting an error:

Error: request entity too large

Turns out, there are default limits on the bodyParser, which handles data coming in across http. Since a REST service backs the CRUD module in angular, I was hitting the limit.

The solution was to configure the bodyParser in app/config/express.js:

app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json());

becomes:

app.use(bodyParser.urlencoded({ extended: true, limit: '20mb' })); app.use(bodyParser.json({ limit: '20mb' }));

Make sure the limits are set in lowercase, ie '20mb' NOT '20MB', that won't work.

chris c
  • 381
  • 2
  • 4

0 Answers0