How to add pre-hook in keystonejs? - keystonejs

I want to add multiple select options field. But the docs state that doesn't allow for multiple select. But recommends pre-hook for that case.
Stores a String or Number in the model. Displayed as a select field in
the Admin UI. Does not allow for multiple items to be selected. If you
want to provide multiple values, you can use TextArray or NumberArray,
although neither will have the same constrained input. You can limit
the options using a pre-save hook.
I search for pre-hook but it seems came from mongoose. And in my case, I create the model using Keystone so that I can use it in admin page
var keystone = require('keystone');
var Types = keystone.Field.Types;
var MyModel = new keystone.List('MyModel');
MyModel.add({
aField: { type: Types.TextArray, required: false, initial: true },
});
so how do I create the pre-hook? for example, I want to limit the TextArray to be set of ('a','b','c')?

I have set up pre-save hooks like this (or something similar to this. Did not test this code).
var keystone = require('keystone');
var Types = keystone.Field.Types;
/**
* Musician Model
* ==========
*/
var Musician = new keystone.List('Musician', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
});
Musician.add({
title: { type: String, required: true },
published: { type: Types.Boolean, default: false },
musicianId: { type: String, note: noteUpdateId },
});
Musician.schema.pre('save', function (next) {
console.log(this.title);
console.log(this.isNew);
if (this.isNew) {
// generates a random ID when the item is created
this.musicianId = Math.random().toString(36).slice(-8);
}
next();
});
Musician.defaultColumns = 'title, published, musicianId';
Musician.register();

Related

Virtual "name" field?

