Why can't I access 'User story' as a type? - youtrack

I have a (correctly working) workflow script starting with this guard function:
var entities = require('#jetbrains/youtrack-scripting-api/entities');
exports.rule = entities.Issue.action({
title: 'Create default subtasks',
command: 'tt-create-subtasks',
guard: function(ctx) {
return ctx.issue.fields.Type.name == 'User Story';
},
I thought I would replace that with something like
return ctx.issue.fields.Type == UserStory;
and therefore change the requirements from:
requirements: {
Type: {
type: entities.EnumField.fieldType,
Task: {},
}
}
to:
requirements: {
Type: {
type: entities.EnumField.fieldType,
Task: {},
UserStory: {
name: 'User Story'
}
}
}
Task is used elsewhere in a similar fashion and that works:
newIssue.fields.Type = ctx.Type.Task;
But the editor gives red errors on UserStory in the giard function. Am I doing something wrong in the requirements?

If you declare the requirements like you described
requirements: {
Type: {
type: entities.EnumField.fieldType,
Task: {},
UserStory: {
name: 'User Story'
}
}
}
you'll be able to check the value the following way: issue.fields.is(ctx.Type, ctx.Type.UserStory).

Related

Automatically assign assignee when changing status of ticket

I created an "Agile-Board" in youtrack and I want every ticket that is moved to the column (which is mapped to the field Status) "In Produktivsetzung" to be automatically assigned to my user.
Like this:
How can this be done?
One can set it up with a custom workflow script as follows
var entities = require('#jetbrains/youtrack-scripting-api/entities');
exports.rule = entities.Issue.onChange({
title: 'Set logged-in user as an assignee when they move it to In Produktivsetzung state',
guard: function(ctx) {
var issue = ctx.issue;
return issue.isReported &&
issue.fields.Assignee === null &&
issue.fields.becomes(ctx.State, ctx.State.InProgress) &&
!issue.fields.isChanged("project");
},
action: function(ctx) {
var isCurrentUserAssignee = false;
ctx.Assignee.values.forEach(function(it) {
if (it.login == ctx.currentUser.login) {
isCurrentUserAssignee = true;
}
});
if (isCurrentUserAssignee) {
ctx.issue.Assignee = ctx.currentUser;
}
},
requirements: {
Assignee: {
type: entities.User.fieldType
},
State: {
type: entities.State.fieldType,
InProgress: {
name: 'In Produktivsetzung'
}
}
}
});
I want to set assignee on every state change. After a couple hours trial & error (the documentation is really not that good) I had success:
var entities = require('#jetbrains/youtrack-scripting-api/entities');
exports.rule = entities.Issue.onChange({
title: 'Assign issue to current user when state changes',
guard: function(ctx) {
return ctx.issue.fields.isChanged(ctx.State);
},
action: (ctx) => {
ctx.issue.fields.Assignee = ctx.currentUser;
},
requirements: {
Assignee: {
type: entities.User.fieldType
},
State: {
type: entities.State.fieldType
}
}
});
I don't really understand why I have to use a "guard function" - I could just use a conditional statement in the action and the whole "requirements" section doesn't make any sense to me but if it is necessary... I don't care. Finally works as expected... I hope that it works some years longer than the "legacy scripts" - I don't want to touch it again. 🙂
Based on the answer this is what I'm using now, I created multiple modules where I just had to change the two variables at the top of my code:
var entities = require('#jetbrains/youtrack-scripting-api/entities');
var assigneeLogin = '<some.login>';
var stateName = '<Some Statename, see possible values in console.log(ctx.State)>';
exports.rule = entities.Issue.onChange({
title: 'Set ' + assigneeLogin + ' as the assignee when ticket is moved to "'+ stateName + '"',
guard: function(ctx) {
var issue = ctx.issue;
return issue.fields.becomes(ctx.State, ctx.State.InProgress);
},
action: function(ctx) {
ctx.Assignee.values.forEach(function(it) {
if (it.login === assigneeLogin) {
ctx.issue.Assignee = it;
}
});
},
requirements: {
Assignee: {
type: entities.User.fieldType
},
State: {
type: entities.State.fieldType,
InProgress: {
name: stateName
}
}
}
});

Sequelize Many to Many Relationship using Through does not insert additional attributes

I have a many to many relationship between: Step and Control Through ControlsConfig.
When creating a Control object and call addStep function and specify the additional attributes (which exist in the relation table), Sequelize creates the records in the relational table ControlsConfig but the additional attributes are NULLs.
PS: The tables are creating correctly in the database.
Table 1: Step
Table 2: Control
Relation table: ControlsConfig
Step
var Step = sequelize.define('Step', {
title: { type: DataTypes.STRING, allowNull: false },
description: DataTypes.STRING,
type: { type: DataTypes.ENUM('task', 'approval'), allowNull: false, defaultValue: 'task' },
order: DataTypes.INTEGER
});
Step.associate = function(models) {
models.Step.belongsTo(models.User);
models.Step.belongsTo(models.Template);
models.Step.hasMany(models.Action);
};
Control
var Control = sequelize.define('Control', {
label: { type: DataTypes.STRING, allowNull: false },
order: { type: DataTypes.INTEGER },
type: { type: DataTypes.ENUM('text', 'yes/no') },
config: { type: DataTypes.TEXT },
controlUiId: { type: DataTypes.STRING }
});
Control.associate = function(models) {
models.Control.belongsTo(models.Section);
};
ControlsConfigs
module.exports = (sequelize, DataTypes) => {
var ControlsConfig = sequelize.define('ControlsConfig', {
visibility: { type: DataTypes.ENUM('hidden', 'readonly', 'editable', 'required') },
config: { type: DataTypes.TEXT }
});
ControlsConfig.associate = function(models) {
models.Control.belongsToMany(models.Step, { through: models.ControlsConfig });
models.Step.belongsToMany(models.Control, { through: models.ControlsConfig });
models.ControlsConfig.belongsTo(models.Template);
};
return ControlsConfig;
};
Insertion:
try {
var step1 = await Step.create({ /*bla bla*/ });
var control1 = await Control.create({ /*bla bla*/ });
var OK = await control1.addStep(step1, {through: { config: 'THIS FIELD ALWAYS APPEARS NULL' }});
} catch (error) { /* No errors*/ }
I am following the same strategy stated at the documentation
//If you want additional attributes in your join table, you can define a model for the join table in sequelize, before you define the association, and then tell sequelize that it should use that model for joining, instead of creating a new one:
const User = sequelize.define('user', {})
const Project = sequelize.define('project', {})
const UserProjects = sequelize.define('userProjects', {
status: DataTypes.STRING
})
User.belongsToMany(Project, { through: UserProjects })
Project.belongsToMany(User, { through: UserProjects })
//To add a new project to a user and set its status, you pass extra options.through to the setter, which contains the attributes for the join table
user.addProject(project, { through: { status: 'started' }})
You have to pass edit: true to the addProject and addStep method.
See this answer it has a similar issue
Sequelize belongsToMany additional attributes in join table

Keystone.js filtering on related fields within own model

I am working on filtering my subsection selection to display only subSections that are related to the current mainNavigationSection. Each of these subsections also has a mainNavigation section. For some reason the current implementation is not returning any results.
Here is my Page Model:
Page.add({
name: { type: String, required: true },
mainNavigationSection: { type: Types.Relationship, ref: 'NavItem', refPath: 'key', many: true, index: true },
subSection: { type: Types.Relationship, ref: 'SubSection', filters: { mainNavigationSection:':mainNavigationSection' }, many: true, index: true, note: 'lorem ipsum' },
state: { type: Types.Select, options: 'draft, published, archived', default: 'draft', index: true },
author: { type: Types.Relationship, ref: 'User', index: true }
}
Here is my subSectionModel:
SubSection.add({
name: { type: String, required: true, index: true },
mainNavigationSection: { type: Types.Relationship, ref: 'NavItem', many: true, required: true, initial: true},
showInFooterNav: { type: Boolean, default: false },
defaultPage: { type: Types.Relationship, ref: 'Page' },
description: { type: Types.Html, wysiwyg: true, height: 150, hint: 'optional description' }
});
From what it seems, you have the possibility of many mainNavigationSections on your model. You'd have to iterate over each of them on the current Page, and find the related SubSections. You'll need to use the async Node module to run all the queries and get the results from each.
var async = require('async');
var pID = req.params.pid; // Or however you are identifying the current page
keystone.list('Page').model.findOne({page: pID}).exec(function (err, page) {
if (page && !err) {
async.each(page.mainNavigationSection, function (curMainNavigationSection, cb) {
keystone.list('SubSection').model
.find({mainNavigationSection: curMainNavigationSection._id.toString()})
.exec(function (err2, curSubSections) {
if (curSubSections.length !== 0 && !err2) {
// Do what you need to do with the navigation subSections here
// I recommend using a local variable, which will persist through
// every iteration of this loop and into the callback function in order
// to persist data
return cb(null)
}
else {
return cb(err || "An unexpected error occurred.");
}
});
}, function (err) {
if (!err) {
return next(); // Or do whatever
}
else {
// Handle error
}
});
}
else {
// There were no pages or you have an error loading them
}
});

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));
}

Rally - More efficient way to get item by ID

I have been trying to query Rally just to get a certain object by its ObjectID, but then I end up needing its parent in many cases. For example, for a task, I need its associated User Story, and that Story's Feature. It ended up being quite the cascade of callbacks (fair warning, it's ugly) - can anyone recommend a more efficient solution? The ability to query by OID is nice, but its too bad I need more than just information about that OID. (Note - solution must utilize WSAPI, not LBAPI).
Rally.data.WsapiModelFactory.getModel({
type: 'Task',
context: {
workspace: Rally.util.Ref.getRelativeUri()
},
success: function(taskModel) {
taskModel.load(oid, {
scope: this,
callback: function(taskRecord, op, success) {
if (taskRecord && taskRecord.data.WorkProduct && taskRecord.data.WorkProduct._type == "HierarchicalRequirement") {
// get User Story
Rally.data.WsapiModelFactory.getModel({
type: 'User Story',
context: {
workspace: Rally.util.Ref.getRelativeUri()
},
success: function(userStoryModel) {
userStoryModel.load(taskRecord.data.WorkProduct._ref, {
scope: this,
callback: function(storyRecord, op, success) {
if (storyRecord && storyRecord.data && storyRecord.data.Feature) {
// Get Feature
Rally.data.WsapiModelFactory.getModel({
type: 'PortfolioItem/Feature',
context: {
workspace: Rally.util.Ref.getRelativeUri()
},
success: function(featureModel) {
featureModel.load(storyRecord.data.Feature._ref, {
scope: this,
callback: function(featureRecord) {
displayTask(oid, taskRecord, storyRecord, featureRecord);
}
});
}
});
}
}
});
}
});
}
}
});
}
});
You can pull in the Work Product parent and its associated Feature directly in a single query. Try this:
Ext.create('Rally.data.WsapiDataStore', {
model : 'Task',
fetch : ['WorkProduct','Name','Feature'],
filters : [{
property : 'ObjectID',
value : OID
}]
}).load({
callback : function(records, operation, success) {
var task = records[0];
var userStory = task.get('WorkProduct');
var feature = userStory.Feature;
}
});