dynamic schema file uploads

This commit is contained in:
Dan Ribbens
2019-10-21 13:50:42 -04:00
parent e216e11076
commit 5b08c33a72
13 changed files with 223 additions and 136 deletions

2
.gitignore vendored
View File

@@ -225,4 +225,4 @@ demo**/*.css
dist
# Ignore demo/uploads
demo/media
demo/upload

View File

@@ -1,52 +0,0 @@
module.exports = {
slug: 'media',
labels: {
singular: 'Media',
plural: 'Media',
},
useAsTitle: 'name',
policies: {
create: (req, res, next) => {
return next();
},
read: (req, res, next) => {
return next();
},
update: (req, res, next) => {
return next();
},
destroy: (req, res, next) => {
return next();
},
},
media: {
staticUrl: '/media',
staticDir: 'demo/media',
type: 'image',
accept: ['.jpg', '.jpeg', '.png'],
// TODO: discuss if some sizes are required, what if not resizing?
imageSizes: [
{
name: 'tablet',
width: 640,
height: 480,
crop: 'left top' // would it make sense for this to be set by the uploader?
},
{
name: 'mobile',
width: 320,
height: 240,
crop: 'left top'
},
{ // Is the icon size required for the admin dashboard to work?
name: 'icon',
width: 16,
height: 16
}
],
},
fields: [
// users could have fields if they want, not required
],
timestamps: true
};

View File

@@ -1,11 +1,9 @@
const Category = require('./Category');
const User = require('./User');
const Media = require('./Media');
const Page = require('./Page');
module.exports = {
Category,
User,
Media,
Page,
};

View File

@@ -21,8 +21,42 @@ module.exports = {
defaultLocale: 'en',
fallback: true
},
staticUrl: '/media',
staticDir: 'demo/media',
// uploads: false, // To disable upload routes otherwise defaults will be use and if set to an object
uploads: {
image: {
imageSizes: [
{
name: 'tablet',
width: 640,
height: 480,
crop: 'left top' // would it make sense for this to be set by the uploader?
},
{
name: 'mobile',
width: 320,
height: 240,
crop: 'left top'
},
{ // Is the icon size required for the admin dashboard to work?
name: 'icon',
width: 16,
height: 16
}
]
},
profile: {
imageSizes: [
{
name: 'full',
width: 640,
height: 480,
crop: 'center'
}
]
},
},
staticUrl: '/uploads',
staticDir: 'demo/upload',
email: {
provider: 'mock'
},

View File

@@ -3,11 +3,10 @@ import passport from 'passport';
import express from 'express';
import bodyParser from 'body-parser';
import methodOverride from 'method-override';
import jwtStrategy from './auth/jwt';
import fileUpload from 'express-fileupload';
import {upload as uploadMedia, update as updateMedia} from './media/requestHandlers';
import mediaConfig from './media/media.config';
import jwtStrategy from './auth/jwt';
import initRoutes from './routes/init.routes';
import uploadRoutes from './uploads/upload.routes';
import autopopulate from './mongoose/autopopulate.plugin';
import paginate from './mongoose/paginate.plugin';
import buildQueryPlugin from './mongoose/buildQuery.plugin';
@@ -41,10 +40,6 @@ class Payload {
}
});
options.app.use(fileUpload({}));
const staticUrl = options.config.staticUrl ? options.config.staticUrl : `/${options.config.staticDir}`;
options.app.use(staticUrl, express.static(options.config.staticDir));
// Configure passport for Auth
options.app.use(passport.initialize());
options.app.use(passport.session());
@@ -64,36 +59,41 @@ class Payload {
});
}
if (!options.config.uploads === false) {
options.app.use(fileUpload());
options.router.use('', uploadRoutes(options.config)
);
}
options.app.use(express.json());
options.app.use(methodOverride('X-HTTP-Method-Override'));
options.app.use(express.urlencoded({ extended: true }));
options.app.use(bodyParser.urlencoded({ extended: true }));
options.app.use(express.urlencoded({extended: true}));
options.app.use(bodyParser.urlencoded({extended: true}));
options.app.use(localizationMiddleware(options.config.localization));
options.app.use(options.router);
const staticUrl = options.config.staticUrl ? options.config.staticUrl : `/${options.config.staticDir}`;
options.app.use(staticUrl, express.static(options.config.staticDir));
// TODO: Build safe config before initializing models and routes
options.config.collections && Object.values(options.config.collections).forEach(config => {
validateCollection(config, this.collections);
this.collections[config.labels.singular] = config;
const fields = { ...schemaBaseFields };
const fields = {...schemaBaseFields};
// authentication
if (config.auth && config.auth.passwordResets) {
config.fields.push(...passwordResetConfig.fields);
}
// media
if (config.media) {
config.fields.push(...mediaConfig.fields);
}
config.fields.forEach(field => {
const fieldSchema = fieldToSchemaMap[field.type];
if (fieldSchema) fields[field.name] = fieldSchema(field);
});
const Schema = new mongoose.Schema(fields, { timestamps: config.timestamps });
const Schema = new mongoose.Schema(fields, {timestamps: config.timestamps});
Schema.plugin(paginate);
Schema.plugin(buildQueryPlugin);
@@ -129,18 +129,13 @@ class Payload {
setModelLocaleMiddleware()
);
// TODO: this feels sloppy, need to discuss media enabled collection handlers
let createHandler = config.media ? (req, res, next) => uploadMedia(req, res, next, config.media) : create;
let updateHandler = config.media ? (req, res, next) => updateMedia(req, res, next, config.media) : update;
// TODO: Do we need a delete?
options.router.route(`/${config.slug}`)
.get(config.policies.read, query)
.post(config.policies.create, createHandler);
.post(config.policies.create, create);
options.router.route(`/${config.slug}/:id`)
.get(config.policies.read, findOne)
.put(config.policies.update, updateHandler)
.put(config.policies.update, update)
.delete(config.policies.destroy, destroy);
});
@@ -158,16 +153,16 @@ class Payload {
const fieldSchema = fieldToSchemaMap[field.type];
if (fieldSchema) globalFields[config.slug][field.name] = fieldSchema(field);
});
globalSchemaGroups[config.slug] = new mongoose.Schema(globalFields[config.slug], { _id : false });
});
globalSchemaGroups[config.slug] = new mongoose.Schema(globalFields[config.slug], {_id: false});
});
if (options.config.globals) {
globalModel = mongoose.model(
'global',
new mongoose.Schema({...globalSchemaGroups, timestamps: false})
.plugin(localizationPlugin, options.config.localization)
.plugin(autopopulate)
);
globalModel = mongoose.model(
'global',
new mongoose.Schema({...globalSchemaGroups, timestamps: false})
.plugin(localizationPlugin, options.config.localization)
.plugin(autopopulate)
);
}
options.router.all('/globals*',
@@ -179,7 +174,8 @@ class Payload {
.route('/globals')
.get(fetch);
options.router.route('/globals/:key')
options.router
.route('/globals/:key')
.get(fetch)
.post(upsert)
.put(upsert);

View File

@@ -1,24 +1,24 @@
const modelById = (query, options) => {
return new Promise((resolve, reject) => {
query.Model.findOne({ _id: query.id }, {}, options, (err, doc) => {
if (err || !doc) {
return reject({ message: 'not found' })
}
let result = doc;
if (query.locale) {
doc.setLocale(query.locale, query.fallback);
result = doc.toJSON({ virtuals: true });
}
resolve(options.returnRawDoc
? doc
: result);
})
})
};
export default modelById;
const modelById = (query, options) => {
return new Promise((resolve, reject) => {
query.Model.findOne({ _id: query.id }, {}, options, (err, doc) => {
if (err || !doc) {
return reject({ message: 'not found' })
}
let result = doc;
if (query.locale) {
doc.setLocale(query.locale, query.fallback);
result = doc.toJSON({ virtuals: true });
}
resolve(options.returnRawDoc
? doc
: result);
})
})
};
export default modelById;

View File

@@ -1,9 +1,5 @@
export default {
fields: [
{
name: 'filename',
type: 'input',
},
{
name: 'sizes',
type: 'repeater',
@@ -17,7 +13,7 @@ export default {
name: 'width',
type: 'number',
},
]
],
}
]
}

View File

@@ -0,0 +1,23 @@
import mongoose from 'mongoose';
import localizationPlugin from '../../localization/localization.plugin';
const imageUploadModelLoader = (Upload, config) => {
const ImageSchema = new mongoose.Schema(
{
sizes: [{
name: {type: String},
height: {type: Number},
width: {type: Number},
_id: false
}]
}
);
ImageSchema.plugin(localizationPlugin, config.localization);
return Upload.discriminator('image', ImageSchema);
};
export default imageUploadModelLoader;

View File

@@ -9,13 +9,13 @@ function getOutputImageName(sourceImage, size) {
return `${filenameWithoutExtension}-${size.width}x${size.height}.${extension}`;
}
export async function resizeAndSave(config, file) {
export async function resizeAndSave(config, uploadConfig, file) {
let sourceImage = `${config.staticDir}/${file.name}`;
let outputSizes = [];
try {
const dimensions = await sizeOf(sourceImage);
for (let desiredSize of config.imageSizes) {
for (let desiredSize of uploadConfig.imageSizes) {
if (desiredSize.width > dimensions.width) {
continue;
}
@@ -25,7 +25,11 @@ export async function resizeAndSave(config, file) {
position: desiredSize.crop || 'centre' // would it make sense for this to be set by the uploader?
})
.toFile(outputImageName);
outputSizes.push({ height: desiredSize.height, width: desiredSize.width });
outputSizes.push({
name: desiredSize.name,
height: desiredSize.height,
width: desiredSize.width
});
}
} catch (e) {
console.log('error in resize and save', e.message);

View File

@@ -1,30 +1,27 @@
import mkdirp from 'mkdirp';
import { resizeAndSave } from './imageResizer';
import { resizeAndSave } from './images/imageResizer';
import httpStatus from 'http-status';
import modelById from '../mongoose/resolvers/modelById';
const update = async (req, res, next, config) => {
req.model.setDefaultLocale(req.locale);
const query = {
Model: req.model,
id: req.params._id,
id: req.params.id,
locale: req.locale,
};
let doc = await modelById(query, {returnRawDoc: true});
if (!doc)
return res.status(httpStatus.NOT_FOUND).send('Not Found');
Object.keys(req.body).forEach(e => {
doc[e] = req.body[e];
});
if (req.files && req.files.file) {
doc['filename'] = req.files.file.name;
let outputFilepath = `${config.staticDir}/${req.files.file.name}`;
let moveError = await req.files.file.mv(outputFilepath);
if (moveError) return res.status(500).send(moveError);
doc['sizes'] = await resizeAndSave(config, req.files.file);
let handlerData = await fileTypeHandler(config, req.uploadConfig, req.files.file);
Object.keys(handlerData).forEach(e => {
doc[e] = handlerData[e];
});
}
doc.save((saveError) => {
@@ -46,21 +43,19 @@ const upload = async (req, res, next, config) => {
mkdirp(config.staticDir, (err) => {
if (err) {
console.error(err);
res.status(500).send('Upload failed.');
return res.status(500).send('Upload failed.');
}
});
let outputFilepath = `${config.staticDir}/${req.files.file.name}`;
let moveError = await req.files.file.mv(outputFilepath);
if (moveError) return res.status(500).send(moveError);
let outputSizes = await resizeAndSave(config, req.files.file);
let handlerData = await fileTypeHandler(config, req.uploadConfig, req.files.file);
req.model.create({
name: req.body.name,
caption: req.body.caption,
description: req.body.description,
filename: req.files.file.name,
sizes: outputSizes
...handlerData
}, (mediaCreateError, result) => {
if (mediaCreateError)
return res.status(500).json({error: mediaCreateError});
@@ -73,4 +68,13 @@ const upload = async (req, res, next, config) => {
});
};
async function fileTypeHandler(config, uploadConfig, file) {
const data = {};
if (uploadConfig.imageSizes) {
data.sizes = await resizeAndSave(config, uploadConfig, file);
}
return data;
}
export {update, upload};

View File

@@ -0,0 +1,24 @@
import httpStatus from 'http-status';
const uploadMiddleware = (config, UploadModels) => {
return (req, res, next) => {
// set the req.model to the correct type of upload
if (req.body.type) {
if (config.uploads[req.body.type]) {
req.uploadConfig = config.uploads[req.body.type];
if (req.uploadConfig.imageSizes) {
req.model = UploadModels.image;
}
return next();
} else {
return res.status(httpStatus.BAD_REQUEST).send('Upload type is not recognized');
}
}
req.uploadConfig = {};
req.model = UploadModels.default;
next();
};
};
export default uploadMiddleware;

View File

@@ -0,0 +1,22 @@
import mongoose from 'mongoose';
import buildQueryPlugin from '../mongoose/buildQuery.plugin';
import paginate from '../mongoose/paginate.plugin';
import localizationPlugin from '../localization/localization.plugin';
const uploadModelLoader = (config) => {
const UploadSchema = new mongoose.Schema({
filename: {type: String},
}, {
timestamps: true,
discriminatorKey: 'type'
});
UploadSchema.plugin(paginate);
UploadSchema.plugin(buildQueryPlugin);
UploadSchema.plugin(localizationPlugin, config.localization);
return mongoose.model('Upload', UploadSchema);
};
export default uploadModelLoader;

View File

@@ -0,0 +1,38 @@
import express from 'express';
import {upload, update} from '../uploads/requestHandlers';
import uploadMiddleware from './upload.middleware';
import uploadModelLoader from './upload.model';
import imageUploadModelLoader from './images/image.model';
import setModelLocaleMiddleware from '../mongoose/setModelLocale.middleware';
import passport from 'passport';
const router = express.Router();
const uploadRoutes = config => {
const Upload = uploadModelLoader(config);
const UploadModels = {
default: Upload,
image: imageUploadModelLoader(Upload, config),
};
router.all('/upload*',
passport.authenticate('jwt', {session: false}),
uploadMiddleware(config, UploadModels),
setModelLocaleMiddleware(),
);
router.route('/upload')
.post(
(req, res, next) => upload(req, res, next, config)
);
router.route('/upload/:id')
.put(
(req, res, next) => update(req, res, next, config)
);
return router;
};
export default uploadRoutes;