I need to have the name field of a model be virtual, created by concatenating two real fields together. This name is just for display only. I've tried the virtual examples in the doc, no luck. Keystone 4 beta5.
var keystone = require('keystone')
_ = require('underscore');
var Types = keystone.Field.Types;
/**
* Foo Model
* ==================
*/
var Foo = new keystone.List('Foo', {
map: {name: 'fooname'},
track: true
});
Foo.add({
bar: { type: Types.Relationship, required: true, initial: true, label: 'Barref', ref: 'Bar', many: false },
order: { type: Types.Select, required: true, initial: true, label: 'Order', options: _.range(1,100) },
price: { type: Types.Money, format: '$0,0.00', label: 'Price', required: true, initial: true },
});
Foo.schema.virtual('fooname').get(function() {
return this.bar+ ' ' + this.order;
});
Foo.defaultColumns = 'fooname, bar, order, price';
Foo.register();
When I use this model definition, I don't see the virtual name in the defaultcolumns list. I want to make a virtual name so lookups are easier when this model is used as a relationship.
You don't need a virtual to do this. Keystone allows you to track and recalculate a field every time the document is saved. You can enable those options in order to create a function which concatenates these two values for you (either synchronously or asynchronously, your choice.)
One other thing I noticed is that bar is a Relationship, which means you will need to populate that relationship prior to getting any useful information out of it. That also means your value function will have to be asynchronous, which is as simple as passing a callback function as an argument to that function. Keystone does the rest. If you don't need any information from this bar, and you only need the _id (which the model always has), you can do without the keystone.list('Bar') function that I included.
http://keystonejs.com/docs/database/#fields-watching
The map object also refers to an option on your model, so you'll need a fooname attribute on your model in any scenario, though it gets calculated dynamically.
var keystone = require('keystone'),
_ = require('underscore');
var Types = keystone.Field.Types;
/**
* Foo Model
* ==================
*/
var Foo = new keystone.List('Foo', {
map: {name: 'fooname'},
track: true
});
Foo.add({
fooname: { type: Types.Text, watch: true, value: function (cb) {
// Use this if the "bar" that this document refers to has some information that is relevant to the naming of this document.
keystone.list('Bar').model.findOne({_id: this.bar.toString()}).exec(function (err, result) {
if (!err && result) {
// Result now has all the information of the current "bar"
// If you just need the _id of the "bar", and don't need any information from it, uncomment the code underneath the closure of the "keystone.list('Bar')" function.
return cb(this.bar.name + " " + this.order);
}
});
// Use this if you don't need anything out of the "bar" that this document refers to, just its _id.
// return cb(this.bar.toString() + " " + this.order);
} },
bar: { type: Types.Relationship, required: true, initial: true, label: 'Barref', ref: 'Bar', many: false },
order: { type: Types.Select, required: true, initial: true, label: 'Order', options: _.range(1,100) },
price: { type: Types.Money, format: '$0,0.00', label: 'Price', required: true, initial: true },
});
Foo.defaultColumns = 'fooname, bar, order, price';
Foo.register();
try this:
Foo.schema.pre('save', function (next) {
this.name = this.bar+ ' '+ this.order;
next();
});
Could you provide more information? What is currently working? How should it work?
Sample Code?
EDIT:
After creating the model Foo, you can access the Mongoose schema using the attribute Foo.schema. (Keystone Concepts)
This schema provides a pre-hook for all methods, which registered hooks. (Mongoose API Schema#pre)
One of those methods is save, which can be used like this:
Foo.schema.pre('save', function(next){
console.log('pre-save');
next();
});

GraphQL queries with tables join using Node.js

I am learning GraphQL so I built a little project. Let's say I have 2 models, User and Comment.
const Comment = Model.define('Comment', {
content: {
type: DataType.TEXT,
allowNull: false,
validate: {
notEmpty: true,
},
},
});
const User = Model.define('User', {
name: {
type: DataType.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
phone: DataType.STRING,
picture: DataType.STRING,
});
The relations are one-to-many, where a user can have many comments.
I have built the schema like this:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: {
type: GraphQLString
},
name: {
type: GraphQLString
},
phone: {
type: GraphQLString
},
comments: {
type: new GraphQLList(CommentType),
resolve: user => user.getComments()
}
})
});
And the query:
const user = {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve(_, {id}) => User.findById(id)
};
Executing the query for a user and his comments is done with 1 request, like so:
{
User(id:"1"){
Comments{
content
}
}
}
As I understand, the client will get the results using 1 query, this is the benefit using GraphQL. But the server will execute 2 queries, one for the user and another one for his comments.
My question is, what are the best practices for building the GraphQL schema and types and combining join between tables, so that the server could also execute the query with 1 request?
The concept you are refering to is called batching. There are several libraries out there that offer this. For example:
Dataloader: generic utility maintained by Facebook that provides "a consistent API over various backends and reduce requests to those backends via batching and caching"
join-monster: "A GraphQL-to-SQL query execution layer for batch data fetching."
To anyone using .NET and the GraphQL for .NET package, I have made an extension method that converts the GraphQL Query into Entity Framework Includes.
public static class ResolveFieldContextExtensions
{
public static string GetIncludeString(this ResolveFieldContext<object> source)
{
return string.Join(',', GetIncludePaths(source.FieldAst));
}
private static IEnumerable<Field> GetChildren(IHaveSelectionSet root)
{
return root.SelectionSet.Selections.Cast<Field>()
.Where(x => x.SelectionSet.Selections.Any());
}
private static IEnumerable<string> GetIncludePaths(IHaveSelectionSet root)
{
var q = new Queue<Tuple<string, Field>>();
foreach (var child in GetChildren(root))
q.Enqueue(new Tuple<string, Field>(child.Name.ToPascalCase(), child));
while (q.Any())
{
var node = q.Dequeue();
var children = GetChildren(node.Item2).ToList();
if (children.Any())
{
foreach (var child in children)
q.Enqueue(new Tuple<string, Field>
(node.Item1 + "." + child.Name.ToPascalCase(), child));
}
else
{
yield return node.Item1;
}
}}}
Lets say we have the following query:
query {
getHistory {
id
product {
id
category {
id
subCategory {
id
}
subAnything {
id
}
}
}
}
}
We can create a variable in "resolve" method of the field:
var include = context.GetIncludeString();
which generates the following string:
"Product.Category.SubCategory,Product.Category.SubAnything"
and pass it to Entity Framework:
public Task<TEntity> Get(TKey id, string include)
{
var query = Context.Set<TEntity>();
if (!string.IsNullOrEmpty(include))
{
query = include.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (q, p) => q.Include(p));
}
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}

Mongoose relation not working both ways

I can't get a relationship running between my Rides and Comments controller in my app (built using the yeoman angular-fullstack generator).
Comment model:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var CommentSchema = new Schema({
name: String,
comment: String,
active: Boolean,
ride: { type: mongoose.Schema.Types.ObjectId, ref: 'Ride' }
});
module.exports = mongoose.model('Comment', CommentSchema);
Ride model:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var RideSchema = new Schema({
name: String,
distance: String,
climb: String,
rating: String,
active: Boolean,
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }]
});
module.exports = mongoose.model('Ride', RideSchema);
Accessing /api/comments/ gives me a correct result, containing a related Ride:
{"_id":"54ce818f8c2889da58b01e19","name":"NAAM","comment":"COMMENT","ride":"54ce69647a78532057aa98e0","__v":0}]
Accessing /api/rides/ gives me the following result, without the corresponding Comments:
[{"_id":"54ce69647a78532057aa98e0","name":"Ride test ingevuld","distance":"4000","climb":"1200","rating":"1","__v":0,"comments":[]}]
Can anyone tell me what i am doing wrong?
Example from one of my projects:
exports.insertRoom = function(req, res) {
var id = req.body.id;
var r = req.body.room;
var room = new Room({name: r.name});
Floor.update(
{_id : id},
{
$push: { rooms: room}
},
{upsert:true},
function(floor, err)
{
res.sendStatus(200);
}
);
};
As far as I'am concerned it doesn't work like that. Your comments got it's ride, and your ride got it's comments. I think, you should remove
ride: { type: mongoose.Schema.Types.ObjectId, ref: 'Ride' }
and keep comments inside ride collection.
comments: ['Comment']
It is more objective solution as it supposed to be in MONGO DB which was designed for objective(hierarchial) data.

Manually adding columns to rallycardboard component

