GraphQL Syntax for mapping array on mutation field - symfony

I'm looking for advice on syntax. In my query, I need my backend (Graphlite, Symfony) to get the child items. The way I get the regular item is as below, and works correctly. I'm not well versed enough in GraphQL / Apollo / VueJS / GraphQLite to know which technology owns the "{id: $itemId}" syntax, nor do I know what it is called, so I can't find any info about it.
insertItem: gql` mutation insertItem(
$itemId: Uuid!,
$childItemIds: [Uuid!]!
) {
insertItem(
item: {id: $itemId},
childItems: { id: { in : { $childItemIds} }
){
// ... stuff
}
}`
So, given that the {id: $itemId} correctly works for getting an item, I assume there is some graphQL syntax that would work to apply the childItemIds to get the childItems. Is there a name for this type of mapping? What would the syntax be?

As per discussion with #UjinT34, since this couldn't be done inline with GraphQL, I created an array of ItemInput before calling the mutation:
for (var i = 0, len = item.childItems.length; i < len; i++) {
childItems.push({ id : item.childItems[i].id } )
}
The original query then became:
insertItem:`` gql` mutation insertItem(
$itemId: Uuid!,
$childItemIds: [ItemInput!]!
) {
insertItem(
item: {id: $itemId},
childItems: $childItemIds
){
// ... stuff
}
}
Once I understood that the {id: $item} wasn't a magic GQL/GraphQLite syntax and was simply an inline javascript object being created, the solution became fairly simple.

Related

Can we use NgRx Data with pagination style REST API response?

I am currently using NgRx Data to perform CRUD operation on couple of entities on my project. Now, I've to develop pagination. Hence, REST API response is going to be like:
{
"page": 1,
"per_page": 10,
"total": 100,
"total_page": 10,
"data": [
{ ... },
{ ... },
{ ... }
]
}
AFAIK, NgRx Data works well with entities, I've no clue how to deal with this. Could you please redirect me to some light? Thank you.
Even I was facing a similar issue. So for people who are new to NgRx data, I have created an EntityDataListInterface which was similar to:
{
page: number,
per_page: number,
total: number,
total_page: number,
data: EntityDataItem[]
}
For each section I am working on I create a different service. Lets call it ComponentService. Inside this ComponentService I access the EntityService(which implements EntityCollectionServiceBase<EntityDataItem>) and entity's DataService (which implements DefaultDataService<EntityDataListInterface>).
Once the API returns EntityDataListInterface data, you can use addManyToCache to add them into the entity cache.
Inside the module, register the EntityDataItem by passing the filterFn. Now you can call setFilter to filter the entities based on indexes(or any pagination logic like shown below) and the result would be accessible via filteredEntities$.
//eds: EntityDefinitionService in the constructor
const entityMetadata: EntityMetadataMap = {
EntityDataItem: {
filterFn:(entities: EntityDataItem[], pattern:{startIndex: number, endIndex: number}) => {
return entities.filter((entity, index) => {
return ((index >= pattern.startIndex) && (index <= pattern.endIndex));
})
}
}
};
eds.registerMetadataMap(entityMetadata);
Subscribe to filteredEntities$ in your component and it will solve the pagination issue.

How do I return an entire paged set from the Jira API using Ramda?

I'm using the Nodejs library for talking to Jira called jira-connector. I can get all of the boards on my jira instance by calling
jira.board.getAllBoards({ type: "scrum"})
.then(boards => { ...not important stuff... }
the return set looks something like the following:
{
maxResults: 50,
startAt: 0,
isLast: false,
values:
[ { id: ... } ]
}
then while isLast === false I keep calling like so:
jira.board.getAllBoards({ type: "scrum", startAt: XXX })
until isLast is true. then I can organize all of my returns from promises and be done with it.
I'm trying to reason out how I can get all of the data on pages with Ramda, I have a feeling it's possible I just can't seem to sort out the how of it.
Any help? Is this possible using Ramda?
Here's my Rx attempt to make this better:
const pagedCalls = new Subject();
pagedCalls.subscribe(value => {
jira.board.getAllBoards({ type:"scrum", startAt: value })
.then(boards => {
console.log('calling: ' + value);
allBoards.push(boards.values);
if (boards.isLast) {
pagedCalls.complete()
} else {
pagedCalls.next(boards.startAt + 50);
}
});
})
pagedCalls.next(0);
Seems pretty terrible. Here's the simplest solution I have so far with a do/while loop:
let returnResult = [];
let result;
let startAt = -50;
do {
result = await jira.board.getAllBoards( { type: "scrum", startAt: startAt += 50 })
returnResult.push(result.values); // there's an array of results under the values prop.
} while (!result.isLast)
Many of the interactions with Jira use this model and I am trying to avoid writing this kind of loop every time I make a call.
I had to do something similar today, calling the Gitlab API repeatedly until I had retrieved the entire folder/file structure of the project. I did it with a recursive call inside a .then, and it seems to work all right. I have not tried to convert the code to handle your case.
Here's what I wrote, if it will help:
const getAll = (project, perPage = 10, page = 1, res = []) =>
fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/repository/tree?recursive=true&per_page=${perPage}&page=${page}`)
.then(resp => resp.json())
.then(xs => xs.length < perPage
? res.concat(xs)
: getAll(project, perPage, page + 1, res.concat(xs))
)
getAll('gitlab-examples/nodejs')
.then(console.log)
.catch(console.warn)
The technique is pretty simple: Our function accepts whatever parameters are necessary to be able to fetch a particular page and an additional one to hold the results, defaulting it to an empty array. We make the asynchronous call to fetch the page, and in the then, we use the result to see if we need to make another call. If we do, we call the function again, passing in the other parameters needed, the incremented page number, and the merge of the current results and the ones just received. If we don't need to make another call, then we just return that merged list.
Here, the repository contains 21 files and folders. Calling for ten at a time, we make three fetches and when the third one is complete, we resolve our returned Promise with that list of 21 items.
This recursive method definitely feels more functional than your versions above. There is no assignment except for the parameter defaulting, and nothing is mutated along the way.
I think it should be relatively easy to adapt this to your needs.
Here is a way to get all the boards using rubico:
import { pipe, fork, switchCase, get } from 'rubico'
const getAllBoards = boards => pipe([
fork({
type: () => 'scrum',
startAt: get('startAt'),
}),
jira.board.getAllBoards,
switchCase([
get('isLast'),
response => boards.concat(response.values),
response => getAllBoards(boards.concat(response.values))({
startAt: response.startAt + response.values.length,
})
]),
])
getAllBoards([])({ startAt: 0 }) // => [...boards]
getAllBoards will recursively get more boards and append to boards until isLast is true, then it will return the aggregated boards.

