How do you define parentName, parentId, and connectionName in getConfigs for a nested connection? - relayjs

I am running into some issues writing a Relay Mutation on a nested connection. Here is the type structure:
{
viewer {
entity(id) {
events // mutate connection here
}
}
}
In the getConfigs I have both the parentName and parentID pointing to the "viewer", however the connectionName "events" does not exist on the viewer it exists on the "entity" type.
As you will see in the code snippet below I am also unsure how to use variables in the getFatQuery to fetch the mutated data with an entity ID.
getConfigs and getFatQuery:
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'events',
edgeName: 'eventEdge',
rangeBehaviors: {
'': 'append'
},
}];
}
getFatQuery() {
return Relay.QL`
fragment on addEventPayload {
viewer {
entity(id: $entityId) // how do I use variables in the getFatQuery {
events(first: 20) {
edges {
node {
status
}
}
}
}
},
eventEdge
}
`;
}
I am more than happy to help clarify my question if it makes no sense so please feel free to ask questions about my question.
Thanks for the help!

For anyone in the future getting snagged on this you don't have to worry about passing in the id again or dealing with nested queries/fields. Relay will just find and update the appropriate record on the client. This issue on Github was helpful for figuring this out, especially the comments from freiksenet.
In the example above - Instead of going through the viewer we just go straight to the entity.
outputFields:
outputFields: {
eventEdge: {
type: eventEdge,
resolve: async({event}) => {
const eventsByOwner = await Event.getEventsByOwnerId(event.ownerId)
const eventIndex = eventsByOwner.findIndex(evt => evt.id == event.id);
const cursor = offsetToCursor(eventIndex);
return {
cursor: cursor,
node: event
};
}
},
entity: {
type: entity,
resolve: async({event}) => {
return Entity.getEntity(event.ownerId)
}
},
}
getConfigs and getFatQuery:
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'entity',
parentID: this.props.entityId,
connectionName: 'events',
edgeName: 'eventEdge',
rangeBehaviors: {
'': 'append'
},
}];
}
getFatQuery() {
return Relay.QL`
fragment on addEventPayload #relay(pattern: true) {
entity {
events
},
eventEdge
}
`;
}
Note: Using #relay(pattern: true) will make sure you don't run into issues when you don't pass in arguments for connection queries and will fall back to your last query of this type.

Related

Relay Modern updater ConnectionHandler.getConnection() returns undefined when parent record is root