I am creating a rallycardboard where each column represents a release, and the cards are Features to be scheduled into those releases. The default mechanics of the component render all available releases as columns on the board. For our particular application this is unreasonable, since there are thousands of Releases in our workspace.
I was able to overwrite the addColumn method to only include a column if it is a Release which at least one Feature from the group is assigned to. The next step is to make it so that a user can manually add a Release column that doesn't currently have any assigned work. To do this, I stored all the excluded columns from the first step and created a combo box with those values. I would like it so that when the user selects a Release from the combo box, that Release column is added to the board.
I was able to reconfigure my addColumn method to allow for manual override (as apposed to trying to match with an existing Feature's Release). I verified that the column was added to the boards columns by calling board.getColumns() and the configurations look the same for both the existing and added columns. However, I get an error message when calling board.renderColumns() which appears to be the result of trying to write to a container that doesn't yet exist (the column isn't created yet).
Maybe I'm going about this the wrong way. Is there another way I can more easily decide which columns to include, and which to exclude, on the rallycardboard component?
Here is an example where the board columns are based on releases that have features scheduled. To add a columns for releases that have no features currently scheduled select releases from the mulitpicker.
The app is available in this github repo.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
_releasesWithFeatures: [],
_uniqueColumns: [],
_additionalColumns: [],
_updatedColumns: [],
_cardBoard: null,
launch: function() {
var that = this;
this._releasePicker = Ext.create('Rally.ui.picker.MultiObjectPicker', {
fieldLabel: 'Choose a Release',
modelType: 'Release'
});
this.add(this._releasePicker);
this.add({
xtype: 'rallybutton',
id: 'getReleases',
text: 'Add Selected Releases',
handler: function(){
that._getSelectedReleases();
}
})
Ext.create('Rally.data.WsapiDataStore', {
model: 'PortfolioItem/Feature',
fetch: ['FormattedID','Name','Release'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'Release',
operator: '!=',
value: null
}
],
listeners: {
load: this._onScheduledFeaturesLoaded,
scope: this
}
});
},
_onScheduledFeaturesLoaded: function(store, data){
var that = this;
if (data.length !==0) {
_.each(data, function(feature){
console.log('feature ', feature.get('FormattedID'), 'scheduled for ', feature.get('Release')._refObjectName, feature.get('Release')._ref);
that._releasesWithFeatures.push(feature.get('Release'))
});
that._makeBoard();
}
else{
console.log('there are no features scheduled for a release')
}
},
_makeBoard: function(){
if (this._cardBoard) {
this._cardBoard.destroy();
}
var columns = [];
_.each(this._releasesWithFeatures, function(rel){
columns.push({value: rel._ref, columnHeaderConfig: {headerTpl: '{release}', headerData: {release: rel._refObjectName}}});
});
this._uniqueColumns = _.uniq(columns, 'value');
var cardBoard = {
xtype: 'rallycardboard',
itemId: 'piboard',
types: ['PortfolioItem/Feature'],
attribute: 'Release',
fieldToDisplay: 'Release',
columns: this._uniqueColumns
};
this._cardBoard = this.add(cardBoard);
},
_getSelectedReleases: function(){
var that = this;
var expandedColumns = [];
var selectedReleases = this._releasePicker._getRecordValue();
console.log(selectedReleases.length);
if (selectedReleases.length > 0) {
_.each(selectedReleases, function(rel) {
var releaseName = rel.get('Name');
var releaseRef = rel.get('_ref');
that._additionalColumns.push({value: releaseRef, columnHeaderConfig: {headerTpl: '{release}', headerData: {release: releaseName}}});
});
}
expandedColumns = _.union(that._uniqueColumns, that._additionalColumns);
this._updatedColumns = _.uniq(expandedColumns, 'value');
this._updateBoard();
},
_updateBoard: function(){
var that = this;
if (this._cardBoard) {
this._cardBoard.destroy();
}
var cardBoard = {
xtype: 'rallycardboard',
types: ['PortfolioItem/Feature'],
attribute: 'Release',
fieldToDisplay: 'Release',
columns: that._updatedColumns,
};
this._cardBoard = this.add(cardBoard);
}
});

KeystoneJS Markdown Type Passing Validation when empty

Background:
I have a field of type Markdown. When I try unit testing it, the value initializes to what's seen below -- which passes validation.
{
"html": [undefined]
"md": [undefined]
"toJSON": [Function]
"toObject": [Function]
}
The model is as follows:
var keystone = require('keystone');
var Types = keystone.Field.Types;
/**
* Test Model
* ==========
*/
var Test = new keystone.List('Test');
Test.add({
name: {type: Types.Name, required: true, initial: true, index: true},
otherField: {type: Types.Markdown, initial: true, required: true},
});
/**
* Registration
*/
Test.defaultColumns = 'name, otherField';
Test.register();
module.exports = Test;
and the test:
var expect = require('chai').expect;
var keystone = require('keystone');
describe('Test', function() {
keystone.init({name: 'Test Example'});
keystone.import('../../models');
var Test;
it('should should fail validation when otherField is empty',function(done) {
Test = keystone.list('Test');
var test = new Test.model();
test.name.full = 'Jane Doe';
test.save(function(err) {
expect(err.name).to.equal('ValidationError');
done();
});
});
});
When running as is, the save passes with no error, so it doesn't hit done and times out. Switching the type to flat field types such as Text, Html, Boolean, Color, Date, Datetime, or Key will work as expected(ValidationError). I get the same issue if I switch the type to be another embedded type such as Name or Location.
Questions:
Is this the expected behavior?
If so, how can I modify my testing approach?
That is most certainly not expected behaviour. But we may not have tested this edgecase. Please do open an issue on the project. https://github.com/keystonejs/keystone