How to do a recursive prompt in Yeoman with promises? - javascript

I'm trying to figure out how to do a recursive prompt with a yeoman generator using promises. I am trying to produce a form generator that will first ask for a name for the form component and then ask for a name (that will be used as an id) for each input (ie: firstName, lastName, username, etc.). I've found answers for this question using callbacks but I would like to stick with promises. Below is the code I have so far and what I am attempting to do for the recursion but is not working. Any help and advice is appreciated thank you in advance!
const Generator = require('yeoman-generator')
const questions = [
{ type: 'input',
name: 'name',
message: 'What is the name of this form?',
default: 'someForm'
},
{
type: 'input',
name: 'input',
message: 'What is the name of the input?'
},
{
type: 'confirm',
name: 'askAgain',
message: 'Is there another input to add?'
}
]
module.exports = class extends Generator {
prompting() {
return this.prompt(questions).then((answers) => {
if (answers.askAgain) {
this.prompting()
}
this.userOptions = answers
this.log(this.userOptions)
})
}
}

For anyone that stumbles across this post looking for an answer this is what I ended up doing to make it work. As you can see in my Form class that extends Generator I have a method called prompting() in there. This is a method recognized by Yeoman's loop as a priority and it will not leave this method until something is returned. Since I'm returning a promise it will wait until my promise is finished before moving on. For my first prompt that's exactly what I need but for the second one to happen in prompting2 you can add
const done = this.async()
at the start of your method. This tells yeoman that you are going to have some asynchronous code happen and not to move past the method containing this until done is executed. If you do not use this and have another priority method in your class after it, such as writing() for when you are ready to produce your generated code, then yeoman will move past your method without waiting for your asynchronous code to finish. And you can see in my method prompting2() that I recursively call it whenever the user states that there is another input to name and it will continue doing so until they say there is not another input to name. I'm sure there is a better way to do this but it is working great for me this way. I hope this helps anyone that is looking for a way to do this!
const Generator = require('yeoman-generator')
const questions = [
{ type: 'input',
name: 'name',
message: 'What is the name of this form?',
default: 'someForm'
}
]
const names = [
{
type: 'input',
name: 'inputs',
message: 'What is the name of the input?',
default: '.input'
},
{
type: 'confirm',
name: 'another',
message: "Is there another input?",
default: true
}
]
const inputs = []
class Form extends Generator {
prompting() {
return this.prompt(questions).then((answers) => {
this.formName = answers.name
this.log(this.formName)
})
}
prompting2 () {
const done = this.async()
return this.prompt(names).then((answers) => {
inputs.push(answers.inputs)
if (answers.another) this.prompting2()
else {
this.inputs = inputs
this.log(this.inputs)
done()
}
})
}
}
module.exports = Form

Related

Querying child node fields in Gatsby

