dynamic schema file uploads
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -225,4 +225,4 @@ demo**/*.css
|
||||
dist
|
||||
|
||||
# Ignore demo/uploads
|
||||
demo/media
|
||||
demo/upload
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
60
src/index.js
60
src/index.js
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
23
src/uploads/images/image.model.js
Normal file
23
src/uploads/images/image.model.js
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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};
|
||||
24
src/uploads/upload.middleware.js
Normal file
24
src/uploads/upload.middleware.js
Normal 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;
|
||||
22
src/uploads/upload.model.js
Normal file
22
src/uploads/upload.model.js
Normal 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;
|
||||
38
src/uploads/upload.routes.js
Normal file
38
src/uploads/upload.routes.js
Normal 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;
|
||||
Reference in New Issue
Block a user