I have a situation where I'm making an API call to a Laravel back-end and eager-loading some data. The data is a collection of locations, each of the form:
location = {
id: ..
name: ...
...
docks: [
{
id: ...
name: ...
},
...
]
}
The end result of this epic should be to dispatch 2 actions, one with a list of the locations, and another with the list of all the associated docks
Taking it from the response to the api call, the following throws a Typescript error and won't compile:
.mergeMap(({ response }) => {
if (response.messages) {
return Observable.of(GetLocationsForReceiverFailedAction(response.messages))
}
const locations = response.locations.map((locn: any) => ({
id: locn.id,
receiver_id: locn.receiver_id,
name: locn.name,
address: locn.address,
city: locn.city,
state: locn.state,
zip: locn.zip
}))
const location$ = Observable.of(GetLocationsForReceiverSuccessAction(locations))
const docks: any[] = []
response.locations.forEach((locn: any) => {
locn.docks.forEach((dk: any) => {
docks.push(dk)
})
})
const docks$ = Observable.of(GetDocksForReceiverSuccessAction(docks))
return Observable.merge(location$, docks$)
})
.catch(() =>
Observable.of(
GetLocationsForReceiverFailedAction([
'Something went wrong trying to reach the server'
])
)
)
The error reads:
"Argument of type '({ response }: AjaxResponse) => Observable<{ type: ActionTypes; messages: string[]; }> | Observab...' is not assignable to parameter of type '(value: AjaxResponse, index: number) => ObservableInput<{ type: ActionTypes; messages: string[]; }>'."
If, however, I change the line:
return Observable.merge(location$, docks$)
to:
return Observable.merge(location$, Observable.empty(), docks$)
(ie, separating by 2 observables by an empty observable in the merge (or concat, which works the same way here), it compiles and runs.
What am I not understanding here?
Related
I have a DB code that might throw a MongoError. I would like to catch it and manipulate it, transforming it into my own type of error (I don't want the MongoError to be exposed to other parts of my application). So I do it as follows:
// request = { deviceId: 2, name: 'Device' }
public create(request: DeviceCreationRequest): Observable<Device> {
return fromPromise(this.model.create(request)).pipe(
catchError(err =>
throwError({ code: 409, body: { msg: 'duplicated key', key: err.keyPattern } }))
map(DeviceRepository.toDevice));
It works fine in the endpoint, but I don't know how to test it.
it('create with duplicated key', done => {
const request = { deviceId: 2, name: 'Device' };
repository.create(request).pipe(
catchError(err => {
console.log(err); // shows the original MongoDB error
return of(undefined);
}),
flatMap(_ => repository.getAll()))
.subscribe(devices => {/* some assertions */});
});
Looks like my test catchError overwrites the old one (or something). And if I don't do it, the test case will error out.
EDIT
I boiled down the problem. The following code yields an error in tests, but works as expected in the browser (see https://github.com/prumand/jest-marbles-merge-map and https://github.com/ReactiveX/rxjs/issues/4837)
tests: returns a WE_FINISH
browser (expected): MY_NEW_ERROR
// code
export default function basicMergeMapObs(
action$: Observable<Action>
) : Observable<any> {
return action$.pipe(
filter((val: Action) => {
throw new Error('We stop here')
}),
map((val: Action) => ({
type: 'WE_FINISH',
})),
catchError(() => of({
type: 'MY_NEW_ERROR',
}))
)
}
// test
it('should yield an MY_ERROR', () => {
const source = of({
type: 'TEST',
status: 'NEW'
})
getScheduler().run(helpers => {
const { expectObservable, cold } = helpers
expectObservable(
basicMergeMapObs(
source
)
).toBe(
'(t|)',
{
t: { type: 'MY_NEW_ERROR' }
}
)
})
})
function getScheduler() {
return new TestScheduler((actual, expected) => {
expect(actual).toMatchObject(expected);
});
}
UPDATE 19.06.2019
I added cartants example from the given github issue, which works fine. Still my example fails. No idea why. IMO it should always throw an error.
And yet another update, the tests don't fail on linux, but only on my windows machine
UPDATE 02.07.2019
:O seemed to be a issue with endpoint-security solution we use ...
I am making the following query in GraphQL:
{
metal(silver_bid_usd_toz: 1) {
silver_bid_usd_toz
}
}
which returns
{
"data": {
"metal": {
"silver_bid_usd_toz": 16.45
}
}
}
The JSON object returned by the API is flat:
{
silver_bid_usd_toz: 123,
gold_bid_usd_toz: 123,
copper_bid_usd_toz: 123
}
I don't understand what the int 1 in my graphql query means metal(silver_bid_usd_toz: 1)
It doesn't matter what I change it to, it could be 1 or 355, but it is required for the query to work. Why cant I just do
{
metal(silver_bid_usd_toz) {
silver_bid_usd_toz
}
}
My schema looks like this:
module.exports = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
description: '...',
fields: () => ({
metal: {
type: MetalType,
args: {
gold_bid_usd_toz: { type: GraphQLFloat },
silver_bid_usd_toz: { type: GraphQLFloat }
},
resolve: (root, args) => fetch(
`api_url`
)
.then(response => response.json())
}
})
})
});
You are passing silver_bid_usd_toz as an argument for the field, but apparently you are not using it in the resolve function, so it's being ignored.
It seems to be the reason why the result is always the same when you change the argument value.
But it is weird when you say that it is required for the query to work, since it is not defined as a GraphQLNonNull type.
It should be possible to query this field without passing any argument, according to the Schema you passed us.
Apologies if this is a stupid question. this is the code for relay/graphql pagination that's confusing me:
const GraphQLTodo = new GraphQLObjectType({
name: 'Todo',
fields: {
id: globalIdField('Todo'),
text: {
type: GraphQLString,
resolve: (obj) => obj.text,
},
complete: {
type: GraphQLBoolean,
resolve: (obj) => obj.complete,
},
},
interfaces: [nodeInterface],
});
/* When pagination is needed, make a connection */
const {
connectionType: TodosConnection,
edgeType: GraphQLTodoEdge,
} = connectionDefinitions({
name: 'Todo',
nodeType: GraphQLTodo,
});
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField('User'),
todos: {
type: TodosConnection,
args: {
status: {
type: GraphQLString,
defaultValue: 'any',
},
...connectionArgs,
},
resolve: (obj, {status, ...args}) =>
connectionFromArray(getTodos(status), args),
},
totalCount: {
type: GraphQLInt,
resolve: () => getTodos().length,
},
completedCount: {
type: GraphQLInt,
resolve: () => getTodos('completed').length,
},
},
interfaces: [nodeInterface],
});
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
node: nodeField,
},
});
You can see that on the GraphQLTodo field, it has text and complete fields with resolve function passed an obj parameter, how is obj passed there? is it from GraphQLUser resolve? I've read on docs that source(in this case obj) - The object resolved from the field on the parent type. is it not from the root query? how is obj here created?
The Connection
Here is where (some of) the magic happens:
const {
connectionType: TodosConnection,
edgeType: GraphQLTodoEdge,
} = connectionDefinitions({
name: 'Todo',
nodeType: GraphQLTodo,
});
You have now told GraphQL that a TodosConnection is going to be made up of GraphQLTodo nodes. Now, let's take a look at where the objects are actually fetched for the connection in your GraphQLUser object, which is on the todos field:
todos: {
type: TodosConnection,
args: {
status: {
type: GraphQLString,
defaultValue: 'any',
},
...connectionArgs,
},
resolve: (obj, {status, ...args}) =>
connectionFromArray(getTodos(status), args),
},
So where does the object come from? The key part here is the getTodos function, which is responsible for actually getting an array of the objects from your data source. Since this field is a TodosConnection and we've already specified in the connection definitions that the nodes are GraphQLTodos, GraphQL knows that the text and complete fields are resolved by getting (in this case) identically named fields on the objects that have been returned. In other words, the returned object is passed to the resolve method on each field.
Querying the Root
You have two fields exposed on Root: viewer and node. Ignoring node for a moment, you have just one way to actually query todos. Since viewer is of type GraphQLUser, and GraphQLUser has that todos field, they can be fetched only as a subfield of viewer, like this:
{
viewer {
todos(first: 10) {
edges {
# each node is a Todo item
node {
text
complete
}
}
}
}
}
Mystery of the Node
But what about that node field? Relay wants to be able to fetch any object using a top-level query, i.e. on your Root field, when given a unique globalId, which is just a base64 encoding of the type name and the id, so Todo:1 is encoded to VG9kbzox. This is set up in the nodeDefinitions (which you haven't included here, but probably have). In those definitions, the globalId is parsed back into the type (Todo) and id (1), and once again you then tell it how to fetch the correct object from your data source. It might look something like:
const { nodeInterface, nodeField } = nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId);
if (type === 'Todo') {
return getTodo(id)
} else if (type === 'User') {
return getUser(id)
}
...
Because you're implementing the nodeInterface in both your GraphQLTodo and GraphQLUser types, Relay will be able query for either of them from the Root's node field.
I'm trying to implement a windowed pagination using a "List". I don't need the cursor based solution with connections, because I need to show numbered pages to the user.
There are "User" and "Post" objects."User" has one-to-many relation to "Post".
Using graphql-js for schema,
here is my schema for userType and postType:
var userType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: globalIdField('User'),
posts: {
type: new GraphQLList(postType),
args: {
page:{
type: GraphQLInt,
defaultValue: 0
}
},
resolve: (_, args) => {
//code to return relevant result set
},
},
totalPosts:{
type: GraphQLInt,
resolve: () => {
//code to return total count
}
},
}),
interfaces: [nodeInterface],
});
var postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
name: {type: GraphQLString},
//other fields
}),
interfaces: [nodeInterface],
});
Please notice the "totalPosts" field in "userType". Since there is going to be other Lists for the user,with the same paging needs, I'm going to end up maintaining lot of "total{Type}" variables in the fragment. This can be solved if I can send the totalCount within the List result somehow.
https://github.com/facebook/graphql/issues/4 this issue talks about implementing a wrapper over the List to include the totalCount in the result set.
I tried creating a wrapper like this:
var postList = new GraphQLObjectType({
name: 'PostList',
fields:()=>({
count: {
type: GraphQLInt,
resolve: ()=>getPosts().length //this is total count
},
edges: {
type: new GraphQLList(postType),
resolve: () => {
return getPosts() ; // this is results for the page, though I don't know how to use 'page' argument here
},
}
}),
interfaces: [nodeInterface],
});
but how should I connect this to the userType's posts field? And how can I use a 'page' argument on this wrapper, like I have in original userType?
how should I connect this to the userType's posts field? And how can I use a 'page' argument on this wrapper, like I have in original userType?
One simple way to implement what you're trying to do is to define a dumb wrapper type postList like this:
var postList = new GraphQLObjectType({
name: 'PostList',
fields:()=>({
count: { type: GraphQLInt },
edges: { type: new GraphQLList(postType) }
// Consider renaming 'edges'. In your case, it's a list, not a
// connection. So, it can cause confusion in the long run.
}),
});
Then in the userType definition, add a field of that wrapper type and define its resolve function like below. As for argument page, just describe it while defining the field type posts.
posts: {
type: postList,
args: {
page:{
type: GraphQLInt,
defaultValue: 0
},
...otherArgs
},
resolve: async (_, {page, ...otherArgs}) => {
// Get posts for the given page number.
const posts = await db.getPosts(page);
// Prepare a server-side object, which corresponds to GraphQL
// object type postList.
const postListObj = {
count: posts.length,
edges: posts
};
// Consider renaming 'edges'. In your case, it's a list, not a
// connection. So, it can cause confusion in the long run.
},
},