I have the following GraphQL schema, which defines 3 types: a CondaPackage which hasmany CondaVersion, which hasmany CondaExecutable. I want to be able to query a CondaVersion and ask "how many CondaExecutables do you own which succeeded my analysis". Currently I've written a succeededExeCount and allExeCount which resolve this field by loading all children and manually counting the number of children that succeeded.
exports.createSchemaCustomization = ({ actions: { createTypes }, schema }) => {
createTypes([
schema.buildObjectType({
name: "CondaPackage",
fields: {
succeededExeCount: {
type: "Int!",
resolve(source, args, context){
// TODO
}
},
allExeCount: {
type: "Int!",
resolve(source, args, context){
// TODO
}
}
},
interfaces: ["Node"]
}),
schema.buildObjectType({
name: "CondaVersion",
fields: {
succeededExeCount: {
type: "Float!",
resolve(source, args, context){
const children = context.nodeModel.getNodesByIds({
ids: source.children,
type: "CondaExecutable"
})
return children.reduce((acc, curr) => acc + curr.fields.succeeded, 0)
}
},
allExeCount: {
type: "Int!",
resolve(source, args, context){
return source.children.length;
}
}
},
interfaces: ["Node"]
}),
schema.buildObjectType({
name: "CondaExecutable",
fields: {
succeeded: {
type: "Boolean!",
resolve(source, args, context, info) {
return source.fields.succeeded || false;
}
},
},
interfaces: ["Node"]
})
])
}
My first problem is that this seems incredibly inefficient. For each CondaVersion I'm running a separate query for its children, which is a classic N+1 query problem. Is there a way to tell Gatsby/GraphQL to simply "join" the two tables like I would using SQL to avoid this?
My second problem is that I now need to count the number of succeeding children from the top level type: CondaPackage. I want to ask "how many CondaExecutables do your child CondaVersions own which succeeded my analysis". Again, in SQL this would be easy because I would just JOIN the 3 types. However, the only way I can currently do this is by using getNodesByIds for each child, and then for each child's child, which is n*m*o runtime, which is terrifying. I would like to run a GraphQL query as part of the field resolution which lets me grab the succeededExeCount from each child. However, Gatsby's runQuery seems to return nodes without including derived fields, and it won't let me select additional fields to return. How can I access fields on a node's child's child in Gatsby?
Edit
Here's the response from a Gatsby maintainer regarding the workaround:
Gatsby has an internal mechanism to filter/sort by fields with custom resolvers. We call it materialization. [...] The problem is that this is not a public API. This is a sort of implementation detail that may change someday and that's why it is not documented.
See the full thread here.
Original Answer
Here's a little 'secret' (not mentioned anywhere in the docs at the time of writing):
When you use runQuery, Gatsby will try to resolve derived fields... but only if that field is passed to the query's options (filter, sort, group, distinct).
For example, in CondaVersion, instead of accessing children nodes and look up fields.succeeded, you can do this:
const succeededNodes = await context.nodeModel.runQuery({
type: "CondaExecutable",
query: { filter: { succeeded: { eq: true } } }
})
Same thing for CondaPackage. You might try to do this
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
query: {}
})
return versionNodes.reduce((acc, nodes) => acc + node.succeededExeCount, 0) // Error
You'll probably find that succeededExeCount is undefined.
The trick is to do this:
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
- query: {}
+ query: { filter: { succeededExeCount: { gte: 0 } } }
})
It's counter intuitive, because you'd think Gatsby would just resolve all resolvable fields on a type. Instead it only resolves fields that is 'used'. So to get around this, we add a filter that supposedly does nothing.
But that's not all yet, node.succeededExeCount is still undefined.
The resolved data (succeededExeCount) is not directly stored on the node itself, but in node.__gatsby_resolved source. We'll have to access it there instead.
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
query: { filter: { succeededExeCount: { gte: 0 } } }
})
return versionNodes.reduce((acc, node) => acc + node.__gatsby_resolved.succeededExeCount, 0)
Give it a try & let me know if that works.
PS: I notice that you probably use createNodeField (in CondaExec's node.fields.succeeded?) createTypes is also accessible in exports.sourceNodes, so you might be able to add this succeeded field directly.

Yeoman invoke generator by code with args

I've having yeoman generator with sub generator.
I need to invoke the sub generator via code and I use the code below which is working, I see that the sub generator is invoked and I got the question in the terminal.
docs:
https://yeoman.io/authoring/integrating-yeoman.html
var yeoman = require('yeoman-environment');
var env = yeoman.createEnv();
env.lookup(function () {
env.run('main:sub',err => {
console.log('done' ,err);
});
});
The sub generator have only one question
prompting() {
const prompts = [
{
name: "app",
message: "which app to generate?",
type: "input",
default: this.props.app,
},
];
...
I want to call it silently, which means to pass the value for app question via code and not using terminal and I try this which doesn't works, (I see the question in the terminal)
env.lookup(function () {
env.run('main:sub',{"app":"nodejs"}, err => {
console.log('done' ,err);
});
});
and also tried this which doesnt works
env.lookup(function () {
env.run('main:sub --app nodejs', err => {
console.log('done' ,err);
});
});
How can I do it ? pass the values using code (maybe like it's done on unit test but this code is not unit test... when the terminal is not invoked)
From the docs im not sure how to pass the values
https://yeoman.io/authoring/integrating-yeoman.html
I've also found this but didn't quite understand how to use it to pass parameter to generator
http://yeoman.github.io/environment/Environment.html#.lookupGenerator
is it possible?
You can just do:
env.lookup(function () {
env.run('main:sub',{"app":"nodejs"}, err => {
console.log('done' ,err);
});
});
and inside the sub sub-generator, you can find the value via this.options.app.
To disable the question prompt, defined when field inside the Question Object like this:
prompting() {
const prompts = [
{
name: "app",
message: "which app to generate?",
type: "input",
default: this.props.app,
when: !this.options.app
},
];
. . .
return this.prompt(prompts).then((props) => {
this.props = props;
this.props.app = this.options.app || this.props.app;
});
}

How to customize assertions in Cypress

I need to test that an array of object contains a certain value. The test is written with Cypress, and for that, I use cy.wrap and .some().
My code looks like this:
const myCustomArray = [{ name: 'Lisa' }, { name: 'Katie' }];
cy.wrap(myCustomArray.some((user) => {
if (user.name === 'Lisa') {
return true;
} else {
return false;
}
})).should('eq', true);
This works well, but the problem is that it then returns me a very non-specific message in the Cypress console.
What I would like to have, is to change my code in a way that the message would be understandable. In the idea, it would be something like that:
const myCustomArray = [{ name: 'Lisa' }, { name: 'Katie' }];
cy.wrap(myCustomArray.some((user) => {
if (user.name === 'Lisa') {
return 'user name is Lisa';
}
})).should('eq', 'user name is Lisa');
But this can't work as .some() can only return a boolean. I suppose there is an array function that could help me do that, but I can't find which one.
I am not sure whether:
There are Cypress commands I am unaware of that could solve this issue, eg. customizing the assertion message.
Or it can just be solved with using JavaScript
Both solutions would be fine for me.
How about using .find() instead of .some(), and deep-eq the result,
cy.wrap(myCustomArray.find(user => user.name === 'Lisa'))
.should('deep.eq', { name: 'Lisa' });
ASSERT expected { name: Lisa } to deeply equal { name: Lisa }
or if you have big objects and just want to see the name,
cy.wrap(myCustomArray.map(user => user.name).find(name => name === 'Lisa'))
.should('eq', 'Lisa');
ASSERT expected Lisa to equal Lisa

Resolve Custom Types at the root in GraphQL

I feel like I'm missing something obvious. I have IDs stored as [String] that I want to be able to resolve to the full objects they represent.
Background
This is what I want to enable. The missing ingredient is the resolvers:
const bookstore = `
type Author {
id: ID!
books: [Book]
}
type Book {
id: ID!
title: String
}
type Query {
getAuthor(id: ID!): Author
}
`;
const my_query = `
query {
getAuthor(id: 1) {
books { /* <-- should resolve bookIds to actual books I can query */
title
}
}
}
`;
const REAL_AUTHOR_DATA = [
{
id: 1,
books: ['a', 'b'],
},
];
const REAL_BOOK_DATA = [
{
id: 'a',
title: 'First Book',
},
{
id: 'b',
title: 'Second Book',
},
];
Desired result
I want to be able to drop a [Book] in the SCHEMA anywhere a [String] exists in the DATA and have Books load themselves from those Strings. Something like this:
const resolve = {
Book: id => fetchToJson(`/some/external/api/${id}`),
};
What I've Tried
This resolver does nothing, the console.log doesn't even get called
const resolve = {
Book(...args) {
console.log(args);
}
}
HOWEVER, this does get some results...
const resolve = {
Book: {
id(id) {
console.log(id)
return id;
}
}
}
Where the console.log does emit 'a' and 'b'. But I obviously can't scale that up to X number of fields and that'd be ridiculous.
What my team currently does is tackle it from the parent:
const resolve = {
Author: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
}
This isn't ideal because maybe I have a type Publisher { books: [Book]} or a type User { favoriteBooks: [Book] } or a type Bookstore { newBooks: [Book] }. In each of these cases, the data under the hood is actually [String] and I do not want to have to repeat this code:
const resolve = {
X: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
};
The fact that defining the Book.id resolver lead to console.log actually firing is making me think this should be possible, but I'm not finding my answer anywhere online and this seems like it'd be a pretty common use case, but I'm not finding implementation details anywhere.
What I've Investigated
Schema Directives seems like overkill to get what I want, and I just want to be able to plug [Books] anywhere a [String] actually exists in the data without having to do [Books] #rest('/external/api') in every single place.
Schema Delegation. In my use case, making Books publicly queryable isn't really appropriate and just clutters my Public schema with unused Queries.
Thanks for reading this far. Hopefully there's a simple solution I'm overlooking. If not, then GQL why are you like this...
If it helps, you can think of this way: types describe the kind of data returned in the response, while fields describe the actual value of the data. With this in mind, only a field can have a resolver (i.e. a function to tell it what kind of value to resolve to). A resolver for a type doesn't make sense in GraphQL.
So, you can either:
1. Deal with the repetition. Even if you have ten different types that all have a books field that needs to be resolved the same way, it doesn't have to be a big deal. Obviously in a production app, you wouldn't be storing your data in a variable and your code would be potentially more complex. However, the common logic can easily be extracted into a function that can be reused across multiple resolvers:
const mapIdsToBooks = ({ books }) => books.map(id => fetchBookById(id))
const resolvers = {
Author: {
books: mapIdsToBooks,
},
Library: {
books: mapIdsToBooks,
}
}
2. Fetch all the data at the root level instead. Rather than writing a separate resolver for the books field, you can return the author along with their books inside the getAuthor resolver:
function resolve(root, args) {
const author = REAL_AUTHOR_DATA.find(row => row.id === args.id)
if (!author) {
return null
}
return {
...author,
books: author.books.map(id => fetchBookById(id)),
}
}
When dealing with databases, this is often the better approach anyway because it reduces the number of requests you make to the database. However, if you're wrapping an existing API (which is what it sounds like you're doing), you won't really gain anything by going this route.

Iterate list of strings in mithril and create drop down

I tried searching a lot of internet but could not find answer to a simple question. I am very new to mithril (do not know why people chose mithril for project :( ). I want to iterate through a list of strings and use its value in drop down with a checkbox.
const DefaultListView = {
view(ctrl, args) {
const parentCtrl = args.parentCtrl;
const attr = args.attr;
const cssClass = args.cssClass;
const filterOptions = ['Pending', 'Paid', 'Rejected'];
// as of now, all are isMultipleSelection filter
const selectedValue =
parentCtrl.filterModel.getSelectedFilterValue(attr);
function isOptionSelected(value) {
return selectedValue.indexOf(value) > -1;
}
return m('.filter-dialog__default-attr-listing', {
class: cssClass
}, [
m('.attributes', {
onscroll: () => {
m.redraw(true);
}
}, [
filterOptions.list.map(filter => [
m('.dropdown-item', {
onclick() {
// Todo: Add click metrics.
// To be done at time of backend integration.
document.body.click();
}
}, [
m('input.form-check-input', {
type: 'checkbox',
checked: isOptionSelected(filter)
}),
m('.dropdown-text', 'Pending')
])
])
])
]);
}
};
Not sure. How to do it. This is what I have tried so far but no luck. Can someone help me this?
At the beginning of the view function you define an array:
const filterOptions = ['Pending', 'Paid', 'Rejected'];
But later on in the view code where you perform the list iteration, filterOptions is expected to be an object with a list property:
filterOptions.list.map(filter =>
That should be filterOptions.map(filter =>.
There may be other issues with your code but it's impossible to tell without seeing the containing component which passes down the args. You might find it more helpful to ask the Mithril chatroom, where myself and plenty of others are available to discuss & assist with tricky situations.

Categories

Resources