Flow error when using react-apollo Query component render prop

I have the following code:
import { Query } from 'react-apollo';
type Post = {
id: string
};
interface Data {
posts: Array<Post>;
}
class PostsQuery extends Query<Data> {}
When using the above as follows:
<PostsQuery query={POSTS_QUERY}>
{({ loading, data }) => {
...
{data.posts.map(...)}
...
}
</PostsQuery>
I get the following error from flow:
Error:(151, 27) Cannot get 'data.posts' because property 'posts' is missing in object type [1].
Any idea why?
I did use flow-typed to add apollo-client_v2.x.x.js to my project by the way
Solution to the problem
Continued from the answer explaining how to make a verifiable example and research the problem.
So it looks like this part of react-apollo isn't typed in such a way to make accessing the data contents straightforward. Okay, that's fine, we can take their recommendation on destructuring and check for empty data. At the same time, we can also add an id property to the Post type so flow stops complaining about that:
(Try - Scroll to bottom for relevant code)
type Post = {
id: string,
title: string;
};
...snip...
// Look ma, no errors
class Search extends React.Component<{}> {
render = () => (
<PostsQuery query={QUERY}>
{({ loading, error, data }) => {
if (error) {
return <p>Error</p>
}
if (loading) return <p>Loading</p>
const nonNullData = (data || {})
const dataWithAllPosts = {allPosts: [], ...nonNullData}
const {allPosts} = dataWithAllPosts
if (allPosts.length == 0) {
return <p>Empty response or something</p>
}
return (
<div>
{allPosts.map(post => {
return <div key={post.id}>{post.title}</div>;
})}
</div>
);
}}
</PostsQuery>
);
}
I'm not familiar with the react-apollo library, so I'm not sure how you want to handle that case where there are no posts. I just added a message as seen above. It's entirely possible that the case never occurs (again, you would know better than I do). If that's the case, you might want to skip some of the above steps and just assert the desired type with a cast through any.
How to make a reproducible example and research the problem
So the first thing we need to do while analyzing these types is to go lookup the typedefs in the flow-typed repo. I went ahead a copy-pasted the react-apollo typedefs into flow.org/try, modified them slightly (added an any somewhere, set gql to any), and was able to replicate your errors:
(Try - Scroll to the bottom for your code)
Referencing the relevant lines of the QueryRenderProps type, we can see why flow is throwing the error:
{
data: TData | {||} | void,
...
}
It looks like data can either be TData (probably what you want), an empty object, or undefined. Cross checking this with the typescript typings for react-apollo, we can see why the type is the way it is:
{
...
// we create an empty object to make checking for data
// easier for consumers (i.e. instead of data && data.user
// you can just check data.user) this also makes destructring
// easier (i.e. { data: { user } })
// however, this isn't realy possible with TypeScript that
// I'm aware of. So intead we enforce checking for data
// like so result.data!.user. This tells TS to use TData
// XXX is there a better way to do this?
data: TData | undefined;
...
}
Unfortunately, due to the extreme length of these links and stackoverflow's limit on answer lengths, I have to continue my answer in another answer. I guess this answer can serve as an explanation of how to start debugging the problem.

Meteor minimongo dynamic cursor

In my client UI I have a form with differents search criterias, and I'd like to reactively update the results list. The search query is transformed into a classical minimongo selector, saved in a Session variable, and then I have observers to do things with the results:
// Think of a AirBnb-like application
// The session variable `search-query` is updated via a form
// example: Session.set('search-query', {price: {$lt: 100}});
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
resultsCursor = Offers.find(Session.get('search-query'));
// I want to add and remove pins on a map
resultCursor.observe({
added: Map.addPin,
removed: Map.removePin
});
Deps.autorun(function() {
// I want to modify the cursor selector and keep the observers
// so that I would only have the diff between the old search and
// the new one
// This `modifySelector` method doesn't exist
resultsCursor.modifySelector(Session.get('search-query'));
});
How could I implement this modifySelector method on the cursor object?
Basically I think this method needs to update the compiled version of the cursor, ie the selector_f attribute, and then rerun observers (without losing the cache of the previous results). Or is there any better solution?
Edit: Some of you have misunderstood what I'm trying to do. Let me provide a complete example:
Offers = new Meteor.Collection('offers');
if (Meteor.isServer && Offers.find().count() === 0) {
for (var i = 1; i < 4; i++) {
// Inserting documents {price: 1}, {price: 2} and {price: 3}
Offers.insert({price:i})
}
}
if (Meteor.isClient) {
Session.setDefault('search-query', {price:1});
resultsCursor = Offers.find(Session.get('search-query'));
resultsCursor.observe({
added: function (doc) {
// First, this added observer is fired once with the document
// matching the default query {price: 1}
console.log('added:', doc);
}
});
setTimeout(function() {
console.log('new search query');
// Then one second later, I'd like to have my "added observer" fired
// twice with docs {price: 2} and {price: 3}.
Session.set('search-query', {});
}, 1000);
}
This doesn't solve the problem in the way you seem to be wanting to, but I think the result is still the same. If this is a solution you explicitly don't want, let me know and I can remove the answer. I just didn't want to put code in a comment.
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
Template.map.pins = function() {
return Offers.find(Session.get('search-query'));
}
Template.map.placepins = function(pins) {
// use d3 or whatever to clear the map and then place all pins on the map
}
Assuming your template is something like this:
<template name="map">
{{placepins pins}}
</template>
One solution is to manually diff the old and the new cursors:
# Every time the query change, do a diff to add, move and remove pins on the screen
# Assuming that the pins order are always the same, this use a single loop of complexity
# o(n) rather than the naive loop in loop of complexity o(n^2)
Deps.autorun =>
old_pins = #pins
new_pins = []
position = 0
old_pin = undefined # This variable needs to be in the Deps.autorun scope
# This is a simple algo to implement a kind of "reactive cursor"
# Sorting is done on the server, it's important to keep the order
collection.find(Session.get('search-query'), sort: [['mark', 'desc']]).forEach (product) =>
if not old_pin?
old_pin = old_pins.shift()
while old_pin?.mark > product.mark
#removePin(old_pin)
old_pin = old_pins.shift()
if old_pin?._id == product._id
#movePin(old_pin, position++)
new_pins.push(old_pin)
old_pin = old_pins.shift()
else
newPin = #render(product, position++)
new_pins.push(newPin)
# Finish the job
if old_pin?
#removePin(old_pin)
for old_pin in old_pins
#removePin(old_pin)
#pins = new_pins
But it's a bit hacky and not so efficient. Moreover the diff logic is already implemented in minimongo so it's better to reuse it.
Perhaps an acceptable solution would be to keep track of old pins in a local collection? Something like this:
Session.setDefault('search-query', {});
var Offers = new Meteor.Collection('offers');
var OldOffers = new Meteor.Collection(null);
var addNewPin = function(offer) {
// Add a pin only if it's a new offer, and then mark it as an old offer
if (!OldOffers.findOne({_id: offer._id})) {
Map.addPin(offer);
OldOffers.insert(offer);
}
};
var removePinsExcept = function(ids) {
// Clean out the pins that no longer exist in the updated query,
// and remove them from the OldOffers collection
OldOffers.find({_id: {$nin: ids}}).forEach(function(offer) {
Map.removePin(offer);
OldOffers.remove({_id: offer._id});
});
};
Deps.autorun(function() {
var offers = Offers.find(Session.get('search-query'));
removePinsExcept(offers.map(function(offer) {
return offer._id;
}));
offers.observe({
added: addNewPin,
removed: Map.removePin
});
});
I'm not sure how much faster this is than your array answer, though I think it's much more readable. The thing you need to consider is whether diffing the results as the query changes is really much faster than removing all the pins and redrawing them each time. I would suspect that this might be a case of premature optimization. How often do you expect a user to change the search query, such that there will be a significant amount of overlap between the results of the old and new queries?
I have the same problem in my own hobby Meteor project.
There is filter session var where selector is storing. Triggering any checkbox or button changes filter and all UI rerender.
That solution have some cons and the main - you can't share app state with other users.
So i realized that better way is storing app state in URL.
May be it is also better in your case?
Clicking button now change URL and UI rendering based on it. I realize it with FlowRouter.
Helpful reading: Keeping App State on the URL

How to sort a client-side-only (local) Meteor Collection

I have client side only (local) Meteor collection defined like that (coffeescript):
Products = new Meteor.Collection null
However when I try to find() providing sorting parameters Meteor tells me that sorting of local collections is not supported. This is understandable.
I would like to know what is the easiest/simplest way to get sorted results. Essentially I always use all the data in the Collection, so keeping it in sorted state would solve the problem.
It works for me, are you using the latest version of Meteor? Running this code works on the Meteor Docs site:
var foos = new Meteor.Collection( null );
for ( var i = 0; i < 100; i++ ) {
foos.insert({ num: i });
}
foos.findOne( {} ).num; // => 0
foos.findOne( {}, { sort: [[ "num", "desc" ]] } ).num; // => 99

Resources