HomeBack-End & DatabaseKeystone Js

Keystone Js

1: INTRODUCTION:

KeystoneJS is an open source framework for developing database-driven websites, applications, and APIs in Node.js. And guess what, it is built on Express and MongoDB

2: Installations:

To easily install keystone js, please follow the link below
https://keystonejs.com/getting-started/yo-generator


3. ONCE you have successfully installed keystone project, we can start working on it right away

  • Lets start with a simple task that will help us print hello world
  • And before we create router, we have to what is contained in the keystone.js file
// this will set all environment variable from .env files
require('dotenv').config();

// Require keystone
var keystone = require('keystone');

keystone.init({
	'name': 'sampleApp',
	'brand': 'sampleApp',
	'admin path': 'admin',
	'less': 'public',
	'static': 'public',
	'favicon': 'public/favicon.png',
	'views': 'templates/views',
	'view engine': 'pug',

	'auto update': true,
	'session': true,
	'auth': true,
	'user model': 'User',
	'file limit': '50MB'
});

// Load your project's Models
// this the models in which consists mongo schema we will discuss in detail later
keystone.import('models');
// setting local vaiables
keystone.set('locals', {
	_: require('lodash'),
	env: keystone.get('env'),
	utils: keystone.utils,
	editable: keystone.content.editable,
});

// Load your project's Routes
keystone.set('routes', require('./routes'));
keystone.set('cors allow origin', true);

// Configure the navigation bar in Keystone's Admin UI
keystone.set('nav', {
	posts: ['posts', 'post-categories'],
	galleries: 'galleries',
	enquiries: 'enquiries',
	users: 'users',
});

keystone.start();

Now that we know what is in the keystone.js file, we can proceed to create a route in the router/index.js file and console hello word using the following.

var keystone = require('keystone');
var importRoutes = keystone.importer(__dirname);

// Common Middleware
keystone.pre('routes', middleware.initLocals);
keystone.pre('render', middleware.flashMessages);

// Import Route Controllers
var routes = {
	views: importRoutes('./views'),
};

var cors = require('cors');
exports = module.exports = function (app) {
	app.use(cors());
	app.get('/', routes.views.index);
	app.get('/blog/:category?', routes.views.blog);
	app.get('/blog/post/:post', routes.views.post);
	app.get('/gallery', routes.views.gallery);
	app.all('/contact', routes.views.contact);

       // create first custom route for printing hello world
	app.get('/hello', function(req, res, next){
             console.log('hello world');
      })
};

Having done that, we can now move to create a user login routes. After we doing that, we can create a mango schema for users. And all of these can be simply done using the following code:

var keystone = require('keystone');
var Types = keystone.Field.Types;

/**
 * User Model
 * ==========
 */
var User = new keystone.List('User'); 

User.add({
	name: { type: Types.Name, required: true, index: true },
	email: { type: Types.Email, initial: true, required: true, unique: true, index: true },
	password: { type: Types.Password, initial: true }
}, 'Permissions', {
		isAdmin: { type: Boolean, label: 'Can access Keystone', index: true },
	});

// Provide access to Keystone
User.schema.virtual('canAccessKeystone').get(function () {
	return this.isAdmin;
});

User.defaultColumns = 'name, email, isAdmin';
User.register();

4.Method

Using keystone JS provides you with the following methods and functions:

I. set()
keystone.set() is used to set option value for keystone. For instance, lets say we we want to set different PORT for an app, we can simply specify the port by setting the method.

keystone.set('port', 8080)

Here is another simple example
Usually, the URL for keystone is set in the following pattern:

http://0.0.0.0:3000/keystone

However, to change the default URL path, we can simply set it using keystone.set()

keystone.set(‘admin path’, ‘theadmin’)

To this end, the URL will look thus:

http://0.0.0.0:3000/theadmin

II. init()
keystone.init() is used to initialize the value for the keystone. For example, let’s assume we want to initialize the name of the app, favicon will provide us with a view of keystone app just like the example below:

keystone.init({
	'name': 'FirstApp',
	'brand': 'FirstApp',
	'less': 'public',
	'static': 'public',
	'favicon': 'public/favicon.png',
	'views': 'templates/views',
	'view engine': 'pug',
	'auto update': true
	'session': true,
	'auth': true,
	'user model': 'User',
});

III. get ()
keystone.get() is used to get the value initialize in keystone option .
for example:
to get the value of PORT on which app is running:
keystone.get('port');