Debugging update:
So, we went a bit further in debugging this and it seems like 'client:root' cannot access the connection at all by itself.
To debug the complete store, we added this line in the updater function after exporting the store variable from the relay/environment.
console.log(relayEnvStore.getSource().toJSON())
If I use .get() with the specific string client:root:__ItemList_items_connection, I can access the records I have been looking for but it's definitely not pretty.
const testStore = store.get('client:root:__ItemList_items_connection')
console.log(testStore.getLinkedRecords('edges'))
Original:
I'm using Relay Modern and trying to update the cache after the updateItem mutation is completed with the updater. The call to ConnectionHandler.getConnection('client:root', 'ItemList_items') returns undefined.
I'm not sure if it's because I'm trying to use 'client:root' as my parent record or if there's a problem with my code. Has anyone found themselves with a similar issue?
Here's the paginationContainer:
const ItemListPaginationContainer = createPaginationContainer(
ItemList,
{
node: graphql`
fragment ItemList_node on Query
#argumentDefinitions(count: { type: "Int", defaultValue: 3 }, cursor: { type: "String" }) {
items(first: $count, after: $cursor) #connection(key: "ItemList_items") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
},
{
direction: 'forward',
getConnectionFromProps: props => props.node && props.node.items,
getVariables(props, { count, cursor }) {
return {
count,
cursor
}
},
query: graphql`
query ItemListQuery($count: Int!, $cursor: String) {
...ItemList_node #arguments(count: $count, cursor: $cursor)
}
`
}
)
Here's the mutation:
const mutation = graphql`
mutation UpdateItemMutation($id: ID!, $name: String) {
updateItem(id: $id, name: $name) {
id
name
}
}
`
Here's the updater:
updater: (store) => {
const root = store.getRoot()
const conn = ConnectionHandler.getConnection(
root, // parent record
'ItemList_items' // connection key
)
console.log(conn)
},
Turns out that I was setting my environment incorrectly. The store would reset itself every time I would make a query or a mutation, hence why I couldn't access any of the connections. I initially had the following:
export default server => {
return new Environment({
network: network(server),
store: new Store(new RecordSource())
})
}
All connections are accessible with this change:
const storeObject = new Store(new RecordSource())
export default server => {
return new Environment({
network: network(server),
store: storeObject
})
}

unable to get correct data using `useLazyLoadQuery` when compose fragment into query

I do see a sucessfull API request
const data = useLazyLoadQuery<brandQuery>(
graphql`query
brandQuery {
...brand_autoBrands
}
`,
{
first: 10,
},
{
fetchPolicy: "network-only",
}
);
console.log(data);
But I got following output. it is supposed to be a json object returned from my API.
brand_autoBrands fragment somewhere
const autoBrands = graphql`
fragment brand_autoBrands on AdminQuery
#argumentDefinitions(
first: { type: "Int", defaultValue: 10 }
after: { type: "String", defaultValue: "" }
last: { type: "Int" }
before: { type: "String" }
filters: { type: "[Filter]" }
sorters: { type: "[Sorter]"}
)
#refetchable(queryName: "BrandListPaginationQuery") {
autoBrands(first: $first, after: $after, last: $last, before: $before, filters: $filters, sorters: $sorters)
#connection(key: "BrandList_autoBrands") {
edges {
node {
...brandFragment
}
}
pageInfo {
startCursor
}
totalCount
}
}
`;
const {
data,
loadNext,
hasNext
} = usePaginationFragment<BrandListPaginationQuery, _>(
autoBrands,
props.autoBrands,
);
You can't log a fragment data object in the parent. That data belongs to the component that defined a fragment. For performance, relay needs to know that this data is fetched for a child component but the parent doesn't need to know what the actual data object is. You just need to pass the data to its component then you should be able to log it in the child component.
const data = useLazyLoadQuery<brandQuery>(
graphql`query
brandQuery {
...brand_autoBrands
}
`,
{
first: 10,
},
{
fetchPolicy: "network-only",
}
);
data && <Brand autoBrands={data} />

how to optimisticResponse a connection property in relay modern mutation?

i have a comments connection in a mutation, this is the query:
export default mutationFromQuery(graphql`
mutation AddBookMutation($input: AddBookInput! $count: Int $cursor: String ) {
addBook(input: $input) {
book {
__typename
cursor
node {
id
title
owner
createdAt
comments(first: $count, after: $cursor)
#connection(key: "BookComments_comments", filters: []) {
__typename
edges {
node {
id
}
}
}
}
}
}
}
`)
This is how i did my optimisticUpdater that don't work:
optimisticUpdater: (store) => {
const userProxy = store.get(viewerId)
const owner = userProxy.getValue('username')
const id = uuidv1();
const book = store.create(id, 'Book');
book.setValue(id, 'id');
book.setValue(bookTitle, 'title');
book.setValue(owner, 'owner');
book.setValue(Date.now(), 'createdAt');
const comments = store.create(uuidv1(), 'comments')
comments.setLinkedRecords([], 'edges')
const pageInfo = store.create(uuidv1(), 'pageInfo')
pageInfo.setValue(null, 'endCursor')
pageInfo.setValue(false, 'hasNextPage')
pageInfo.setValue(false, 'hasPreviousPage')
pageInfo.setValue(null, 'startCursor')
comments.setLinkedRecord(pageInfo, 'pageInfo')
book.setLinkedRecord(comments, 'comments')
const bookEdge = store.create(uuidv1(), 'BookEdge');
bookEdge.setLinkedRecord(book, 'node');
console.log('bookEdge ', bookEdge)
booksUpdater(userProxy, bookEdge);
},
The problem i have is that comments always ends up on undefined as you can see above i've already set it. I also did this but i am still not getting an optimistic UI:
optimisticResponse: {
addBook: {
book: {
__typename: 'BookEdge',
cursor: uuidv1(),
node: {
id: uuidv1(),
title: bookTitle,
owner: username,
createdAt: Date.now(),
comments: {
__typename: 'CommentConnection',
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false
}
}
}
}
}
},
App don't crash with optimisticResponse code but no optimistic UI effect, but with the optimisticUpdater it's crashing with comments being undefined, for now I am settling with my updater:
updater: (store) => {
const userProxy = store.get(viewerId)
const payload = store.getRootField('addBook');
booksUpdater(userProxy, payload.getLinkedRecord('book'));
},
since the comments is undefined I guess we cannot use this for optimistic effect:
const comments = store.create(uuidv1(), 'comments')
comments.setLinkedRecords([], 'edges')
book.setLinkedRecord(comments, 'comments')
on my Book, this is the query which has the comments fragment that is undefined on optimistic update with the code above:
export default createRefetchContainer(
BookItem,
{
book: graphql`
fragment BookItem_book on Book
#argumentDefinitions(
count: { type: "Int", defaultValue: 5 }
cursor: { type: "String", defaultValue: null }
) {
id
title
owner
createdAt
...BookComments_book
}
`
},
graphql`
query BookItemQuery($id: ID!, $count: Int, $cursor: String) {
book: node(id: $id) {
...BookItem_book #arguments(count: $count, cursor: $cursor)
}
}
`
);
and now the query for the comments component where it gets the book.comments.edges is undefined:
export default createPaginationContainer(
BookComments,
{
book: graphql`
fragment BookComments_book on Book
#argumentDefinitions(
count: { type: "Int", defaultValue: 3 }
cursor: { type: "String", defaultValue: null }
) {
id
title
comments(first: $count, after: $cursor)
#connection(key: "BookComments_comments", filters: []) {
__typename
edges {
node {
id
text
owner
createdAt
}
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
`
},
{
direction: 'forward',
getConnectionFromProps: (props) => props.book && props.book.comments,
getFragmentVariables: (prevVars, totalCount) => ({
...prevVars,
count: totalCount
}),
getVariables: (props, { count, cursor }, _fragmentVariables) => ({
count,
cursor,
id: props.book.id
}),
query: graphql`
query BookCommentsQuery($id: ID!, $count: Int, $cursor: String) {
book: node(id: $id) {
...BookComments_book #arguments(count: $count, cursor: $cursor)
}
}
`
}
);
maybe this is an anti pattern? but i just wanted to have a optimistic effect for this
Some things are still not very clear to me about how those components and queries work, so I'll update this answer later. (I don't know if you want to return new book optimistically from node() query or add it to some list/connection of books)
Please check if I used correct type names (CommentConnection / CommentsConnection, etc)
optimisticUpdater: (store) => {
const userProxy = store.get(viewerId)
const owner = userProxy.getValue('username')
const commentsParams = { // must be same keys and values as in comments(first: $count, after: $cursor)
first: count,
after: cursor
}
// Create Book
const id = uuidv1();
const book = store.create(id, 'Book');
book.setValue(id, 'id');
book.setValue(bookTitle, 'title');
book.setValue(owner, 'owner');
book.setValue(Date.now(), 'createdAt');
// Create comments connection
const comments = store.create(uuidv1(), 'CommentConnection')
comments.setLinkedRecords([], 'edges')
// Create PageInfo
const pageInfo = store.create(uuidv1(), 'PageInfo')
pageInfo.setValue(null, 'endCursor')
pageInfo.setValue(false, 'hasNextPage')
pageInfo.setValue(false, 'hasPreviousPage')
pageInfo.setValue(null, 'startCursor')
// Link created records
comments.setLinkedRecord(pageInfo, 'pageInfo')
book.setLinkedRecord(comments, 'comments', commentsParams) // don't forget commentsParams with same values as are used in comments graphql query
// I'm not sure about this final part, because I don't really get how that app works, but if you want this book to show as optimistic response for `node(id: $id)`, you'll do something like this:
store.getRoot().setLinkedRecord(book, 'node', { id: id }) // same id as used in BookItemQuery
}

how to implement mutation responses on a local falcor Model dataset

Given that I have an example Model:
var model = new falcor.Model({
cache: {
userById: {
"1": {
name: "User",
email: "user#email.com"
}
},
users: {
current: null
}
}
});
This is a local model that I'm using for testing purposes, and I would like to implement it on a call to users.login so the user so that I can call:
model.call(['users', 'login'], ['user', 'password'])
I realized that if I do this:
var model = new falcor.Model({
cache: {
userById: {
"1": {
name: "User",
email: "user#email.com"
}
},
users: {
current: null,
login: function(user, password) {
console.log('this code is reached', user, password);
// what to return in order to mutate model?
}
},
}
});
When I do the call it gets there, but I can't figure out how to mutate the model as part of the response; on the server side we return the paths with values and invalidates, and it just works, but here I tried:
// trying returning as a jsonGraph response, don't work
login: function() {
return {
jsonGraph: {
users: {
current: {$type: "ref", value: ['userById', '1']}
}
},
paths: [['users', 'current']]
}
}
// trying returning as a path set mutation list, don't work
login: function() {
return [{path: ['users', 'current'], value: {$type: "ref", value: ['userById', '1']}}]
}
// trying force call to set on the model, don't work
login: function() {
this.set([
{path: ['users', 'current'], value: {$type: "ref", value: ['userById', '1']}}
])
}
// trying using ModelResponse, got an example on some external sources, don't work
login: funtion() {
return new ModelResponse((observer) => {
observer.onNext({
jsonGraph: {
users: {
current: {$type: "ref", value: ['userById', '1']}
}
},
paths: [['users', 'current']]
});
observer.onCompleted();
});
}
Now I don't know what else to try; I need a simple way to declare mutations after a call into a local model, if you know how to solve this, please let me know here.
Thanks.
The client model cache only supports JSONGraph, which b/c it is essentially just JSON with some conventions, doesn't support functions. So, when working with a falcor model cache and no dataSource/middle tier router, it is not possible to implement calls.
This can be kind of annoying when prototyping/testing, as a router is conceptually more difficult than a simple JSON cache object. I ran into this a while ago, so I wrote a dataSource module to support it: falcor-local-datasource. The dataSource is initialized with a graph object that does support function nodes, and as with your above examples, will mutate the graph based on the function's returned JSONGraphEnvelope or an array of PathValues.

Breeze issue with complexType change tracking

I use Angular and Breeze in my app and I use "hasChangesChanged" event for handling a state of entities stored in EntityManager. I have a problem when I have entity with property that is complexType and isScalar=false.
The problem occurs when I make request twice (without changing any entity) and get the same entity. On second request "hasChangesChanged" event is fired with hasChanges=true.
In moment when this event is fired my entity has state "Modified", but after data are loaded that state is changed to "Unchanged".
I've wrote a (Jasmine) unit test. In comments are information which assertion throws error.
var entity,
hasChanges = false,
listeners = {
onChange: function (event) {
console.log('change', event.hasChanges);
}
};
spyOn(listeners, 'onChange');
$httpBackend.expectGET('json/SampleEntity?').respond(200, [
{
id: 1,
name: 'some name',
data: {},
$type: 'SampleEntity',
elements: [
{
etype: 'el1'
}
]
}
]);
manager.hasChangesChanged.subscribe(function (event) {
hasChanges = event.hasChanges;
});
var query = new breeze.EntityQuery('SampleEntity');
manager.executeQuery(query).then(function (data) {
entity = data.results[0];
});
$httpBackend.flush();
expect(hasChanges).toBe(false); // OK
expect(entity.entityAspect.entityState.isUnchanged()).toBe(true); // OK
$httpBackend.expectGET('json/SampleEntity?').respond(200, [
{
id: 1,
name: 'some name',
data: {},
$type: 'SampleEntity',
elements: [
{
etype: 'el1'
}
]
}
]);
manager.executeQuery(query).then(function (data) {
entity = data.results[0];
});
$httpBackend.flush();
expect(hasChanges).toBe(false); // ERROR
expect(entity.entityAspect.entityState.isUnchanged()).toBe(true); // OK
Is this expected behavior? And if not how I can fix it?

Resources