IV. list()
keystone.list() is used to get the defined database model (list), to perform the operation. As we define a model of Users, so list() is used to get the list of model.
for example:

var post = keystone.list('Post').model;
post.findAll({})

V. import

  1. keystone.import(‘routes’);
  2. keystone.importer(__dirname);

VI. Paginate()
paginate() is used to fetch the data from database with pagination. Following fields are supported for pagination:

  1. page : starting page on condition of pagination
  2. perPage : how much item on a page.
  3. maxPages: total page to get in response of query

for example:

module.exports.getEvents = (req, res, next) => {
	
	req.query['state'] = 'published';
	let page = req.query.page;
	delete req.query.page;
	var q = keystone.list('Post').paginate({
		page: page || 1,
		perPage: 10,
		maxPages: 10,
		filters: req.query,
		select: 'slug content featured image title EventCity EventState categories publishedDate interested interested phone_number checkedinBy checkedinBy reviews Price EventPlace website contentType start end state phone_number'
	})
		.sort('-start')
		.populate('categories')
		.populate({
			path: 'comments',
			populate: {
				path: 'user_id',
				select: 'name email'
			}
		})
		.populate({
			path: 'reviews',
			select: 'user_id comment stars',
			populate: {
				path: 'user_id',
				select: 'name email'
			}
		}).populate({
			path: 'interested',
			select: 'name email'
		}).populate({
			path: 'checkedinBy',
			select: 'name email'
		});

	q.exec(function (err, results) {
		if (!err) {
			return res.json({
				success: true,
				session: true,
				data: results
			})
		}
	});
}

VII. pre()
keystone.pre() is used to perform an action before an event. for example: if we want to show particular model on UI, for that we have to check user before every route execute.
another example the nav link are initialise before the execution of user defined Routes.
Basically it worked with a middleware. when a condition is satisfy then the particular middleware execute.

following is the code in the /routes/index.js

keystone.pre('routes', middleware.initLocals);

following is the code in the /routes/middleware.js

exports.initLocals = function (req, res, next) {
	res.locals.navLinks = [
		{ label: 'Home', key: 'home', href: '/' },
		{ label: 'Blog', key: 'blog', href: '/blog' },
		{ label: 'Gallery', key: 'gallery', href: '/gallery' },
		{ label: 'Contact', key: 'contact', href: '/contact' },
	];
	res.locals.user = req.user;
	next();
};

And pre() can be use as Hooks with the model or list Schema of the keystone to perform a task as per the action.
for example:

User.schema.pre('save', function(next) {
	if(this.isSubAdmin && !this.isAdmin) {
		this.isAdmin = true
	}

	next()
});

VIII. Underscore methods
For manipulate and perform an action on a field data, underscore methods.
example: if you want to compare password with stored in database underscore field method is use.

module.exports.updatePasswordForApp = (req, res, next) => {
	var User = keystone.list('User');
	User.model.findOne({
		_id: req.user
	}, function (findError, user) {
		if (findError) {
			res.json({
				success: false,
				session: true,
				message: (findError && findError.message ? findError.message : false) || 'Sorry, there was an issue while getting user password, please try again.'
			});
		} else {
			user._.password.compare(req.body.currentPassword, (errOnCompare, result) => {
				if (errOnCompare) {
					res.json({
						success: false,
						session: true,
						message: (errOnCompare && errOnCompare.message ? errOnCompare.message : false) || 'Sorry, there was an issue while comparing the user password, please try again.'
					});
				} else {
					if (result) {
						user.password = req.body.password;
						user.save(function (saveError, response) {
							if (saveError) {
								console.log(saveError)
								res.json({
									success: false,
									session: true,
									message: (saveError && saveError.message ? saveError.message : false) || 'Sorry, there was an issue while updating the user password, please try again.'
								});
							} else {
								res.json({
									success: true,
									session: true,
									message: response
								})
							}
						});
					} else {
						res.json({
							success: false,
							session: true,
							message: "Wrong current Password, try again"
						})
					}
				}
			})
		}
	})
}

5. csv Download

For download or export of the saved data stored in database, keystone provide a method getCSVData.
that’s allow which field should include in downloaded or exported file.
for example: If we don’t want to download the user email then make that field undefinded.

User.schema.methods.getCSVData = function (data, options) {
  return {
    password: undefined
  };
};

also keystone provide functionality to specify on model Schema .
for example:

	image: {
		type: Types.CloudinaryImage,
		label: 'Upload Event Image',
		toCSV: function (field, options) {
			return this.image;
		}
	},

6. View Model as per the Role (User Role Authentication)

To hide a specific model for a user we can implement the user role authentication. In which we will show or hide a model for specific user.

for example if we create a user with role Sub Admin, then that user will access all model on UI but can not access the user model means that user cannot create, edit, update or read a user details from a user model.

also that sub admin should have the admin access.

for that we have to add the user role permissions.
following is the user model/schema file.

var keystone = require('keystone');
var Types = keystone.Field.Types;

/**
 * User Model
 * ==========
 */
var User = new keystone.List('User');

User.add({
	...
}, 'Permissions', {
		isAdmin: { type: Boolean, label: 'Can access Keystone', index: true },
		isSubAdmin: { type: Boolean, label: 'Can access Keystone But Not User', index: true },
	});
...

now we have to check the routes before each Keystone admin UI route is executed.
for that we will use the pre() method.
with the condition of admin action, and we will create a function in middleware.

following code should be put in the index.js file of routes directory.

keystone.pre('admin', middleware.enforcePermissions);

following is the structure of the index.js of routes directory.

var keystone = require('keystone');
var middleware = require('./middleware');
var importRoutes = keystone.importer(__dirname);

// Common Middleware
keystone.pre('routes', middleware.initLocals);
keystone.pre('render', middleware.flashMessages);

// Middleware for role authentication 
keystone.pre('admin', middleware.enforcePermissions);

// Import Route Controllers
var routes = {
	views: importRoutes('./views'),
};

// custom routes controller
var auth = require('./auth');
var user = require('./user');
var events = require('./events');
var location = require('./location');
var contact = require('./contactUs');

...
...
...

now we have to create a function in the middleware.js for show or hide the model as per the role.
put the following code in the middleware.js

exports.enforcePermissions = function(req, res, next) {
	var nav = {
		posts: ['posts', 'post-categories'],
		galleries: 'galleries',
		enquiries: 'enquiries',
		users: 'users'
	};

	if (req.user) {
		var hideLists = (name, hidden) => {
			keystone.list(name).set("hidden", hidden);
			if(hidden) {
				delete nav[name]
			}
		}

		["users"].map(list => hideLists(list, req.user.isSubAdmin));

		keystone.set("nav", nav);

		keystone.nav = keystone.initNav(nav);
	}
	next();
};

in this code the nav variable contains the the details of the ADMIN UI of the keystone.
as posts: ['posts', 'post-categories'] means the posts and post-categories will show on the admin UI under the hood of posts, same goes with other as users: 'users' means user model will be under the User.

following is the screenshot when user is admin, and have access to all model(s).


when user is admin then that user can assign another user as sub-admin who can access the keystone but not User model.

following is the image for assigning permission, by admin.

when an user have access to keystone but not to User model then that user can not access the user model.
following is the image when user have access to keystone but not to user model.

7. Image Upload on Cloudinary

For image upload in Keystone Cloudinary is use. While defining the schema we can specify the type of field as following:

	image: {
		type: Types.CloudinaryImage,
		label: 'Upload Image'
	}

For configuration of cloudinary, we have to put the cloudinary details in .env file.
CLOUDINARY_URL= #cloudinary url as per you account

for API implementation of the cloudinary image upload, the request payload should be in the multipart/form-data.
and the file will be in the req.files .
also there is no need of library to create readable "multipart/form-data" streams, like Multer etc in keystone.
following is the example of image upload for API.

var keystone = require('keystone');
const fs = require('fs');
const cloudinary = require('cloudinary').v2

module.exports.addEvent = (req, res, next) => {
	req.body.content = req.body.contentBrief ? {brief: req.body.contentBrief} : ``
	uploadImage(req.files, (errorOnUploading, responseOfImageUpload)=> {
		if (errorOnUploading) {
			res.json(errorOnUploading)
		} else {
			req.body.image = responseOfImageUpload
			...
			// now the image details in req.body and now body will use
                        ...
		}
	})
}
function uploadImage (data, callback) {
	if (data && data.image) {
		var path = data.image.path;
		cloudinary.uploader.upload(path,(err, image) => {
			if (err) {
				callback(err, null)
			} else {
				console.log('file uploaded to Cloudinary')
				fs.unlinkSync(path)
				callback (null, image)
			}
		})
	} else {
		callback (null, null)
	}
}

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: